amalia/03-jun-26 #55
@@ -39,5 +39,7 @@ app-example
|
|||||||
|
|
||||||
x.ts
|
x.ts
|
||||||
x.sh
|
x.sh
|
||||||
/android
|
|
||||||
/ios
|
.env
|
||||||
|
|
||||||
|
android/
|
||||||
6
.gitignore
vendored
@@ -32,6 +32,9 @@ yarn-error.*
|
|||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
#env
|
||||||
|
.env
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
@@ -41,4 +44,5 @@ x.ts
|
|||||||
x.sh
|
x.sh
|
||||||
|
|
||||||
google-services.json
|
google-services.json
|
||||||
mobile-darmasaba-firebase-adminsdk-fbsvc-f5abb292b5.json
|
service-account.json
|
||||||
|
|
||||||
|
|||||||
37
CLAUDE.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Desa+** is a React Native (Expo) mobile app for village administration — managing announcements, projects, discussions, members, divisions, and documents. Primary platforms are Android and iOS.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start # Start Expo dev server
|
||||||
|
npm run android # Run on Android
|
||||||
|
npm run ios # Run on iOS
|
||||||
|
npm run lint # Expo lint
|
||||||
|
npm run test # Jest tests
|
||||||
|
npm run build:android # Production Android build via EAS (bumps version first)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test file:
|
||||||
|
```bash
|
||||||
|
bunx jest path/to/test.tsx --no-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
> Project uses **Bun** as the package manager (`bun.lock` present). Use `bun add` / `bunx` instead of `npm install` / `npx`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See @docs/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
See @docs/CONVENTIONS.md
|
||||||
|
|
||||||
|
## File Health
|
||||||
|
|
||||||
|
See @docs/FILE-HEALTH.md
|
||||||
86
GEMINI.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Project Overview: Desa+
|
||||||
|
|
||||||
|
Desa+ is a mobile application built with React Native and Expo, designed to facilitate management and communication within villages/communities. It aims to streamline village administration, inter-community communication, and the management of essential information.
|
||||||
|
|
||||||
|
## Key Features:
|
||||||
|
- Village announcements and information
|
||||||
|
- Community discussion forum
|
||||||
|
- Village activity calendar
|
||||||
|
- Village documentation and archives
|
||||||
|
- Project and task management
|
||||||
|
- Member and organizational structure management
|
||||||
|
- Push notifications for important updates
|
||||||
|
- Verification and authentication features
|
||||||
|
|
||||||
|
## Technologies Used:
|
||||||
|
- **React Native**: Cross-platform mobile development framework.
|
||||||
|
- **Expo**: Platform for React Native application development.
|
||||||
|
- **Firebase**: Backend services including Authentication, Realtime Database, and Cloud Messaging.
|
||||||
|
- **Redux Toolkit**: State management.
|
||||||
|
- **React Navigation**: Application navigation.
|
||||||
|
- **TypeScript**: For type safety.
|
||||||
|
|
||||||
|
## Building and Running:
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd mobile-darmasaba
|
||||||
|
```
|
||||||
|
2. **Install dependencies:**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. **Configure environment variables:**
|
||||||
|
Create a `.env` file in the root directory and add the following variables:
|
||||||
|
```
|
||||||
|
URL_API=<api-endpoint>
|
||||||
|
URL_OTP=<otp-service-endpoint>
|
||||||
|
URL_STORAGE=<storage-endpoint>
|
||||||
|
URL_FIREBASE_DB=<firebase-database-url>
|
||||||
|
PASS_ENC=<encryption-password>
|
||||||
|
WA_SERVER_TOKEN=<whatsapp-server-token>
|
||||||
|
IOS_GOOGLE_SERVICES_FILE=<path-to-ios-google-services>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
- **Start development server:**
|
||||||
|
```bash
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
- **Run on Android emulator/device:**
|
||||||
|
```bash
|
||||||
|
npm run android
|
||||||
|
```
|
||||||
|
- **Run on iOS simulator/device:**
|
||||||
|
```bash
|
||||||
|
npm run ios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Production
|
||||||
|
- **Build Android production package:**
|
||||||
|
```bash
|
||||||
|
npm run build:android
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Conventions:
|
||||||
|
|
||||||
|
### Project Structure:
|
||||||
|
- `app/`: Main page files.
|
||||||
|
- `components/`: Reusable UI components, categorized by feature (e.g., `announcement/`, `auth/`, `discussion/`).
|
||||||
|
- `assets/`: Images and static assets.
|
||||||
|
- `constants/`: Global constants.
|
||||||
|
- `lib/`: Libraries and utilities.
|
||||||
|
|
||||||
|
### Contribution Guidelines:
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create a new feature branch (`git checkout -b feature/FeatureName`).
|
||||||
|
3. Commit your changes (`git commit -m 'Add FeatureName feature'`).
|
||||||
|
4. Push to the branch (`git push origin feature/FeatureName`).
|
||||||
|
5. Create a pull request.
|
||||||
|
|
||||||
|
## Platform Support:
|
||||||
|
- ✅ Android
|
||||||
|
- ✅ iOS
|
||||||
|
- ❌ Web (not yet optimized)
|
||||||
249
Panduan-Penggunaan-Aplikasi.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Panduan Aplikasi Desa+
|
||||||
|
|
||||||
|
## Daftar Isi
|
||||||
|
|
||||||
|
1. [Gambaran Umum Aplikasi](#gambaran-umum-aplikasi)
|
||||||
|
2. [User Roles dan Hak Akses](#user-roles-dan-hak-akses)
|
||||||
|
3. [Fitur-fitur Aplikasi](#fitur-fitur-aplikasi)
|
||||||
|
4. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Gambaran Umum Aplikasi
|
||||||
|
|
||||||
|
Aplikasi Desa+ adalah platform digital berbasis mobile yang dirancang untuk khusus untuk pegawai desa dalam mengelola data dan memantau progres kegiatan internal. Aplikasi ini menyediakan berbagai fitur seperti pengelolaan data per divisi, pemantauan kegiatan umum, forum diskusi, pengumuman, hingga manajemen folder dokumen, aplikasi ini membantu meningkatkan efisiensi kerja, koordinasi, serta transparansi di lingkungan desa.
|
||||||
|
|
||||||
|
### Teknologi yang Digunakan
|
||||||
|
|
||||||
|
- React Native dengan Expo
|
||||||
|
- Firebase (Authentication, Realtime Database, Cloud Messaging)
|
||||||
|
- Redux Toolkit untuk manajemen state
|
||||||
|
- TypeScript untuk type safety
|
||||||
|
|
||||||
|
## User Roles dan Hak Akses
|
||||||
|
|
||||||
|
Aplikasi Desa+ memiliki sistem hierarki peran pengguna sebagai berikut:
|
||||||
|
|
||||||
|
### 1. Super Admin
|
||||||
|
|
||||||
|
- **Hak akses:**
|
||||||
|
- Semua fitur dan fungsi dalam aplikasi
|
||||||
|
- Manajemen pengguna dengan role Wakil Super Admin, Admin, Wakil Admin, dan User
|
||||||
|
- Akses ke semua data dan fungsi administratif
|
||||||
|
|
||||||
|
### 2. Wakil Super Admin
|
||||||
|
|
||||||
|
- **Hak akses:**
|
||||||
|
- Manajemen pengguna dengan role Admin, Wakil Admin, dan User
|
||||||
|
- Akses ke sebagian besar fitur administratif
|
||||||
|
- Dapat mengelola banner
|
||||||
|
|
||||||
|
### 3. Admin
|
||||||
|
|
||||||
|
- **Hak akses:**
|
||||||
|
- Manajemen pengguna dengan role Wakil Admin dan User
|
||||||
|
- Akses ke fitur-fitur administratif dasar
|
||||||
|
- Tidak dapat mengelola Wakil Super Admin dan Super Admin
|
||||||
|
|
||||||
|
### 4. Wakil Admin
|
||||||
|
|
||||||
|
- **Hak akses:**
|
||||||
|
- Manajemen pengguna dengan role User
|
||||||
|
- Akses terbatas ke fitur-fitur administratif
|
||||||
|
- Tidak dapat mengelola Admin ke atas
|
||||||
|
|
||||||
|
### 5. User
|
||||||
|
|
||||||
|
- **Hak akses:**
|
||||||
|
- Akses ke fitur-fitur umum
|
||||||
|
- Tidak dapat mengelola pengguna lain
|
||||||
|
- Tidak dapat mengakses fungsi administratif (kecuali dalam divisi dimana pengguna tersebut adalah anggota)
|
||||||
|
|
||||||
|
## Fitur-fitur Aplikasi
|
||||||
|
|
||||||
|
### 1. Otentikasi (Login & Verifikasi)
|
||||||
|
|
||||||
|
**Deskripsi:** Sistem login menggunakan nomor telepon dan verifikasi OTP (One Time Password)
|
||||||
|
|
||||||
|
- **Fungsi:** Memverifikasi identitas pengguna sebelum mengakses aplikasi
|
||||||
|
- **Siapa yang bisa mengakses:** Semua pengguna yang terdaftar
|
||||||
|
|
||||||
|
### 2. Dashboard/Home Screen
|
||||||
|
|
||||||
|
**Deskripsi:** Tampilan utama aplikasi yang menampilkan informasi dan akses cepat ke berbagai fitur
|
||||||
|
|
||||||
|
- **Fungsi:** Menyediakan ringkasan informasi desa dan akses cepat ke fitur-fitur utama
|
||||||
|
- **Siapa yang bisa mengakses:** Semua pengguna yang telah login
|
||||||
|
- **Komponen:**
|
||||||
|
- Carousel banner untuk promosi atau informasi penting
|
||||||
|
- Fitur untuk mengakses semua fitur aplikasi
|
||||||
|
- Grafik progres kegiatan
|
||||||
|
- Grafik jumlah dokumen
|
||||||
|
- Daftar kegiatan terupdate
|
||||||
|
- Daftar divisi aktif
|
||||||
|
- Daftar acara mendatang
|
||||||
|
- Diskusi terbaru
|
||||||
|
|
||||||
|
### 3. Pencarian
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mencari anggota, kegiatan dan divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Mencari anggota, kegiatan dan divisi
|
||||||
|
- **Siapa yang bisa mengakses:** Semua pengguna
|
||||||
|
|
||||||
|
### 4. Notifikasi
|
||||||
|
|
||||||
|
**Deskripsi:** Sistem notifikasi untuk memberitahu pengguna tentang aktivitas penting
|
||||||
|
|
||||||
|
- **Fungsi:** Memberitahu pengguna tentang pengumuman, komentar, atau aktivitas lainnya
|
||||||
|
- **Siapa yang bisa mengakses:** Semua pengguna
|
||||||
|
|
||||||
|
### 5. Profil
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk melihat dan mengedit informasi pribadi pengguna
|
||||||
|
|
||||||
|
- **Fungsi:** Menampilkan dan mengelola informasi akun pengguna
|
||||||
|
- **Siapa yang bisa mengakses:** Pengguna yang bersangkutan
|
||||||
|
|
||||||
|
### 6. Banner
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola banner promosi atau informasi penting di halaman utama
|
||||||
|
|
||||||
|
- **Fungsi:** Menampilkan informasi atau promosi penting di tampilan awal
|
||||||
|
- **Siapa yang bisa mengakses:** Super Admin, Wakil Super Admin
|
||||||
|
|
||||||
|
### 7. Lembaga Desa
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola berbagai lembaga dalam desa
|
||||||
|
|
||||||
|
- **Fungsi:** Mengorganisir struktur organisasi desa berdasarkan lembaga
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin
|
||||||
|
- Melihat: Super Admin
|
||||||
|
|
||||||
|
### 8. Jabatan
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola posisi atau jabatan
|
||||||
|
|
||||||
|
- **Fungsi:** Mengelola data jabatan
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Wakil Admin
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 9. Anggota
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola data pengguna
|
||||||
|
|
||||||
|
- **Fungsi:** Menyimpan dan mengelola informasi tentang pengguna
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Wakil Admin
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 10. Diskusi Umum
|
||||||
|
|
||||||
|
**Deskripsi:** Forum diskusi untuk komunikasi anggota terpilih
|
||||||
|
|
||||||
|
- **Fungsi:** Tempat berdiskusi mengenai berbagai topik yang berkaitan dengan desa
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
- Berkomentar: Pengguna terpilih
|
||||||
|
|
||||||
|
### 11. Kegiatan/Proyek
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola dan melacak proyek atau kegiatan desa
|
||||||
|
|
||||||
|
- **Fungsi:** Mengelola dan memonitor kemajuan proyek-proyek desa
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus/Membatalkan/Mengelola anggota: Super Admin, Wakil Super Admin, Admin
|
||||||
|
- Mengelola detail (file, task, link, laporan) : Super Admin, Wakil Super Admin, Admin, Anggota dari kegiatan
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
- **Status Kegiatan:**
|
||||||
|
- Segera: Proyek yang akan segera dimulai
|
||||||
|
- Dikerjakan: Proyek yang sedang dalam proses pengerjaan
|
||||||
|
- Selesai: Proyek yang telah selesai
|
||||||
|
- Batal: Proyek yang dibatalkan
|
||||||
|
|
||||||
|
### 12. Pengumuman
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk membuat, melihat, dan mengelola pengumuman desa
|
||||||
|
|
||||||
|
- **Fungsi:** Menyebarkan informasi penting kepada anggota divisi terpilih
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||||
|
- Melihat:
|
||||||
|
- Super admin: Semua pengumuman
|
||||||
|
- Wakil super admin & admin : Pengumuman sesuai lembaga desa
|
||||||
|
- Lainnya: Pengumuman yang ditujukan ke divisi mereka
|
||||||
|
|
||||||
|
### 13. Divisi
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola data desa berdasarkan divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Mengorganisir tugas-tugas berdasarkan divisi-divisi tertentu
|
||||||
|
- **Catatan:** Anggota divisi (role : Wakil Admin dan User) yg diangkat menjadi "Admin Divisi", mendapat akses khusus untuk mengelola divisi tersebut
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||||
|
- Edit Divisi / Non aktifkan Divisi tertentu / Mengelola Anggota divisi tertentu : Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||||
|
- Laporan semua divisi : Super Admin, Wakil Super Admin
|
||||||
|
- Laporan divisi tertentu : semua pengguna
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 14. Diskusi Divisi
|
||||||
|
|
||||||
|
**Deskripsi:** Forum diskusi khusus untuk masing-masing divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Tempat berdiskusi secara internal dalam divisi
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||||
|
- Memberi komentar : Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 15. Tugas Divisi
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk mengelola tugas-tugas dalam masing-masing divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Menetapkan dan melacak tugas-tugas yang harus diselesaikan oleh anggota divisi
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||||
|
- Mengelola detail (file, task, link, laporan) : Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 16. Dokumen Divisi
|
||||||
|
|
||||||
|
**Deskripsi:** Sistem manajemen dokumen untuk menyimpan dan mengelola file-file disetiap divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Menyimpan dokumen penting dalam struktur folder disetiap divisi
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
|
||||||
|
### 17. Kalender/Acara Divisi
|
||||||
|
|
||||||
|
**Deskripsi:** Fitur untuk menjadwalkan dan mengelola acara-acara desa disetiap divisi
|
||||||
|
|
||||||
|
- **Fungsi:** Menjadwalkan kegiatan dan acara penting desa disetiap divisi
|
||||||
|
- **Siapa yang bisa mengakses:**
|
||||||
|
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||||
|
- Melihat: Semua pengguna
|
||||||
|
- Riwayat: Semua pengguna
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Masalah Login
|
||||||
|
|
||||||
|
- Pastikan nomor telepon yang dimasukkan sudah benar dan terdaftar
|
||||||
|
- Pastikan koneksi internet stabil saat menerima OTP
|
||||||
|
- Jika tidak menerima OTP, coba kirim ulang setelah beberapa menit
|
||||||
|
|
||||||
|
### Tidak Bisa Mengakses Fitur Tertentu
|
||||||
|
|
||||||
|
- Pastikan peran Anda memiliki hak akses ke fitur tersebut
|
||||||
|
- Beberapa fitur hanya tersedia untuk peran tertentu (misalnya Admin ke atas)
|
||||||
|
|
||||||
|
### Lupa Password
|
||||||
|
|
||||||
|
- Aplikasi ini menggunakan sistem login OTP, jadi tidak ada password yang disimpan
|
||||||
|
- Cukup gunakan nomor telepon dan minta OTP kembali
|
||||||
|
|
||||||
|
## Dukungan dan Bantuan
|
||||||
|
|
||||||
|
Jika Anda mengalami masalah atau memiliki pertanyaan tentang penggunaan aplikasi, silakan hubungi tim pengembang aplikasi.
|
||||||
153
QWEN.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Desa+ Mobile Application - Project Overview
|
||||||
|
|
||||||
|
## Project Summary
|
||||||
|
Desa+ is a comprehensive village/desa management mobile application built with React Native and Expo. The application serves as a digital platform for village administration, community communication, and information management. It provides various features to facilitate village governance, resident communication, and administrative tasks.
|
||||||
|
|
||||||
|
## Architecture & Technology Stack
|
||||||
|
- **Framework**: React Native with Expo (SDK 53)
|
||||||
|
- **State Management**: Redux Toolkit with React-Redux
|
||||||
|
- **Navigation**: Expo Router with React Navigation
|
||||||
|
- **Backend Services**: Firebase (Authentication, Realtime Database, Cloud Messaging)
|
||||||
|
- **UI Components**: Custom-built components with React Native elements
|
||||||
|
- **Language**: TypeScript for type safety
|
||||||
|
- **Build System**: EAS (Expo Application Service) for builds and deployments
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- Announcement and village information system
|
||||||
|
- Community discussion forums
|
||||||
|
- Village event calendar
|
||||||
|
- Document management and archiving
|
||||||
|
- Project and task management
|
||||||
|
- Member and organizational structure management
|
||||||
|
- Push notifications for important updates
|
||||||
|
- Verification and authentication features
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
├── app/ # Application routes and pages (Expo Router)
|
||||||
|
│ ├── (application)/ # Main application screens
|
||||||
|
│ │ ├── announcement/
|
||||||
|
│ │ ├── banner/
|
||||||
|
│ │ ├── discussion/
|
||||||
|
│ │ ├── division/
|
||||||
|
│ │ ├── group/
|
||||||
|
│ │ ├── member/
|
||||||
|
│ │ ├── position/
|
||||||
|
│ │ ├── project/
|
||||||
|
│ │ ├── _layout.tsx
|
||||||
|
│ │ ├── edit-profile.tsx
|
||||||
|
│ │ ├── feature.tsx
|
||||||
|
│ │ ├── home.tsx
|
||||||
|
│ │ ├── notification.tsx
|
||||||
|
│ │ ├── profile.tsx
|
||||||
|
│ │ └── search.tsx
|
||||||
|
│ ├── _layout.tsx # Root layout
|
||||||
|
│ ├── index.tsx # Splash/login screen
|
||||||
|
│ ├── verification.tsx # OTP verification screen
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
│ ├── announcement/
|
||||||
|
│ ├── auth/
|
||||||
|
│ ├── banner/
|
||||||
|
│ ├── calendar/
|
||||||
|
│ ├── discussion/
|
||||||
|
│ ├── division/
|
||||||
|
│ ├── document/
|
||||||
|
│ ├── group/
|
||||||
|
│ ├── home/
|
||||||
|
│ ├── member/
|
||||||
|
│ ├── position/
|
||||||
|
│ ├── project/
|
||||||
|
│ ├── task/
|
||||||
|
│ ├── alertKonfirmasi.ts
|
||||||
|
│ ├── AppHeader.tsx
|
||||||
|
│ ├── Text.tsx
|
||||||
|
│ └── ... (many more components)
|
||||||
|
├── constants/ # Constants and styles
|
||||||
|
│ ├── Colors.ts
|
||||||
|
│ ├── ColorsStatus.ts
|
||||||
|
│ ├── ConstEnv.ts
|
||||||
|
│ ├── Headers.ts
|
||||||
|
│ ├── RoleUser.ts
|
||||||
|
│ ├── Styles.ts
|
||||||
|
│ └── ... (other constants)
|
||||||
|
├── assets/ # Static assets (images, fonts)
|
||||||
|
├── lib/ # Business logic and API utilities
|
||||||
|
├── providers/ # Context providers (AuthProvider)
|
||||||
|
├── android/ # Android native code
|
||||||
|
├── ios/ # iOS native code
|
||||||
|
├── scripts/ # Build and utility scripts
|
||||||
|
├── index.js # Entry point
|
||||||
|
├── app.config.js # Expo configuration
|
||||||
|
├── package.json # Dependencies and scripts
|
||||||
|
└── eas.json # EAS build configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
The application uses environment variables defined in a `.env` file:
|
||||||
|
- `URL_API` - API endpoint
|
||||||
|
- `URL_OTP` - OTP service endpoint
|
||||||
|
- `URL_STORAGE` - Storage endpoint
|
||||||
|
- `URL_FIREBASE_DB` - Firebase database URL
|
||||||
|
- `PASS_ENC` - Encryption password
|
||||||
|
- `WA_SERVER_TOKEN` - WhatsApp server token
|
||||||
|
- `IOS_GOOGLE_SERVICES_FILE` - Path to iOS Google services file
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Development
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the development server:
|
||||||
|
```bash
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. For platform-specific builds:
|
||||||
|
- Android: `npm run android`
|
||||||
|
- iOS: `npm run ios`
|
||||||
|
- Web: `npm run web`
|
||||||
|
|
||||||
|
### Production Builds
|
||||||
|
- Android: `npm run build:android` (uses EAS to build an app bundle)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Run tests: `npm run test`
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
- Check code quality: `npm run lint`
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
- `@react-native-firebase/*` - Firebase integration
|
||||||
|
- `@react-navigation/*` - Navigation solutions
|
||||||
|
- `@reduxjs/toolkit` - State management
|
||||||
|
- `expo-router` - File-based routing
|
||||||
|
- `react-native-gesture-handler` - Touch gesture support
|
||||||
|
- `react-native-reanimated` - Advanced animations
|
||||||
|
- `react-native-svg` - SVG rendering support
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
- Uses TypeScript for type safety
|
||||||
|
- Implements custom styling through the Styles.ts constant file
|
||||||
|
- Follows Expo's file-based routing convention
|
||||||
|
- Uses Redux Toolkit for centralized state management
|
||||||
|
- Implements custom components in the components directory
|
||||||
|
- Uses absolute imports with @/ alias (e.g., "@/components/...")
|
||||||
|
- Implements Firebase for authentication and real-time data
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- Uses EAS for building and submitting to app stores
|
||||||
|
- Supports both Android (APK and App Bundle) and iOS (TestFlight/App Store)
|
||||||
|
- Configured for internal testing, preview, and production distributions
|
||||||
|
|
||||||
|
## Special Features
|
||||||
|
- Background message handling for push notifications
|
||||||
|
- Biometric authentication support
|
||||||
|
- Image picking and media library access
|
||||||
|
- Document picker functionality
|
||||||
|
- Date/time pickers with localization
|
||||||
|
- Custom toast notifications
|
||||||
|
- Carousel components for featured content
|
||||||
|
- Data visualization with charts
|
||||||
124
README.md
@@ -1,50 +1,114 @@
|
|||||||
# Welcome to your Expo app 👋
|
# Desa+
|
||||||
|
|
||||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
Desa+ (Desa Plus) adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
|
||||||
|
|
||||||
## Get started
|
## Fitur Utama
|
||||||
|
|
||||||
1. Install dependencies
|
- 📢 Pengumuman dan informasi desa
|
||||||
|
- 💬 Forum diskusi komunitas
|
||||||
|
- 📅 Kalender kegiatan desa
|
||||||
|
- 📄 Dokumentasi dan arsip desa
|
||||||
|
- 📊 Pengelolaan proyek dan tugas desa
|
||||||
|
- 👥 Manajemen anggota dan struktur organisasi
|
||||||
|
- 📱 Notifikasi push untuk informasi penting
|
||||||
|
- 🎯 Fitur verifikasi dan otentikasi
|
||||||
|
|
||||||
|
## Teknologi yang Digunakan
|
||||||
|
|
||||||
|
- [React Native](https://reactnative.dev/) - Framework mobile cross-platform
|
||||||
|
- [Expo](https://expo.dev/) - Platform pengembangan aplikasi React Native
|
||||||
|
- [Firebase](https://firebase.google.com/) - Backend services (Authentication, Realtime Database, Cloud Messaging)
|
||||||
|
- [Redux Toolkit](https://redux-toolkit.js.org/) - State management
|
||||||
|
- [React Navigation](https://reactnavigation.org/) - Navigasi aplikasi
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/) - Type safety
|
||||||
|
|
||||||
|
## Instalasi
|
||||||
|
|
||||||
|
1. Clone repository ini
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd mobile-darmasaba
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the app
|
3. Konfigurasi environment variables
|
||||||
|
Buat file `.env` di root direktori dan tambahkan variabel berikut:
|
||||||
```bash
|
```
|
||||||
npx expo start
|
URL_API=<api-endpoint>
|
||||||
|
URL_OTP=<otp-service-endpoint>
|
||||||
|
URL_STORAGE=<storage-endpoint>
|
||||||
|
URL_FIREBASE_DB=<firebase-database-url>
|
||||||
|
PASS_ENC=<encryption-password>
|
||||||
|
WA_SERVER_TOKEN=<whatsapp-server-token>
|
||||||
|
IOS_GOOGLE_SERVICES_FILE=<path-to-ios-google-services>
|
||||||
```
|
```
|
||||||
|
|
||||||
In the output, you'll find options to open the app in a
|
4. Jalankan aplikasi
|
||||||
|
```bash
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
## Struktur Proyek
|
||||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
||||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
||||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
||||||
|
|
||||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
```
|
||||||
|
├── app/ # File-file halaman utama
|
||||||
## Get a fresh project
|
├── components/ # Komponen reusable
|
||||||
|
│ ├── announcement/ # Komponen pengumuman
|
||||||
When you're ready, run:
|
│ ├── auth/ # Komponen otentikasi
|
||||||
|
│ ├── discussion/ # Komponen forum diskusi
|
||||||
```bash
|
│ ├── document/ # Komponen dokumentasi
|
||||||
npm run reset-project
|
│ ├── project/ # Komponen pengelolaan proyek
|
||||||
|
│ └── ...
|
||||||
|
├── assets/ # Gambar dan aset statis
|
||||||
|
├── constants/ # Konstanta global
|
||||||
|
├── lib/ # Library dan utilitas
|
||||||
|
└── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
## Platform Support
|
||||||
|
|
||||||
## Learn more
|
Aplikasi ini didukung untuk:
|
||||||
|
- ✅ Android
|
||||||
|
- ✅ iOS
|
||||||
|
- ❌ Web (belum dioptimalkan)
|
||||||
|
|
||||||
To learn more about developing your project with Expo, look at the following resources:
|
## Development
|
||||||
|
|
||||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
Untuk menjalankan aplikasi di masing-masing platform:
|
||||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
|
||||||
|
|
||||||
## Join the community
|
### Android
|
||||||
|
```bash
|
||||||
|
npm run android
|
||||||
|
```
|
||||||
|
|
||||||
Join our community of developers creating universal apps.
|
### iOS
|
||||||
|
```bash
|
||||||
|
npm run ios
|
||||||
|
```
|
||||||
|
|
||||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
### Build Production
|
||||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
||||||
|
Untuk membuat build production Android:
|
||||||
|
```bash
|
||||||
|
npm run build:android
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kontribusi
|
||||||
|
|
||||||
|
1. Fork repository ini
|
||||||
|
2. Buat branch fitur baru (`git checkout -b fitur/NamaFitur`)
|
||||||
|
3. Commit perubahan Anda (`git commit -m 'Tambahkan fitur NamaFitur'`)
|
||||||
|
4. Push ke branch (`git push origin fitur/NamaFitur`)
|
||||||
|
5. Buat pull request
|
||||||
|
|
||||||
|
## Lisensi
|
||||||
|
|
||||||
|
Proyek ini dilisensikan di bawah lisensi MIT - lihat file [LICENSE](LICENSE) untuk detail selengkapnya.
|
||||||
|
|
||||||
|
## Dukungan
|
||||||
|
|
||||||
|
Jika Anda menemukan masalah atau memiliki pertanyaan, silakan buka issue di repository ini.
|
||||||
|
|||||||
77
__tests__/ErrorBoundary-test.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent } from '@testing-library/react-native';
|
||||||
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
|
// Komponen yang sengaja throw error saat render
|
||||||
|
const BrokenComponent = () => {
|
||||||
|
throw new Error('Test error boundary!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Komponen normal
|
||||||
|
const NormalComponent = () => <></>;
|
||||||
|
|
||||||
|
// Suppress React's error boundary console output selama test
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
(console.error as jest.Mock).mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ErrorBoundary', () => {
|
||||||
|
it('merender children dengan normal jika tidak ada error', () => {
|
||||||
|
// Tidak boleh throw dan tidak menampilkan teks error
|
||||||
|
const { queryByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<NormalComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(queryByText('Terjadi Kesalahan')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('menampilkan UI fallback ketika child throw error', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(getByText('Terjadi Kesalahan')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('menampilkan pesan error yang dilempar', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(getByText('Test error boundary!')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merender custom fallback jika prop fallback diberikan', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary fallback={<></>}>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
// Custom fallback fragment kosong — pastikan teks default tidak muncul
|
||||||
|
expect(() => getByText('Terjadi Kesalahan')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mereset error state saat tombol Coba Lagi ditekan', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = getByText('Coba Lagi');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
|
||||||
|
// Tekan tombol reset — hasError kembali false, BrokenComponent throw lagi
|
||||||
|
// sehingga fallback muncul kembali (membuktikan reset berjalan)
|
||||||
|
fireEvent.press(button);
|
||||||
|
expect(getByText('Terjadi Kesalahan')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -92,8 +92,8 @@ android {
|
|||||||
applicationId 'mobiledarmasaba.app'
|
applicationId 'mobiledarmasaba.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 21
|
||||||
versionName "1.0.0"
|
versionName "2.2.0"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 575 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 904 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">mobile-darmasaba</string>
|
<string name="app_name">Desa+</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="android:statusBarColor">#ffffff</item>
|
<item name="android:statusBarColor">#ffffff</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
|||||||
}
|
}
|
||||||
expoAutolinking.useExpoModules()
|
expoAutolinking.useExpoModules()
|
||||||
|
|
||||||
rootProject.name = 'mobile-darmasaba'
|
rootProject.name = 'Desa+'
|
||||||
|
|
||||||
expoAutolinking.useExpoVersionCatalog()
|
expoAutolinking.useExpoVersionCatalog()
|
||||||
|
|
||||||
|
|||||||
91
app.config.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
expo: {
|
||||||
|
name: "Desa+",
|
||||||
|
slug: "mobile-darmasaba",
|
||||||
|
version: "2.2.0", // Versi aplikasi (App Store)
|
||||||
|
jsEngine: "jsc",
|
||||||
|
orientation: "portrait",
|
||||||
|
icon: "./assets/images/logo-icon-small.png",
|
||||||
|
scheme: "myapp",
|
||||||
|
userInterfaceStyle: "automatic",
|
||||||
|
newArchEnabled: false,
|
||||||
|
ios: {
|
||||||
|
supportsTablet: true,
|
||||||
|
bundleIdentifier: "mobiledarmasaba.app",
|
||||||
|
buildNumber: "10",
|
||||||
|
infoPlist: {
|
||||||
|
ITSAppUsesNonExemptEncryption: false,
|
||||||
|
CFBundleDisplayName: "Desa+"
|
||||||
|
},
|
||||||
|
googleServicesFile: process.env.IOS_GOOGLE_SERVICES_FILE
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
package: "mobiledarmasaba.app",
|
||||||
|
versionCode: 21,
|
||||||
|
adaptiveIcon: {
|
||||||
|
foregroundImage: "./assets/images/logo-icon-small.png",
|
||||||
|
backgroundColor: "#ffffff"
|
||||||
|
},
|
||||||
|
googleServicesFile: "./google-services.json",
|
||||||
|
permissions: [
|
||||||
|
"READ_EXTERNAL_STORAGE",
|
||||||
|
"WRITE_EXTERNAL_STORAGE",
|
||||||
|
"READ_MEDIA_AUDIO"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
bundler: "metro",
|
||||||
|
output: "static",
|
||||||
|
favicon: "./assets/images/favicon.png"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
"expo-router",
|
||||||
|
[
|
||||||
|
"expo-splash-screen",
|
||||||
|
{
|
||||||
|
image: "./assets/images/logo-icon-small.png",
|
||||||
|
imageWidth: 200,
|
||||||
|
resizeMode: "contain",
|
||||||
|
backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expo-font",
|
||||||
|
"expo-image-picker",
|
||||||
|
"expo-web-browser",
|
||||||
|
"./plugins/withRemoveMediaPermissions",
|
||||||
|
[
|
||||||
|
"@react-native-firebase/app",
|
||||||
|
{
|
||||||
|
ios: {
|
||||||
|
googleServicesFile: process.env.IOS_GOOGLE_SERVICES_FILE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
experiments: {
|
||||||
|
typedRoutes: true
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
router: {},
|
||||||
|
eas: {
|
||||||
|
projectId: "cfe34fb8-da8c-4004-b5c6-29d07df75cf2"
|
||||||
|
},
|
||||||
|
URL_API: process.env.URL_API,
|
||||||
|
URL_OTP: process.env.URL_OTP,
|
||||||
|
URL_STORAGE: process.env.URL_STORAGE,
|
||||||
|
URL_FIREBASE_DB: process.env.URL_FIREBASE_DB,
|
||||||
|
PASS_ENC: process.env.PASS_ENC,
|
||||||
|
WA_SERVER_TOKEN: process.env.WA_SERVER_TOKEN,
|
||||||
|
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
|
||||||
|
FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
|
||||||
|
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
|
||||||
|
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
|
||||||
|
URL_MONITORING: process.env.URL_MONITORING,
|
||||||
|
KEY_API_MONITORING: process.env.KEY_API_MONITORING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import HeaderRightAnnouncementList from "@/components/announcement/headerAnnouncementList";
|
import HeaderRightAnnouncementList from "@/components/announcement/headerAnnouncementList";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import Styles from "@/constants/Styles";
|
||||||
import HeaderDiscussionGeneral from "@/components/discussion_general/headerDiscussionGeneral";
|
import HeaderDiscussionGeneral from "@/components/discussion_general/headerDiscussionGeneral";
|
||||||
import HeaderRightDivisionList from "@/components/division/headerDivisionList";
|
import HeaderRightDivisionList from "@/components/division/headerDivisionList";
|
||||||
import HeaderRightGroupList from "@/components/group/headerGroupList";
|
import HeaderRightGroupList from "@/components/group/headerGroupList";
|
||||||
@@ -8,25 +9,107 @@ import HeaderRightPositionList from "@/components/position/headerRightPositionLi
|
|||||||
import HeaderRightProjectList from "@/components/project/headerProjectList";
|
import HeaderRightProjectList from "@/components/project/headerProjectList";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import ToastCustom from "@/components/toastCustom";
|
import ToastCustom from "@/components/toastCustom";
|
||||||
import { Headers } from "@/constants/Headers";
|
import ModalUpdateMaintenance from "@/components/ModalUpdateMaintenance";
|
||||||
import { apiReadOneNotification } from "@/lib/api";
|
import { apiGetVersion, apiReadOneNotification } from "@/lib/api";
|
||||||
import { pushToPage } from "@/lib/pushToPage";
|
import { pushToPage } from "@/lib/pushToPage";
|
||||||
import store from "@/lib/store";
|
import store from "@/lib/store";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import firebase from '@react-native-firebase/app';
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { Redirect, router, Stack } from "expo-router";
|
import Constants from "expo-constants";
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { getApp } from "@react-native-firebase/app";
|
||||||
import { useEffect } from "react";
|
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
|
||||||
import { Easing, Notifier } from 'react-native-notifier';
|
import { Redirect, router, Stack, usePathname } from "expo-router";
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Easing, Notifier, NotifierComponents } from 'react-native-notifier';
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const { token, decryptToken, isLoading } = useAuthSession()
|
const { token, decryptToken, isLoading } = useAuthSession()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const [modalUpdateMaintenance, setModalUpdateMaintenance] = useState(false)
|
||||||
|
const [modalType, setModalType] = useState<'update' | 'maintenance'>('update')
|
||||||
|
const [isForceUpdate, setIsForceUpdate] = useState(false)
|
||||||
|
const [updateMessage, setUpdateMessage] = useState('')
|
||||||
|
|
||||||
async function handleReadNotification(id: string, category: string, idContent: string) {
|
const currentVersion = Constants.expoConfig?.version ?? '0.0.0'
|
||||||
|
|
||||||
|
const compareVersions = (v1: string, v2: string) => {
|
||||||
|
const parts1 = v1.split('.').map(Number);
|
||||||
|
const parts2 = v2.split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||||
|
const p1 = parts1[i] || 0;
|
||||||
|
const p2 = parts2[i] || 0;
|
||||||
|
if (p1 < p2) return -1;
|
||||||
|
if (p1 > p2) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkVersion = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiGetVersion();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const maintenance = response.data.find((item: any) => item.id === 'mobile_maintenance')?.value === 'true';
|
||||||
|
const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value || '0.0.0';
|
||||||
|
const minVersion = response.data.find((item: any) => item.id === 'mobile_minimum_version')?.value || '0.0.0';
|
||||||
|
const message = response.data.find((item: any) => item.id === 'mobile_message_update')?.value || '';
|
||||||
|
|
||||||
|
if (maintenance) {
|
||||||
|
setModalType('maintenance');
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
setIsForceUpdate(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compareVersions(currentVersion, minVersion) === -1) {
|
||||||
|
setModalType('update');
|
||||||
|
setIsForceUpdate(true);
|
||||||
|
setUpdateMessage(message);
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
} else if (compareVersions(currentVersion, latestVersion) === -1) {
|
||||||
|
// Check if this soft update version was already dismissed
|
||||||
|
const dismissedVersion = await AsyncStorage.getItem('dismissed_update_version');
|
||||||
|
if (dismissedVersion !== latestVersion) {
|
||||||
|
setModalType('update');
|
||||||
|
setIsForceUpdate(false);
|
||||||
|
setUpdateMessage(message);
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check version:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkVersion();
|
||||||
|
}, [currentVersion]);
|
||||||
|
|
||||||
|
const handleDismissUpdate = async () => {
|
||||||
|
if (!isForceUpdate) {
|
||||||
|
try {
|
||||||
|
const response = await apiGetVersion();
|
||||||
|
const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value;
|
||||||
|
if (latestVersion) {
|
||||||
|
await AsyncStorage.setItem('dismissed_update_version', latestVersion);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
setModalUpdateMaintenance(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReadNotification(id: string, category: string, idContent: string, title: string) {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
if (title !== "Komentar Baru") {
|
||||||
const response = await apiReadOneNotification({ user: hasil, id: id })
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
await apiReadOneNotification({ user: hasil, id: id })
|
||||||
|
}
|
||||||
pushToPage(category, idContent)
|
pushToPage(category, idContent)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -34,25 +117,68 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = firebase.app().messaging().onMessage(async remoteMessage => {
|
const checkNavigation = async () => {
|
||||||
|
const navData = await AsyncStorage.getItem('navigateOnOpen');
|
||||||
|
if (navData) {
|
||||||
|
const { screen, content } = JSON.parse(navData);
|
||||||
|
await AsyncStorage.removeItem('navigateOnOpen'); // reset
|
||||||
|
pushToPage(screen, content)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkNavigation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mess = getMessaging(getApp());
|
||||||
|
|
||||||
|
const unsubscribe = onMessage(mess, async remoteMessage => {
|
||||||
const id = remoteMessage?.data?.id;
|
const id = remoteMessage?.data?.id;
|
||||||
const category = remoteMessage?.data?.category;
|
const category = remoteMessage?.data?.category;
|
||||||
const content = remoteMessage?.data?.content;
|
const content = remoteMessage?.data?.content;
|
||||||
if (remoteMessage.notification != undefined && remoteMessage.notification.title != undefined && remoteMessage.notification.body != undefined) {
|
const title = remoteMessage?.notification?.title;
|
||||||
Notifier.showNotification({
|
|
||||||
title: remoteMessage.notification?.title,
|
if (remoteMessage.notification?.title && remoteMessage.notification?.body) {
|
||||||
description: remoteMessage.notification?.body,
|
if (category === 'discussion-general' && pathname === '/discussion/' + content) {
|
||||||
duration: 3000,
|
return null;
|
||||||
animationDuration: 300,
|
} else if (pathname !== `/${category}/${content}`) {
|
||||||
showEasing: Easing.ease,
|
Notifier.showNotification({
|
||||||
onPress: () => handleReadNotification(String(id), String(category), String(content)),
|
title: title,
|
||||||
hideOnPress: true,
|
description: String(remoteMessage.notification?.body),
|
||||||
});
|
duration: 3000,
|
||||||
|
animationDuration: 300,
|
||||||
|
showEasing: Easing.ease,
|
||||||
|
onPress: () => handleReadNotification(String(id), String(category), String(content), String(title)),
|
||||||
|
hideOnPress: true,
|
||||||
|
Component: NotifierComponents.Notification,
|
||||||
|
componentProps: {
|
||||||
|
containerStyle: [
|
||||||
|
Styles.shadowBox,
|
||||||
|
{
|
||||||
|
backgroundColor: colors.modalBackground,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
marginTop: 10,
|
||||||
|
padding: 15,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
titleStyle: {
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
descriptionStyle: {
|
||||||
|
color: colors.dimmed,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, [pathname]);
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -65,65 +191,126 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Stack screenOptions={Headers.shadow} >
|
<Stack screenOptions={{
|
||||||
|
headerShown: true,
|
||||||
|
animation: "slide_from_right",
|
||||||
|
animationTypeForReplace: "pop",
|
||||||
|
fullScreenGestureEnabled: true,
|
||||||
|
gestureEnabled: true,
|
||||||
|
contentStyle: { backgroundColor: colors.header },
|
||||||
|
}} >
|
||||||
<Stack.Screen name="home" options={{ title: 'Home' }} />
|
<Stack.Screen name="home" options={{ title: 'Home' }} />
|
||||||
<Stack.Screen name="feature" options={{ title: 'Fitur' }} />
|
<Stack.Screen name="feature" options={{ title: 'Fitur' }} />
|
||||||
<Stack.Screen name="search" options={{ title: 'Pencarian' }} />
|
<Stack.Screen name="search" options={{ title: 'Pencarian' }} />
|
||||||
<Stack.Screen name="notification" options={{
|
<Stack.Screen name="notification" options={{
|
||||||
title: 'Notifikasi',
|
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Notifikasi',
|
headerTitle: 'Notifikasi',
|
||||||
headerTitleAlign: 'center'
|
headerTitleAlign: 'center',
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Notifikasi" showBack={true} onPressLeft={() => router.back()} />
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="profile" options={{ title: 'Profile' }} />
|
<Stack.Screen name="profile" options={{ title: 'Profile' }} />
|
||||||
|
<Stack.Screen name="setting/index" options={{
|
||||||
|
title: 'Pengaturan',
|
||||||
|
headerTitleAlign: 'center',
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Pengaturan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}} />
|
||||||
<Stack.Screen name="member/index" options={{
|
<Stack.Screen name="member/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Anggota',
|
title: 'Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderMemberList />
|
header: () => (
|
||||||
|
<AppHeader title="Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderMemberList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="discussion/index" options={{
|
<Stack.Screen name="discussion/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Diskusi Umum',
|
title: 'Diskusi Umum',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderDiscussionGeneral />
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Diskusi Umum"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderDiscussionGeneral />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="project/index" options={{
|
<Stack.Screen name="project/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Kegiatan',
|
title: 'Kegiatan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightProjectList />
|
header: () => (
|
||||||
|
<AppHeader title="Kegiatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightProjectList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="division/index" options={{
|
<Stack.Screen name="division/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Divisi',
|
title: 'Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightDivisionList />
|
header: () => (
|
||||||
|
<AppHeader title="Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightDivisionList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} />
|
<Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="group/index" options={{
|
<Stack.Screen name="group/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Lembaga Desa',
|
headerTitle: 'Lembaga Desa',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightGroupList />
|
header: () => (
|
||||||
|
<AppHeader title="Lembaga Desa"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightGroupList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="position/index" options={{
|
<Stack.Screen name="position/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Jabatan',
|
headerTitle: 'Jabatan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightPositionList />
|
header: () => (
|
||||||
|
<AppHeader title="Jabatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightPositionList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="announcement/index"
|
<Stack.Screen name="announcement/index"
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Pengumuman',
|
headerTitle: 'Pengumuman',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightAnnouncementList />
|
header: () => (
|
||||||
|
<AppHeader title="Pengumuman"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightAnnouncementList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="light" translucent={false} backgroundColor="black" />
|
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
|
||||||
<ToastCustom />
|
<ToastCustom />
|
||||||
|
<ModalUpdateMaintenance
|
||||||
|
visible={modalUpdateMaintenance}
|
||||||
|
type={modalType}
|
||||||
|
isForceUpdate={isForceUpdate}
|
||||||
|
customDescription={updateMessage}
|
||||||
|
onDismiss={handleDismissUpdate}
|
||||||
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,327 @@
|
|||||||
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
|
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
|
import ErrorView from "@/components/ErrorView";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
|
import { isImageFile } from "@/constants/FileExtensions";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetAnnouncementOne } from "@/lib/api";
|
import { apiGetAnnouncementOne } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Entypo, MaterialIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import { startActivityAsync } from 'expo-intent-launcher';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import * as Sharing from 'expo-sharing';
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Dimensions, SafeAreaView, ScrollView, View } from "react-native";
|
import { Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
|
import ImageViewing from 'react-native-image-viewing';
|
||||||
|
import * as mime from 'react-native-mime-types';
|
||||||
import RenderHTML from 'react-native-render-html';
|
import RenderHTML from 'react-native-render-html';
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
interface AnnouncementData {
|
||||||
id: string
|
id: string;
|
||||||
title: string
|
title: string;
|
||||||
desc: string
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileData {
|
||||||
|
id: string;
|
||||||
|
idStorage: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemberData {
|
||||||
|
group: string;
|
||||||
|
division: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: AnnouncementData;
|
||||||
|
member: Record<string, MemberData[]>;
|
||||||
|
file: FileData[];
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailAnnouncement() {
|
export default function DetailAnnouncement() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [data, setData] = useState<Props>({ id: '', title: '', desc: '' })
|
const { colors } = useTheme();
|
||||||
const [dataMember, setDataMember] = useState<any>({})
|
const [data, setData] = useState<AnnouncementData>({ id: '', title: '', desc: '' })
|
||||||
|
const [dataMember, setDataMember] = useState<Record<string, MemberData[]>>({})
|
||||||
|
const [dataFile, setDataFile] = useState<FileData[]>([])
|
||||||
const update = useSelector((state: any) => state.announcementUpdate)
|
const update = useSelector((state: any) => state.announcementUpdate)
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const contentWidth = Dimensions.get('window').width
|
const contentWidth = Dimensions.get('window').width - 62
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
|
const [preview, setPreview] = useState(false)
|
||||||
|
const [chooseFile, setChooseFile] = useState<FileData>()
|
||||||
|
const [isError, setIsError] = useState(false)
|
||||||
|
|
||||||
|
const themed = {
|
||||||
|
background: { backgroundColor: colors.background },
|
||||||
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '18' },
|
||||||
|
iconBox: { backgroundColor: colors.icon + '18' },
|
||||||
|
sectionLabel: { color: colors.dimmed },
|
||||||
|
titleText: { color: colors.text },
|
||||||
|
fileChipBorder: { borderColor: colors.icon + '30' },
|
||||||
|
fileChipPressed: { backgroundColor: colors.icon + '10' },
|
||||||
|
groupSeparator: { borderTopColor: colors.icon + '18' },
|
||||||
|
divisionIconBg: { backgroundColor: colors.icon + '15' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChooseFile(item: FileData) {
|
||||||
|
setChooseFile(item)
|
||||||
|
setPreview(true)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
|
setIsError(false)
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetAnnouncementOne({ id: id, user: hasil })
|
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
||||||
setData(response.data)
|
if (response.success) {
|
||||||
setDataMember(response.member)
|
setData(response.data)
|
||||||
} catch (error) {
|
setDataMember(response.member)
|
||||||
console.error(error)
|
setDataFile(response.file)
|
||||||
|
} else {
|
||||||
|
setIsError(true)
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
setIsError(true)
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false) }, [update])
|
||||||
handleLoad(false)
|
useEffect(() => { handleLoad(true) }, [])
|
||||||
}, [update])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
function hasHtmlTags(text: string) {
|
function hasHtmlTags(text: string) {
|
||||||
const htmlRegex = /<[a-z][\s\S]*>/i;
|
return /<[a-z][\s\S]*>/i.test(text);
|
||||||
return htmlRegex.test(text);
|
}
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
handleLoad(false)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFile = async (item: FileData) => {
|
||||||
|
try {
|
||||||
|
setLoadingOpen(true);
|
||||||
|
const remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
|
||||||
|
const fileName = item.name + '.' + item.extension;
|
||||||
|
const localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
||||||
|
const mimeType = mime.lookup(fileName);
|
||||||
|
const downloadResult = await FileSystem.downloadAsync(remoteUrl, localPath);
|
||||||
|
if (downloadResult.status !== 200) {
|
||||||
|
throw new Error(`Download failed with status ${downloadResult.status}`);
|
||||||
|
}
|
||||||
|
const contentURL = await FileSystem.getContentUriAsync(downloadResult.uri);
|
||||||
|
try {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
await startActivityAsync('android.intent.action.VIEW', {
|
||||||
|
data: contentURL,
|
||||||
|
flags: 1,
|
||||||
|
type: mimeType as string,
|
||||||
|
});
|
||||||
|
} else if (Platform.OS === 'ios') {
|
||||||
|
await Sharing.shareAsync(localPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Toast.show({ type: 'error', text1: 'Tidak ada aplikasi yang dapat membuka file ini' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Toast.show({ type: 'error', text1: 'Gagal membuka file', text2: 'Silakan coba lagi nanti' });
|
||||||
|
} finally {
|
||||||
|
setLoadingOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, themed.background]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
header: () => (
|
||||||
headerTitle: 'Pengumuman',
|
<AppHeader
|
||||||
headerTitleAlign: 'center',
|
title="Pengumuman"
|
||||||
headerRight: () => entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>,
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={entityUser.role != 'user' && entityUser.role != 'coadmin'
|
||||||
|
? <HeaderRightAnnouncementDetail id={id} />
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={[Styles.p15]}>
|
showsVerticalScrollIndicator={false}
|
||||||
<View style={[Styles.wrapPaper]}>
|
style={[Styles.flex1, themed.background]}
|
||||||
{
|
refreshControl={
|
||||||
loading ?
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />
|
||||||
<View>
|
}
|
||||||
<View style={[Styles.rowOnly]}>
|
>
|
||||||
<Skeleton width={30} height={30} borderRadius={10} />
|
{isError && !loading ? (
|
||||||
<View style={[{ flex: 1 }, Styles.ph05]}>
|
<View style={Styles.mv50}>
|
||||||
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
<ErrorView />
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={Styles.announcementDetailContainer}>
|
||||||
|
|
||||||
|
{/* Title + Description */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, themed.card]}>
|
||||||
|
{loading ? (
|
||||||
|
<View style={Styles.announcementDetailSkeletonGap}>
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSkeletonIconRow]}>
|
||||||
|
<Skeleton width={38} height={38} borderRadius={10} />
|
||||||
|
<Skeleton width={60} widthType="percent" height={16} borderRadius={6} />
|
||||||
</View>
|
</View>
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
<Skeleton width={80} widthType="percent" height={10} borderRadius={6} />
|
||||||
</View>
|
</View>
|
||||||
:
|
) : (
|
||||||
<>
|
<>
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={[Styles.rowItemsCenter, Styles.announcementDetailTitleRow]}>
|
||||||
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} />
|
<View style={[Styles.sectionIconBox, Styles.announcementDetailIconBox, themed.iconBox]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{data?.title}</Text>
|
<MaterialIcons name="campaign" size={22} color={colors.icon} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.mt10]}>
|
<Text style={[Styles.textDefaultSemiBold, Styles.announcementDetailTitleText, themed.titleText]} numberOfLines={2}>
|
||||||
{
|
{data.title}
|
||||||
hasHtmlTags(data?.desc) ?
|
</Text>
|
||||||
<RenderHTML
|
|
||||||
contentWidth={contentWidth}
|
|
||||||
source={{ html: data?.desc }}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Text>{data?.desc}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
</View>
|
||||||
|
{hasHtmlTags(data.desc)
|
||||||
|
? <RenderHTML
|
||||||
|
contentWidth={contentWidth}
|
||||||
|
source={{ html: data.desc }}
|
||||||
|
baseStyle={{ color: colors.text }}
|
||||||
|
/>
|
||||||
|
: <Text style={Styles.textDefault}>{data.desc}</Text>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
{/* Files */}
|
||||||
<View style={[Styles.wrapPaper, Styles.mv15]}>
|
{dataFile.length > 0 && (
|
||||||
{
|
<View>
|
||||||
loading ?
|
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSectionLabelRow]}>
|
||||||
arrSkeleton.map((item, index) => {
|
<MaterialCommunityIcons name="paperclip" size={14} color={colors.dimmed} />
|
||||||
return (
|
<Text style={[Styles.textInformation, themed.sectionLabel]}>
|
||||||
<View key={index}>
|
Lampiran ({dataFile.length})
|
||||||
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
</Text>
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
</View>
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, Styles.announcementDetailFileCardPadding, themed.card]}>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.announcementDetailFileChipList]}>
|
||||||
|
{dataFile.map((item, index) => (
|
||||||
|
<Pressable
|
||||||
|
key={`${item.id}-${index}`}
|
||||||
|
onPress={() => isImageFile(item.extension) ? handleChooseFile(item) : openFile(item)}
|
||||||
|
style={({ pressed }) => [Styles.announcementDetailFileChip, themed.fileChipBorder,
|
||||||
|
pressed ? themed.fileChipPressed : themed.background]}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
||||||
|
size={16}
|
||||||
|
color={colors.icon}
|
||||||
|
/>
|
||||||
|
<Text style={[Styles.textInformation, Styles.announcementDetailFileChipText, themed.titleText]} numberOfLines={1}>
|
||||||
|
{item.name}.{item.extension}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
)
|
</ScrollView>
|
||||||
})
|
</View>
|
||||||
:
|
</View>
|
||||||
Object.keys(dataMember).map((v: any, i: any) => {
|
)}
|
||||||
return (
|
|
||||||
<View key={i} style={[Styles.mb05]}>
|
{/* Recipients */}
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
<View>
|
||||||
{
|
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSectionLabelRow]}>
|
||||||
dataMember[v].map((item: any, x: any) => {
|
<MaterialIcons name="groups" size={14} color={colors.dimmed} />
|
||||||
return (
|
<Text style={[Styles.textInformation, themed.sectionLabel]}>
|
||||||
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
Ditujukan Kepada
|
||||||
<Entypo name="dot-single" size={24} color="black" />
|
</Text>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
</View>
|
||||||
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, themed.card]}>
|
||||||
|
{loading ? (
|
||||||
|
<View style={Styles.announcementDetailRecipientGap}>
|
||||||
|
<Skeleton width={40} widthType="percent" height={10} borderRadius={6} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||||
|
<Skeleton width={60} widthType="percent" height={10} borderRadius={6} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
Object.keys(dataMember).map((v, i) => (
|
||||||
|
<View key={i} style={i > 0 ? [Styles.announcementDetailGroupSeparator, themed.groupSeparator] : undefined}>
|
||||||
|
<Text style={[Styles.textInformation, Styles.announcementDetailGroupLabel, themed.sectionLabel]}>
|
||||||
|
{dataMember[v]?.[0].group}
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
{dataMember[v].map((item, x) => (
|
||||||
|
<View key={x} style={[Styles.rowItemsCenter, Styles.announcementDetailDivisionRow]}>
|
||||||
|
<View style={[Styles.announcementDetailDivisionIconCircle, themed.divisionIconBg]}>
|
||||||
|
<MaterialIcons name="group" size={14} color={colors.icon} />
|
||||||
</View>
|
</View>
|
||||||
)
|
<Text style={[Styles.textDefault, Styles.flex1, themed.titleText]}>
|
||||||
})
|
{item.division}
|
||||||
}
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
))
|
||||||
})
|
)}
|
||||||
}
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<ImageViewing
|
||||||
|
images={[{ uri: `${ConstEnv.url_storage}/files/${chooseFile?.idStorage}` }]}
|
||||||
|
imageIndex={0}
|
||||||
|
visible={preview}
|
||||||
|
onRequestClose={() => setPreview(false)}
|
||||||
|
doubleTapToZoomEnabled
|
||||||
|
HeaderComponent={() => (
|
||||||
|
<View style={Styles.headerModalViewImg}>
|
||||||
|
<Pressable onPress={() => setPreview(false)} accessibilityRole="button">
|
||||||
|
<Text style={[Styles.textWhite, Styles.font26]}>✕</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => chooseFile && openFile(chooseFile)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
disabled={loadingOpen}
|
||||||
|
>
|
||||||
|
<Text style={[Styles.font26, { color: loadingOpen ? 'gray' : 'white' }]}>⋯</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
FooterComponent={() => (
|
||||||
|
<View style={[Styles.pb20, Styles.ph16, Styles.alignCenter]}>
|
||||||
|
<Text style={[Styles.textWhite, Styles.font16]}>{chooseFile?.name}.{chooseFile?.extension}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,162 +1,274 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ButtonSelect from "@/components/buttonSelect";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
||||||
import { apiCreateAnnouncement } from "@/lib/api";
|
import { apiCreateAnnouncement } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Entypo } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateAnnouncement() {
|
export default function CreateAnnouncement() {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.announcementUpdate)
|
const update = useSelector((state: any) => state.announcementUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const [disableBtn, setDisableBtn] = useState(true);
|
const [disableBtn, setDisableBtn] = useState(true);
|
||||||
const [modalDivisi, setModalDivisi] = useState(false);
|
const [modalDivisi, setModalDivisi] = useState(false);
|
||||||
const [divisionMember, setDivisionMember] = useState<any>([]);
|
const [divisionMember, setDivisionMember] = useState<any[]>([])
|
||||||
const [dataForm, setDataForm] = useState({
|
const [loading, setLoading] = useState(false)
|
||||||
title: "",
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
desc: "",
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
});
|
const [indexDelFile, setIndexDelFile] = useState<number>(0)
|
||||||
const [error, setError] = useState({
|
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
||||||
title: false,
|
const [error, setError] = useState({ title: false, desc: false });
|
||||||
desc: false,
|
|
||||||
});
|
const totalDivisi = divisionMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
||||||
|
|
||||||
function validationForm(cat: string, val: any) {
|
function validationForm(cat: string, val: any) {
|
||||||
if (cat == "title") {
|
if (cat === "title") {
|
||||||
setDataForm({ ...dataForm, title: val });
|
setDataForm({ ...dataForm, title: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, title: val === "" || val === "null" });
|
||||||
setError({ ...error, title: true });
|
} else if (cat === "desc") {
|
||||||
} else {
|
|
||||||
setError({ ...error, title: false });
|
|
||||||
}
|
|
||||||
} else if (cat == "desc") {
|
|
||||||
setDataForm({ ...dataForm, desc: val });
|
setDataForm({ ...dataForm, desc: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, desc: val === "" || val === "null" });
|
||||||
setError({ ...error, desc: true });
|
|
||||||
} else {
|
|
||||||
setError({ ...error, desc: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (
|
const hasError = Object.values(error).some(v => v)
|
||||||
Object.values(error).some((v) => v == true) ||
|
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||||
Object.values(dataForm).some((v) => v == "")
|
setDisableBtn(hasError || hasEmpty);
|
||||||
) {
|
|
||||||
setDisableBtn(true);
|
|
||||||
} else {
|
|
||||||
setDisableBtn(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { checkForm() }, [error, dataForm]);
|
||||||
checkForm();
|
|
||||||
}, [error, dataForm]);
|
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiCreateAnnouncement({
|
const fd = new FormData()
|
||||||
data: { ...dataForm, user: hasil, groups: divisionMember },
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
});
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ user: hasil, groups: divisionMember, ...dataForm }))
|
||||||
|
const response = await apiCreateAnnouncement(fd)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateAnnouncement(!update))
|
dispatch(setUpdateAnnouncement(!update))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data' })
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Tidak dapat terhubung ke server" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickDocumentAsync = async () => {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
if (fileForm.some(f => f.name === asset.name)) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteFile(index: number) {
|
||||||
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
|
setModalFile(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
header: () => (
|
||||||
<ButtonBackHeader
|
<AppHeader
|
||||||
onPress={() => {
|
title="Tambah Pengumuman"
|
||||||
router.back();
|
showBack={true}
|
||||||
}}
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || divisionMember.length === 0 || loading}
|
||||||
|
category="create"
|
||||||
|
onPress={() => {
|
||||||
|
divisionMember.length === 0
|
||||||
|
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
||||||
|
: handleCreate()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
headerTitle: "Tambah Pengumuman",
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerRight: () => (
|
|
||||||
<ButtonSaveHeader
|
|
||||||
disable={disableBtn}
|
|
||||||
category="create"
|
|
||||||
onPress={() => {
|
|
||||||
divisionMember.length == 0
|
|
||||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
|
||||||
: handleCreate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={Styles.p15}>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul"
|
label="Judul"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Pengumuman"
|
placeholder="Judul Pengumuman"
|
||||||
required
|
required
|
||||||
error={error.title}
|
error={error.title}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Judul harus diisi"
|
errorText="Judul harus diisi"
|
||||||
onChange={(val) => validationForm("title", val)}
|
onChange={(val) => validationForm("title", val)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Pengumuman"
|
label="Pengumuman"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Deskripsi Pengumuman"
|
placeholder="Deskripsi Pengumuman"
|
||||||
required
|
required
|
||||||
error={error.desc}
|
error={error.desc}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Pengumuman harus diisi"
|
errorText="Pengumuman harus diisi"
|
||||||
onChange={(val) => validationForm("desc", val)}
|
onChange={(val) => validationForm("desc", val)}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
<ButtonSelect
|
|
||||||
value="Pilih divisi penerima pengumuman"
|
|
||||||
onPress={() => {
|
|
||||||
setModalDivisi(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
{/* File */}
|
||||||
divisionMember.length > 0
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
&&
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
<View style={[Styles.borderAll, Styles.round10, Styles.p10]}>
|
<Pressable
|
||||||
{
|
onPress={pickDocumentAsync}
|
||||||
divisionMember.map((item: { name: any; Division: any }, index: any) => {
|
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{fileForm.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
return (
|
return (
|
||||||
<View key={index}>
|
<Pressable
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
|
key={index}
|
||||||
{
|
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
|
||||||
item.Division.map((division: any, i: any) => (
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
<View key={i} style={[Styles.rowItemsCenter, Styles.mv05]}>
|
>
|
||||||
<Entypo name="dot-single" size={24} color="black" />
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
<Text style={[Styles.textDefault]}>{division.name}</Text>
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
</View>
|
</View>
|
||||||
))
|
<View style={Styles.flex1}>
|
||||||
}
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
</View>
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
</View>
|
||||||
</View>
|
)}
|
||||||
}
|
</View>
|
||||||
|
|
||||||
|
{/* Divisi Penerima */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setModalDivisi(true)}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: divisionMember.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<MaterialIcons name="groups" size={18} color={colors.tabActive} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Divisi Penerima</Text>
|
||||||
|
{divisionMember.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{divisionMember.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{totalDivisi} divisi</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{divisionMember.length > 0 && (
|
||||||
|
<View style={{ gap: 10 }}>
|
||||||
|
{divisionMember.map((item: any, index: number) => (
|
||||||
|
<View key={index}>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<View style={{ gap: 6 }}>
|
||||||
|
{item.Division.map((division: any, i: number) => (
|
||||||
|
<View key={i} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18', width: 28, height: 28, borderRadius: 8 }]}>
|
||||||
|
<MaterialIcons name="group" size={14} color={colors.tabActive} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{division.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
@@ -171,19 +283,16 @@ export default function CreateAnnouncement() {
|
|||||||
setModalDivisi(false)
|
setModalDivisi(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
textArea: {
|
|
||||||
height: 100, // Or use flex-based sizing
|
|
||||||
borderColor: 'gray',
|
|
||||||
borderWidth: 1,
|
|
||||||
padding: 10,
|
|
||||||
textAlignVertical: 'top', // Important for Android to align text at the top
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ButtonSelect from "@/components/buttonSelect";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
||||||
import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api";
|
import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Entypo } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
type GroupDivision = {
|
type GroupDivision = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
Division: {
|
Division: { id: string; name: string }[];
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditAnnouncement() {
|
export default function EditAnnouncement() {
|
||||||
@@ -30,177 +50,282 @@ export default function EditAnnouncement() {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.announcementUpdate)
|
const update = useSelector((state: any) => state.announcementUpdate)
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
|
const { colors } = useTheme();
|
||||||
const [modalDivisi, setModalDivisi] = useState(false);
|
const [modalDivisi, setModalDivisi] = useState(false);
|
||||||
const [disableBtn, setDisableBtn] = useState(true);
|
const [disableBtn, setDisableBtn] = useState(true);
|
||||||
const [dataMember, setDataMember] = useState<any>([]);
|
const [dataMember, setDataMember] = useState<GroupDivision[]>([]);
|
||||||
const [dataForm, setDataForm] = useState({
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
title: "",
|
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
|
||||||
desc: "",
|
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
|
||||||
});
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
const [error, setError] = useState({
|
const [loading, setLoading] = useState(false)
|
||||||
title: false,
|
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
||||||
desc: false,
|
const [error, setError] = useState({ title: false, desc: false });
|
||||||
});
|
|
||||||
|
const visibleOldFiles = dataFile.filter(v => !v.delete)
|
||||||
|
const totalFiles = fileForm.length + visibleOldFiles.length
|
||||||
|
const totalDivisi = dataMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetAnnouncementOne({ id: id, user: hasil });
|
const response = await apiGetAnnouncementOne({ id: id, user: hasil });
|
||||||
setDataForm(response.data);
|
setDataForm(response.data);
|
||||||
|
const arrNew: GroupDivision[] = Object.keys(response.member).map((v) => ({
|
||||||
const arrNew: GroupDivision[] = []
|
id: response.member[v][0].idGroup,
|
||||||
const coba = Object.keys(response.member).map((v: any, i: any) => {
|
name: v,
|
||||||
const newObject = {
|
Division: response.member[v].map((m: any) => ({ id: m.idDivision, name: m.division }))
|
||||||
"id": response.member[v][0].idGroup,
|
}))
|
||||||
"name": v,
|
|
||||||
"Division": response.member[v]
|
|
||||||
}
|
|
||||||
|
|
||||||
response.member[v].map((v: any, i: any) => {
|
|
||||||
newObject["Division"][i] = {
|
|
||||||
"id": v.idDivision,
|
|
||||||
"name": v.division
|
|
||||||
}
|
|
||||||
})
|
|
||||||
arrNew.push(newObject)
|
|
||||||
})
|
|
||||||
setDataMember(arrNew);
|
setDataMember(arrNew);
|
||||||
|
setDataFile(response.file);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad() }, []);
|
||||||
handleLoad();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function validationForm(cat: string, val: any) {
|
function validationForm(cat: string, val: any) {
|
||||||
if (cat == "title") {
|
if (cat === "title") {
|
||||||
setDataForm({ ...dataForm, title: val });
|
setDataForm({ ...dataForm, title: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, title: val === "" || val === "null" });
|
||||||
setError({ ...error, title: true });
|
} else if (cat === "desc") {
|
||||||
} else {
|
|
||||||
setError({ ...error, title: false });
|
|
||||||
}
|
|
||||||
} else if (cat == "desc") {
|
|
||||||
setDataForm({ ...dataForm, desc: val });
|
setDataForm({ ...dataForm, desc: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, desc: val === "" || val === "null" });
|
||||||
setError({ ...error, desc: true });
|
|
||||||
} else {
|
|
||||||
setError({ ...error, desc: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (
|
const hasError = Object.values(error).some(v => v)
|
||||||
Object.values(error).some((v) => v == true) ||
|
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||||
Object.values(dataForm).some((v) => v == "")
|
setDisableBtn(hasError || hasEmpty);
|
||||||
) {
|
|
||||||
setDisableBtn(true);
|
|
||||||
} else {
|
|
||||||
setDisableBtn(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { checkForm() }, [error, dataForm]);
|
||||||
checkForm();
|
|
||||||
}, [error, dataForm]);
|
|
||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiEditAnnouncement({
|
const fd = new FormData()
|
||||||
...dataForm, user: hasil, groups: dataMember,
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
}, id);
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ ...dataForm, user: hasil, groups: dataMember, oldFile: dataFile }))
|
||||||
|
const response = await apiEditAnnouncement(fd, id);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateAnnouncement(!update))
|
dispatch(setUpdateAnnouncement(!update))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data' })
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: 'Gagal mengubah data' })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengubah data" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickDocumentAsync = async () => {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
const isDup = fileForm.some(f => f.name === asset.name) ||
|
||||||
|
visibleOldFiles.some(f => `${f.name}.${f.extension}` === asset.name)
|
||||||
|
if (isDup) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
|
||||||
|
if (cat === "newFile") {
|
||||||
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
|
} else {
|
||||||
|
setDataFile(prev => prev.map(item => item.id === index ? { ...item, delete: true } : item))
|
||||||
|
}
|
||||||
|
setModalFile(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
header: () => (
|
||||||
<ButtonBackHeader
|
<AppHeader
|
||||||
onPress={() => {
|
title="Edit Pengumuman"
|
||||||
router.back();
|
showBack={true}
|
||||||
}}
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || dataMember.length === 0 || loading}
|
||||||
|
category="update"
|
||||||
|
onPress={() => {
|
||||||
|
dataMember.length === 0
|
||||||
|
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
||||||
|
: handleEdit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
headerTitle: "Edit Pengumuman",
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerRight: () => (
|
|
||||||
<ButtonSaveHeader
|
|
||||||
disable={disableBtn}
|
|
||||||
category="update"
|
|
||||||
onPress={() => {
|
|
||||||
dataMember.length == 0
|
|
||||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
|
||||||
: handleEdit();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={Styles.p15}>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul"
|
label="Judul"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Pengumuman"
|
placeholder="Judul Pengumuman"
|
||||||
required
|
required
|
||||||
error={error.title}
|
error={error.title}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Judul harus diisi"
|
errorText="Judul harus diisi"
|
||||||
onChange={(val) => validationForm("title", val)}
|
onChange={(val) => validationForm("title", val)}
|
||||||
value={dataForm.title}
|
value={dataForm.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Pengumuman"
|
label="Pengumuman"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Deskripsi Pengumuman"
|
placeholder="Deskripsi Pengumuman"
|
||||||
required
|
required
|
||||||
error={error.desc}
|
error={error.desc}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Pengumuman harus diisi"
|
errorText="Pengumuman harus diisi"
|
||||||
onChange={(val) => validationForm("desc", val)}
|
onChange={(val) => validationForm("desc", val)}
|
||||||
value={dataForm.desc}
|
value={dataForm.desc}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
<ButtonSelect
|
|
||||||
value="Pilih divisi penerima pengumuman"
|
{/* File */}
|
||||||
onPress={() => {
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
setModalDivisi(true)
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
}}
|
<Pressable
|
||||||
/>
|
onPress={pickDocumentAsync}
|
||||||
{
|
style={[Styles.sectionActionRow, { marginBottom: totalFiles > 0 ? 12 : 0 }]}
|
||||||
dataMember.length > 0
|
>
|
||||||
&&
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
<View style={[Styles.borderAll, Styles.round10, Styles.p10]}>
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
{
|
</View>
|
||||||
dataMember.map((item: { name: any; Division: any }, index: any) => {
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{totalFiles === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{visibleOldFiles.map((item, index) => {
|
||||||
|
const ext = item.extension.toLowerCase()
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
return (
|
return (
|
||||||
<View key={index}>
|
<Pressable
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
|
key={`old-${index}`}
|
||||||
{
|
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
||||||
item.Division.map((division: any, i: any) => (
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
<View key={i} style={[Styles.rowItemsCenter, Styles.mv05]}>
|
>
|
||||||
<Entypo name="dot-single" size={24} color="black" />
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
<Text style={[Styles.textDefault]}>{division.name}</Text>
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
</View>
|
</View>
|
||||||
))
|
<View style={Styles.flex1}>
|
||||||
}
|
<Text style={Styles.textDefault} numberOfLines={1}>{item.name}</Text>
|
||||||
</View>
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
{fileForm.map((item, index) => {
|
||||||
</View>
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
}
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={`new-${index}`}
|
||||||
|
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Divisi Penerima */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setModalDivisi(true)}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: dataMember.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<MaterialIcons name="groups" size={18} color={colors.tabActive} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Divisi Penerima</Text>
|
||||||
|
{dataMember.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{dataMember.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{totalDivisi} divisi</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{dataMember.length > 0 && (
|
||||||
|
<View style={{ gap: 10 }}>
|
||||||
|
{dataMember.map((item, index) => (
|
||||||
|
<View key={index}>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<View style={{ gap: 6 }}>
|
||||||
|
{item.Division.map((division, i) => (
|
||||||
|
<View key={i} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18', width: 28, height: 28, borderRadius: 8 }]}>
|
||||||
|
<MaterialIcons name="group" size={14} color={colors.tabActive} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{division.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
@@ -216,6 +341,16 @@ export default function EditAnnouncement() {
|
|||||||
}}
|
}}
|
||||||
value={dataMember}
|
value={dataMember}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile.id, indexDelFile.cat)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,155 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetAnnouncement } from "@/lib/api";
|
import { apiGetAnnouncement } from "@/lib/api";
|
||||||
|
import { GUIDE_ANNOUNCEMENT } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string,
|
id: string
|
||||||
title: string,
|
title: string
|
||||||
desc: string,
|
desc: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Announcement() {
|
export default function Announcement() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const { colors } = useTheme();
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const update = useSelector((state: any) => state.announcementUpdate)
|
const update = useSelector((state: any) => state.announcementUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const isFirstRender = useRef(true)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('announcement')
|
||||||
const [page, setPage] = useState(1)
|
const arrSkeleton = Array.from({ length: 5 }, (_, i) => i)
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
const themed = {
|
||||||
try {
|
background: { backgroundColor: colors.background },
|
||||||
setWaiting(true)
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '18' },
|
||||||
setLoading(loading)
|
iconBox: { backgroundColor: colors.icon + '18' },
|
||||||
setPage(thisPage)
|
title: { color: colors.text },
|
||||||
const hasil = await decryptToken(String(token?.current))
|
desc: { color: colors.dimmed },
|
||||||
const response = await apiGetAnnouncement({ user: hasil, search: search, page: thisPage })
|
date: { color: colors.dimmed },
|
||||||
if (thisPage == 1) {
|
cardPressed: { backgroundColor: colors.icon + '08' },
|
||||||
setData(response.data)
|
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
|
||||||
setData([...data, ...response.data])
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
handleLoad(false, 1)
|
data,
|
||||||
}, [update])
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
useEffect(() => {
|
isFetchingNextPage,
|
||||||
handleLoad(true, 1)
|
isLoading,
|
||||||
}, [search])
|
refetch,
|
||||||
|
isRefetching
|
||||||
const loadMoreData = () => {
|
} = useInfiniteQuery({
|
||||||
if (waiting) return
|
queryKey: ['announcements', search],
|
||||||
setTimeout(() => {
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
handleLoad(false, page + 1)
|
const hasil = await decryptToken(String(token?.current))
|
||||||
}, 1000);
|
const response = await apiGetAnnouncement({ user: hasil, search, page: pageParam })
|
||||||
};
|
return response.data
|
||||||
|
},
|
||||||
const handleRefresh = async () => {
|
initialPageParam: 1,
|
||||||
setRefreshing(true)
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
handleLoad(false, 1)
|
return lastPage.length > 0 ? allPages.length + 1 : undefined
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
},
|
||||||
setRefreshing(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
|
||||||
id: data[index].id,
|
|
||||||
title: data[index].title,
|
|
||||||
desc: data[index].desc,
|
|
||||||
createdAt: data[index].createdAt,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
if (isFirstRender.current) { isFirstRender.current = false; return }
|
||||||
<View>
|
refetch()
|
||||||
<InputSearch onChange={setSearch} />
|
}, [update])
|
||||||
|
|
||||||
|
const flattenedData = useMemo(() => data?.pages.flat() || [], [data])
|
||||||
|
|
||||||
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
|
id: flattenedData[index].id,
|
||||||
|
title: flattenedData[index].title,
|
||||||
|
desc: flattenedData[index].desc,
|
||||||
|
createdAt: flattenedData[index].createdAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderSkeleton = () => (
|
||||||
|
<View style={Styles.announcementListSkeletonCard}>
|
||||||
|
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListSkeletonHeader]}>
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.announcementListSkeletonTitleRow]}>
|
||||||
|
<Skeleton width={28} height={28} borderRadius={8} />
|
||||||
|
<Skeleton width={50} widthType="percent" height={12} borderRadius={6} />
|
||||||
|
</View>
|
||||||
|
<Skeleton width={15} widthType="percent" height={10} borderRadius={6} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }, Styles.mb50]}>
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||||
{
|
<Skeleton width={80} widthType="percent" height={10} borderRadius={6} />
|
||||||
loading ?
|
</View>
|
||||||
arrSkeleton.map((item, index) => {
|
)
|
||||||
return (
|
|
||||||
<SkeletonContent key={index} />
|
const renderItem = ({ item }: { item: Props }) => (
|
||||||
)
|
<Pressable
|
||||||
})
|
onPress={() => router.push(`/announcement/${item.id}`)}
|
||||||
:
|
style={({ pressed }) => [Styles.announcementListCard, themed.card, pressed && themed.cardPressed]}
|
||||||
data.length > 0
|
>
|
||||||
?
|
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListCardHeader]}>
|
||||||
<VirtualizedList
|
<View style={[Styles.rowItemsCenter, Styles.announcementListTitleRow]}>
|
||||||
data={data}
|
<View style={[Styles.announcementListIconBox, themed.iconBox]}>
|
||||||
getItemCount={() => data.length}
|
<MaterialIcons name="campaign" size={16} color={colors.icon} />
|
||||||
getItem={getItem}
|
</View>
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
<Text style={[Styles.textDefaultSemiBold, Styles.announcementListTitleText, themed.title]} numberOfLines={1}>
|
||||||
return (
|
{item.title}
|
||||||
<BorderBottomItem
|
</Text>
|
||||||
key={index}
|
</View>
|
||||||
onPress={() => { router.push(`/announcement/${item.id}`) }}
|
<Text style={[Styles.textInformation, Styles.announcementListDateText, themed.date]}>
|
||||||
borderType="bottom"
|
{item.createdAt}
|
||||||
icon={
|
</Text>
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
</View>
|
||||||
<MaterialIcons name="campaign" size={25} color={'#384288'} />
|
<Text style={[Styles.textMediumNormal, Styles.announcementListDescText, themed.title]} numberOfLines={2}>
|
||||||
</View>
|
{item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
||||||
}
|
</Text>
|
||||||
title={item.title}
|
</Pressable>
|
||||||
desc={item.desc.replace(/<[^>]*>?/gm, '')}
|
)
|
||||||
rightTopInfo={item.createdAt}
|
|
||||||
/>
|
return (
|
||||||
)
|
<View style={[Styles.flex1, Styles.announcementListContainer, themed.background]}>
|
||||||
}}
|
<GuideOverlay visible={guideVisible} steps={GUIDE_ANNOUNCEMENT} onDismiss={dismissGuide} />
|
||||||
keyExtractor={(item, index) => String(index)}
|
<InputSearch onChange={setSearch} />
|
||||||
onEndReached={loadMoreData}
|
<View style={[Styles.flex1, Styles.announcementListInner]}>
|
||||||
onEndReachedThreshold={0.5}
|
{isLoading && !flattenedData.length ? (
|
||||||
showsVerticalScrollIndicator={false}
|
arrSkeleton.map((_, i) => (
|
||||||
refreshControl={
|
<View key={i} style={[Styles.announcementListCard, themed.card]}>
|
||||||
<RefreshControl
|
{renderSkeleton()}
|
||||||
refreshing={refreshing}
|
</View>
|
||||||
onRefresh={handleRefresh}
|
))
|
||||||
/>
|
) : flattenedData.length > 0 ? (
|
||||||
}
|
<VirtualizedList
|
||||||
|
data={flattenedData}
|
||||||
|
getItemCount={() => flattenedData.length}
|
||||||
|
getItem={getItem}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item, index) => String(item.id || index)}
|
||||||
|
onEndReached={() => { if (hasNextPage && !isFetchingNextPage) fetchNextPage() }}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={Styles.announcementListSeparator} />}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={isRefetching && !isFetchingNextPage}
|
||||||
|
onRefresh={refetch}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
:
|
}
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada pengumuman</Text>
|
/>
|
||||||
}
|
) : (
|
||||||
|
<Text style={[Styles.textDefault, Styles.textCenter, Styles.mt30, themed.desc]}>
|
||||||
|
Tidak ada pengumuman
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditBanner, apiGetBanner, apiGetBannerOne } from "@/lib/api";
|
import { apiEditBanner, apiGetBanner, apiGetBannerOne } from "@/lib/api";
|
||||||
import { setEntities } from "@/lib/bannerSlice";
|
import { setEntities } from "@/lib/bannerSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Entypo } from "@expo/vector-icons";
|
import { Entypo } from "@expo/vector-icons";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
@@ -23,6 +26,7 @@ import { useDispatch } from "react-redux";
|
|||||||
export default function EditBanner() {
|
export default function EditBanner() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { decryptToken, token } = useAuthSession();
|
const { decryptToken, token } = useAuthSession();
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const [selectedImage, setSelectedImage] = useState<
|
const [selectedImage, setSelectedImage] = useState<
|
||||||
string | undefined | { uri: string }
|
string | undefined | { uri: string }
|
||||||
@@ -30,6 +34,7 @@ export default function EditBanner() {
|
|||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [imgForm, setImgForm] = useState<any>();
|
const [imgForm, setImgForm] = useState<any>();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const pickImageAsync = async () => {
|
const pickImageAsync = async () => {
|
||||||
let result = await ImagePicker.launchImageLibraryAsync({
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
@@ -42,8 +47,6 @@ export default function EditBanner() {
|
|||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
setSelectedImage(result.assets[0].uri);
|
setSelectedImage(result.assets[0].uri);
|
||||||
setImgForm(result.assets[0]);
|
setImgForm(result.assets[0]);
|
||||||
} else {
|
|
||||||
alert("Tidak ada gambar yang dipilih");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ export default function EditBanner() {
|
|||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const data = await apiGetBannerOne({ user: hasil, id });
|
const data = await apiGetBannerOne({ user: hasil, id });
|
||||||
setSelectedImage({
|
setSelectedImage({
|
||||||
uri: `https://wibu-storage.wibudev.com/api/files/${data.data.image}`,
|
uri: `${ConstEnv.url_storage}/files/${data.data.image}`,
|
||||||
});
|
});
|
||||||
setTitle(data.data.title);
|
setTitle(data.data.title);
|
||||||
};
|
};
|
||||||
@@ -71,6 +74,7 @@ export default function EditBanner() {
|
|||||||
|
|
||||||
const handleUpdateEntity = async () => {
|
const handleUpdateEntity = async () => {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
|
|
||||||
@@ -102,32 +106,50 @@ export default function EditBanner() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Edit Banner",
|
headerTitle: "Edit Banner",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => <ButtonSaveHeader
|
// headerRight: () => <ButtonSaveHeader
|
||||||
disable={title == "" || error ? true : false}
|
// disable={title == "" || error || loading ? true : false}
|
||||||
onPress={() => { handleUpdateEntity() }}
|
// onPress={() => { handleUpdateEntity() }}
|
||||||
category="update" />,
|
// category="update" />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Edit Banner"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={title == "" || error || loading ? true : false}
|
||||||
|
onPress={() => { handleUpdateEntity() }}
|
||||||
|
category="update" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.mb15]}>
|
<View style={[Styles.mb15]}>
|
||||||
{selectedImage != undefined ? (
|
{selectedImage != undefined ? (
|
||||||
@@ -138,7 +160,7 @@ export default function EditBanner() {
|
|||||||
? selectedImage
|
? selectedImage
|
||||||
: selectedImage.uri
|
: selectedImage.uri
|
||||||
}
|
}
|
||||||
style={{ resizeMode: "contain", width: "100%", height: 100 }}
|
style={[Styles.resizeContain, Styles.w100, { height: 100 }]}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
@@ -147,11 +169,11 @@ export default function EditBanner() {
|
|||||||
style={[Styles.wrapPaper, Styles.contentItemCenter]}
|
style={[Styles.wrapPaper, Styles.contentItemCenter]}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{ justifyContent: "center", alignItems: "center" }}
|
style={[Styles.contentItemCenter]}
|
||||||
>
|
>
|
||||||
<Entypo name="image" size={50} color={"#aeaeae"} />
|
<Entypo name="image" size={50} color={"#aeaeae"} />
|
||||||
<Text style={[Styles.textInformation, Styles.mt05]}>
|
<Text style={[Styles.textInformation, Styles.mt05]}>
|
||||||
Mohon unggah gambar dalam resolusi 1535 x 450 piksel untuk
|
Mohon unggah gambar dalam resolusi 1650 x 720 pixel untuk
|
||||||
memastikan
|
memastikan
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -163,7 +185,7 @@ export default function EditBanner() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul"
|
placeholder="Judul"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={title}
|
value={title}
|
||||||
error={error}
|
error={error}
|
||||||
onChange={onValidate}
|
onChange={onValidate}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateBanner, apiGetBanner } from "@/lib/api";
|
import { apiCreateBanner, apiGetBanner } from "@/lib/api";
|
||||||
import { setEntities } from "@/lib/bannerSlice";
|
import { setEntities } from "@/lib/bannerSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Entypo } from "@expo/vector-icons";
|
import { Entypo } from "@expo/vector-icons";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
@@ -22,6 +24,7 @@ import { useDispatch } from "react-redux";
|
|||||||
|
|
||||||
export default function CreateBanner() {
|
export default function CreateBanner() {
|
||||||
const { decryptToken, token } = useAuthSession();
|
const { decryptToken, token } = useAuthSession();
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [selectedImage, setSelectedImage] = useState<string | undefined>(
|
const [selectedImage, setSelectedImage] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
@@ -29,6 +32,7 @@ export default function CreateBanner() {
|
|||||||
const [imgForm, setImgForm] = useState<any>();
|
const [imgForm, setImgForm] = useState<any>();
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const pickImageAsync = async () => {
|
const pickImageAsync = async () => {
|
||||||
let result = await ImagePicker.launchImageLibraryAsync({
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
@@ -41,8 +45,6 @@ export default function CreateBanner() {
|
|||||||
if (result.assets?.[0].uri) {
|
if (result.assets?.[0].uri) {
|
||||||
setSelectedImage(result.assets[0].uri);
|
setSelectedImage(result.assets[0].uri);
|
||||||
setImgForm(result.assets[0]);
|
setImgForm(result.assets[0]);
|
||||||
} else {
|
|
||||||
alert("Tidak ada gambar yang dipilih");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -57,80 +59,91 @@ export default function CreateBanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateEntity = async () => {
|
const handleCreateEntity = async () => {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
try {
|
||||||
const fd = new FormData();
|
setLoading(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const fd = new FormData();
|
||||||
|
|
||||||
fd.append("file", {
|
fd.append("file", {
|
||||||
uri: imgForm.uri,
|
uri: imgForm.uri,
|
||||||
type: imgForm.mimeType,
|
type: imgForm.mimeType,
|
||||||
name: imgForm.fileName,
|
name: imgForm.fileName,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
fd.append(
|
fd.append(
|
||||||
"data",
|
"data",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
title,
|
title,
|
||||||
user: hasil,
|
user: hasil,
|
||||||
})
|
})
|
||||||
);
|
|
||||||
|
|
||||||
const createdEntity = await apiCreateBanner(fd);
|
|
||||||
if (createdEntity.success) {
|
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
|
||||||
apiGetBanner({ user: hasil }).then((data) =>
|
|
||||||
dispatch(setEntities(data.data))
|
|
||||||
);
|
);
|
||||||
router.back();
|
|
||||||
} else {
|
const createdEntity = await apiCreateBanner(fd);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
if (createdEntity.success) {
|
||||||
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
||||||
|
apiGetBanner({ user: hasil }).then((data) =>
|
||||||
|
dispatch(setEntities(data.data))
|
||||||
|
);
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
|
||||||
<ButtonBackHeader
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerTitle: "Tambah Banner",
|
headerTitle: "Tambah Banner",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
header: () => (
|
||||||
<ButtonSaveHeader
|
<AppHeader
|
||||||
disable={title == "" || selectedImage == undefined || error ? true : false}
|
title="Fitur"
|
||||||
category="create"
|
showBack={true}
|
||||||
onPress={() => {
|
onPressLeft={() => router.back()}
|
||||||
handleCreateEntity();
|
right={
|
||||||
}}
|
<ButtonSaveHeader
|
||||||
|
disable={title == "" || selectedImage == undefined || error || loading ? true : false}
|
||||||
|
category="create"
|
||||||
|
onPress={() => {
|
||||||
|
handleCreateEntity();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
<View style={[Styles.mb15]}>
|
<View style={[Styles.mb15]}>
|
||||||
{selectedImage != undefined ? (
|
{selectedImage != undefined ? (
|
||||||
<Pressable onPress={pickImageAsync}>
|
<Pressable onPress={pickImageAsync}>
|
||||||
<Image
|
<Image
|
||||||
src={selectedImage}
|
src={selectedImage}
|
||||||
style={{ resizeMode: "contain", width: "100%", height: 100 }}
|
style={[Styles.resizeContain, Styles.w100, { height: 100 }]}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={pickImageAsync}
|
onPress={pickImageAsync}
|
||||||
style={[Styles.wrapPaper, Styles.contentItemCenter]}
|
style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{ justifyContent: "center", alignItems: "center" }}
|
style={[Styles.contentItemCenter]}
|
||||||
>
|
>
|
||||||
<Entypo name="image" size={50} color={"#aeaeae"} />
|
<Entypo name="image" size={50} color={colors.dimmed} />
|
||||||
<Text style={[Styles.textInformation, Styles.mt05]}>
|
<Text style={[Styles.textInformation, Styles.mt05, { color: colors.dimmed }]}>
|
||||||
Mohon unggah gambar dalam resolusi 1535 x 450 pixel untuk
|
Mohon unggah gambar dalam resolusi 1650 x 720 pixel untuk
|
||||||
memastikan
|
memastikan
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -142,7 +155,7 @@ export default function CreateBanner() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul"
|
placeholder="Judul"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
onChange={onValidate}
|
onChange={onValidate}
|
||||||
error={error}
|
error={error}
|
||||||
errorText="Judul harus diisi"
|
errorText="Judul harus diisi"
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi"
|
import AppHeader from "@/components/AppHeader"
|
||||||
|
import GuideOverlay from "@/components/GuideOverlay"
|
||||||
import HeaderRightBannerList from "@/components/banner/headerBannerList"
|
import HeaderRightBannerList from "@/components/banner/headerBannerList"
|
||||||
import BorderBottomItem from "@/components/borderBottomItem"
|
import BorderBottomItem from "@/components/borderBottomItem"
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
|
||||||
import DrawerBottom from "@/components/drawerBottom"
|
import DrawerBottom from "@/components/drawerBottom"
|
||||||
import MenuItemRow from "@/components/menuItemRow"
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||||
import ModalLoading from "@/components/modalLoading"
|
import ModalLoading from "@/components/modalLoading"
|
||||||
|
import Skeleton from "@/components/skeleton"
|
||||||
|
import Text from "@/components/Text"
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv"
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiDeleteBanner, apiGetBanner } from "@/lib/api"
|
import { apiDeleteBanner, apiGetBanner } from "@/lib/api"
|
||||||
import { setEntities } from "@/lib/bannerSlice"
|
import { setEntities } from "@/lib/bannerSlice"
|
||||||
|
import { GUIDE_BANNER } from "@/lib/guideSteps"
|
||||||
|
import { useGuide } from "@/lib/useGuide"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import * as FileSystem from 'expo-file-system'
|
import * as FileSystem from 'expo-file-system'
|
||||||
import { startActivityAsync } from 'expo-intent-launcher'
|
import { startActivityAsync } from 'expo-intent-launcher'
|
||||||
import { router, Stack } from "expo-router"
|
import { router, Stack } from "expo-router"
|
||||||
import * as Sharing from 'expo-sharing'
|
import * as Sharing from 'expo-sharing'
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
|
import ImageViewing from 'react-native-image-viewing'
|
||||||
import * as mime from 'react-native-mime-types'
|
import * as mime from 'react-native-mime-types'
|
||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
import { useDispatch, useSelector } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -30,6 +38,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function BannerList() {
|
export default function BannerList() {
|
||||||
const { decryptToken, token } = useAuthSession()
|
const { decryptToken, token } = useAuthSession()
|
||||||
|
const { colors } = useTheme()
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const entities = useSelector((state: any) => state.banner)
|
const entities = useSelector((state: any) => state.banner)
|
||||||
const [dataId, setDataId] = useState('')
|
const [dataId, setDataId] = useState('')
|
||||||
@@ -37,41 +46,61 @@ export default function BannerList() {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('banner')
|
||||||
|
const [viewImg, setViewImg] = useState(false)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const handleDeleteEntity = async () => {
|
// 1. Fetching logic with useQuery
|
||||||
try {
|
const { data: bannersRes, isLoading } = useQuery({
|
||||||
const hasil = await decryptToken(String(token?.current));
|
queryKey: ['banners'],
|
||||||
const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId);
|
queryFn: async () => {
|
||||||
if (deletedEntity.success) {
|
const hasil = await decryptToken(String(token?.current))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
|
const response = await apiGetBanner({ user: hasil })
|
||||||
apiGetBanner({ user: hasil }).then((data) =>
|
return response.data || []
|
||||||
dispatch(setEntities(data.data))
|
},
|
||||||
);
|
enabled: !!token?.current,
|
||||||
} else {
|
staleTime: 0,
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menghapus data', })
|
})
|
||||||
}
|
|
||||||
} catch (error) {
|
// Sync results with Redux
|
||||||
console.error(error)
|
useEffect(() => {
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
if (bannersRes) {
|
||||||
} finally {
|
dispatch(setEntities(bannersRes))
|
||||||
setModal(false)
|
|
||||||
}
|
}
|
||||||
|
}, [bannersRes, dispatch])
|
||||||
|
|
||||||
|
// 2. Deletion logic with useMutation
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
return await apiDeleteBanner({ user: hasil }, id)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDeleteEntity = () => {
|
||||||
|
deleteMutation.mutate(dataId)
|
||||||
|
setModal(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
await queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
apiGetBanner({ user: hasil }).then((data) =>
|
|
||||||
dispatch(setEntities(data.data))
|
|
||||||
);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFile = () => {
|
const openFile = () => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
setLoadingOpen(true)
|
setLoadingOpen(true)
|
||||||
let remoteUrl = 'https://wibu-storage.wibudev.com/api/files/' + selectFile?.image;
|
let remoteUrl = ConstEnv.url_storage + '/files/' + selectFile?.image;
|
||||||
const fileName = selectFile?.title + '.' + selectFile?.extension;
|
const fileName = selectFile?.title + '.' + selectFile?.extension;
|
||||||
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
||||||
const mimeType = mime.lookup(fileName)
|
const mimeType = mime.lookup(fileName)
|
||||||
@@ -102,51 +131,77 @@ export default function BannerList() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Banner',
|
headerTitle: 'Banner',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightBannerList />
|
// headerRight: () => <HeaderRightBannerList />
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Banner"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<HeaderRightBannerList />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GuideOverlay visible={guideVisible} steps={GUIDE_BANNER} onDismiss={dismissGuide} />
|
||||||
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
style={[Styles.h100]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
{entities.map((index: any, key: number) => (
|
{
|
||||||
<BorderBottomItem
|
isLoading ? (
|
||||||
key={key}
|
<>
|
||||||
onPress={() => {
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
setDataId(index.id)
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
setSelectFile(index)
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
setModal(true)
|
</>
|
||||||
}}
|
) :
|
||||||
borderType="all"
|
entities.length > 0 ?
|
||||||
icon={
|
entities.map((index: any, key: number) => (
|
||||||
<Image
|
<BorderBottomItem
|
||||||
source={{ uri: `https://wibu-storage.wibudev.com/api/files/${index.image}` }}
|
key={key}
|
||||||
style={[Styles.imgListBanner]}
|
onPress={() => {
|
||||||
/>
|
setDataId(index.id)
|
||||||
}
|
setSelectFile(index)
|
||||||
title={index.title}
|
setModal(true)
|
||||||
/>
|
}}
|
||||||
))}
|
borderType="all"
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
|
||||||
|
style={[Styles.imgListBanner]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={index.title}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
:
|
||||||
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
|
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||||
title="Edit"
|
title="Edit"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
@@ -154,24 +209,49 @@ export default function BannerList() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="file-eye" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="file-eye" color={colors.text} size={25} />}
|
||||||
title="Lihat File"
|
title="Lihat"
|
||||||
onPress={() => { openFile() }}
|
onPress={() => {
|
||||||
|
setModal(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
setViewImg(true);
|
||||||
|
}, 1000);
|
||||||
|
// openFile()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<Ionicons name="trash" color="black" size={25} />}
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
title="Hapus"
|
title="Hapus"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => {
|
||||||
title: 'Konfirmasi',
|
setShowDeleteModal(true)
|
||||||
desc: 'Apakah anda yakin ingin menghapus data?',
|
}, 600)
|
||||||
onPress: () => { handleDeleteEntity() }
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ImageViewing
|
||||||
|
images={[{ uri: `${ConstEnv.url_storage}/files/${selectFile?.image}` }]}
|
||||||
|
imageIndex={0}
|
||||||
|
visible={viewImg}
|
||||||
|
onRequestClose={() => setViewImg(false)}
|
||||||
|
doubleTapToZoomEnabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin menghapus data?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDeleteEntity()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
||||||
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
|
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
|
||||||
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
|
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar } from "@/lib/api";
|
import { apiDeleteDiscussionGeneralCommentar, apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar, apiUpdateDiscussionGeneralCommentar } from "@/lib/api";
|
||||||
|
import { getDB } from "@/lib/firebaseDatabase";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { firebase } from '@react-native-firebase/database';
|
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { ref } from '@react-native-firebase/database';
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { KeyboardAvoidingView, Platform, Pressable, ScrollView, View } from "react-native";
|
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -34,21 +41,44 @@ type PropsKomentar = {
|
|||||||
idUser: string
|
idUser: string
|
||||||
img: string
|
img: string
|
||||||
username: string
|
username: string
|
||||||
|
isEdited: boolean
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropsFile = {
|
||||||
|
id: string;
|
||||||
|
idStorage: string;
|
||||||
|
name: string;
|
||||||
|
extension: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailDiscussionGeneral() {
|
export default function DetailDiscussionGeneral() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const [data, setData] = useState<Props>()
|
const [data, setData] = useState<Props>()
|
||||||
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
|
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
|
||||||
const [memberDiscussion, setMemberDiscussion] = useState(false)
|
const [memberDiscussion, setMemberDiscussion] = useState(false)
|
||||||
|
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
|
||||||
const [komentar, setKomentar] = useState('')
|
const [komentar, setKomentar] = useState('')
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [loadingKomentar, setLoadingKomentar] = useState(true)
|
const [loadingKomentar, setLoadingKomentar] = useState(true)
|
||||||
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
|
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
|
||||||
const reference = firebase.app().database('https://mobile-darmasaba-default-rtdb.asia-southeast1.firebasedatabase.app').ref(`/discussion-general/${id}`);
|
const reference = ref(getDB(), `/discussion-general/${id}`);
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
|
const [detailMore, setDetailMore] = useState<any>([])
|
||||||
|
const [loadingSendKomentar, setLoadingSendKomentar] = useState(false)
|
||||||
|
const [isVisible, setVisible] = useState(false)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [selectKomentar, setSelectKomentar] = useState({
|
||||||
|
id: '',
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
const [viewEdit, setViewEdit] = useState(false)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onValueChange = reference.on('value', snapshot => {
|
const onValueChange = reference.on('value', snapshot => {
|
||||||
@@ -70,7 +100,7 @@ export default function DetailDiscussionGeneral() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota', loading: boolean) {
|
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota' | 'file', loading: boolean) {
|
||||||
try {
|
try {
|
||||||
if (cat == "detail") {
|
if (cat == "detail") {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
@@ -87,6 +117,8 @@ export default function DetailDiscussionGeneral() {
|
|||||||
setDataKomentar(response.data)
|
setDataKomentar(response.data)
|
||||||
} else if (cat == 'cek-anggota') {
|
} else if (cat == 'cek-anggota') {
|
||||||
setMemberDiscussion(response.data)
|
setMemberDiscussion(response.data)
|
||||||
|
} else if (cat == 'file') {
|
||||||
|
setFileDiscussion(response.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -101,73 +133,176 @@ export default function DetailDiscussionGeneral() {
|
|||||||
handleLoad('detail', false)
|
handleLoad('detail', false)
|
||||||
handleLoad('komentar', false)
|
handleLoad('komentar', false)
|
||||||
handleLoad('cek-anggota', false)
|
handleLoad('cek-anggota', false)
|
||||||
|
handleLoad('file', false)
|
||||||
}, [update]);
|
}, [update]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad('detail', true)
|
handleLoad('detail', true)
|
||||||
handleLoad('komentar', true)
|
handleLoad('komentar', true)
|
||||||
handleLoad('cek-anggota', true)
|
handleLoad('cek-anggota', true)
|
||||||
|
handleLoad('file', true)
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleKomentar() {
|
async function handleKomentar() {
|
||||||
try {
|
try {
|
||||||
|
setLoadingSendKomentar(true)
|
||||||
if (komentar != '') {
|
if (komentar != '') {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiSendDiscussionGeneralCommentar({ id: id, data: { desc: komentar, user: hasil } })
|
const response = await apiSendDiscussionGeneralCommentar({ id: id, data: { desc: komentar, user: hasil } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setKomentar('')
|
setKomentar('')
|
||||||
updateTrigger()
|
updateTrigger()
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoadingSendKomentar(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleEditKomentar() {
|
||||||
|
try {
|
||||||
|
setLoadingSendKomentar(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiUpdateDiscussionGeneralCommentar({ id: selectKomentar.id, data: { desc: selectKomentar.comment, user: hasil } })
|
||||||
|
if (response.success) {
|
||||||
|
updateTrigger()
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoadingSendKomentar(false)
|
||||||
|
handleViewEditKomentar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteKomentar() {
|
||||||
|
try {
|
||||||
|
setLoadingSendKomentar(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiDeleteDiscussionGeneralCommentar({ id: selectKomentar.id, data: { user: hasil } })
|
||||||
|
if (response.success) {
|
||||||
|
updateTrigger()
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoadingSendKomentar(false)
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenuKomentar(id: string, comment: string) {
|
||||||
|
setSelectKomentar({ id, comment })
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewEditKomentar() {
|
||||||
|
setVisible(false)
|
||||||
|
setViewEdit(!viewEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
handleLoad('detail', false)
|
||||||
|
handleLoad('komentar', false)
|
||||||
|
handleLoad('cek-anggota', false)
|
||||||
|
handleLoad('file', false)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Diskusi',
|
headerTitle: 'Diskusi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive !== undefined ? data.isActive : false} status={data?.status !== undefined ? data.status : 0} />,
|
// headerRight: () => <HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive !== undefined ? data.isActive : false} status={data?.status !== undefined ? data.status : 0} />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Diskusi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive !== undefined ? data.isActive : false} status={data?.status !== undefined ? data.status : 0} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={() => handleRefresh()}
|
||||||
|
tintColor={colors.icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
{
|
{
|
||||||
loading ?
|
loading ?
|
||||||
<SkeletonContent />
|
<SkeletonContent />
|
||||||
:
|
:
|
||||||
<BorderBottomItem
|
<BorderBottomItem2
|
||||||
|
dataFile={fileDiscussion}
|
||||||
descEllipsize={false}
|
descEllipsize={false}
|
||||||
width={55}
|
borderType="all"
|
||||||
borderType="bottom"
|
bgColor="white"
|
||||||
icon={
|
icon={
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
<View style={[Styles.discussionIconCircleLg, { backgroundColor: colors.icon + '20' }]}>
|
||||||
<MaterialIcons name="chat" size={25} color={'#384288'} />
|
<Feather name="message-circle" size={22} color={colors.icon} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={data?.title}
|
title={data?.title}
|
||||||
|
titleShowAll={true}
|
||||||
subtitle={
|
subtitle={
|
||||||
!data?.isActive ?
|
<View style={[Styles.discussionStatusPill, {
|
||||||
<LabelStatus category='warning' text='ARSIP' size="small" />
|
borderColor: !data?.isActive
|
||||||
:
|
? '#F59E0B'
|
||||||
<LabelStatus category={data.status == 1 ? 'success' : 'error'} text={data.status == 1 ? 'BUKA' : 'TUTUP'} size="small" />
|
: data?.status == 1 ? '#10B981' : colors.dimmed + '80',
|
||||||
|
}]}>
|
||||||
|
<Text style={[Styles.discussionStatusText, {
|
||||||
|
color: !data?.isActive
|
||||||
|
? '#F59E0B'
|
||||||
|
: data?.status == 1 ? '#10B981' : colors.dimmed,
|
||||||
|
}]}>
|
||||||
|
{!data?.isActive ? 'Arsip' : data?.status == 1 ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
}
|
}
|
||||||
rightTopInfo={data?.createdAt}
|
|
||||||
desc={data?.desc}
|
desc={data?.desc}
|
||||||
leftBottomInfo={
|
leftBottomInfo={
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
|
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||||
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{dataKomentar.length} Komentar</Text>
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataKomentar.length} Komentar</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
rightBottomInfo={
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loadingKomentar ?
|
loadingKomentar ?
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
arrSkeleton.map((item: any, i: number) => {
|
||||||
@@ -176,54 +311,174 @@ export default function DetailDiscussionGeneral() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
dataKomentar.map((item, i) => {
|
dataKomentar.map((item, i) => (
|
||||||
return (
|
<Pressable
|
||||||
<BorderBottomItem
|
key={i}
|
||||||
key={i}
|
onPress={() => {
|
||||||
width={55}
|
setDetailMore((prev: any) =>
|
||||||
borderType="bottom"
|
prev.includes(item.id)
|
||||||
icon={
|
? prev.filter((id: string) => id !== item.id)
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="xs" />
|
: [...prev, item.id]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onLongPress={() => {
|
||||||
|
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
|
||||||
|
}}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
Styles.discussionCommentCard,
|
||||||
|
{
|
||||||
|
backgroundColor: pressed ? colors.icon + '10' : colors.card,
|
||||||
|
borderColor: colors.icon + '20',
|
||||||
}
|
}
|
||||||
title={item.username}
|
]}
|
||||||
rightTopInfo={item.createdAt}
|
>
|
||||||
desc={item.comment}
|
<View style={Styles.flex1}>
|
||||||
/>
|
{/* Name + time */}
|
||||||
)
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||||
})
|
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
|
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{item.username}
|
||||||
|
</Text>
|
||||||
|
{item.isEdited && (
|
||||||
|
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>
|
||||||
|
diedit
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||||
|
{item.createdAt}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Comment text */}
|
||||||
|
<Text
|
||||||
|
style={[Styles.textDefault, { color: colors.text }]}
|
||||||
|
numberOfLines={detailMore.includes(item.id) ? 0 : 3}
|
||||||
|
>
|
||||||
|
{item.comment}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
Styles.contentItemCenter,
|
Styles.contentItemCenter,
|
||||||
Styles.w100,
|
Styles.w100,
|
||||||
{ backgroundColor: "#f4f4f4" },
|
{ backgroundColor: colors.background },
|
||||||
|
viewEdit && Styles.borderTop
|
||||||
]}>
|
]}>
|
||||||
<InputForm
|
{
|
||||||
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
|
viewEdit ?
|
||||||
type="default"
|
<>
|
||||||
round
|
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
||||||
placeholder="Kirim Komentar"
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
bg="white"
|
<Feather name="edit-3" color={colors.text} size={22} style={[Styles.mh05]} />
|
||||||
onChange={setKomentar}
|
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
|
||||||
value={komentar}
|
</View>
|
||||||
itemRight={
|
<Pressable onPress={() => handleViewEditKomentar()}>
|
||||||
<Pressable onPress={() => {
|
<MaterialIcons name="close" color={colors.text} size={22} />
|
||||||
(komentar != '' && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
</Pressable>
|
||||||
&& handleKomentar()
|
</View>
|
||||||
}}>
|
<InputForm
|
||||||
<MaterialIcons name="send" size={25} style={(komentar == '' || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
|
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
|
||||||
</Pressable>
|
type="default"
|
||||||
}
|
round
|
||||||
/>
|
placeholder="Kirim Komentar"
|
||||||
|
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
|
||||||
|
value={selectKomentar.comment}
|
||||||
|
multiline
|
||||||
|
focus={viewEdit}
|
||||||
|
itemRight={
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
(!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
||||||
|
&& handleEditKomentar()
|
||||||
|
}}
|
||||||
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || selectKomentar.comment == '' || regexOnlySpacesOrEnter.test(selectKomentar.comment) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || memberDiscussion)
|
||||||
|
?
|
||||||
|
<InputForm
|
||||||
|
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
|
||||||
|
type="default"
|
||||||
|
round
|
||||||
|
placeholder="Kirim Komentar"
|
||||||
|
onChange={setKomentar}
|
||||||
|
value={komentar}
|
||||||
|
multiline
|
||||||
|
focus={viewEdit}
|
||||||
|
itemRight={
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
(!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
||||||
|
&& handleKomentar()
|
||||||
|
}}
|
||||||
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || komentar == '' || regexOnlySpacesOrEnter.test(komentar) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<View style={[Styles.pv20, Styles.itemsCenter]}>
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
|
||||||
|
{
|
||||||
|
data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota diskusi yang dapat memberikan komentar"
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</View >
|
</View >
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||||
|
title="Edit"
|
||||||
|
onPress={() => { handleViewEditKomentar() }}
|
||||||
|
/>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => {
|
||||||
|
setVisible(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowDeleteModal(true)
|
||||||
|
}, 600)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin menghapus komentar?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDeleteKomentar()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiAddMemberDiscussionGeneral, apiGetDiscussionGeneralOne, apiGetUser } from "@/lib/api";
|
import { apiAddMemberDiscussionGeneral, apiGetDiscussionGeneralOne, apiGetUser } from "@/lib/api";
|
||||||
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -25,12 +27,14 @@ export default function AddMemberDiscussionDetail() {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
const [dataOld, setDataOld] = useState<Props[]>([])
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const [idGroup, setIdGroup] = useState('')
|
const [idGroup, setIdGroup] = useState('')
|
||||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
@@ -70,51 +74,75 @@ export default function AddMemberDiscussionDetail() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiAddMemberDiscussionGeneral({ id: id, data: { user: hasil, member: selectMember } })
|
const response = await apiAddMemberDiscussionGeneral({ id: id, data: { user: hasil, member: selectMember } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan anggota', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan anggota', })
|
||||||
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
||||||
router.back()
|
router.back()
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah Anggota Diskusi',
|
headerTitle: 'Tambah Anggota Diskusi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Anggota Diskusi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={setSearch} value={search} />
|
<InputSearch onChange={setSearch} value={search} />
|
||||||
|
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -123,9 +151,11 @@ export default function AddMemberDiscussionDetail() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -134,32 +164,32 @@ export default function AddMemberDiscussionDetail() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.id, item.name, item.img)
|
!found && onChoose(item.id, item.name, item.img)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]}>{item.name}</Text>
|
<Text style={[Styles.textDefault]}>{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>Tidak ada data</Text>
|
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
|
||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,51 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ButtonSelect from "@/components/buttonSelect";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SelectForm from "@/components/selectForm";
|
import SelectForm from "@/components/selectForm";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateDiscussionGeneral } from "@/lib/api";
|
import { apiCreateDiscussionGeneral } from "@/lib/api";
|
||||||
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
||||||
import { setMemberChoose } from "@/lib/memberChoose";
|
import { setMemberChoose } from "@/lib/memberChoose";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateDiscussionGeneral() {
|
export default function CreateDiscussionGeneral() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const entityUser = useSelector((state: any) => state.user);
|
const entityUser = useSelector((state: any) => state.user);
|
||||||
const userLogin = useSelector((state: any) => state.entities)
|
const userLogin = useSelector((state: any) => state.entities)
|
||||||
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
||||||
@@ -31,210 +56,273 @@ export default function CreateDiscussionGeneral() {
|
|||||||
const [isSelect, setSelect] = useState(false);
|
const [isSelect, setSelect] = useState(false);
|
||||||
const entitiesMember = useSelector((state: any) => state.memberChoose)
|
const entitiesMember = useSelector((state: any) => state.memberChoose)
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [dataForm, setDataForm] = useState({
|
const [loading, setLoading] = useState(false)
|
||||||
idGroup: "",
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
title: "",
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
desc: "",
|
const [indexDelFile, setIndexDelFile] = useState<number>(0)
|
||||||
});
|
const [dataForm, setDataForm] = useState({ idGroup: "", title: "", desc: "" });
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({ group: false, title: false, desc: false });
|
||||||
group: false,
|
|
||||||
title: false,
|
|
||||||
desc: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
function validationForm(cat: string, val: any, label?: string) {
|
function validationForm(cat: string, val: any, label?: string) {
|
||||||
if (cat == "group") {
|
if (cat === "group") {
|
||||||
setChooseGroup({ val, label: String(label) });
|
setChooseGroup({ val, label: String(label) });
|
||||||
dispatch(setMemberChoose([]))
|
dispatch(setMemberChoose([]))
|
||||||
setDataForm({ ...dataForm, idGroup: val });
|
setDataForm({ ...dataForm, idGroup: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, group: val === "" || val === "null" });
|
||||||
setError({ ...error, group: true });
|
} else if (cat === "title") {
|
||||||
} else {
|
|
||||||
setError({ ...error, group: false });
|
|
||||||
}
|
|
||||||
} else if (cat == "title") {
|
|
||||||
setDataForm({ ...dataForm, title: val });
|
setDataForm({ ...dataForm, title: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, title: val === "" || val === "null" });
|
||||||
setError({ ...error, title: true });
|
} else if (cat === "desc") {
|
||||||
} else {
|
|
||||||
setError({ ...error, title: false });
|
|
||||||
}
|
|
||||||
} else if (cat == "desc") {
|
|
||||||
setDataForm({ ...dataForm, desc: val });
|
setDataForm({ ...dataForm, desc: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, desc: val === "" || val === "null" });
|
||||||
setError({ ...error, desc: true });
|
|
||||||
} else {
|
|
||||||
setError({ ...error, desc: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (
|
const hasError = Object.values(error).some(v => v)
|
||||||
Object.values(error).some((v) => v == true) ||
|
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||||
Object.values(dataForm).some((v) => v == "")
|
setDisableBtn(hasError || hasEmpty);
|
||||||
) {
|
}
|
||||||
setDisableBtn(true);
|
|
||||||
|
useEffect(() => { checkForm() }, [error, dataForm]);
|
||||||
|
useEffect(() => { dispatch(setMemberChoose([])) }, [])
|
||||||
|
|
||||||
|
function handleOpenMemberPicker() {
|
||||||
|
if (entityUser.role === "supadmin" || entityUser.role === "developer") {
|
||||||
|
if (chooseGroup.val !== "") {
|
||||||
|
setSelect(true);
|
||||||
|
setValSelect("member");
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: 'Pilih Lembaga Desa terlebih dahulu' })
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setDisableBtn(false);
|
validationForm('group', userLogin.idGroup, userLogin.group);
|
||||||
|
setValChoose(userLogin.idGroup)
|
||||||
|
setSelect(true);
|
||||||
|
setValSelect("member");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const pickDocumentAsync = async () => {
|
||||||
checkForm();
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
}, [error, dataForm]);
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
if (fileForm.some(f => f.name === asset.name)) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
function deleteFile(index: number) {
|
||||||
dispatch(setMemberChoose([]))
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
}, [])
|
setModalFile(false)
|
||||||
|
|
||||||
function handleBack() {
|
|
||||||
dispatch(setMemberChoose([]))
|
|
||||||
router.back()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiCreateDiscussionGeneral({
|
const fd = new FormData()
|
||||||
data: { ...dataForm, user: hasil, member: entitiesMember },
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
})
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ ...dataForm, user: hasil, member: entitiesMember }))
|
||||||
|
const response = await apiCreateDiscussionGeneral(fd)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setMemberChoose([]))
|
dispatch(setMemberChoose([]))
|
||||||
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data' })
|
||||||
router.back()
|
router.back()
|
||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan data" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
header: () => (
|
||||||
<ButtonBackHeader
|
<AppHeader
|
||||||
onPress={() => { handleBack() }}
|
title="Tambah Diskusi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => { dispatch(setMemberChoose([])); router.back() }}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={disableBtn || entitiesMember.length === 0 || loading}
|
||||||
|
onPress={() => {
|
||||||
|
entitiesMember.length === 0
|
||||||
|
? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota' })
|
||||||
|
: handleCreate()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
headerTitle: "Tambah Diskusi",
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerRight: () => (
|
|
||||||
<ButtonSaveHeader
|
|
||||||
category="create"
|
|
||||||
disable={disableBtn}
|
|
||||||
onPress={() => {
|
|
||||||
entitiesMember.length == 0
|
|
||||||
? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', })
|
|
||||||
: handleCreate()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
{
|
|
||||||
(entityUser.role == "supadmin" ||
|
{(entityUser.role === "supadmin" || entityUser.role === "developer") && (
|
||||||
entityUser.role == "developer") && (
|
<SelectForm
|
||||||
<SelectForm
|
label="Lembaga Desa"
|
||||||
label="Lembaga Desa"
|
placeholder="Pilih Lembaga Desa"
|
||||||
placeholder="Pilih Lembaga Desa"
|
value={chooseGroup.label}
|
||||||
value={chooseGroup.label}
|
required
|
||||||
required
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => { setValChoose(chooseGroup.val); setValSelect("group"); setSelect(true) }}
|
||||||
setValChoose(chooseGroup.val);
|
error={error.group}
|
||||||
setValSelect("group");
|
errorText="Lembaga Desa tidak boleh kosong"
|
||||||
setSelect(true);
|
/>
|
||||||
}}
|
)}
|
||||||
error={error.group}
|
|
||||||
errorText="Lembaga Desa tidak boleh kosong"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul"
|
label="Judul"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul"
|
placeholder="Judul"
|
||||||
required
|
required
|
||||||
error={error.title}
|
error={error.title}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
onChange={(val) => { validationForm("title", val) }}
|
onChange={(val) => validationForm("title", val)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Diskusi"
|
label="Diskusi"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Hal yang didiskusikan"
|
placeholder="Hal yang didiskusikan"
|
||||||
required
|
required
|
||||||
error={error.desc}
|
error={error.desc}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Diskusi tidak boleh kosong"
|
errorText="Diskusi tidak boleh kosong"
|
||||||
onChange={(val) => { validationForm("desc", val) }}
|
onChange={(val) => validationForm("desc", val)}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
<ButtonSelect
|
|
||||||
value="Pilih Anggota"
|
|
||||||
onPress={() => {
|
|
||||||
if (entityUser.role == "supadmin" || entityUser.role == "developer") {
|
|
||||||
if (chooseGroup.val != "") {
|
|
||||||
setSelect(true);
|
|
||||||
setValSelect("member");
|
|
||||||
} else {
|
|
||||||
Toast.show({ type: 'small', text1: 'Pilih Lembaga Desa terlebih dahulu', })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
validationForm('group', userLogin.idGroup, userLogin.group);
|
|
||||||
setValChoose(userLogin.idGroup)
|
|
||||||
setSelect(true);
|
|
||||||
setValSelect("member");
|
|
||||||
}
|
|
||||||
|
|
||||||
}}
|
{/* File */}
|
||||||
/>
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
{
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
entitiesMember.length > 0 &&
|
<Pressable
|
||||||
<View>
|
onPress={pickDocumentAsync}
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 0 ? 12 : 0 }]}
|
||||||
<Text>Anggota</Text>
|
>
|
||||||
<Text>Total {entitiesMember.length} Anggota</Text>
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
</View>
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{fileForm.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={index}
|
||||||
|
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={[Styles.borderAll, Styles.round10, Styles.p10]}>
|
{/* Anggota */}
|
||||||
{
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
entitiesMember.map((item: { img: any; name: any; }, index: any) => {
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
return (
|
<Pressable
|
||||||
<BorderBottomItem
|
onPress={handleOpenMemberPicker}
|
||||||
key={index}
|
style={[Styles.sectionActionRow, { marginBottom: entitiesMember.length > 0 ? 12 : 0 }]}
|
||||||
borderType="bottom"
|
>
|
||||||
icon={
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
|
<MaterialIcons name="people" size={18} color={colors.tabActive} />
|
||||||
}
|
|
||||||
title={item.name}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<View style={Styles.flex1}>
|
||||||
}
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Anggota</Text>
|
||||||
|
{entitiesMember.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada anggota dipilih</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{entitiesMember.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{entitiesMember.length} anggota</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{entitiesMember.length > 0 && (
|
||||||
|
<View style={{ gap: 6 }}>
|
||||||
|
{entitiesMember.map((item: any, index: number) => (
|
||||||
|
<View key={index} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<ModalSelect
|
<ModalSelect
|
||||||
category={valSelect}
|
category={valSelect}
|
||||||
close={setSelect}
|
close={setSelect}
|
||||||
onSelect={(value) => {
|
onSelect={(value) => validationForm(valSelect, value.val, value.label)}
|
||||||
validationForm(valSelect, value.val, value.label);
|
title={valSelect === "group" ? "Lembaga Desa" : "Pilih Anggota"}
|
||||||
}}
|
|
||||||
title={valSelect == "group" ? "Lembaga Desa" : "Pilih Anggota"}
|
|
||||||
open={isSelect}
|
open={isSelect}
|
||||||
idParent={valSelect == "member" ? chooseGroup.val : ""}
|
idParent={valSelect === "member" ? chooseGroup.val : ""}
|
||||||
valChoose={valChoose}
|
valChoose={valChoose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,144 +1,276 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
|
import { apiEditDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
|
||||||
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function EditDiscussionGeneral() {
|
export default function EditDiscussionGeneral() {
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const [disableBtn, setDisableBtn] = useState(false)
|
const [disableBtn, setDisableBtn] = useState(false)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
|
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
|
||||||
|
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [dataForm, setDataForm] = useState({
|
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
||||||
title: "",
|
const [error, setError] = useState({ title: false, desc: false })
|
||||||
desc: "",
|
|
||||||
});
|
const visibleOldFiles = dataFile.filter(v => !v.delete)
|
||||||
const [error, setError] = useState({
|
const totalFiles = fileForm.length + visibleOldFiles.length
|
||||||
title: false,
|
|
||||||
desc: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionGeneralOne({
|
const response = await apiGetDiscussionGeneralOne({ id, user: hasil, cat: "detail" });
|
||||||
id: id,
|
const responseFile = await apiGetDiscussionGeneralOne({ id, user: hasil, cat: "file" });
|
||||||
user: hasil,
|
if (response.success) setDataForm(response.data);
|
||||||
cat: "detail",
|
if (responseFile.success) setDataFile(responseFile.data);
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
setDataForm(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => { handleLoad() }, []);
|
||||||
useEffect(() => {
|
|
||||||
handleLoad();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function validationForm(cat: string, val: any) {
|
function validationForm(cat: string, val: any) {
|
||||||
if (cat == "title") {
|
if (cat === "title") {
|
||||||
setDataForm({ ...dataForm, title: val });
|
setDataForm({ ...dataForm, title: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, title: val === "" || val === "null" });
|
||||||
setError({ ...error, title: true });
|
} else if (cat === "desc") {
|
||||||
} else {
|
|
||||||
setError({ ...error, title: false });
|
|
||||||
}
|
|
||||||
} else if (cat == "desc") {
|
|
||||||
setDataForm({ ...dataForm, desc: val });
|
setDataForm({ ...dataForm, desc: val });
|
||||||
if (val == "" || val == "null") {
|
setError({ ...error, desc: val === "" || val === "null" });
|
||||||
setError({ ...error, desc: true });
|
|
||||||
} else {
|
|
||||||
setError({ ...error, desc: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (Object.values(error).some((v) => v == true) == true || Object.values(dataForm).some((v) => v == "") == true) {
|
const hasError = Object.values(error).some(v => v)
|
||||||
setDisableBtn(true)
|
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||||
} else {
|
setDisableBtn(hasError || hasEmpty);
|
||||||
setDisableBtn(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { checkForm() }, [error, dataForm])
|
||||||
checkForm()
|
|
||||||
}, [error, dataForm])
|
|
||||||
|
|
||||||
|
const pickDocumentAsync = async () => {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
const isDup = fileForm.some(f => f.name === asset.name) ||
|
||||||
|
visibleOldFiles.some(f => `${f.name}.${f.extension}` === asset.name)
|
||||||
|
if (isDup) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
|
||||||
|
if (cat === "newFile") {
|
||||||
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
|
} else {
|
||||||
|
setDataFile(prev => prev.map(item => item.id === index ? { ...item, delete: true } : item))
|
||||||
|
}
|
||||||
|
setModalFile(false)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiEditDiscussionGeneral({ user: hasil, title: dataForm.title, desc: dataForm.desc }, id);
|
const fd = new FormData()
|
||||||
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ user: hasil, title: dataForm.title, desc: dataForm.desc, oldFile: dataFile }))
|
||||||
|
const response = await apiEditDiscussionGeneral(fd, id);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data' })
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: 'Gagal mengubah data' })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengubah data" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
header: () => (
|
||||||
<ButtonBackHeader
|
<AppHeader
|
||||||
onPress={() => {
|
title="Edit Diskusi"
|
||||||
router.back();
|
showBack={true}
|
||||||
}}
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || loading}
|
||||||
|
category="update"
|
||||||
|
onPress={() => handleEdit()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
headerTitle: "Edit Diskusi",
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerRight: () => (
|
|
||||||
<ButtonSaveHeader
|
|
||||||
disable={disableBtn}
|
|
||||||
category="update"
|
|
||||||
onPress={() => { handleEdit() }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
<View style={[Styles.p15]}>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={Styles.p15}>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul"
|
label="Judul"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul"
|
placeholder="Judul"
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
value={dataForm.title}
|
value={dataForm.title}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
onChange={(val) => validationForm("title", val)}
|
onChange={(val) => validationForm("title", val)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Diskusi"
|
label="Diskusi"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Hal yang didiskusikan"
|
placeholder="Hal yang didiskusikan"
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
error={error.desc}
|
error={error.desc}
|
||||||
value={dataForm.desc}
|
value={dataForm.desc}
|
||||||
errorText="Diskusi tidak boleh kosong"
|
errorText="Diskusi tidak boleh kosong"
|
||||||
onChange={(val) => validationForm("desc", val)}
|
onChange={(val) => validationForm("desc", val)}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* File */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={pickDocumentAsync}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: totalFiles > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{totalFiles === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{visibleOldFiles.map((item, index) => {
|
||||||
|
const ext = item.extension.toLowerCase()
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={`old-${index}`}
|
||||||
|
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{item.name}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={`new-${index}`}
|
||||||
|
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile.id, indexDelFile.cat)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import WrapTab from "@/components/wrapTab";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDiscussionGeneral } from "@/lib/api";
|
import { apiGetDiscussionGeneral } from "@/lib/api";
|
||||||
|
import { GUIDE_DISCUSSION } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { AntDesign, Feather } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { FlatList, Pressable, RefreshControl, View } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -27,187 +30,196 @@ type Props = {
|
|||||||
export default function Discussion() {
|
export default function Discussion() {
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const queryClient = useQueryClient()
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('discussion')
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
const {
|
||||||
try {
|
data,
|
||||||
setWaiting(true)
|
fetchNextPage,
|
||||||
setLoading(loading)
|
hasNextPage,
|
||||||
setPage(thisPage)
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['discussions', { status, search, group }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDiscussionGeneral({ user: hasil, active: status, search: search, group: String(group), page: thisPage })
|
const response = await apiGetDiscussionGeneral({
|
||||||
if (thisPage == 1) {
|
user: hasil,
|
||||||
setData(response.data)
|
active: status,
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
search: search,
|
||||||
setData([...data, ...response.data])
|
group: String(group),
|
||||||
} else {
|
page: pageParam
|
||||||
return;
|
})
|
||||||
}
|
return response;
|
||||||
setNameGroup(response.filter.name)
|
},
|
||||||
} catch (error) {
|
initialPageParam: 1,
|
||||||
console.error(error)
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} finally {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
setLoading(false)
|
},
|
||||||
setWaiting(false)
|
enabled: !!token?.current,
|
||||||
}
|
staleTime: 0,
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const flatData = useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const nameGroup = useMemo(() => {
|
||||||
|
return data?.pages[0]?.filter?.name || "";
|
||||||
|
}, [data])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [status, search, group])
|
|
||||||
|
|
||||||
|
|
||||||
const loadMoreData = () => {
|
|
||||||
if (waiting) return
|
|
||||||
setTimeout(() => {
|
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['discussions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const isOpen = (item: Props) => item.status === 1
|
||||||
id: data[index].id,
|
|
||||||
title: data[index].title,
|
const themed = {
|
||||||
desc: data[index].desc,
|
background: { backgroundColor: colors.background },
|
||||||
status: data[index].status,
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '20' },
|
||||||
total_komentar: data[index].total_komentar,
|
cardPressed: { backgroundColor: colors.icon + '10' },
|
||||||
createdAt: data[index].createdAt,
|
iconCircle: { backgroundColor: colors.icon + '20' },
|
||||||
})
|
title: { color: colors.text },
|
||||||
|
dimmed: { color: colors.dimmed },
|
||||||
|
statusOpen: { borderColor: '#10B981' as const },
|
||||||
|
statusClosed: { borderColor: colors.dimmed + '80' },
|
||||||
|
statusTextOpen: { color: '#10B981' as const },
|
||||||
|
statusTextClosed: { color: colors.dimmed },
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.flex1, themed.background]}>
|
||||||
<View>
|
<GuideOverlay visible={guideVisible} steps={GUIDE_DISCUSSION} onDismiss={dismissGuide} />
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
{/* Header controls */}
|
||||||
<ButtonTab
|
<View style={[Styles.ph15, Styles.discussionHeaderPadding]}>
|
||||||
active={status == "false" ? "false" : "true"}
|
{entityUser.role != "user" && entityUser.role != "coadmin" && (
|
||||||
value="true"
|
<WrapTab>
|
||||||
onPress={() => { setStatus("true") }}
|
<ButtonTab
|
||||||
label="Aktif"
|
active={status == "false" ? "false" : "true"}
|
||||||
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
|
value="true"
|
||||||
n={2} />
|
onPress={() => setStatus("true")}
|
||||||
<ButtonTab
|
label="Aktif"
|
||||||
active={status == "false" ? "false" : "true"}
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
||||||
value="false"
|
n={2}
|
||||||
onPress={() => { setStatus("false") }}
|
/>
|
||||||
label="Arsip"
|
<ButtonTab
|
||||||
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
|
active={status == "false" ? "false" : "true"}
|
||||||
n={2} />
|
value="false"
|
||||||
</View>
|
onPress={() => setStatus("false")}
|
||||||
|
label="Arsip"
|
||||||
|
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
||||||
|
n={2}
|
||||||
|
/>
|
||||||
|
</WrapTab>
|
||||||
|
)}
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
{
|
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
<View style={[Styles.mt10, Styles.rowOnly]}>
|
||||||
<View style={[Styles.mv05]}>
|
<Text>Filter :</Text>
|
||||||
<Text>Filter : {nameGroup}</Text>
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }]}>
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
|
||||||
return (
|
|
||||||
<SkeletonContent key={i} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
data.length > 0
|
|
||||||
?
|
|
||||||
<VirtualizedList
|
|
||||||
data={data}
|
|
||||||
getItemCount={() => data.length}
|
|
||||||
getItem={getItem}
|
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
|
||||||
return (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
onPress={() => { router.push(`/discussion/${item.id}`) }}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
|
||||||
<MaterialIcons name="chat" size={25} color={'#384288'} />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.title}
|
|
||||||
subtitle={
|
|
||||||
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
|
|
||||||
}
|
|
||||||
rightTopInfo={item.createdAt}
|
|
||||||
desc={item.desc}
|
|
||||||
leftBottomInfo={
|
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
|
|
||||||
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Diskusikan</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
rightBottomInfo={`${item.total_komentar} Komentar`}
|
|
||||||
|
|
||||||
/>
|
{/* List */}
|
||||||
)
|
<View style={[Styles.flex1, Styles.ph15, Styles.discussionListPadding]}>
|
||||||
}}
|
{isLoading ? (
|
||||||
keyExtractor={(item, index) => String(index)}
|
[0, 1, 2, 3, 4].map((_, i) => <SkeletonContent key={i} />)
|
||||||
onEndReached={loadMoreData}
|
) : flatData.length === 0 ? (
|
||||||
onEndReachedThreshold={0.5}
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
showsVerticalScrollIndicator={false}
|
<Feather name="message-circle" size={42} color={colors.icon + '40'} />
|
||||||
refreshControl={
|
<Text style={[Styles.mt10, Styles.discussionEmptyText, themed.dimmed]}>
|
||||||
<RefreshControl
|
Tidak ada diskusi
|
||||||
refreshing={refreshing}
|
</Text>
|
||||||
onRefresh={handleRefresh}
|
</View>
|
||||||
/>
|
) : (
|
||||||
}
|
<FlatList
|
||||||
|
data={flatData}
|
||||||
|
keyExtractor={(_, i) => String(i)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onEndReached={() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
||||||
|
}}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
// data.map((item: any, i: number) => {
|
}
|
||||||
// return (
|
ItemSeparatorComponent={() => <View style={Styles.discussionSeparator} />}
|
||||||
// <BorderBottomItem
|
renderItem={({ item }: { item: Props }) => (
|
||||||
// key={i}
|
<Pressable
|
||||||
// onPress={() => { router.push(`/discussion/${item.id}`) }}
|
onPress={() => router.push(`/discussion/${item.id}`)}
|
||||||
// borderType="bottom"
|
style={({ pressed }) => [
|
||||||
// icon={
|
Styles.discussionCard,
|
||||||
// <View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
themed.card,
|
||||||
// <MaterialIcons name="chat" size={25} color={'#384288'} />
|
pressed && themed.cardPressed,
|
||||||
// </View>
|
]}
|
||||||
// }
|
>
|
||||||
// title={item.title}
|
{/* Top row: icon + title + status badge */}
|
||||||
// subtitle={
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||||
// status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
|
{/* Discussion icon */}
|
||||||
// }
|
<View style={[Styles.discussionIconCircle, themed.iconCircle]}>
|
||||||
// rightTopInfo={item.createdAt}
|
<Feather name="message-circle" size={20} color={colors.icon} />
|
||||||
// desc={item.desc}
|
</View>
|
||||||
// leftBottomInfo={
|
|
||||||
// <View style={[Styles.rowItemsCenter]}>
|
|
||||||
// <Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
|
|
||||||
// <Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Diskusikan</Text>
|
|
||||||
// </View>
|
|
||||||
// }
|
|
||||||
// rightBottomInfo={`${item.total_komentar} Komentar`}
|
|
||||||
|
|
||||||
// />
|
{/* Title + status badge */}
|
||||||
// )
|
<View style={[Styles.flex1, Styles.discussionTitleCol]}>
|
||||||
// })
|
<Text style={[Styles.textDefaultSemiBold, themed.title]} numberOfLines={1}>
|
||||||
:
|
{item.title}
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
</Text>
|
||||||
}
|
{status !== "false" && (
|
||||||
|
<View style={[Styles.discussionStatusPill, isOpen(item) ? themed.statusOpen : themed.statusClosed]}>
|
||||||
|
<Text style={[Styles.discussionStatusText, isOpen(item) ? themed.statusTextOpen : themed.statusTextClosed]}>
|
||||||
|
{isOpen(item) ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{item.desc ? (
|
||||||
|
<Text
|
||||||
|
style={[Styles.textMediumNormal, Styles.discussionCardIndent, Styles.discussionDescMargin, themed.title]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Bottom row: comment count + date */}
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.rowSpaceBetween, Styles.discussionCardIndent]}>
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<Feather name="message-square" size={14} color={colors.dimmed} />
|
||||||
|
<Text style={[Styles.discussionCommentText, themed.dimmed]}>
|
||||||
|
{item.total_komentar} Komentar
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.discussionDateText, themed.dimmed]}>
|
||||||
|
{item.createdAt}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import MenuItemRow from "@/components/menuItemRow";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiDeleteMemberDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
|
import { apiDeleteMemberDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -23,8 +22,11 @@ type Props = {
|
|||||||
img: string
|
img: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SKELETON_COUNT = 5
|
||||||
|
|
||||||
export default function MemberDiscussionDetail() {
|
export default function MemberDiscussionDetail() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
@@ -32,11 +34,12 @@ export default function MemberDiscussionDetail() {
|
|||||||
const [chooseUser, setChooseUser] = useState({ idUser: '', name: '', img: '' })
|
const [chooseUser, setChooseUser] = useState({ idUser: '', name: '', img: '' })
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const canManage = entityUser.role !== "user" && entityUser.role !== "coadmin"
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(showLoadingIndicator: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(showLoadingIndicator)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat: 'anggota' })
|
const response = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat: 'anggota' })
|
||||||
setData(response.data)
|
setData(response.data)
|
||||||
@@ -47,111 +50,147 @@ export default function MemberDiscussionDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false) }, [update]);
|
||||||
handleLoad(false)
|
useEffect(() => { handleLoad(true) }, []);
|
||||||
}, [update]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleDeleteUser() {
|
async function handleDeleteUser() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
await apiDeleteMemberDiscussionGeneral({ user: hasil, idUser: chooseUser.idUser }, id)
|
await apiDeleteMemberDiscussionGeneral({ user: hasil, idUser: chooseUser.idUser }, id)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi' })
|
||||||
handleLoad(false)
|
handleLoad(false)
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengeluarkan anggota" })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
header: () => (
|
||||||
headerTitle: 'Anggota Diskusi',
|
<AppHeader
|
||||||
headerTitleAlign: 'center',
|
title="Anggota Diskusi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<Text style={[Styles.textDefault, Styles.mv05]}>{data.length} Anggota</Text>
|
|
||||||
<View style={[Styles.wrapPaper, Styles.mb100]}>
|
{/* Tombol tambah anggota */}
|
||||||
{
|
{canManage && (
|
||||||
entityUser.role != "user" && entityUser.role != "coadmin" &&
|
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.mb15,
|
||||||
<BorderBottomItem
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
onPress={() => { router.push(`/discussion/add-member/${id}`) }}
|
<Pressable
|
||||||
borderType="none"
|
onPress={() => router.push(`/discussion/add-member/${id}`)}
|
||||||
icon={
|
style={Styles.sectionActionRow}
|
||||||
<View style={[Styles.iconContent, ColorsStatus.gray]}>
|
>
|
||||||
<Feather name="user-plus" size={25} color={'#384288'} />
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name="account-plus-outline" size={18} color={colors.icon} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Tambah Anggota</Text>
|
||||||
|
</View>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full list */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||||
|
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||||
|
{!loading && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{data.length} anggota</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{loading
|
||||||
|
? Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[Styles.rowItemsCenter, Styles.ph15,
|
||||||
|
{ paddingVertical: 14, gap: 14, borderBottomWidth: i < SKELETON_COUNT - 1 ? 1 : 0, borderBottomColor: colors.icon + '14' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.userProfileExtraSmall, { backgroundColor: colors.icon + '20', borderRadius: 100 }]} />
|
||||||
|
<View style={{ height: 13, borderRadius: 6, flex: 1, backgroundColor: colors.icon + '20', maxWidth: 140 + (i % 3) * 30 }} />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
: data.length === 0
|
||||||
|
? (
|
||||||
|
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||||
|
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||||
|
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
)
|
||||||
title="Tambah Anggota"
|
: data.map((item, index) => (
|
||||||
/>
|
<Pressable
|
||||||
}
|
key={index}
|
||||||
{
|
onPress={() => { setChooseUser(item); setModal(true) }}
|
||||||
loading ?
|
style={({ pressed }) => [
|
||||||
arrSkeleton.map((item, index) => {
|
Styles.rowItemsCenter, Styles.ph15,
|
||||||
return (
|
{
|
||||||
<SkeletonTwoItem key={index} />
|
paddingVertical: 13, gap: 14,
|
||||||
)
|
borderBottomWidth: index < data.length - 1 ? 1 : 0,
|
||||||
})
|
borderBottomColor: colors.icon + '14',
|
||||||
:
|
backgroundColor: pressed ? colors.icon + '0E' : 'transparent',
|
||||||
data.map((item, index) => {
|
},
|
||||||
return (
|
]}
|
||||||
<BorderBottomItem
|
>
|
||||||
key={index}
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
borderType="bottom"
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||||
icon={
|
{item.name}
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
|
</Text>
|
||||||
}
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />
|
||||||
title={item.name}
|
</Pressable>
|
||||||
onPress={() => {
|
))
|
||||||
setChooseUser(item)
|
|
||||||
setModal(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title={chooseUser.name}>
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title={chooseUser.name}>
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="account-eye" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="account-eye" color={colors.text} size={25} />}
|
||||||
title="Lihat Profil"
|
title="Lihat Profil"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
router.push(`/member/${chooseUser.idUser}`)
|
router.push(`/member/${chooseUser.idUser}`)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{canManage && (
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="account-remove" color={colors.text} size={25} />}
|
||||||
title="Keluarkan"
|
title="Keluarkan"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => setShowDeleteModal(true), 600)
|
||||||
title: 'Konfirmasi',
|
}}
|
||||||
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
|
/>
|
||||||
onPress: () => {
|
)}
|
||||||
handleDeleteUser()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin mengeluarkan anggota?"
|
||||||
|
onConfirm={() => { setShowDeleteModal(false); handleDeleteUser() }}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import HeaderRightDiscussionList from "@/components/discussion/headerDiscussionList"
|
import HeaderRightDiscussionList from "@/components/discussion/headerDiscussionList"
|
||||||
import HeaderRightTaskList from "@/components/task/headerTaskList"
|
import HeaderRightTaskList from "@/components/task/headerTaskList"
|
||||||
import { Headers } from "@/constants/Headers"
|
import { Headers } from "@/constants/Headers"
|
||||||
@@ -9,22 +9,49 @@ export default function RootLayout() {
|
|||||||
<>
|
<>
|
||||||
<Stack screenOptions={Headers.shadow}>
|
<Stack screenOptions={Headers.shadow}>
|
||||||
<Stack.Screen name="task/index" options={{
|
<Stack.Screen name="task/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
title: 'Tugas Divisi',
|
title: 'Tugas Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightTaskList />
|
// headerRight: () => <HeaderRightTaskList />
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tugas Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<HeaderRightTaskList />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="discussion/index" options={{
|
<Stack.Screen name="discussion/index" options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
title: 'Diskusi Divisi',
|
title: 'Diskusi Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightDiscussionList />
|
// headerRight: () => <HeaderRightDiscussionList />
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Diskusi Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<HeaderRightDiscussionList />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="calendar/history"
|
<Stack.Screen name="calendar/history"
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Riwayat Acara',
|
headerTitle: 'Riwayat Acara',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Riwayat Acara"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiAddMemberCalendar, apiGetCalendarOne, apiGetDivisionMember } from "@/lib/api";
|
import { apiAddMemberCalendar, apiGetCalendarOne, apiGetDivisionMember } from "@/lib/api";
|
||||||
import { setUpdateCalendar } from "@/lib/calendarUpdate";
|
import { setUpdateCalendar } from "@/lib/calendarUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -22,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddMemberCalendarEvent() {
|
export default function AddMemberCalendarEvent() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.calendarUpdate)
|
const update = useSelector((state: any) => state.calendarUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -31,6 +34,7 @@ export default function AddMemberCalendarEvent() {
|
|||||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [idCalendar, setIdCalendar] = useState('')
|
const [idCalendar, setIdCalendar] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoadOldMember() {
|
async function handleLoadOldMember() {
|
||||||
try {
|
try {
|
||||||
@@ -78,6 +82,7 @@ export default function AddMemberCalendarEvent() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiAddMemberCalendar({ id: idCalendar, data: { user: hasil, member: selectMember } })
|
const response = await apiAddMemberCalendar({ id: idCalendar, data: { user: hasil, member: selectMember } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -87,27 +92,47 @@ export default function AddMemberCalendarEvent() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah Anggota',
|
headerTitle: 'Tambah Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -118,13 +143,13 @@ export default function AddMemberCalendarEvent() {
|
|||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -133,9 +158,12 @@ export default function AddMemberCalendarEvent() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
data.map((item: any, index: any) => {
|
data.map((item: any, index: any) => {
|
||||||
@@ -143,23 +171,22 @@ export default function AddMemberCalendarEvent() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, {borderColor: colors.icon + '20'}]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.idUser, item.name, item.img)
|
!found && onChoose(item.idUser, item.name, item.img)
|
||||||
onChoose(item.idUser, item.name, item.img)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10, { width: '80%' }]}>
|
<View style={[Styles.ml10, { width: '80%' }]}>
|
||||||
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textDefault]}>{item.name}</Text>
|
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textDefault]}>{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
||||||
import { InputDate } from "@/components/inputDate"
|
import { InputDate } from "@/components/inputDate"
|
||||||
import { InputForm } from "@/components/inputForm"
|
import { InputForm } from "@/components/inputForm"
|
||||||
@@ -9,18 +9,23 @@ import { valueTypeEventRepeat } from "@/constants/TypeEventRepeat"
|
|||||||
import { apiGetCalendarOne, apiUpdateCalendar } from "@/lib/api"
|
import { apiGetCalendarOne, apiUpdateCalendar } from "@/lib/api"
|
||||||
import { stringToDateTime } from "@/lib/fun_stringToDate"
|
import { stringToDateTime } from "@/lib/fun_stringToDate"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useHeaderHeight } from "@react-navigation/elements"
|
||||||
import { Stack, router, useLocalSearchParams } from "expo-router"
|
import { Stack, router, useLocalSearchParams } from "expo-router"
|
||||||
import moment from "moment"
|
import moment from "moment"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native"
|
import { KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
|
|
||||||
export default function EditEventCalendar() {
|
export default function EditEventCalendar() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [choose, setChoose] = useState({ val: "", label: "" })
|
const [choose, setChoose] = useState({ val: "", label: "" })
|
||||||
const [isSelect, setSelect] = useState(false)
|
const [isSelect, setSelect] = useState(false)
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>()
|
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>()
|
||||||
const [idCalendar, setIdCalendar] = useState('')
|
const [idCalendar, setIdCalendar] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const headerHeight = useHeaderHeight()
|
||||||
|
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
title: false,
|
title: false,
|
||||||
@@ -52,9 +57,11 @@ export default function EditEventCalendar() {
|
|||||||
setData({ ...response.data, dateStart: moment(response.data.dateStartFormat, 'DD-MM-YYYY').format('DD-MM-YYYY') })
|
setData({ ...response.data, dateStart: moment(response.data.dateStartFormat, 'DD-MM-YYYY').format('DD-MM-YYYY') })
|
||||||
setIdCalendar(response.data.idCalendar)
|
setIdCalendar(response.data.idCalendar)
|
||||||
setChoose({ val: response.data.repeatEventTyper, label: valueTypeEventRepeat.find((item) => item.id == response.data.repeatEventTyper)?.name || "" })
|
setChoose({ val: response.data.repeatEventTyper, label: valueTypeEventRepeat.find((item) => item.id == response.data.repeatEventTyper)?.name || "" })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mendapatkan data', })
|
const message = error?.response?.data?.message || "Gagal mendapatkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +147,7 @@ export default function EditEventCalendar() {
|
|||||||
|
|
||||||
async function handleUpdate() {
|
async function handleUpdate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiUpdateCalendar({ data: { ...data, user: hasil }, id: idCalendar })
|
const response = await apiUpdateCalendar({ data: { ...data, user: hasil }, id: idCalendar })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -148,118 +156,144 @@ export default function EditEventCalendar() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengubah acara', })
|
const message = error?.response?.data?.message || "Gagal mengubah acara"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Edit Acara',
|
headerTitle: 'Edit Acara',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () =>
|
// headerRight: () =>
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventTyper == ""}
|
// disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventTyper == "" || loading}
|
||||||
category="update-calendar"
|
// category="update-calendar"
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleUpdate()
|
// handleUpdate()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Edit Acara"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventTyper == "" || loading}
|
||||||
|
category="update-calendar"
|
||||||
|
onPress={() => {
|
||||||
|
handleUpdate()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<KeyboardAvoidingView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
<InputForm
|
keyboardVerticalOffset={headerHeight}
|
||||||
label="Nama Acara"
|
>
|
||||||
type="default"
|
<ScrollView>
|
||||||
placeholder="Nama Acara"
|
<View style={[Styles.p15]}>
|
||||||
required
|
<InputForm
|
||||||
bg="white"
|
label="Nama Acara"
|
||||||
value={data.title}
|
type="default"
|
||||||
onChange={(val) => validationForm("title", val)}
|
placeholder="Nama Acara"
|
||||||
error={error.title}
|
required
|
||||||
errorText="Nama acara tidak boleh kosong"
|
bg={colors.card}
|
||||||
/>
|
value={data.title}
|
||||||
<InputDate
|
onChange={(val) => validationForm("title", val)}
|
||||||
onChange={(val) => validationForm("dateStart", val)}
|
error={error.title}
|
||||||
mode="date"
|
errorText="Nama acara tidak boleh kosong"
|
||||||
value={data.dateStart}
|
/>
|
||||||
label="Tanggal Acara"
|
<InputDate
|
||||||
required
|
onChange={(val) => validationForm("dateStart", val)}
|
||||||
error={error.dateStart}
|
mode="date"
|
||||||
errorText="Tanggal acara tidak boleh kosong"
|
value={data.dateStart}
|
||||||
placeholder="Pilih Tanggal Acara"
|
label="Tanggal Acara"
|
||||||
/>
|
required
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mv10]}>
|
error={error.dateStart}
|
||||||
<View style={[{ width: "48%" }]}>
|
errorText="Tanggal acara tidak boleh kosong"
|
||||||
<InputDate
|
placeholder="Pilih Tanggal Acara"
|
||||||
onChange={(val) => validationForm("timeStart", val)}
|
/>
|
||||||
mode="time"
|
<View style={[Styles.rowSpaceBetween, Styles.mv10]}>
|
||||||
value={data.timeStart}
|
<View style={[{ width: "48%" }]}>
|
||||||
label="Waktu Awal"
|
<InputDate
|
||||||
required
|
onChange={(val) => validationForm("timeStart", val)}
|
||||||
error={error.timeStart}
|
mode="time"
|
||||||
errorText="Waktu awal tidak valid"
|
value={data.timeStart}
|
||||||
placeholder="--:--"
|
label="Waktu Awal"
|
||||||
/>
|
required
|
||||||
</View>
|
error={error.timeStart}
|
||||||
<View style={[{ width: "48%" }]}>
|
errorText="Waktu awal tidak valid"
|
||||||
<InputDate
|
placeholder="--:--"
|
||||||
onChange={(val) => validationForm("timeEnd", val)}
|
/>
|
||||||
mode="time"
|
</View>
|
||||||
value={data.timeEnd}
|
<View style={[{ width: "48%" }]}>
|
||||||
label="Waktu Akhir"
|
<InputDate
|
||||||
required
|
onChange={(val) => validationForm("timeEnd", val)}
|
||||||
error={error.timeEnd}
|
mode="time"
|
||||||
errorText="Waktu akhir tidak valid"
|
value={data.timeEnd}
|
||||||
placeholder="--:--"
|
label="Waktu Akhir"
|
||||||
/>
|
required
|
||||||
|
error={error.timeEnd}
|
||||||
|
errorText="Waktu akhir tidak valid"
|
||||||
|
placeholder="--:--"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<InputForm
|
||||||
|
label="Link Meet"
|
||||||
|
type="default"
|
||||||
|
placeholder="Link Meet"
|
||||||
|
bg={colors.card}
|
||||||
|
value={data.linkMeet}
|
||||||
|
onChange={(val) => validationForm("linkMeet", val)}
|
||||||
|
/>
|
||||||
|
<SelectForm
|
||||||
|
bg={colors.card}
|
||||||
|
label="Ulangi Acara"
|
||||||
|
placeholder="Ulangi Acara"
|
||||||
|
value={choose.label}
|
||||||
|
required
|
||||||
|
onPress={() => { setSelect(true) }}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Jumlah Pengulangan"
|
||||||
|
type="numeric"
|
||||||
|
placeholder="Jumlah Pengulangan"
|
||||||
|
required
|
||||||
|
bg={colors.card}
|
||||||
|
value={String(data.repeatValue)}
|
||||||
|
onChange={(val) => validationForm("repeatValue", val)}
|
||||||
|
error={error.repeatValue}
|
||||||
|
errorText="Jumlah pengulangan tidak valid"
|
||||||
|
disable={choose.val == "once"}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Deskripsi"
|
||||||
|
type="default"
|
||||||
|
placeholder="Deskripsi"
|
||||||
|
bg={colors.card}
|
||||||
|
value={data.desc}
|
||||||
|
onChange={(val) => validationForm("desc", val)}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<InputForm
|
</ScrollView>
|
||||||
label="Link Meet"
|
</KeyboardAvoidingView>
|
||||||
type="default"
|
|
||||||
placeholder="Link Meet"
|
|
||||||
bg="white"
|
|
||||||
value={data.linkMeet}
|
|
||||||
onChange={(val) => validationForm("linkMeet", val)}
|
|
||||||
/>
|
|
||||||
<SelectForm
|
|
||||||
bg="white"
|
|
||||||
label="Ulangi Acara"
|
|
||||||
placeholder="Ulangi Acara"
|
|
||||||
value={choose.label}
|
|
||||||
required
|
|
||||||
onPress={() => { setSelect(true) }}
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Jumlah Pengulangan"
|
|
||||||
type="numeric"
|
|
||||||
placeholder="Jumlah Pengulangan"
|
|
||||||
required
|
|
||||||
bg="white"
|
|
||||||
value={String(data.repeatValue)}
|
|
||||||
onChange={(val) => validationForm("repeatValue", val)}
|
|
||||||
error={error.repeatValue}
|
|
||||||
errorText="Jumlah pengulangan tidak valid"
|
|
||||||
disable={choose.val == "once"}
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Deskripsi"
|
|
||||||
type="default"
|
|
||||||
placeholder="Deskripsi"
|
|
||||||
bg="white"
|
|
||||||
value={data.desc}
|
|
||||||
onChange={(val) => validationForm("desc", val)}
|
|
||||||
multiline
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<ModalSelect
|
<ModalSelect
|
||||||
category={"type-event-repeat"}
|
category={"type-event-repeat"}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import BorderBottomItem from "@/components/borderBottomItem"
|
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
|
||||||
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
|
|
||||||
import DrawerBottom from "@/components/drawerBottom"
|
import DrawerBottom from "@/components/drawerBottom"
|
||||||
|
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
|
||||||
import ImageUser from "@/components/imageNew"
|
import ImageUser from "@/components/imageNew"
|
||||||
import MenuItemRow from "@/components/menuItemRow"
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
import Skeleton from "@/components/skeleton"
|
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||||
import Text from "@/components/Text"
|
import Text from "@/components/Text"
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv"
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiDeleteCalendarMember, apiGetCalendarOne, apiGetDivisionOneFeature } from "@/lib/api"
|
import { apiDeleteCalendarMember, apiGetCalendarOne, apiGetDivisionOneFeature } from "@/lib/api"
|
||||||
import { setUpdateCalendar } from "@/lib/calendarUpdate"
|
import { setUpdateCalendar } from "@/lib/calendarUpdate"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons"
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
||||||
|
import Clipboard from "@react-native-clipboard/clipboard"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
import { useDispatch, useSelector } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ type PropsMember = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailEventCalendar() {
|
export default function DetailEventCalendar() {
|
||||||
|
const { colors } = useTheme()
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
||||||
const [data, setData] = useState<Props>()
|
const [data, setData] = useState<Props>()
|
||||||
const [member, setMember] = useState<PropsMember[]>([])
|
const [member, setMember] = useState<PropsMember[]>([])
|
||||||
@@ -52,6 +54,7 @@ export default function DetailEventCalendar() {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const entityUser = useSelector((state: any) => state.user);
|
const entityUser = useSelector((state: any) => state.user);
|
||||||
const [isMemberDivision, setIsMemberDivision] = useState(false);
|
const [isMemberDivision, setIsMemberDivision] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ export default function DetailEventCalendar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoad(loading:boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
@@ -114,6 +117,11 @@ export default function DetailEventCalendar() {
|
|||||||
handleLoadMember();
|
handleLoadMember();
|
||||||
}, [update.member]);
|
}, [update.member]);
|
||||||
|
|
||||||
|
const handleCopy = (text: string) => {
|
||||||
|
Clipboard.setString(text);
|
||||||
|
Toast.show({ type: 'small', text1: 'Berhasil menyalin link', })
|
||||||
|
};
|
||||||
|
|
||||||
async function handleDeleteUser() {
|
async function handleDeleteUser() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
@@ -126,9 +134,11 @@ export default function DetailEventCalendar() {
|
|||||||
dispatch(setUpdateCalendar({ ...update, member: !update.member }));
|
dispatch(setUpdateCalendar({ ...update, member: !update.member }));
|
||||||
}
|
}
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menghapus anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModalMember(false)
|
setModalMember(false)
|
||||||
}
|
}
|
||||||
@@ -143,120 +153,142 @@ export default function DetailEventCalendar() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canManage = !((entityUser.role === "user" || entityUser.role === "coadmin") && !isMemberDivision)
|
||||||
|
|
||||||
|
const repeatLabel: Record<string, string> = {
|
||||||
|
once: 'Acara 1 Kali',
|
||||||
|
daily: 'Setiap Hari',
|
||||||
|
weekly: 'Mingguan',
|
||||||
|
monthly: 'Bulanan',
|
||||||
|
yearly: 'Tahunan',
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoRow({ icon, label, value, onCopy }: { icon: string, label: string, value?: string, onCopy?: () => void }) {
|
||||||
|
return (
|
||||||
|
<View style={[Styles.sectionActionRow, { paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name={icon as any} size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, marginBottom: 2 }]}>{label}</Text>
|
||||||
|
{loading
|
||||||
|
? <View style={{ height: 13, borderRadius: 6, backgroundColor: colors.icon + '20', width: '70%' }} />
|
||||||
|
: <Text style={[Styles.textDefault, { color: colors.text }]}>{value || '-'}</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
{onCopy && !loading && value && (
|
||||||
|
<Pressable onPress={onCopy} style={{ padding: 4 }}>
|
||||||
|
<MaterialCommunityIcons name="content-copy" size={16} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
header: () => (
|
||||||
headerTitle: 'Detail Acara',
|
<AppHeader
|
||||||
headerTitleAlign: 'center',
|
title="Detail Acara"
|
||||||
headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
(entityUser.role === "user" || entityUser.role === "coadmin") && !isMemberDivision
|
||||||
|
? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[Styles.h100]}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
style={Styles.h100}
|
||||||
<RefreshControl
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.mb15]}>
|
|
||||||
<View style={Styles.rowItemsCenter}>
|
|
||||||
<MaterialCommunityIcons name="calendar-text" size={30} color="black" style={Styles.mr10} />
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
|
||||||
: <Text style={[Styles.textDefault]}>{data?.title}</Text>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
{/* Info acara */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow, Styles.mb15,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||||
|
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }}>
|
||||||
|
<View style={Styles.sectionActionRow}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name="calendar-text" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Detail Acara</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
<View style={{ paddingHorizontal: 16 }}>
|
||||||
<MaterialCommunityIcons name="calendar-month-outline" size={30} color="black" style={Styles.mr10} />
|
<InfoRow icon="format-title" label="Judul" value={data?.title} />
|
||||||
{
|
<InfoRow icon="calendar-month-outline" label="Tanggal" value={data?.dateStart} />
|
||||||
loading ?
|
<InfoRow icon="clock-outline" label="Waktu" value={data ? `${data.timeStart} – ${data.timeEnd}` : undefined} />
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
<InfoRow icon="repeat" label="Pengulangan" value={data?.repeatEventTyper ? repeatLabel[data.repeatEventTyper] : undefined} />
|
||||||
:
|
<InfoRow icon="link-variant" label="Link Meet" value={data?.linkMeet} onCopy={data?.linkMeet ? () => handleCopy(data.linkMeet) : undefined} />
|
||||||
<Text style={[Styles.textDefault]}>{data?.dateStart}</Text>
|
<View style={[Styles.sectionActionRow, { paddingVertical: 10, alignItems: 'flex-start' }]}>
|
||||||
}
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name="card-text-outline" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, marginBottom: 2 }]}>Deskripsi</Text>
|
||||||
|
{loading
|
||||||
|
? <View style={{ height: 13, borderRadius: 6, backgroundColor: colors.icon + '20', width: '80%' }} />
|
||||||
|
: <Text style={[Styles.textDefault, { color: colors.text, lineHeight: 22 }]}>{data?.desc || '-'}</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
</View>
|
||||||
<MaterialCommunityIcons name="clock-outline" size={30} color="black" style={Styles.mr10} />
|
|
||||||
{
|
{/* Daftar anggota */}
|
||||||
loading ?
|
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow,
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||||
:
|
|
||||||
<Text style={[Styles.textDefault]}>{data?.timeStart} | {data?.timeEnd}</Text>
|
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||||
}
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{member.length} anggota</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
|
||||||
<MaterialCommunityIcons name="repeat" size={30} color="black" style={Styles.mr10} />
|
{member.length === 0
|
||||||
{
|
? (
|
||||||
loading ?
|
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||||
:
|
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||||
<Text style={[Styles.textDefault]}>
|
</View>
|
||||||
|
)
|
||||||
|
: member.map((item, index) => (
|
||||||
|
<Pressable
|
||||||
|
key={index}
|
||||||
|
onPress={() => {
|
||||||
|
if (!canManage) return
|
||||||
|
setMemberChoose({ id: item.idUser, name: item.name })
|
||||||
|
setModalMember(true)
|
||||||
|
}}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
Styles.rowItemsCenter, Styles.ph15,
|
||||||
{
|
{
|
||||||
data?.repeatEventTyper.toString() === 'once' ? 'Acara 1 Kali' :
|
paddingVertical: 12, gap: 14,
|
||||||
data?.repeatEventTyper.toString() === 'daily' ? 'Setiap Hari' :
|
borderBottomWidth: index < member.length - 1 ? 1 : 0,
|
||||||
data?.repeatEventTyper.toString() === 'weekly' ? 'Mingguan' :
|
borderBottomColor: colors.icon + '14',
|
||||||
data?.repeatEventTyper.toString() === 'monthly' ? 'Bulanan' :
|
backgroundColor: pressed && canManage ? colors.icon + '0E' : 'transparent',
|
||||||
data?.repeatEventTyper.toString() === 'yearly' ? 'Tahunan' :
|
},
|
||||||
''
|
]}
|
||||||
}
|
>
|
||||||
</Text>
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
}
|
<View style={Styles.flex1}>
|
||||||
</View>
|
<Text style={[Styles.textDefault, { color: colors.text }]} numberOfLines={1}>{item.name}</Text>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]} numberOfLines={1}>{item.email}</Text>
|
||||||
<MaterialCommunityIcons name="link-variant" size={30} color="black" style={Styles.mr10} />
|
</View>
|
||||||
{
|
{canManage && <MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />}
|
||||||
loading ?
|
</Pressable>
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
))
|
||||||
:
|
}
|
||||||
<Text style={[Styles.textDefault]}>{data?.linkMeet ? data.linkMeet : '-'}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
|
||||||
<MaterialCommunityIcons name="card-text-outline" size={30} color="black" style={Styles.mr10} />
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
|
||||||
:
|
|
||||||
<Text style={[Styles.textDefault]}>{data?.desc}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[Styles.mb15]}>
|
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
|
|
||||||
<Text style={[Styles.textDefault]}>Total {member.length} Anggota</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[Styles.wrapPaper]}>
|
|
||||||
{
|
|
||||||
member.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} />}
|
|
||||||
title={item.name}
|
|
||||||
subtitle={item.email}
|
|
||||||
onPress={() => {
|
|
||||||
if ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
setMemberChoose({ id: item.idUser, name: item.name })
|
|
||||||
setModalMember(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
@@ -264,7 +296,7 @@ export default function DetailEventCalendar() {
|
|||||||
<DrawerBottom animation="slide" isVisible={isModalMember} setVisible={setModalMember} title={memberChoose.name}>
|
<DrawerBottom animation="slide" isVisible={isModalMember} setVisible={setModalMember} title={memberChoose.name}>
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="account-eye" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="account-eye" color={colors.text} size={25} />}
|
||||||
title="Lihat Profil"
|
title="Lihat Profil"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalMember(false)
|
setModalMember(false)
|
||||||
@@ -273,22 +305,30 @@ export default function DetailEventCalendar() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="account-remove" color={colors.text} size={25} />}
|
||||||
title="Keluarkan"
|
title="Keluarkan"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalMember(false)
|
setModalMember(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => {
|
||||||
title: 'Konfirmasi',
|
setShowDeleteModal(true)
|
||||||
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
|
}, 600)
|
||||||
onPress: () => {
|
|
||||||
handleDeleteUser()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah Anda yakin ingin mengeluarkan anggota?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDeleteUser()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Keluar"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateCalendar, apiGetDivisionMember } from "@/lib/api";
|
import { apiCreateCalendar, apiGetDivisionMember } from "@/lib/api";
|
||||||
import { setFormCreateCalendar } from "@/lib/calendarCreate";
|
import { setFormCreateCalendar } from "@/lib/calendarCreate";
|
||||||
import { setUpdateCalendar } from "@/lib/calendarUpdate";
|
import { setUpdateCalendar } from "@/lib/calendarUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -23,6 +25,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateCalendarAddMember() {
|
export default function CreateCalendarAddMember() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
@@ -31,6 +34,7 @@ export default function CreateCalendarAddMember() {
|
|||||||
const update = useSelector((state: any) => state.calendarCreate)
|
const update = useSelector((state: any) => state.calendarCreate)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const updateRefresh = useSelector((state: any) => state.calendarUpdate)
|
const updateRefresh = useSelector((state: any) => state.calendarUpdate)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +62,7 @@ export default function CreateCalendarAddMember() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiCreateCalendar({ data: { ...update, user: hasil, idDivision: id, member: selectMember } })
|
const response = await apiCreateCalendar({ data: { ...update, user: hasil, idDivision: id, member: selectMember } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -78,24 +83,42 @@ export default function CreateCalendarAddMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal membuat acara', })
|
const message = error?.response?.data?.message || "Gagal membuat acara"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Pilih Anggota',
|
headerTitle: 'Pilih Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="create"
|
// category="create"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => { handleAddMember() }}
|
// onPress={() => { handleAddMember() }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Pilih Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => { handleAddMember() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -107,13 +130,13 @@ export default function CreateCalendarAddMember() {
|
|||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -122,9 +145,12 @@ export default function CreateCalendarAddMember() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100]}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -132,17 +158,17 @@ export default function CreateCalendarAddMember() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||||
onPress={() => { onChoose(item.idUser, item.name, item.img) }}
|
onPress={() => { onChoose(item.idUser, item.name, item.img) }}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import ButtonBackHeader from "@/components/buttonBackHeader";
|
||||||
import ButtonNextHeader from "@/components/buttonNextHeader";
|
import ButtonNextHeader from "@/components/buttonNextHeader";
|
||||||
import { InputDate } from "@/components/inputDate";
|
import { InputDate } from "@/components/inputDate";
|
||||||
@@ -7,6 +8,8 @@ import SelectForm from "@/components/selectForm";
|
|||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { setFormCreateCalendar } from "@/lib/calendarCreate";
|
import { setFormCreateCalendar } from "@/lib/calendarCreate";
|
||||||
import { stringToDateTime } from "@/lib/fun_stringToDate";
|
import { stringToDateTime } from "@/lib/fun_stringToDate";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Stack, router, useLocalSearchParams } from "expo-router";
|
import { Stack, router, useLocalSearchParams } from "expo-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -19,11 +22,13 @@ import {
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function CalendarDivisionCreate() {
|
export default function CalendarDivisionCreate() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [choose, setChoose] = useState({ val: "", label: "" })
|
const [choose, setChoose] = useState({ val: "", label: "" })
|
||||||
const [isSelect, setSelect] = useState(false)
|
const [isSelect, setSelect] = useState(false)
|
||||||
const update = useSelector((state: any) => state.calendarCreate)
|
const update = useSelector((state: any) => state.calendarCreate)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
title: false,
|
title: false,
|
||||||
dateStart: false,
|
dateStart: false,
|
||||||
@@ -123,38 +128,54 @@ export default function CalendarDivisionCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah Acara",
|
headerTitle: "Tambah Acara",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonNextHeader
|
// <ButtonNextHeader
|
||||||
onPress={() => { handleSetData() }}
|
// onPress={() => { handleSetData() }}
|
||||||
disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventType == ""}
|
// disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventType == ""}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Acara"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonNextHeader
|
||||||
|
onPress={() => { handleSetData() }}
|
||||||
|
disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventType == ""}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100]}
|
||||||
|
>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Nama Acara"
|
label="Nama Acara"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Nama Acara"
|
placeholder="Nama Acara"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={data.title}
|
value={data.title}
|
||||||
onChange={(val) => validationForm("title", val)}
|
onChange={(val) => validationForm("title", val)}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
@@ -200,12 +221,12 @@ export default function CalendarDivisionCreate() {
|
|||||||
label="Link Meet"
|
label="Link Meet"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Link Meet"
|
placeholder="Link Meet"
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={data.linkMeet}
|
value={data.linkMeet}
|
||||||
onChange={(val) => validationForm("linkMeet", val)}
|
onChange={(val) => validationForm("linkMeet", val)}
|
||||||
/>
|
/>
|
||||||
<SelectForm
|
<SelectForm
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
label="Ulangi Acara"
|
label="Ulangi Acara"
|
||||||
placeholder="Ulangi Acara"
|
placeholder="Ulangi Acara"
|
||||||
value={choose.label}
|
value={choose.label}
|
||||||
@@ -217,7 +238,7 @@ export default function CalendarDivisionCreate() {
|
|||||||
type="numeric"
|
type="numeric"
|
||||||
placeholder="Jumlah Pengulangan"
|
placeholder="Jumlah Pengulangan"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={String(data.repeatValue)}
|
value={String(data.repeatValue)}
|
||||||
onChange={(val) => validationForm("repeatValue", val)}
|
onChange={(val) => validationForm("repeatValue", val)}
|
||||||
error={error.repeatValue}
|
error={error.repeatValue}
|
||||||
@@ -228,7 +249,7 @@ export default function CalendarDivisionCreate() {
|
|||||||
label="Deskripsi"
|
label="Deskripsi"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Deskripsi"
|
placeholder="Deskripsi"
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={data.desc}
|
value={data.desc}
|
||||||
onChange={(val) => validationForm("desc", val)}
|
onChange={(val) => validationForm("desc", val)}
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetCalendarHistory } from "@/lib/api";
|
import { apiGetCalendarHistory } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FlatList, View, VirtualizedList } from "react-native";
|
import { FlatList, View, VirtualizedList } from "react-native";
|
||||||
@@ -15,6 +15,7 @@ type Props = {
|
|||||||
data: []
|
data: []
|
||||||
}
|
}
|
||||||
export default function CalendarHistory() {
|
export default function CalendarHistory() {
|
||||||
|
const { colors, activeTheme } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
@@ -64,11 +65,11 @@ export default function CalendarHistory() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<View>
|
<View>
|
||||||
<InputSearch onChange={(val) => setSearch(val)} />
|
<InputSearch onChange={(val) => setSearch(val)} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2, }]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
loading ?
|
||||||
arrSkeleton.map((item, index) => (
|
arrSkeleton.map((item, index) => (
|
||||||
@@ -81,7 +82,7 @@ export default function CalendarHistory() {
|
|||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
<View key={index} style={[{ flexDirection: 'row' }, Styles.mv05, ColorsStatus.lightGreen, Styles.p10, Styles.round10]}>
|
<View key={index} style={[{ flexDirection: 'row' }, Styles.mb05, Styles.borderAll, { backgroundColor: colors.card }, Styles.p10, Styles.round05, { borderColor: colors.icon + '20' }]}>
|
||||||
<View style={[Styles.mr10, Styles.ph05]}>
|
<View style={[Styles.mr10, Styles.ph05]}>
|
||||||
<Text style={[Styles.textSubtitle]}>{String(item.dateStart)}</Text>
|
<Text style={[Styles.textSubtitle]}>{String(item.dateStart)}</Text>
|
||||||
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>{item.year}</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>{item.year}</Text>
|
||||||
@@ -89,7 +90,7 @@ export default function CalendarHistory() {
|
|||||||
<View style={[{ flex: 1 }]}>
|
<View style={[{ flex: 1 }]}>
|
||||||
<FlatList data={item.data}
|
<FlatList data={item.data}
|
||||||
renderItem={({ item, index }: { item: { title: string, timeStart: string, timeEnd: string }, index: number }) => (
|
renderItem={({ item, index }: { item: { title: string, timeStart: string, timeEnd: string }, index: number }) => (
|
||||||
<View key={index} style={[Styles.mb05, Styles.w80]}>
|
<View key={index} style={[Styles.mb05]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold]} numberOfLines={1} ellipsizeMode="tail">{item.title}</Text>
|
<Text style={[Styles.textDefaultSemiBold]} numberOfLines={1} ellipsizeMode="tail">{item.title}</Text>
|
||||||
<Text style={[Styles.textDefault]}>{item.timeStart} | {item.timeEnd}</Text>
|
<Text style={[Styles.textDefault]}>{item.timeStart} | {item.timeEnd}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import HeaderRightCalendarList from "@/components/calendar/headerCalendarList";
|
import HeaderRightCalendarList from "@/components/calendar/headerCalendarList";
|
||||||
import ItemDateCalendar from "@/components/calendar/itemDateCalendar";
|
import ItemDateCalendar from "@/components/calendar/itemDateCalendar";
|
||||||
import EventItem from "@/components/eventItem";
|
import EventItem from "@/components/eventItem";
|
||||||
@@ -6,9 +7,11 @@ import Skeleton from "@/components/skeleton";
|
|||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetCalendarByDateDivision, apiGetIndicatorCalendar } from "@/lib/api";
|
import { apiGetCalendarByDateDivision, apiGetIndicatorCalendar } from "@/lib/api";
|
||||||
|
import { GUIDE_DIVISION_CALENDAR } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import 'intl';
|
import 'intl';
|
||||||
import 'intl/locale-data/jsonp/id';
|
import 'intl/locale-data/jsonp/id';
|
||||||
@@ -35,6 +38,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function CalendarDivision() {
|
export default function CalendarDivision() {
|
||||||
|
const { colors, activeTheme } = useTheme();
|
||||||
const [selected, setSelected] = useState<any>(new Date())
|
const [selected, setSelected] = useState<any>(new Date())
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -45,6 +49,7 @@ export default function CalendarDivision() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [loadingBtn, setLoadingBtn] = useState(false)
|
const [loadingBtn, setLoadingBtn] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-calendar')
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
@@ -53,7 +58,7 @@ export default function CalendarDivision() {
|
|||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetCalendarByDateDivision({
|
const response = await apiGetCalendarByDateDivision({
|
||||||
user: hasil,
|
user: hasil,
|
||||||
date: dayjs(selected).format("YYYY-MM-DD"),
|
date: moment(selected).format("YYYY-MM-DD"),
|
||||||
division: id,
|
division: id,
|
||||||
});
|
});
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
@@ -71,14 +76,16 @@ export default function CalendarDivision() {
|
|||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetIndicatorCalendar({
|
const response = await apiGetIndicatorCalendar({
|
||||||
user: hasil,
|
user: hasil,
|
||||||
date: dayjs(newDate).format("YYYY-MM-DD"),
|
date: moment(newDate).format("YYYY-MM-DD"),
|
||||||
division: id,
|
division: id,
|
||||||
});
|
});
|
||||||
setDataIndicator(response.data);
|
setDataIndicator(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingBtn(false)
|
setTimeout(() => {
|
||||||
|
setLoadingBtn(false)
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,65 +118,76 @@ export default function CalendarDivision() {
|
|||||||
text={day.text}
|
text={day.text}
|
||||||
isSelected={day.isSelected}
|
isSelected={day.isSelected}
|
||||||
isSign={sign}
|
isSign={sign}
|
||||||
|
onPress={() => setSelected(new Date(today))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
IconNext: <Pressable onPress={() => !loadingBtn ? setMonth(month + 1) : null}>
|
IconNext: <Pressable onPress={() => !loadingBtn ? setMonth(month + 1) : null}>
|
||||||
<Feather name="chevron-right" size={20} color={loadingBtn ? 'gray' : 'black'} />
|
<Feather name="chevron-right" size={20} color={loadingBtn ? 'gray' : colors.text} />
|
||||||
</Pressable>,
|
</Pressable>,
|
||||||
IconPrev: <Pressable onPress={() => !loadingBtn ? setMonth(month - 1) : null}>
|
IconPrev: <Pressable onPress={() => !loadingBtn ? setMonth(month - 1) : null}>
|
||||||
<Feather name="chevron-left" size={20} color={loadingBtn ? 'gray' : 'black'} />
|
<Feather name="chevron-left" size={20} color={loadingBtn ? 'gray' : colors.text} />
|
||||||
</Pressable>,
|
</Pressable>,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Kalender",
|
headerTitle: "Kalender",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => <HeaderRightCalendarList />,
|
// headerRight: () => <HeaderRightCalendarList />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Kalender"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightCalendarList />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_CALENDAR} onDismiss={dismissGuide} />
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
style={[Styles.h100]}
|
style={[Styles.h100]}
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
<Datepicker
|
<Datepicker
|
||||||
components={components}
|
components={components}
|
||||||
mode="single"
|
mode="single"
|
||||||
date={selected}
|
date={selected}
|
||||||
month={month}
|
month={month}
|
||||||
onChange={({ date }) => setSelected(date)}
|
onMonthChange={(month) => setMonth(month)}
|
||||||
styles={{
|
styles={{
|
||||||
selected: Styles.selectedDate,
|
selected: Styles.selectedDate,
|
||||||
month_label: Styles.cBlack,
|
month_label: { color: colors.text },
|
||||||
month_selector_label: Styles.cBlack,
|
month_selector_label: { color: colors.text },
|
||||||
year_label: Styles.cBlack,
|
year_label: { color: colors.text },
|
||||||
year_selector_label: Styles.cBlack,
|
year_selector_label: { color: colors.text },
|
||||||
day_label: Styles.cBlack,
|
day_label: { color: colors.text },
|
||||||
time_label: Styles.cBlack,
|
time_label: { color: colors.text },
|
||||||
weekday_label: Styles.cBlack,
|
weekday_label: { color: colors.text },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.mb15, Styles.mt15]}>
|
<View style={[Styles.mb15, Styles.mt15]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Acara</Text>
|
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Acara</Text>
|
||||||
<View style={[Styles.wrapPaper]}>
|
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
{
|
{
|
||||||
loading ?
|
loading ?
|
||||||
<>
|
<>
|
||||||
@@ -192,7 +210,7 @@ export default function CalendarDivision() {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada acara</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada acara</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,84 +1,148 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditDiscussion, apiGetDiscussionOne } from "@/lib/api";
|
import { apiEditDiscussion, apiGetDiscussionOne } from "@/lib/api";
|
||||||
import { setUpdateDiscussion } from "@/lib/discussionUpdate";
|
import { setUpdateDiscussion } from "@/lib/discussionUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function DiscussionDivisionEdit() {
|
export default function DiscussionDivisionEdit() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const update = useSelector((state: any) => state.discussionUpdate);
|
const update = useSelector((state: any) => state.discussionUpdate);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
|
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
|
||||||
|
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
|
||||||
|
|
||||||
|
const visibleOldFiles = dataFile.filter(v => !v.delete)
|
||||||
|
const totalFiles = fileForm.length + visibleOldFiles.length
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionOne({
|
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "data" });
|
||||||
id: detail,
|
const response2 = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "file" });
|
||||||
user: hasil,
|
|
||||||
cat: "data",
|
|
||||||
});
|
|
||||||
setData(response.data.desc);
|
setData(response.data.desc);
|
||||||
|
setDataFile(response2.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad() }, []);
|
||||||
handleLoad();
|
|
||||||
}, []);
|
const pickDocumentAsync = async () => {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
const isDup = fileForm.some(f => f.name === asset.name) ||
|
||||||
|
visibleOldFiles.some(f => `${f.name}.${f.extension}` === asset.name)
|
||||||
|
if (isDup) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
|
||||||
|
if (cat === "newFile") {
|
||||||
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
|
} else {
|
||||||
|
setDataFile(prev => prev.map(item => item.id === index ? { ...item, delete: true } : item))
|
||||||
|
}
|
||||||
|
setModalFile(false)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUpdate() {
|
async function handleUpdate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiEditDiscussion({
|
const fd = new FormData()
|
||||||
data: { user: hasil, desc: data },
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
id: detail,
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
});
|
}
|
||||||
|
fd.append("data", JSON.stringify({ user: hasil, desc: data, oldFile: dataFile }))
|
||||||
|
const response = await apiEditDiscussion(fd, detail);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data' })
|
||||||
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
|
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengubah data" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
header: () => (
|
||||||
<ButtonBackHeader
|
<AppHeader
|
||||||
onPress={() => {
|
title="Edit Diskusi"
|
||||||
router.back();
|
showBack={true}
|
||||||
}}
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={data == "" || loading}
|
||||||
|
category="update"
|
||||||
|
onPress={() => handleUpdate()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
headerTitle: "Edit Diskusi",
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerRight: () => (
|
|
||||||
<ButtonSaveHeader
|
|
||||||
disable={data == ""}
|
|
||||||
category="update"
|
|
||||||
onPress={() => {
|
|
||||||
handleUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
<View style={[Styles.p15]}>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={Styles.p15}>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Diskusi"
|
label="Diskusi"
|
||||||
type="default"
|
type="default"
|
||||||
@@ -86,9 +150,91 @@ export default function DiscussionDivisionEdit() {
|
|||||||
required
|
required
|
||||||
value={data}
|
value={data}
|
||||||
onChange={setData}
|
onChange={setData}
|
||||||
|
multiline
|
||||||
|
bg={colors.card}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* File */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={pickDocumentAsync}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: totalFiles > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{totalFiles === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{totalFiles > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{visibleOldFiles.map((item, index) => {
|
||||||
|
const ext = item.extension.toLowerCase()
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={`old-${index}`}
|
||||||
|
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{item.name}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={`new-${index}`}
|
||||||
|
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile.id, indexDelFile.cat)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
||||||
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
|
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
|
||||||
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
|
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import {
|
import {
|
||||||
|
apiDeleteDiscussionCommentar,
|
||||||
|
apiEditDiscussionCommentar,
|
||||||
apiGetDiscussionOne,
|
apiGetDiscussionOne,
|
||||||
apiGetDivisionOneFeature,
|
apiGetDivisionOneFeature,
|
||||||
apiSendDiscussionCommentar,
|
apiSendDiscussionCommentar,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { getDB } from "@/lib/firebaseDatabase";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { firebase } from "@react-native-firebase/database";
|
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { ref } from "@react-native-firebase/database";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
|
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -40,12 +50,24 @@ type PropsComment = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
username: string;
|
username: string;
|
||||||
img: string;
|
img: string;
|
||||||
|
idUser: string;
|
||||||
|
isEdited: boolean;
|
||||||
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PropsFile = {
|
||||||
|
id: string;
|
||||||
|
idStorage: string;
|
||||||
|
name: string;
|
||||||
|
extension: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function DiscussionDetail() {
|
export default function DiscussionDetail() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const [data, setData] = useState<Props>();
|
const [data, setData] = useState<Props>();
|
||||||
const [dataComment, setDataComment] = useState<PropsComment[]>([]);
|
const [dataComment, setDataComment] = useState<PropsComment[]>([]);
|
||||||
|
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [komentar, setKomentar] = useState("");
|
const [komentar, setKomentar] = useState("");
|
||||||
const [loadingSend, setLoadingSend] = useState(false);
|
const [loadingSend, setLoadingSend] = useState(false);
|
||||||
@@ -57,19 +79,21 @@ export default function DiscussionDetail() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [loadingKomentar, setLoadingKomentar] = useState(true)
|
const [loadingKomentar, setLoadingKomentar] = useState(true)
|
||||||
const arrSkeleton = Array.from({ length: 3 })
|
const arrSkeleton = Array.from({ length: 3 })
|
||||||
const reference = firebase.app().database('https://mobile-darmasaba-default-rtdb.asia-southeast1.firebasedatabase.app').ref(`/discussion-division/${detail}`);
|
const reference = ref(getDB(), `/discussion-division/${detail}`);
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
|
const [detailMore, setDetailMore] = useState<any>([])
|
||||||
|
const entities = useSelector((state: any) => state.entities)
|
||||||
|
const [isVisible, setVisible] = useState(false)
|
||||||
|
const [selectKomentar, setSelectKomentar] = useState({ id: '', comment: '' })
|
||||||
|
const [viewEdit, setViewEdit] = useState(false)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onValueChange = reference.on('value', snapshot => {
|
const onValueChange = reference.on('value', snapshot => {
|
||||||
if (snapshot.val() == null) {
|
if (snapshot.val() == null) { reference.set({ trigger: true }) }
|
||||||
reference.set({ trigger: true })
|
|
||||||
}
|
|
||||||
handleLoadComment(false)
|
handleLoadComment(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop listening for updates when no longer required
|
|
||||||
return () => reference.off('value', onValueChange);
|
return () => reference.off('value', onValueChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -80,17 +104,14 @@ export default function DiscussionDetail() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionOne({
|
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "data" });
|
||||||
id: detail,
|
const responseFile = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "file" });
|
||||||
user: hasil,
|
|
||||||
cat: "data",
|
|
||||||
});
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
|
setFileDiscussion(responseFile.data)
|
||||||
setIsCreator(response.data.createdBy == hasil);
|
setIsCreator(response.data.createdBy == hasil);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -103,11 +124,7 @@ export default function DiscussionDetail() {
|
|||||||
try {
|
try {
|
||||||
setLoadingKomentar(loading)
|
setLoadingKomentar(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionOne({
|
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "comment" });
|
||||||
id: detail,
|
|
||||||
user: hasil,
|
|
||||||
cat: "comment",
|
|
||||||
});
|
|
||||||
setDataComment(response.data);
|
setDataComment(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -119,17 +136,8 @@ export default function DiscussionDetail() {
|
|||||||
async function handleCheckMember() {
|
async function handleCheckMember() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDivisionOneFeature({
|
const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" });
|
||||||
id,
|
const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" });
|
||||||
user: hasil,
|
|
||||||
cat: "check-member",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response2 = await apiGetDivisionOneFeature({
|
|
||||||
id,
|
|
||||||
user: hasil,
|
|
||||||
cat: "check-admin",
|
|
||||||
});
|
|
||||||
setIsMemberDivision(response.data);
|
setIsMemberDivision(response.data);
|
||||||
setIsAdminDivision(response2.data);
|
setIsAdminDivision(response2.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -137,35 +145,62 @@ export default function DiscussionDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false); }, [update.data]);
|
||||||
handleLoad(false);
|
useEffect(() => { handleLoad(true); handleLoadComment(true); handleCheckMember(); }, []);
|
||||||
}, [update.data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
handleLoadComment(true);
|
|
||||||
handleCheckMember();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleKomentar() {
|
async function handleKomentar() {
|
||||||
try {
|
try {
|
||||||
setLoadingSend(true);
|
setLoadingSend(true);
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiSendDiscussionCommentar({
|
const response = await apiSendDiscussionCommentar({ id: detail, data: { comment: komentar, user: hasil } });
|
||||||
id: detail,
|
if (response.success) { setKomentar(""); updateTrigger() }
|
||||||
data: { comment: komentar, user: hasil },
|
} catch (error: any) {
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
setKomentar("")
|
|
||||||
updateTrigger()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan komentar" })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false);
|
setLoadingSend(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleEditKomentar() {
|
||||||
|
try {
|
||||||
|
setLoadingSend(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiEditDiscussionCommentar({ id: selectKomentar.id, data: { comment: selectKomentar.comment, user: hasil } });
|
||||||
|
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengedit komentar" })
|
||||||
|
} finally {
|
||||||
|
setLoadingSend(false);
|
||||||
|
handleViewEditKomentar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteKomentar() {
|
||||||
|
try {
|
||||||
|
setLoadingSend(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiDeleteDiscussionCommentar({ id: selectKomentar.id, data: { user: hasil } });
|
||||||
|
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus komentar" })
|
||||||
|
} finally {
|
||||||
|
setLoadingSend(false)
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenuKomentar(id: string, comment: string) {
|
||||||
|
setSelectKomentar({ id, comment })
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewEditKomentar() {
|
||||||
|
setVisible(false)
|
||||||
|
setViewEdit(!viewEdit)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
@@ -175,175 +210,205 @@ export default function DiscussionDetail() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canWrite = data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision)
|
||||||
|
const isOpen = data?.status === 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
|
||||||
<ButtonBackHeader
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerTitle: "Diskusi",
|
headerTitle: "Diskusi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () =>
|
header: () => (
|
||||||
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
|
<AppHeader
|
||||||
<HeaderRightDiscussionDetail
|
title="Diskusi"
|
||||||
id={detail}
|
showBack={true}
|
||||||
status={data?.status}
|
onPressLeft={() => router.back()}
|
||||||
isActive={data?.isActive}
|
right={
|
||||||
/> : (<></>)
|
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator) ?
|
||||||
,
|
<HeaderRightDiscussionDetail id={detail} status={data?.status} isActive={data?.isActive} /> : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
showsVerticalScrollIndicator={false}
|
||||||
<RefreshControl
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15]}>
|
||||||
{
|
{loading ? (
|
||||||
loading ?
|
<SkeletonContent />
|
||||||
<SkeletonContent />
|
) : (
|
||||||
:
|
<BorderBottomItem2
|
||||||
<BorderBottomItem
|
dataFile={fileDiscussion}
|
||||||
descEllipsize={false}
|
descEllipsize={false}
|
||||||
width={55}
|
borderType="all"
|
||||||
borderType="bottom"
|
bgColor="white"
|
||||||
icon={
|
icon={
|
||||||
<ImageUser
|
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.user_img}`} size="sm" />
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${data?.user_img}`}
|
}
|
||||||
size="sm"
|
title={data?.username}
|
||||||
/>
|
titleShowAll={true}
|
||||||
}
|
subtitle={
|
||||||
title={data?.username}
|
<View style={[Styles.discussionStatusPill, {
|
||||||
subtitle={
|
borderColor: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed + '80',
|
||||||
data?.isActive ? (
|
}]}>
|
||||||
data?.status == 1 ? (
|
<Text style={[Styles.discussionStatusText, {
|
||||||
<LabelStatus category="success" text="BUKA" size="small" />
|
color: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed,
|
||||||
) : (
|
}]}>
|
||||||
<LabelStatus category="error" text="TUTUP" size="small" />
|
{!data?.isActive ? 'Arsip' : isOpen ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
desc={data?.desc}
|
||||||
|
leftBottomInfo={
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataComment.length} Komentar</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
rightBottomInfo={<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={Styles.mt10}>
|
||||||
|
{loadingKomentar ? (
|
||||||
|
arrSkeleton.map((_, i) => (
|
||||||
|
<Skeleton key={i} width={100} widthType="percent" height={40} borderRadius={5} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
dataComment.map((item, i) => (
|
||||||
|
<Pressable
|
||||||
|
key={i}
|
||||||
|
onPress={() => {
|
||||||
|
setDetailMore((prev: any) =>
|
||||||
|
prev.includes(item.id) ? prev.filter((id: string) => id !== item.id) : [...prev, item.id]
|
||||||
)
|
)
|
||||||
) : (
|
}}
|
||||||
<LabelStatus category="secondary" text="ARSIP" size="small" />
|
onLongPress={() => {
|
||||||
)
|
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
|
||||||
}
|
}}
|
||||||
rightTopInfo={data?.createdAt}
|
style={({ pressed }) => [
|
||||||
desc={data?.desc}
|
Styles.discussionCommentCard,
|
||||||
leftBottomInfo={
|
{ backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' }
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
]}
|
||||||
<Ionicons
|
>
|
||||||
name="chatbox-ellipses-outline"
|
<View style={Styles.flex1}>
|
||||||
size={18}
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||||
color="grey"
|
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||||
style={Styles.mr05}
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
/>
|
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]} >
|
{item.username}
|
||||||
{dataComment.length} Komentar
|
</Text>
|
||||||
|
{item.isEdited && (
|
||||||
|
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>diedit</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||||
|
{item.createdAt}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefault, { color: colors.text }]} numberOfLines={detailMore.includes(item.id) ? 0 : 3}>
|
||||||
|
{item.comment}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
</Pressable>
|
||||||
/>
|
))
|
||||||
}
|
)}
|
||||||
|
|
||||||
<View style={[Styles.p15]}>
|
|
||||||
{
|
|
||||||
loadingKomentar ?
|
|
||||||
arrSkeleton.map((item, index) => (
|
|
||||||
<Skeleton key={index} width={100} widthType="percent" height={40} borderRadius={5} />
|
|
||||||
))
|
|
||||||
:
|
|
||||||
dataComment.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
width={55}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<ImageUser
|
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={item.username}
|
|
||||||
rightTopInfo={item.createdAt}
|
|
||||||
desc={item.comment}
|
|
||||||
descEllipsize={false}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<KeyboardAvoidingView
|
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight}>
|
||||||
keyboardVerticalOffset={110}
|
<View style={[Styles.contentItemCenter, Styles.w100, { backgroundColor: colors.background }, viewEdit && Styles.borderTop]}>
|
||||||
>
|
{viewEdit ? (
|
||||||
<View
|
<>
|
||||||
style={[
|
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
||||||
Styles.contentItemCenter,
|
<View style={Styles.rowItemsCenter}>
|
||||||
Styles.w100,
|
<Feather name="edit-3" color={colors.text} size={22} style={Styles.mh05} />
|
||||||
{ backgroundColor: "#f4f4f4" },
|
<Text style={Styles.textMediumSemiBold}>Edit Komentar</Text>
|
||||||
]}
|
</View>
|
||||||
>
|
<Pressable onPress={() => handleViewEditKomentar()}>
|
||||||
<InputForm
|
<MaterialIcons name="close" color={colors.text} size={22} />
|
||||||
disable={
|
</Pressable>
|
||||||
data?.status == 2 ||
|
</View>
|
||||||
data?.isActive == false ||
|
<InputForm
|
||||||
((entityUser.role == "user" || entityUser.role == "coadmin") &&
|
bg={colors.card}
|
||||||
!isMemberDivision)
|
type="default" round multiline
|
||||||
}
|
placeholder="Kirim Komentar"
|
||||||
bg="white"
|
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
|
||||||
type="default"
|
value={selectKomentar.comment}
|
||||||
round
|
itemRight={
|
||||||
placeholder="Kirim Komentar"
|
<Pressable
|
||||||
onChange={setKomentar}
|
onPress={() => {
|
||||||
value={komentar}
|
selectKomentar.comment != "" && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && !loadingSend && data?.status != 2 && data?.isActive
|
||||||
itemRight={
|
&& (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin")
|
||||||
<Pressable
|
&& handleEditKomentar();
|
||||||
onPress={() => {
|
}}
|
||||||
komentar != "" &&
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
!loadingSend &&
|
>
|
||||||
data?.status != 2 &&
|
<MaterialIcons name="send" size={25}
|
||||||
data?.isActive &&
|
style={[
|
||||||
(((entityUser.role == "user" ||
|
selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
||||||
entityUser.role == "coadmin") &&
|
? { color: colors.dimmed } : { color: colors.tint },
|
||||||
isMemberDivision) ||
|
]}
|
||||||
entityUser.role == "admin" ||
|
/>
|
||||||
entityUser.role == "supadmin" ||
|
</Pressable>
|
||||||
entityUser.role == "developer" ||
|
}
|
||||||
entityUser.role == "cosupadmin") &&
|
/>
|
||||||
handleKomentar();
|
</>
|
||||||
}}
|
) : canWrite ? (
|
||||||
>
|
<InputForm
|
||||||
<MaterialIcons
|
type="default" round multiline
|
||||||
name="send"
|
placeholder="Kirim Komentar"
|
||||||
size={25}
|
onChange={setKomentar} value={komentar}
|
||||||
style={
|
itemRight={
|
||||||
komentar == "" ||
|
<Pressable
|
||||||
loadingSend ||
|
onPress={() => {
|
||||||
data?.status == 2 ||
|
komentar != "" && !regexOnlySpacesOrEnter.test(komentar) && !loadingSend && data?.status != 2 && data?.isActive
|
||||||
data?.isActive == false ||
|
&& (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin")
|
||||||
((entityUser.role == "user" ||
|
&& handleKomentar();
|
||||||
entityUser.role == "coadmin") &&
|
}}
|
||||||
!isMemberDivision)
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
? Styles.cGray
|
>
|
||||||
: Styles.cDefault
|
<MaterialIcons name="send" size={25}
|
||||||
}
|
style={[
|
||||||
/>
|
komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
||||||
</Pressable>
|
? { color: colors.dimmed } : { color: colors.tint },
|
||||||
}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[Styles.pv20, Styles.itemsCenter]}>
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
|
||||||
|
{data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />} title="Edit" onPress={() => handleViewEditKomentar()} />
|
||||||
|
<MenuItemRow icon={<MaterialIcons name="delete-outline" color={colors.text} size={25} />} title="Hapus" onPress={() => { setVisible(false); setTimeout(() => setShowDeleteModal(true), 600) }} />
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin menghapus komentar?"
|
||||||
|
onConfirm={() => { setShowDeleteModal(false); handleDeleteKomentar() }}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,195 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
||||||
|
import DrawerBottom from "@/components/drawerBottom"
|
||||||
import { InputForm } from "@/components/inputForm"
|
import { InputForm } from "@/components/inputForm"
|
||||||
|
import LoadingCenter from "@/components/loadingCenter"
|
||||||
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
|
import Text from "@/components/Text"
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiCreateDiscussion } from "@/lib/api"
|
import { apiCreateDiscussion } from "@/lib/api"
|
||||||
import { setUpdateDiscussion } from "@/lib/discussionUpdate"
|
import { setUpdateDiscussion } from "@/lib/discussionUpdate"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
||||||
|
import * as DocumentPicker from "expo-document-picker"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native"
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
import { useDispatch, useSelector } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateDiscussionDivision() {
|
export default function CreateDiscussionDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [desc, setDesc] = useState('')
|
const [desc, setDesc] = useState('')
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const update = useSelector((state: any) => state.discussionUpdate)
|
const update = useSelector((state: any) => state.discussionUpdate)
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
|
const [isModalFile, setModalFile] = useState(false)
|
||||||
|
const [indexDelFile, setIndexDelFile] = useState<number>(0)
|
||||||
|
|
||||||
|
const pickDocumentAsync = async () => {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (!result.canceled) {
|
||||||
|
let skipped = 0
|
||||||
|
for (const asset of result.assets) {
|
||||||
|
if (!asset.uri) continue
|
||||||
|
if (fileForm.some(f => f.name === asset.name)) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
setFileForm(prev => [...prev, asset])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteFile(index: number) {
|
||||||
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||||
|
setModalFile(false)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiCreateDiscussion({ data: { user: hasil, desc, idDivision: id } })
|
const fd = new FormData()
|
||||||
|
for (let i = 0; i < fileForm.length; i++) {
|
||||||
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ user: hasil, desc, idDivision: id }))
|
||||||
|
const response = await apiCreateDiscussion(fd)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data' })
|
||||||
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
|
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
|
||||||
router.back()
|
router.back()
|
||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan data" })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
header: () => (
|
||||||
headerTitle: 'Tambah Diskusi',
|
<AppHeader
|
||||||
headerTitleAlign: 'center',
|
title="Tambah Diskusi"
|
||||||
headerRight: () => <ButtonSaveHeader
|
showBack={true}
|
||||||
disable={desc == ""}
|
onPressLeft={() => router.back()}
|
||||||
category="create"
|
right={
|
||||||
onPress={() => {
|
<ButtonSaveHeader
|
||||||
handleCreate()
|
disable={desc == "" || loading}
|
||||||
}} />
|
category="create"
|
||||||
|
onPress={() => handleCreate()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{loading && <LoadingCenter />}
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<InputForm label="Diskusi" type="default" placeholder="Hal yang didiskusikan" required onChange={setDesc} />
|
<InputForm
|
||||||
|
label="Diskusi"
|
||||||
|
type="default"
|
||||||
|
placeholder="Hal yang didiskusikan"
|
||||||
|
required
|
||||||
|
onChange={setDesc}
|
||||||
|
multiline
|
||||||
|
bg={colors.card}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* File */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={pickDocumentAsync}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{fileForm.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={index}
|
||||||
|
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => deleteFile(indexDelFile)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import WrapTab from "@/components/wrapTab";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDiscussion } from "@/lib/api";
|
import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api";
|
||||||
|
import { GUIDE_DIVISION_DISCUSSION } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { AntDesign, Feather } from "@expo/vector-icons";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { FlatList, Pressable, RefreshControl, View } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string,
|
id: string,
|
||||||
title: string,
|
title: string,
|
||||||
@@ -27,8 +30,8 @@ type Props = {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function DiscussionDivision() {
|
export default function DiscussionDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, active } = useLocalSearchParams<{ id: string, active?: string }>()
|
const { id, active } = useLocalSearchParams<{ id: string, active?: string }>()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -40,6 +43,22 @@ export default function DiscussionDivision() {
|
|||||||
const [waiting, setWaiting] = useState(false)
|
const [waiting, setWaiting] = useState(false)
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
const [status, setStatus] = useState<'true' | 'false'>('true')
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [isMemberDivision, setIsMemberDivision] = useState(false)
|
||||||
|
const [isAdminDivision, setIsAdminDivision] = useState(false)
|
||||||
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-discussion')
|
||||||
|
|
||||||
|
async function handleCheckMember() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" });
|
||||||
|
const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" });
|
||||||
|
setIsMemberDivision(response.data);
|
||||||
|
setIsAdminDivision(response2.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
async function handleLoad(loading: boolean, thisPage: number) {
|
||||||
try {
|
try {
|
||||||
@@ -52,8 +71,6 @@ export default function DiscussionDivision() {
|
|||||||
setData(response.data)
|
setData(response.data)
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
} else if (thisPage > 1 && response.data.length > 0) {
|
||||||
setData([...data, ...response.data])
|
setData([...data, ...response.data])
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -63,20 +80,13 @@ export default function DiscussionDivision() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false, 1) }, [update.data])
|
||||||
handleLoad(false, 1)
|
useEffect(() => { handleLoad(true, 1) }, [status, search])
|
||||||
}, [update.data])
|
useEffect(() => { handleCheckMember() }, [])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [status, search])
|
|
||||||
|
|
||||||
const loadMoreData = () => {
|
const loadMoreData = () => {
|
||||||
if (waiting) return
|
if (waiting) return
|
||||||
setTimeout(() => {
|
setTimeout(() => { handleLoad(false, page + 1) }, 1000);
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
@@ -86,121 +96,115 @@ export default function DiscussionDivision() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const isOpen = (item: Props) => item.status === 1
|
||||||
id: data[index].id,
|
|
||||||
title: data[index].title,
|
const themed = {
|
||||||
desc: data[index].desc,
|
background: { backgroundColor: colors.background },
|
||||||
status: data[index].status,
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '20' },
|
||||||
user_name: data[index].user_name,
|
cardPressed: { backgroundColor: colors.icon + '10' },
|
||||||
img: data[index].img,
|
title: { color: colors.text },
|
||||||
total_komentar: data[index].total_komentar,
|
dimmed: { color: colors.dimmed },
|
||||||
createdAt: data[index].createdAt,
|
statusOpen: { borderColor: '#10B981' as const },
|
||||||
isActive: data[index].isActive,
|
statusClosed: { borderColor: colors.dimmed + '80' },
|
||||||
})
|
statusTextOpen: { color: '#10B981' as const },
|
||||||
|
statusTextClosed: { color: colors.dimmed },
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.flex1, themed.background]}>
|
||||||
<View>
|
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_DISCUSSION} onDismiss={dismissGuide} />
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
{((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && (
|
||||||
<ButtonTab
|
<View style={[Styles.ph15, Styles.discussionHeaderPadding]}>
|
||||||
active={status == "false" ? "false" : "true"}
|
<WrapTab>
|
||||||
value="true"
|
<ButtonTab
|
||||||
onPress={() => { setStatus("true") }}
|
active={status == "false" ? "false" : "true"}
|
||||||
label="Aktif"
|
value="true"
|
||||||
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
|
onPress={() => setStatus("true")}
|
||||||
n={2} />
|
label="Aktif"
|
||||||
<ButtonTab
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
||||||
active={status == "false" ? "false" : "true"}
|
n={2}
|
||||||
value="false"
|
/>
|
||||||
onPress={() => { setStatus("false") }}
|
<ButtonTab
|
||||||
label="Arsip"
|
active={status == "false" ? "false" : "true"}
|
||||||
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
|
value="false"
|
||||||
n={2} />
|
onPress={() => setStatus("false")}
|
||||||
|
label="Arsip"
|
||||||
|
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
||||||
|
n={2}
|
||||||
|
/>
|
||||||
|
</WrapTab>
|
||||||
|
<InputSearch onChange={setSearch} />
|
||||||
</View>
|
</View>
|
||||||
<InputSearch onChange={setSearch} />
|
)}
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[{ flex: 2 }]}>
|
<View style={[Styles.flex1, Styles.ph15, Styles.discussionListPadding]}>
|
||||||
{
|
{loading ? (
|
||||||
loading ?
|
arrSkeleton.map((_, i) => <SkeletonContent key={i} />)
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
) : data.length === 0 ? (
|
||||||
return (
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
<SkeletonContent key={i} />
|
<Feather name="message-circle" size={42} color={colors.icon + '40'} />
|
||||||
)
|
<Text style={[Styles.mt10, Styles.discussionEmptyText, themed.dimmed]}>
|
||||||
})
|
Tidak ada diskusi
|
||||||
:
|
</Text>
|
||||||
data.length > 0 ?
|
</View>
|
||||||
<VirtualizedList
|
) : (
|
||||||
data={data}
|
<FlatList
|
||||||
getItemCount={() => data.length}
|
data={data}
|
||||||
getItem={getItem}
|
keyExtractor={(_, i) => String(i)}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
showsVerticalScrollIndicator={false}
|
||||||
return (
|
onEndReached={loadMoreData}
|
||||||
<BorderBottomItem
|
onEndReachedThreshold={0.5}
|
||||||
key={index}
|
refreshControl={
|
||||||
width={55}
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />
|
||||||
onPress={() => { router.push(`./discussion/${item.id}`) }}
|
}
|
||||||
borderType="bottom"
|
ItemSeparatorComponent={() => <View style={Styles.discussionSeparator} />}
|
||||||
icon={
|
renderItem={({ item }: { item: Props }) => (
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
|
<Pressable
|
||||||
}
|
onPress={() => router.push(`./discussion/${item.id}`)}
|
||||||
title={item.user_name}
|
style={({ pressed }) => [
|
||||||
subtitle={
|
Styles.discussionCard,
|
||||||
status == "true" ? item.status == 1 ? <LabelStatus category='success' text='BUKA' size="small" /> : <LabelStatus category='error' text='TUTUP' size="small" /> : <></>
|
themed.card,
|
||||||
}
|
pressed && themed.cardPressed,
|
||||||
rightTopInfo={item.createdAt}
|
]}
|
||||||
desc={item.desc}
|
>
|
||||||
leftBottomInfo={
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
|
<View style={[Styles.flex1, Styles.discussionTitleCol]}>
|
||||||
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Diskusikan</Text>
|
<Text style={[Styles.textDefaultSemiBold, themed.title]} numberOfLines={1}>
|
||||||
</View>
|
{item.user_name}
|
||||||
}
|
</Text>
|
||||||
rightBottomInfo={item.total_komentar + ' Komentar'}
|
{status === "true" && (
|
||||||
/>
|
<View style={[Styles.discussionStatusPill, isOpen(item) ? themed.statusOpen : themed.statusClosed]}>
|
||||||
)
|
<Text style={[Styles.discussionStatusText, isOpen(item) ? themed.statusTextOpen : themed.statusTextClosed]}>
|
||||||
}}
|
{isOpen(item) ? 'Buka' : 'Tutup'}
|
||||||
keyExtractor={(item, index) => String(index)}
|
</Text>
|
||||||
onEndReached={loadMoreData}
|
</View>
|
||||||
onEndReachedThreshold={0.5}
|
)}
|
||||||
showsVerticalScrollIndicator={false}
|
</View>
|
||||||
refreshControl={
|
</View>
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
{item.desc ? (
|
||||||
onRefresh={handleRefresh}
|
<Text style={[Styles.textMediumNormal, Styles.discussionCardIndent, Styles.discussionDescMargin, themed.title]} numberOfLines={2}>
|
||||||
/>
|
{item.desc}
|
||||||
}
|
</Text>
|
||||||
/>
|
) : null}
|
||||||
// data.map((item, index) => (
|
|
||||||
// <BorderBottomItem
|
<View style={[Styles.rowItemsCenter, Styles.rowSpaceBetween, Styles.discussionCardIndent]}>
|
||||||
// key={index}
|
<View style={Styles.rowItemsCenter}>
|
||||||
// width={55}
|
<Feather name="message-square" size={14} color={colors.dimmed} />
|
||||||
// onPress={() => { router.push(`./discussion/${item.id}`) }}
|
<Text style={[Styles.discussionCommentText, themed.dimmed]}>
|
||||||
// borderType="bottom"
|
{item.total_komentar} Komentar
|
||||||
// icon={
|
</Text>
|
||||||
// <ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
|
</View>
|
||||||
// }
|
<Text style={[Styles.discussionDateText, themed.dimmed]}>
|
||||||
// title={item.user_name}
|
{item.createdAt}
|
||||||
// subtitle={
|
</Text>
|
||||||
// active == "true" ? item.status == 1 ? <LabelStatus category='success' text='BUKA' size="small" /> : <LabelStatus category='error' text='TUTUP' size="small" /> : <></>
|
</View>
|
||||||
// }
|
</Pressable>
|
||||||
// rightTopInfo={item.createdAt}
|
)}
|
||||||
// desc={item.desc}
|
/>
|
||||||
// leftBottomInfo={
|
)}
|
||||||
// <View style={[Styles.rowItemsCenter]}>
|
|
||||||
// <Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
|
|
||||||
// <Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Diskusikan</Text>
|
|
||||||
// </View>
|
|
||||||
// }
|
|
||||||
// rightBottomInfo={item.total_komentar + ' Komentar'}
|
|
||||||
// />
|
|
||||||
// ))
|
|
||||||
:
|
|
||||||
(
|
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.mv10, { textAlign: "center" }]}>Tidak ada diskusi</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
import { ButtonHeader } from "@/components/buttonHeader";
|
import { ButtonHeader } from "@/components/buttonHeader";
|
||||||
import HeaderRightDocument from "@/components/document/headerDocument";
|
import HeaderRightDocument from "@/components/document/headerDocument";
|
||||||
import ItemFile from "@/components/document/itemFile";
|
import ItemFile from "@/components/document/itemFile";
|
||||||
@@ -12,16 +13,20 @@ import ModalLoading from "@/components/modalLoading";
|
|||||||
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import {
|
import {
|
||||||
apiDocumentDelete,
|
apiDocumentDelete,
|
||||||
apiDocumentRename,
|
apiDocumentRename,
|
||||||
|
apiGetDivisionOneFeature,
|
||||||
apiGetDocument,
|
apiGetDocument,
|
||||||
apiShareDocument,
|
apiShareDocument,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import { setUpdateDokumen } from "@/lib/dokumenUpdate";
|
import { setUpdateDokumen } from "@/lib/dokumenUpdate";
|
||||||
|
import { GUIDE_DIVISION_DOCUMENT } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import {
|
import {
|
||||||
AntDesign,
|
AntDesign,
|
||||||
MaterialCommunityIcons,
|
MaterialCommunityIcons,
|
||||||
@@ -64,6 +69,8 @@ type PropsPath = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function DocumentDivision() {
|
export default function DocumentDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const [loadingRename, setLoadingRename] = useState(false)
|
||||||
const [isShare, setShare] = useState(false)
|
const [isShare, setShare] = useState(false)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
@@ -83,6 +90,10 @@ export default function DocumentDivision() {
|
|||||||
const update = useSelector((state: any) => state.dokumenUpdate)
|
const update = useSelector((state: any) => state.dokumenUpdate)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
|
const [isMemberDivision, setIsMemberDivision] = useState(false)
|
||||||
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-document')
|
||||||
const [bodyRename, setBodyRename] = useState({
|
const [bodyRename, setBodyRename] = useState({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -91,6 +102,24 @@ export default function DocumentDivision() {
|
|||||||
extension: "",
|
extension: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleCheckMember() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetDivisionOneFeature({
|
||||||
|
id,
|
||||||
|
user: hasil,
|
||||||
|
cat: "check-member",
|
||||||
|
});
|
||||||
|
setIsMemberDivision(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleCheckMember()
|
||||||
|
}, [id])
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
@@ -200,6 +229,7 @@ export default function DocumentDivision() {
|
|||||||
|
|
||||||
async function handleRename() {
|
async function handleRename() {
|
||||||
try {
|
try {
|
||||||
|
setLoadingRename(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiDocumentRename({ user: hasil, ...bodyRename });
|
const response = await apiDocumentRename({ user: hasil, ...bodyRename });
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -209,11 +239,14 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah nama"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setRename(false);
|
setLoadingRename(false)
|
||||||
|
setRename(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +264,11 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menghapus"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,9 +292,11 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal membagikan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setShare(false);
|
setShare(false);
|
||||||
}
|
}
|
||||||
@@ -268,7 +305,7 @@ export default function DocumentDivision() {
|
|||||||
|
|
||||||
const openFile = (item: Props) => {
|
const openFile = (item: Props) => {
|
||||||
if (Platform.OS == 'android') setLoadingOpen(true)
|
if (Platform.OS == 'android') setLoadingOpen(true)
|
||||||
let remoteUrl = 'https://wibu-storage.wibudev.com/api/files/' + item.idStorage;
|
let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
|
||||||
const fileName = item.name + '.' + item.extension;
|
const fileName = item.name + '.' + item.extension;
|
||||||
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
||||||
const mimeType = mime.lookup(fileName)
|
const mimeType = mime.lookup(fileName)
|
||||||
@@ -309,75 +346,106 @@ export default function DocumentDivision() {
|
|||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () =>
|
// headerLeft: () =>
|
||||||
selectedFiles.length > 0 || dariSelectAll ? (
|
// selectedFiles.length > 0 || dariSelectAll ? (
|
||||||
<ButtonHeader
|
// <ButtonHeader
|
||||||
item={<MaterialIcons name="close" size={20} color="white" />}
|
// item={<MaterialIcons name="close" size={20} color="white" />}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleBatal();
|
// handleBatal();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
) : (
|
// ) : (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle:
|
headerTitle:
|
||||||
selectedFiles.length > 0 || dariSelectAll
|
selectedFiles.length > 0 || dariSelectAll
|
||||||
? `${selectedFiles.length} item terpilih`
|
? `${selectedFiles.length} item terpilih`
|
||||||
: "Dokumen Divisi",
|
: "Dokumen Divisi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () =>
|
// headerRight: () =>
|
||||||
selectedFiles.length > 0 || dariSelectAll ? (
|
// selectedFiles.length > 0 || dariSelectAll ? (
|
||||||
<ButtonHeader
|
// <ButtonHeader
|
||||||
item={
|
// item={
|
||||||
<MaterialIcons name="checklist-rtl" size={20} color="white" />
|
// <MaterialIcons name="checklist-rtl" size={20} color="white" />
|
||||||
}
|
// }
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleSelectAll();
|
// handleSelectAll();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
) : (
|
// ) : (
|
||||||
<HeaderRightDocument path={path} />
|
// <HeaderRightDocument path={path} isMember={isMemberDivision} />
|
||||||
),
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title={
|
||||||
|
selectedFiles.length > 0 || dariSelectAll
|
||||||
|
? `${selectedFiles.length} item terpilih`
|
||||||
|
: "Dokumen Divisi"
|
||||||
|
}
|
||||||
|
showBack={(selectedFiles.length > 0 || dariSelectAll) ? false : true}
|
||||||
|
left={
|
||||||
|
<ButtonHeader
|
||||||
|
item={<MaterialIcons name="close" size={25} color="white" />}
|
||||||
|
onPress={() => {
|
||||||
|
handleBatal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPressLeft={() => {
|
||||||
|
(selectedFiles.length > 0 || dariSelectAll) ? handleBatal() : router.back();
|
||||||
|
}}
|
||||||
|
right={
|
||||||
|
selectedFiles.length > 0 || dariSelectAll ? (
|
||||||
|
<ButtonHeader
|
||||||
|
item={
|
||||||
|
<MaterialIcons name="checklist-rtl" size={25} color="white" />
|
||||||
|
}
|
||||||
|
onPress={() => {
|
||||||
|
handleSelectAll();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderRightDocument path={path} isMember={isMemberDivision} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_DOCUMENT} onDismiss={dismissGuide} />
|
||||||
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[Styles.h100]}
|
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}>
|
}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
{
|
{
|
||||||
loading ?
|
dataJalur.map((item, index) => (
|
||||||
arrSkeleton.map((item, index) => (
|
<Pressable
|
||||||
<Skeleton key={index} width={60} height={10} borderRadius={10} style={[Styles.mr05]} />
|
key={index}
|
||||||
))
|
style={[Styles.rowItemsCenter]}
|
||||||
:
|
onPress={() => {
|
||||||
dataJalur.map((item, index) => (
|
setPath(item.id);
|
||||||
<Pressable
|
}}
|
||||||
key={index}
|
>
|
||||||
style={[Styles.rowItemsCenter]}
|
{item.id != "home" && (
|
||||||
onPress={() => {
|
<AntDesign name="right" style={[Styles.mh05, Styles.mt02]} color={colors.text} />
|
||||||
setPath(item.id);
|
)}
|
||||||
}}
|
<Text style={{ color: colors.text }}> {item.name} </Text>
|
||||||
>
|
</Pressable>
|
||||||
{item.id != "home" && (
|
))
|
||||||
<AntDesign name="right" style={[Styles.mh05, Styles.mt02]} color="black" />
|
|
||||||
)}
|
|
||||||
<Text> {item.name} </Text>
|
|
||||||
</Pressable>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
@@ -408,6 +476,7 @@ export default function DocumentDivision() {
|
|||||||
: `${item.name}.${item.extension}`
|
: `${item.name}.${item.extension}`
|
||||||
}
|
}
|
||||||
dateTime={item.createdAt}
|
dateTime={item.createdAt}
|
||||||
|
canChecked={(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision}
|
||||||
onChecked={() => {
|
onChecked={() => {
|
||||||
handleCheckboxChange(index);
|
handleCheckboxChange(index);
|
||||||
}}
|
}}
|
||||||
@@ -423,14 +492,7 @@ export default function DocumentDivision() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text style={[Styles.textDefault, Styles.mt15, { textAlign: "center", color: colors.dimmed }]} >
|
||||||
style={[
|
|
||||||
Styles.textDefault,
|
|
||||||
Styles.cGray,
|
|
||||||
Styles.mt15,
|
|
||||||
{ textAlign: "center" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Tidak ada dokumen
|
Tidak ada dokumen
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -438,22 +500,8 @@ export default function DocumentDivision() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{(selectedFiles.length > 0 || dariSelectAll) && (
|
{(selectedFiles.length > 0 || dariSelectAll) && (
|
||||||
<View style={[ColorsStatus.primary, Styles.bottomMenuSelectDocument]}>
|
<View style={[Styles.bottomMenuSelectDocument, { backgroundColor: colors.header }]}>
|
||||||
<View style={[Styles.rowItemsCenter, { justifyContent: "center" }]}>
|
<View style={[Styles.rowItemsCenter, { justifyContent: "center" }]}>
|
||||||
{/* <MenuItemRow
|
|
||||||
icon={
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="download-outline"
|
|
||||||
color="white"
|
|
||||||
size={25}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Unduh"
|
|
||||||
onPress={() => { }}
|
|
||||||
column="many"
|
|
||||||
color="white"
|
|
||||||
disabled={selectedFiles.length == 0 || !copyAllowed}
|
|
||||||
/> */}
|
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={
|
icon={
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
@@ -464,13 +512,7 @@ export default function DocumentDivision() {
|
|||||||
}
|
}
|
||||||
title="Hapus"
|
title="Hapus"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
AlertKonfirmasi({
|
setShowDeleteModal(true)
|
||||||
title: "Konfirmasi",
|
|
||||||
desc: "Apakah anda yakin ingin menghapus dokumen?",
|
|
||||||
onPress: () => {
|
|
||||||
handleDelete();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
column="many"
|
column="many"
|
||||||
color="white"
|
color="white"
|
||||||
@@ -551,7 +593,7 @@ export default function DocumentDivision() {
|
|||||||
isVisible={isRename}
|
isVisible={isRename}
|
||||||
setVisible={() => { setRename(false) }}
|
setVisible={() => { setRename(false) }}
|
||||||
onSubmit={() => { handleRename() }}
|
onSubmit={() => { handleRename() }}
|
||||||
disableSubmit={bodyRename.name == ""}
|
disableSubmit={bodyRename.name == "" || loadingRename}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<InputForm
|
<InputForm
|
||||||
@@ -577,6 +619,19 @@ export default function DocumentDivision() {
|
|||||||
value={id}
|
value={id}
|
||||||
item={selectedFiles[0]?.id}
|
item={selectedFiles[0]?.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin menghapus dokumen?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDelete()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ButtonSelect from "@/components/buttonSelect";
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
@@ -9,6 +9,7 @@ import Styles from "@/constants/Styles";
|
|||||||
import { apiAddFileTask, apiCheckFileTask } from "@/lib/api";
|
import { apiAddFileTask, apiCheckFileTask } from "@/lib/api";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import * as DocumentPicker from "expo-document-picker";
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
@@ -23,6 +24,7 @@ import Toast from "react-native-toast-message";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function TaskDivisionAddFile() {
|
export default function TaskDivisionAddFile() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const [fileForm, setFileForm] = useState<any[]>([]);
|
const [fileForm, setFileForm] = useState<any[]>([]);
|
||||||
const [listFile, setListFile] = useState<any[]>([]);
|
const [listFile, setListFile] = useState<any[]>([]);
|
||||||
@@ -32,6 +34,7 @@ export default function TaskDivisionAddFile() {
|
|||||||
const [loadingCheck, setLoadingCheck] = useState(false);
|
const [loadingCheck, setLoadingCheck] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const update = useSelector((state: any) => state.taskUpdate);
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const pickDocumentAsync = async () => {
|
const pickDocumentAsync = async () => {
|
||||||
let result = await DocumentPicker.getDocumentAsync({
|
let result = await DocumentPicker.getDocumentAsync({
|
||||||
@@ -90,6 +93,7 @@ export default function TaskDivisionAddFile() {
|
|||||||
|
|
||||||
async function handleAddFile() {
|
async function handleAddFile() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
|
|
||||||
@@ -116,32 +120,50 @@ export default function TaskDivisionAddFile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan file"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah File",
|
headerTitle: "Tambah File",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="create"
|
// category="create"
|
||||||
disable={fileForm.length == 0 ? true : false}
|
// disable={fileForm.length == 0 || loading ? true : false}
|
||||||
onPress={() => { handleAddFile() }}
|
// onPress={() => { handleAddFile() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah File"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={fileForm.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => { handleAddFile() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
@@ -151,13 +173,13 @@ export default function TaskDivisionAddFile() {
|
|||||||
listFile.length > 0 && (
|
listFile.length > 0 && (
|
||||||
<View style={[Styles.mb15]}>
|
<View style={[Styles.mb15]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
||||||
<View style={[Styles.wrapPaper]}>
|
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
{
|
{
|
||||||
listFile.map((item, index) => (
|
listFile.map((item, index) => (
|
||||||
<BorderBottomItem
|
<BorderBottomItem
|
||||||
key={index}
|
key={index}
|
||||||
borderType="all"
|
borderType="all"
|
||||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
|
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||||
title={item}
|
title={item}
|
||||||
titleWeight="normal"
|
titleWeight="normal"
|
||||||
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
||||||
@@ -171,12 +193,15 @@ export default function TaskDivisionAddFile() {
|
|||||||
{
|
{
|
||||||
loadingCheck && <ActivityIndicator size="small" />
|
loadingCheck && <ActivityIndicator size="small" />
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
loading && <ActivityIndicator size="large" />
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<Ionicons name="trash" color="black" size={25} />}
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
title="Hapus"
|
title="Hapus"
|
||||||
onPress={() => { deleteFile(indexDelFile) }}
|
onPress={() => { deleteFile(indexDelFile) }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiAddMemberTask, apiGetDivisionMember, apiGetTaskOne } from "@/lib/api";
|
import { apiAddMemberTask, apiGetDivisionMember, apiGetTaskOne } from "@/lib/api";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddMemberTask() {
|
export default function AddMemberTask() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -30,6 +33,7 @@ export default function AddMemberTask() {
|
|||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoadOldMember() {
|
async function handleLoadOldMember() {
|
||||||
try {
|
try {
|
||||||
@@ -72,6 +76,7 @@ export default function AddMemberTask() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiAddMemberTask({ id: detail, data: { user: hasil, member: selectMember, idDivision: id } })
|
const response = await apiAddMemberTask({ id: detail, data: { user: hasil, member: selectMember, idDivision: id } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -81,45 +86,65 @@ export default function AddMemberTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah Anggota Kegiatan',
|
headerTitle: 'Tambah Anggota Kegiatan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Anggota Kegiatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
||||||
|
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -128,9 +153,11 @@ export default function AddMemberTask() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -139,22 +166,22 @@ export default function AddMemberTask() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.idUser, item.name, item.img)
|
!found && onChoose(item.idUser, item.name, item.img)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.w80]}>
|
<View style={[Styles.rowItemsCenter, Styles.w80,]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1}>{item.name}</Text>
|
<Text style={[Styles.textDefault]} numberOfLines={1}>{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -165,6 +192,6 @@ export default function AddMemberTask() {
|
|||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateTaskTugas } from "@/lib/api";
|
import { apiCreateTaskTugas } from "@/lib/api";
|
||||||
|
import { formatDateOnly } from "@/lib/fun_formatDateOnly";
|
||||||
|
import { getDatesInRange } from "@/lib/fun_getDatesInRange";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import dayjs from "dayjs";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import 'intl';
|
||||||
|
import 'intl/locale-data/jsonp/id';
|
||||||
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView, Platform, SafeAreaView,
|
KeyboardAvoidingView, Platform,
|
||||||
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
View
|
View
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
@@ -19,11 +28,14 @@ import DateTimePicker, { DateType } from "react-native-ui-datepicker";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function TaskDivisionAddTask() {
|
export default function TaskDivisionAddTask() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const update = useSelector((state: any) => state.taskUpdate);
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const [disable, setDisable] = useState(true);
|
const [disable, setDisable] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const [range, setRange] = useState<{
|
const [range, setRange] = useState<{
|
||||||
startDate: DateType;
|
startDate: DateType;
|
||||||
endDate: DateType;
|
endDate: DateType;
|
||||||
@@ -33,12 +45,13 @@ export default function TaskDivisionAddTask() {
|
|||||||
endDate: false,
|
endDate: false,
|
||||||
title: false,
|
title: false,
|
||||||
});
|
});
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("")
|
||||||
|
const [dataDetail, setDataDetail] = useState<any>([])
|
||||||
|
const [modalDetail, setModalDetail] = useState(false)
|
||||||
|
const [dsbButton, setDsbButton] = useState(true)
|
||||||
|
|
||||||
const from = range.startDate
|
const from = formatDateOnly(range.startDate);
|
||||||
? dayjs(range.startDate).format("DD-MM-YYYY")
|
const to = formatDateOnly(range.endDate);
|
||||||
: "";
|
|
||||||
const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : "";
|
|
||||||
|
|
||||||
function checkAll() {
|
function checkAll() {
|
||||||
if (
|
if (
|
||||||
@@ -67,20 +80,49 @@ export default function TaskDivisionAddTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkButton() {
|
||||||
|
if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) {
|
||||||
|
setDsbButton(true)
|
||||||
|
setDataDetail([])
|
||||||
|
} else {
|
||||||
|
setDsbButton(false)
|
||||||
|
const awal = formatDateOnly(range.startDate, "YYYY-MM-DD")
|
||||||
|
const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD")
|
||||||
|
const datanya = getDatesInRange(awal, akhir)
|
||||||
|
setDataDetail(datanya.map((item: any) => ({
|
||||||
|
date: item,
|
||||||
|
timeStart: null,
|
||||||
|
timeEnd: null,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAll();
|
checkAll();
|
||||||
}, [from, to, title, error]);
|
}, [from, to, title, error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkButton()
|
||||||
|
}, [range])
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const dataDetailFix = dataDetail.map((item: any) => ({
|
||||||
|
date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"),
|
||||||
|
timeStart: item.timeStart,
|
||||||
|
timeEnd: item.timeEnd,
|
||||||
|
}))
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiCreateTaskTugas({
|
const response = await apiCreateTaskTugas({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
dateStart: dayjs(range.startDate).format("YYYY-MM-DD"),
|
dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"),
|
||||||
dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"),
|
dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"),
|
||||||
user: hasil,
|
user: hasil,
|
||||||
idDivision: id,
|
idDivision: id,
|
||||||
|
dataDetail: dataDetailFix,
|
||||||
},
|
},
|
||||||
id: detail,
|
id: detail,
|
||||||
});
|
});
|
||||||
@@ -91,43 +133,63 @@ export default function TaskDivisionAddTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambah data', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah Tugas",
|
headerTitle: "Tambah Tugas",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="create"
|
// category="create"
|
||||||
disable={disable}
|
// disable={disable || loading}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleCreate();
|
// handleCreate();
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={disable || loading}
|
||||||
|
onPress={() => {
|
||||||
|
handleCreate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
mode="range"
|
mode="range"
|
||||||
startDate={range.startDate}
|
startDate={range.startDate}
|
||||||
@@ -137,6 +199,15 @@ export default function TaskDivisionAddTask() {
|
|||||||
selected: Styles.selectedDate,
|
selected: Styles.selectedDate,
|
||||||
selected_label: Styles.cWhite,
|
selected_label: Styles.cWhite,
|
||||||
range_fill: Styles.selectRangeDate,
|
range_fill: Styles.selectRangeDate,
|
||||||
|
month_label: { color: colors.text },
|
||||||
|
month_selector_label: { color: colors.text },
|
||||||
|
year_label: { color: colors.text },
|
||||||
|
year_selector_label: { color: colors.text },
|
||||||
|
day_label: { color: colors.text },
|
||||||
|
time_label: { color: colors.text },
|
||||||
|
weekday_label: { color: colors.text },
|
||||||
|
button_next_image: { tintColor: colors.text },
|
||||||
|
button_prev_image: { tintColor: colors.text },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -144,31 +215,39 @@ export default function TaskDivisionAddTask() {
|
|||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Mulai <Text style={Styles.cError}>*</Text>
|
Tanggal Mulai <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{from}</Text>
|
<Text style={{ textAlign: "center" }}>{from}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Berakhir <Text style={Styles.cError}>*</Text>
|
Tanggal Berakhir <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{to}</Text>
|
<Text style={{ textAlign: "center" }}>{to}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, Styles.cError, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, { color: colors.error }, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
||||||
}
|
}
|
||||||
|
{/* <Pressable
|
||||||
|
style={[Styles.btnTab, Styles.btnLainnya, dsbButton && Styles.btnDisabled]}
|
||||||
|
disabled={dsbButton}
|
||||||
|
onPress={() => { setModalDetail(true) }}
|
||||||
|
>
|
||||||
|
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||||
|
</Pressable> */}
|
||||||
|
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||||
</View>
|
</View>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul Tugas"
|
label="Judul Tugas"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Tugas"
|
placeholder="Judul Tugas"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={title}
|
value={title}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
@@ -179,7 +258,14 @@ export default function TaskDivisionAddTask() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
<ModalAddDetailTugasTask
|
||||||
|
isVisible={modalDetail}
|
||||||
|
setVisible={setModalDetail}
|
||||||
|
dataTanggal={dataDetail}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
setDataDetail(data)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCancelTask } from "@/lib/api";
|
import { apiCancelTask } from "@/lib/api";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
@@ -12,6 +13,7 @@ import Toast from "react-native-toast-message";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function TaskDivisionCancel() {
|
export default function TaskDivisionCancel() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -19,6 +21,7 @@ export default function TaskDivisionCancel() {
|
|||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [disable, setDisable] = useState(false);
|
const [disable, setDisable] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
function onValidation(val: string) {
|
function onValidation(val: string) {
|
||||||
setReason(val);
|
setReason(val);
|
||||||
@@ -43,6 +46,7 @@ export default function TaskDivisionCancel() {
|
|||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiCancelTask(
|
const response = await apiCancelTask(
|
||||||
{
|
{
|
||||||
@@ -58,34 +62,54 @@ export default function TaskDivisionCancel() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal membatalkan kegiatan', })
|
const message = error?.response?.data?.message || "Gagal membatalkan kegiatan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Pembatalan Tugas",
|
headerTitle: "Pembatalan Tugas",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={disable}
|
// disable={disable || loading}
|
||||||
category="cancel"
|
// category="cancel"
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleCancel();
|
// handleCancel();
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Pembatalan Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disable || loading}
|
||||||
|
category="cancel"
|
||||||
|
onPress={() => {
|
||||||
|
handleCancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
@@ -95,7 +119,7 @@ export default function TaskDivisionCancel() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Alasan Pembatalan"
|
placeholder="Alasan Pembatalan"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
error={error}
|
error={error}
|
||||||
errorText="Alasan pembatalan harus diisi"
|
errorText="Alasan pembatalan harus diisi"
|
||||||
onChange={(val) => onValidation(val)}
|
onChange={(val) => onValidation(val)}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditTask, apiGetTaskOne } from "@/lib/api";
|
import { apiEditTask, apiGetTaskOne } from "@/lib/api";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
@@ -12,6 +13,7 @@ import Toast from "react-native-toast-message";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function TaskDivisionEdit() {
|
export default function TaskDivisionEdit() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [judul, setJudul] = useState("");
|
const [judul, setJudul] = useState("");
|
||||||
@@ -19,6 +21,7 @@ export default function TaskDivisionEdit() {
|
|||||||
const [disable, setDisable] = useState(false);
|
const [disable, setDisable] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const update = useSelector((state: any) => state.taskUpdate);
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
@@ -61,6 +64,7 @@ export default function TaskDivisionEdit() {
|
|||||||
|
|
||||||
async function handleUpdate() {
|
async function handleUpdate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiEditTask(
|
const response = await apiEditTask(
|
||||||
{
|
{
|
||||||
@@ -76,32 +80,49 @@ export default function TaskDivisionEdit() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Edit Judul",
|
headerTitle: "Edit Judul",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={disable}
|
// disable={disable || loading}
|
||||||
onPress={() => { handleUpdate() }}
|
// onPress={() => { handleUpdate() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Tambah Kegiatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={disable || loading}
|
||||||
|
onPress={() => { handleUpdate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
@@ -111,7 +132,7 @@ export default function TaskDivisionEdit() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Kegiatan"
|
placeholder="Judul Kegiatan"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={judul}
|
value={judul}
|
||||||
onChange={(val) => { onValidation(val) }}
|
onChange={(val) => { onValidation(val) }}
|
||||||
error={error}
|
error={error}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import SectionCancel from "@/components/sectionCancel";
|
import SectionCancel from "@/components/sectionCancel";
|
||||||
import SectionProgress from "@/components/sectionProgress";
|
import SectionProgress from "@/components/sectionProgress";
|
||||||
import HeaderRightTaskDetail from "@/components/task/headerTaskDetail";
|
import HeaderRightTaskDetail from "@/components/task/headerTaskDetail";
|
||||||
import SectionFileTask from "@/components/task/sectionFileTask";
|
import SectionFileTask from "@/components/task/sectionFileTask";
|
||||||
|
import SectionLinkTask from "@/components/task/sectionLinkTask";
|
||||||
import SectionMemberTask from "@/components/task/sectionMemberTask";
|
import SectionMemberTask from "@/components/task/sectionMemberTask";
|
||||||
|
import SectionReportTask from "@/components/task/sectionReportTask";
|
||||||
import SectionTanggalTugasTask from "@/components/task/sectionTanggalTugasTask";
|
import SectionTanggalTugasTask from "@/components/task/sectionTanggalTugasTask";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetTaskOne } from "@/lib/api";
|
import { apiGetDivisionOneFeature, apiGetTaskOne } from "@/lib/api";
|
||||||
|
import { GUIDE_PROJECT_DETAIL } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
@@ -20,17 +26,61 @@ type Props = {
|
|||||||
reason: string
|
reason: string
|
||||||
status: number
|
status: number
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
idGroup: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailTaskDivision() {
|
export default function DetailTaskDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [data, setData] = useState<Props>()
|
const [data, setData] = useState<Props>()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [progress, setProgress] = useState(0)
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [taskStats, setTaskStats] = useState<{ done: number, total: number } | undefined>()
|
||||||
const update = useSelector((state: any) => state.taskUpdate)
|
const update = useSelector((state: any) => state.taskUpdate)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [isMemberDivision, setIsMemberDivision] = useState(false);
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-task-detail')
|
||||||
|
const [isAdminDivision, setIsAdminDivision] = useState(false);
|
||||||
|
const entityUser = useSelector((state: any) => state.user);
|
||||||
|
|
||||||
|
async function handleCheckMember() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetDivisionOneFeature({
|
||||||
|
id,
|
||||||
|
user: hasil,
|
||||||
|
cat: "check-member",
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsMemberDivision(response.data);
|
||||||
|
|
||||||
|
const response2 = await apiGetDivisionOneFeature({
|
||||||
|
id,
|
||||||
|
user: hasil,
|
||||||
|
cat: "check-admin",
|
||||||
|
});
|
||||||
|
setIsAdminDivision(response2.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleCheckMember()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
async function handleLoadTaskStats() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiGetTaskOne({ id: detail, user: hasil, cat: 'task' })
|
||||||
|
const tasks: { status: number }[] = response.data
|
||||||
|
setTaskStats({ done: tasks.filter(t => t.status === 1).length, total: tasks.length })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLoad(cat: 'data' | 'progress') {
|
async function handleLoad(cat: 'data' | 'progress') {
|
||||||
try {
|
try {
|
||||||
@@ -57,29 +107,50 @@ export default function DetailTaskDivision() {
|
|||||||
handleLoad('progress')
|
handleLoad('progress')
|
||||||
}, [update.progress])
|
}, [update.progress])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleLoadTaskStats()
|
||||||
|
}, [update.task])
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
await handleLoad('data')
|
await handleLoad('data')
|
||||||
await handleLoad('progress')
|
await handleLoad('progress')
|
||||||
|
await handleLoadTaskStats()
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: loading ? 'Loading... ' : data?.title,
|
headerTitle: loading ? 'Loading... ' : data?.title,
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightTaskDetail id={detail} division={id} status={data?.status} />,
|
// headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision
|
||||||
|
// ? <></>
|
||||||
|
// : <HeaderRightTaskDetail id={detail} division={id} status={data?.status} isAdminDivision={isAdminDivision} />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title={loading ? 'Loading...' : data ? data?.title : ''}
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
(entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision
|
||||||
|
? <></>
|
||||||
|
: <HeaderRightTaskDetail id={detail} division={id} status={data?.status} isAdminDivision={isAdminDivision} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GuideOverlay visible={guideVisible} steps={GUIDE_PROJECT_DETAIL} onDismiss={dismissGuide} />
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -87,10 +158,12 @@ export default function DetailTaskDivision() {
|
|||||||
{
|
{
|
||||||
data?.reason != null && data?.reason != "" && <SectionCancel text={data?.reason} />
|
data?.reason != null && data?.reason != "" && <SectionCancel text={data?.reason} />
|
||||||
}
|
}
|
||||||
<SectionProgress text={`Kemajuan Kegiatan ${progress}%`} progress={progress} />
|
<SectionProgress progress={progress} doneCount={taskStats?.done} totalCount={taskStats?.total} />
|
||||||
<SectionTanggalTugasTask refreshing={refreshing}/>
|
<SectionReportTask refreshing={refreshing} />
|
||||||
<SectionFileTask refreshing={refreshing}/>
|
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} isAdminDivision={isAdminDivision} status={data?.status} idGroup={data?.idGroup ?? ''} />
|
||||||
<SectionMemberTask refreshing={refreshing}/>
|
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||||
|
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||||
|
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import Styles from "@/constants/Styles";
|
||||||
|
import { apiGetTaskOne, apiReportTask } from "@/lib/api";
|
||||||
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
export default function TaskDivisionReport() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
|
||||||
|
const { token, decryptToken } = useAuthSession();
|
||||||
|
const [laporan, setLaporan] = useState("");
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [disable, setDisable] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function handleLoad() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetTaskOne({
|
||||||
|
user: hasil,
|
||||||
|
cat: "data",
|
||||||
|
id: detail,
|
||||||
|
});
|
||||||
|
setLaporan(response.data.report);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleLoad();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function onValidation(val: string) {
|
||||||
|
setLaporan(val);
|
||||||
|
if (val == "" || val == "null") {
|
||||||
|
setError(true);
|
||||||
|
} else {
|
||||||
|
setError(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAll() {
|
||||||
|
if (laporan == "" || laporan == "null" || laporan == undefined || laporan == null || error) {
|
||||||
|
setDisable(true);
|
||||||
|
} else {
|
||||||
|
setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAll();
|
||||||
|
}, [laporan, error]);
|
||||||
|
|
||||||
|
async function handleUpdate() {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiReportTask(
|
||||||
|
{
|
||||||
|
report: laporan,
|
||||||
|
user: hasil,
|
||||||
|
},
|
||||||
|
detail
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
dispatch(setUpdateTask({ ...update, report: !update.report }));
|
||||||
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
|
}
|
||||||
|
} catch (error : any ) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
// headerLeft: () => (
|
||||||
|
// <ButtonBackHeader
|
||||||
|
// onPress={() => {
|
||||||
|
// router.back();
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
headerTitle: "Laporan Kegiatan",
|
||||||
|
headerTitleAlign: "center",
|
||||||
|
// headerRight: () => (
|
||||||
|
// <ButtonSaveHeader
|
||||||
|
// category="update"
|
||||||
|
// disable={disable || loading}
|
||||||
|
// onPress={() => { handleUpdate() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Laporan Kegiatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={disable || loading}
|
||||||
|
onPress={() => { handleUpdate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ScrollView>
|
||||||
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
|
<InputForm
|
||||||
|
label="Laporan Kegiatan"
|
||||||
|
type="default"
|
||||||
|
placeholder="Laporan Kegiatan"
|
||||||
|
required
|
||||||
|
bg={colors.card}
|
||||||
|
value={laporan}
|
||||||
|
onChange={(val) => { onValidation(val) }}
|
||||||
|
error={error}
|
||||||
|
errorText="Laporan kegiatan harus diisi"
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
|
import { ButtonForm } from "@/components/buttonForm";
|
||||||
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
|
import ModalLoading from "@/components/modalLoading";
|
||||||
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import Skeleton from "@/components/skeleton";
|
||||||
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
|
import Styles from "@/constants/Styles";
|
||||||
|
import {
|
||||||
|
apiAddTugasTaskFile,
|
||||||
|
apiDeleteTugasTaskFile,
|
||||||
|
apiGetTaskOne,
|
||||||
|
apiGetTugasTaskFile,
|
||||||
|
apiLinkTugasTaskFile,
|
||||||
|
} from "@/lib/api";
|
||||||
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { startActivityAsync } from "expo-intent-launcher";
|
||||||
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import * as Sharing from "expo-sharing";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
Platform,
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import * as mime from "react-native-mime-types";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
type FileItem = {
|
||||||
|
id: string; // DivisionProjectTaskFile.id
|
||||||
|
idFile: string; // DivisionProjectFile.id
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
idStorage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectFile = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
idStorage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TugasFileScreen() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { id, detail, taskId, member: memberParam } = useLocalSearchParams<{
|
||||||
|
id: string;
|
||||||
|
detail: string;
|
||||||
|
taskId: string;
|
||||||
|
member: string;
|
||||||
|
}>();
|
||||||
|
const { token, decryptToken } = useAuthSession();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
|
const entityUser = useSelector((state: any) => state.user);
|
||||||
|
const isMember = memberParam === "true";
|
||||||
|
const canEdit = isMember || (entityUser.role !== "user" && entityUser.role !== "coadmin");
|
||||||
|
|
||||||
|
const [data, setData] = useState<FileItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingOpen, setLoadingOpen] = useState(false);
|
||||||
|
const [loadingUpload, setLoadingUpload] = useState(false);
|
||||||
|
const [loadingLink, setLoadingLink] = useState(false);
|
||||||
|
|
||||||
|
const [selectFile, setSelectFile] = useState<FileItem | null>(null);
|
||||||
|
const [isMenuModal, setMenuModal] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
|
const [projectFiles, setProjectFiles] = useState<ProjectFile[]>([]);
|
||||||
|
const [isPickerModal, setPickerModal] = useState(false);
|
||||||
|
const [loadingProjectFiles, setLoadingProjectFiles] = useState(false);
|
||||||
|
const [selectedProjectFiles, setSelectedProjectFiles] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const arrSkeleton = Array.from({ length: 4 });
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetTugasTaskFile({ user: hasil, id: taskId });
|
||||||
|
setData(response.data ?? []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjectFiles() {
|
||||||
|
try {
|
||||||
|
setLoadingProjectFiles(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetTaskOne({ id: detail, user: hasil, cat: "file" });
|
||||||
|
setProjectFiles(response.data ?? []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoadingProjectFiles(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openFile = () => {
|
||||||
|
setMenuModal(false);
|
||||||
|
setLoadingOpen(true);
|
||||||
|
const remoteUrl = ConstEnv.url_storage + "/files/" + selectFile?.idStorage;
|
||||||
|
const fileName = selectFile?.name + "." + selectFile?.extension;
|
||||||
|
const localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
||||||
|
const mimeType = mime.lookup(fileName);
|
||||||
|
|
||||||
|
FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => {
|
||||||
|
const contentURL = await FileSystem.getContentUriAsync(uri);
|
||||||
|
try {
|
||||||
|
if (Platform.OS === "android") {
|
||||||
|
await startActivityAsync("android.intent.action.VIEW", {
|
||||||
|
data: contentURL,
|
||||||
|
flags: 1,
|
||||||
|
type: mimeType as string,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Sharing.shareAsync(localPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Alert.alert("INFO", "Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini");
|
||||||
|
} finally {
|
||||||
|
setLoadingOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiDeleteTugasTaskFile({ user: hasil }, String(selectFile?.id));
|
||||||
|
if (response.success) {
|
||||||
|
Toast.show({ type: "small", text1: "Berhasil menghapus file" });
|
||||||
|
dispatch(setUpdateTask({ ...update, task: !update.task }));
|
||||||
|
loadFiles();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: "small", text1: response.message });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus file";
|
||||||
|
Toast.show({ type: "small", text1: message });
|
||||||
|
} finally {
|
||||||
|
setMenuModal(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload() {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||||
|
if (result.canceled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingUpload(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const fd = new FormData();
|
||||||
|
|
||||||
|
for (let i = 0; i < result.assets.length; i++) {
|
||||||
|
fd.append(`file${i}`, {
|
||||||
|
uri: result.assets[i].uri,
|
||||||
|
type: "application/octet-stream",
|
||||||
|
name: result.assets[i].name,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
fd.append("data", JSON.stringify({ user: hasil }));
|
||||||
|
|
||||||
|
const response = await apiAddTugasTaskFile({ data: fd, id: taskId });
|
||||||
|
if (response.success) {
|
||||||
|
Toast.show({ type: "small", text1: "Berhasil menambahkan file" });
|
||||||
|
dispatch(setUpdateTask({ ...update, task: !update.task }));
|
||||||
|
loadFiles();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: "small", text1: response.message });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan file";
|
||||||
|
Toast.show({ type: "small", text1: message });
|
||||||
|
} finally {
|
||||||
|
setLoadingUpload(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleProjectFileSelect(id: string) {
|
||||||
|
setSelectedProjectFiles((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLinkFiles() {
|
||||||
|
if (selectedProjectFiles.length === 0) return;
|
||||||
|
try {
|
||||||
|
setLoadingLink(true);
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
for (const idFile of selectedProjectFiles) {
|
||||||
|
await apiLinkTugasTaskFile({ user: hasil, idFile, id: taskId });
|
||||||
|
}
|
||||||
|
Toast.show({ type: "small", text1: "Berhasil menambahkan file" });
|
||||||
|
dispatch(setUpdateTask({ ...update, task: !update.task }));
|
||||||
|
setPickerModal(false);
|
||||||
|
setSelectedProjectFiles([]);
|
||||||
|
loadFiles();
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan file";
|
||||||
|
Toast.show({ type: "small", text1: message });
|
||||||
|
} finally {
|
||||||
|
setLoadingLink(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachedFileIds = new Set(data.map((f) => f.idFile));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="File Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
||||||
|
|
||||||
|
<ScrollView>
|
||||||
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
|
{canEdit && (
|
||||||
|
<>
|
||||||
|
<ButtonSelect
|
||||||
|
value="Upload dari Perangkat"
|
||||||
|
onPress={handleUpload}
|
||||||
|
disabled={loadingUpload}
|
||||||
|
/>
|
||||||
|
<ButtonSelect
|
||||||
|
value="Pilih dari File Kegiatan ini"
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedProjectFiles([]);
|
||||||
|
setPickerModal(true);
|
||||||
|
loadProjectFiles();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loadingUpload && <ActivityIndicator size="small" style={Styles.mv05} />}
|
||||||
|
|
||||||
|
<View style={[Styles.mb15, Styles.mt10]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File Terlampir</Text>
|
||||||
|
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
|
{loading ? (
|
||||||
|
arrSkeleton.map((_, index) => (
|
||||||
|
<Skeleton key={index} width={100} height={40} widthType="percent" borderRadius={10} />
|
||||||
|
))
|
||||||
|
) : data.length > 0 ? (
|
||||||
|
data.map((item, index) => (
|
||||||
|
<BorderBottomItem
|
||||||
|
key={index}
|
||||||
|
borderType="all"
|
||||||
|
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||||
|
title={item.name + "." + item.extension}
|
||||||
|
titleWeight="normal"
|
||||||
|
onPress={() => {
|
||||||
|
setSelectFile(item);
|
||||||
|
setMenuModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text style={[Styles.textDefault, { textAlign: "center", color: colors.dimmed }]}>
|
||||||
|
Tidak ada file
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Menu per file */}
|
||||||
|
<DrawerBottom animation="slide" isVisible={isMenuModal} setVisible={setMenuModal} title="Menu">
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<MaterialCommunityIcons name="file-eye" color={colors.text} size={25} />}
|
||||||
|
title="Lihat / Share"
|
||||||
|
onPress={openFile}
|
||||||
|
/>
|
||||||
|
{canEdit && (
|
||||||
|
<MenuItemRow
|
||||||
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
|
title="Hapus"
|
||||||
|
onPress={() => {
|
||||||
|
setMenuModal(false);
|
||||||
|
setTimeout(() => setShowDeleteModal(true), 600);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah Anda yakin ingin menghapus file ini?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Hapus"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Picker file dari proyek */}
|
||||||
|
<DrawerBottom
|
||||||
|
animation="slide"
|
||||||
|
isVisible={isPickerModal}
|
||||||
|
setVisible={setPickerModal}
|
||||||
|
title="Pilih File Proyek"
|
||||||
|
height={60}
|
||||||
|
>
|
||||||
|
<ScrollView>
|
||||||
|
{loadingProjectFiles ? (
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
) : projectFiles.length > 0 ? (
|
||||||
|
projectFiles.map((item, index) => {
|
||||||
|
const isAttached = attachedFileIds.has(item.id);
|
||||||
|
const isSelected = selectedProjectFiles.includes(item.id);
|
||||||
|
return (
|
||||||
|
<View key={index} style={isAttached ? { opacity: 0.4 } : undefined}>
|
||||||
|
<BorderBottomItem
|
||||||
|
borderType="bottom"
|
||||||
|
icon={
|
||||||
|
isAttached || isSelected ? (
|
||||||
|
<Ionicons name="checkmark-circle" size={25} color={colors.primary} />
|
||||||
|
) : (
|
||||||
|
<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={item.name + "." + item.extension}
|
||||||
|
titleWeight="normal"
|
||||||
|
onPress={() => !isAttached && toggleProjectFileSelect(item.id)}
|
||||||
|
bgColor="transparent"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Text style={[Styles.textDefault, { textAlign: "center", color: colors.dimmed }]}>
|
||||||
|
Tidak ada file tersedia
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
{projectFiles.length > 0 && (
|
||||||
|
<View>
|
||||||
|
<ButtonForm
|
||||||
|
text={loadingLink ? "Menyimpan..." : `Tambahkan (${selectedProjectFiles.length})`}
|
||||||
|
disabled={selectedProjectFiles.length === 0 || loadingLink}
|
||||||
|
onPress={handleLinkFiles} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</DrawerBottom>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ButtonSelect from "@/components/buttonSelect";
|
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
@@ -9,22 +7,45 @@ import MenuItemRow from "@/components/menuItemRow";
|
|||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SectionListAddTask from "@/components/project/sectionListAddTask";
|
import SectionListAddTask from "@/components/project/sectionListAddTask";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateTask } from "@/lib/api";
|
import { apiCreateTask } from "@/lib/api";
|
||||||
import { setMemberChoose } from "@/lib/memberChoose";
|
import { setMemberChoose } from "@/lib/memberChoose";
|
||||||
import { setTaskCreate } from "@/lib/taskCreate";
|
import { setTaskCreate } from "@/lib/taskCreate";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import * as DocumentPicker from "expo-document-picker";
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
|
||||||
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||||
|
if (ext === 'pdf') return 'file-pdf-box'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||||
|
return 'file-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileColor(ext: string): string {
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||||
|
if (ext === 'pdf') return '#F03E3E'
|
||||||
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||||
|
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||||
|
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||||
|
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||||
|
return '#868E96'
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateTaskDivision() {
|
export default function CreateTaskDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -38,6 +59,7 @@ export default function CreateTaskDivision() {
|
|||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
let hitung = 0;
|
let hitung = 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,6 +99,7 @@ export default function CreateTaskDivision() {
|
|||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|
||||||
@@ -100,32 +123,51 @@ export default function CreateTaskDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleBack();
|
// handleBack();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: `Tambah Tugas`,
|
headerTitle: `Tambah Tugas`,
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={title == "" || entitiesMember.length == 0 || taskCreate.length == 0}
|
// disable={title == "" || entitiesMember.length == 0 || taskCreate.length == 0 || loading}
|
||||||
category="create"
|
// category="create"
|
||||||
onPress={() => { handleCreate() }}
|
// onPress={() => { handleCreate() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={title == "" || entitiesMember.length == 0 || taskCreate.length == 0 || loading}
|
||||||
|
category="create"
|
||||||
|
onPress={() => { handleCreate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
@@ -141,61 +183,134 @@ export default function CreateTaskDivision() {
|
|||||||
val == "" || val == "null" ? setError(true) : setError(false);
|
val == "" || val == "null" ? setError(true) : setError(false);
|
||||||
}}
|
}}
|
||||||
error={error}
|
error={error}
|
||||||
|
bg={colors.card}
|
||||||
errorText="Judul Tugas tidak boleh kosong"
|
errorText="Judul Tugas tidak boleh kosong"
|
||||||
/>
|
/>
|
||||||
<ButtonSelect value="Tambah Tanggal & Tugas" onPress={() => { router.push(`/division/${id}/task/create/task`); }} />
|
|
||||||
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
|
|
||||||
<ButtonSelect value="Tambah Anggota" onPress={() => { router.push(`/division/${id}/task/create/member`); }} />
|
|
||||||
<SectionListAddTask />
|
|
||||||
{
|
|
||||||
fileForm.length > 0 && (
|
|
||||||
<View style={[Styles.mb15]}>
|
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
|
||||||
<View style={[Styles.wrapPaper]}>
|
|
||||||
{
|
|
||||||
fileForm.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="all"
|
|
||||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
|
|
||||||
title={item.name}
|
|
||||||
titleWeight="normal"
|
|
||||||
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{entitiesMember.length > 0 && (
|
|
||||||
<View>
|
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
|
||||||
<Text>Anggota</Text>
|
|
||||||
<Text>Total {entitiesMember.length} Anggota</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[Styles.borderAll, Styles.round10, Styles.p10]}>
|
{/* Tanggal & Tugas */}
|
||||||
{entitiesMember.map(
|
<View style={[
|
||||||
(item: { img: any; name: any }, index: any) => {
|
Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
return (
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }
|
||||||
<BorderBottomItem
|
]}>
|
||||||
key={index}
|
<Pressable
|
||||||
borderType="bottom"
|
onPress={() => router.push(`/division/${id}/task/create/task`)}
|
||||||
icon={
|
style={[Styles.sectionActionRow, { marginBottom: taskCreate.length > 0 ? 12 : 0 }]}
|
||||||
<ImageUser
|
>
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
size="sm"
|
<MaterialCommunityIcons name="calendar-check-outline" size={18} color={colors.tabActive} />
|
||||||
/>
|
</View>
|
||||||
}
|
<View style={Styles.flex1}>
|
||||||
title={item.name}
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Tanggal & Tugas</Text>
|
||||||
/>
|
{taskCreate.length === 0 && (
|
||||||
);
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada tugas ditambahkan</Text>
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
{taskCreate.length > 0 && (
|
||||||
)}
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{taskCreate.length} tugas</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{taskCreate.length > 0 && <SectionListAddTask showTitle={false} />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* File */}
|
||||||
|
<View style={[
|
||||||
|
Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }
|
||||||
|
]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={pickDocumentAsync}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||||
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||||
|
{fileForm.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{fileForm.length > 0 && (
|
||||||
|
<View style={Styles.fileGrid}>
|
||||||
|
{fileForm.map((item, index) => {
|
||||||
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||||
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||||
|
const iconName = getFileIcon(ext)
|
||||||
|
const iconColor = getFileColor(ext)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={index}
|
||||||
|
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
||||||
|
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||||
|
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Anggota */}
|
||||||
|
<View style={[
|
||||||
|
Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }
|
||||||
|
]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push(`/division/${id}/task/create/member`)}
|
||||||
|
style={[Styles.sectionActionRow, { marginBottom: entitiesMember.length > 0 ? 12 : 0 }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name="account-group-outline" size={18} color={colors.tabActive} />
|
||||||
|
</View>
|
||||||
|
<View style={Styles.flex1}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Anggota</Text>
|
||||||
|
{entitiesMember.length === 0 && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada anggota dipilih</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{entitiesMember.length > 0 && (
|
||||||
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{entitiesMember.length} orang</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
{entitiesMember.length > 0 && (
|
||||||
|
<View style={{ gap: 6 }}>
|
||||||
|
{entitiesMember.map((item: { img: any; name: any; position?: string }, index: any) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}
|
||||||
|
>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>{item.name}</Text>
|
||||||
|
{item.position && (
|
||||||
|
<View style={[Styles.positionBadge, { backgroundColor: colors.dimmed + '15' }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]} numberOfLines={1}>{item.position}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -203,7 +318,7 @@ export default function CreateTaskDivision() {
|
|||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<Ionicons name="trash" color="black" size={25} />}
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
title="Hapus"
|
title="Hapus"
|
||||||
onPress={() => { deleteFile(indexDelFile) }}
|
onPress={() => { deleteFile(indexDelFile) }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDivisionMember } from "@/lib/api";
|
import { apiGetDivisionMember } from "@/lib/api";
|
||||||
import { setMemberChoose } from "@/lib/memberChoose";
|
import { setMemberChoose } from "@/lib/memberChoose";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -22,10 +24,10 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddMemberCreateTask() {
|
export default function AddMemberCreateTask() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string, detail: string }>()
|
const { id } = useLocalSearchParams<{ id: string, detail: string }>()
|
||||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
@@ -64,37 +66,53 @@ export default function AddMemberCreateTask() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Pilih Anggota',
|
headerTitle: 'Pilih Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="create"
|
// category="create"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length > 0 ? false : true}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Pilih Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={selectMember.length > 0 ? false : true}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
||||||
|
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -103,9 +121,11 @@ export default function AddMemberCreateTask() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -113,19 +133,19 @@ export default function AddMemberCreateTask() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onChoose(item.idUser, item.name, item.img)
|
onChoose(item.idUser, item.name, item.img)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]}>{item.name}</Text>
|
<Text style={[Styles.textDefault]}>{item.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.idUser) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -136,6 +156,6 @@ export default function AddMemberCreateTask() {
|
|||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
|
import { formatDateOnly } from "@/lib/fun_formatDateOnly";
|
||||||
|
import { getDatesInRange } from "@/lib/fun_getDatesInRange";
|
||||||
import { setTaskCreate } from "@/lib/taskCreate";
|
import { setTaskCreate } from "@/lib/taskCreate";
|
||||||
import dayjs from "dayjs";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
|
import 'intl';
|
||||||
|
import 'intl/locale-data/jsonp/id';
|
||||||
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
@@ -20,6 +28,8 @@ import DateTimePicker, {
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function CreateTaskAddTugas() {
|
export default function CreateTaskAddTugas() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const [disable, setDisable] = useState(true);
|
const [disable, setDisable] = useState(true);
|
||||||
const [range, setRange] = useState<{
|
const [range, setRange] = useState<{
|
||||||
@@ -33,11 +43,12 @@ export default function CreateTaskAddTugas() {
|
|||||||
})
|
})
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const taskCreate = useSelector((state: any) => state.taskCreate)
|
const taskCreate = useSelector((state: any) => state.taskCreate)
|
||||||
|
const [dsbButton, setDsbButton] = useState(true)
|
||||||
|
const [dataDetail, setDataDetail] = useState<any>([])
|
||||||
|
const [modalDetail, setModalDetail] = useState(false)
|
||||||
|
|
||||||
const from = range.startDate
|
const from = formatDateOnly(range.startDate, "DD-MM-YYYY")
|
||||||
? dayjs(range.startDate).format("DD-MM-YYYY")
|
const to = formatDateOnly(range.endDate, "DD-MM-YYYY")
|
||||||
: "";
|
|
||||||
const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : "";
|
|
||||||
|
|
||||||
function checkAll() {
|
function checkAll() {
|
||||||
if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) {
|
if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) {
|
||||||
@@ -58,19 +69,50 @@ export default function CreateTaskAddTugas() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkButton() {
|
||||||
|
if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) {
|
||||||
|
setDsbButton(true)
|
||||||
|
setDataDetail([])
|
||||||
|
} else {
|
||||||
|
setDsbButton(false)
|
||||||
|
const awal = formatDateOnly(range.startDate, "YYYY-MM-DD")
|
||||||
|
const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD")
|
||||||
|
const datanya = getDatesInRange(awal, akhir)
|
||||||
|
setDataDetail(datanya.map((item: any) => ({
|
||||||
|
date: item,
|
||||||
|
timeStart: null,
|
||||||
|
timeEnd: null,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAll()
|
checkAll()
|
||||||
}, [from, to, title, error])
|
}, [from, to, title, error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkButton()
|
||||||
|
}, [range])
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
dispatch(setTaskCreate([...taskCreate, {
|
const dataDetailFix = dataDetail.map((item: any) => ({
|
||||||
|
date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"),
|
||||||
|
timeStart: item.timeStart,
|
||||||
|
timeEnd: item.timeEnd,
|
||||||
|
}))
|
||||||
|
const hasilOrder = [...taskCreate, {
|
||||||
title: title,
|
title: title,
|
||||||
dateStart: from,
|
dateStart: from,
|
||||||
dateEnd: to,
|
dateEnd: to,
|
||||||
dateStartFix: dayjs(range.startDate).format("YYYY-MM-DD"),
|
dateStartFix: formatDateOnly(range.startDate, "YYYY-MM-DD"),
|
||||||
dateEndFix: dayjs(range.endDate).format("YYYY-MM-DD"),
|
dateEndFix: formatDateOnly(range.endDate, "YYYY-MM-DD"),
|
||||||
}]))
|
dataDetail: dataDetailFix
|
||||||
|
}].sort((a, b) => {
|
||||||
|
return new Date(a.dateStartFix).getTime() - new Date(b.dateStartFix).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setTaskCreate(hasilOrder))
|
||||||
router.back();
|
router.back();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -78,34 +120,47 @@ export default function CreateTaskAddTugas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah Tugas",
|
headerTitle: "Tambah Tugas",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={disable}
|
// disable={disable}
|
||||||
category="create"
|
// category="create"
|
||||||
onPress={() => { handleCreate() }}
|
// onPress={() => { handleCreate() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Tambah Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disable}
|
||||||
|
category="create"
|
||||||
|
onPress={() => { handleCreate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
mode="range"
|
mode="range"
|
||||||
startDate={range.startDate}
|
startDate={range.startDate}
|
||||||
@@ -115,6 +170,15 @@ export default function CreateTaskAddTugas() {
|
|||||||
selected: Styles.selectedDate,
|
selected: Styles.selectedDate,
|
||||||
selected_label: Styles.cWhite,
|
selected_label: Styles.cWhite,
|
||||||
range_fill: Styles.selectRangeDate,
|
range_fill: Styles.selectRangeDate,
|
||||||
|
month_label: { color: colors.text },
|
||||||
|
month_selector_label: { color: colors.text },
|
||||||
|
year_label: { color: colors.text },
|
||||||
|
year_selector_label: { color: colors.text },
|
||||||
|
day_label: { color: colors.text },
|
||||||
|
time_label: { color: colors.text },
|
||||||
|
weekday_label: { color: colors.text },
|
||||||
|
button_next_image: { tintColor: colors.text },
|
||||||
|
button_prev_image: { tintColor: colors.text },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -122,31 +186,39 @@ export default function CreateTaskAddTugas() {
|
|||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Mulai <Text style={Styles.cError}>*</Text>
|
Tanggal Mulai <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{from}</Text>
|
<Text style={{ textAlign: "center" }}>{from}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Berakhir <Text style={Styles.cError}>*</Text>
|
Tanggal Berakhir <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{to}</Text>
|
<Text style={{ textAlign: "center" }}>{to}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, Styles.cError, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, { color: colors.error }, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
||||||
}
|
}
|
||||||
|
{/* <Pressable
|
||||||
|
style={[Styles.btnTab, Styles.btnLainnya, dsbButton && Styles.btnDisabled]}
|
||||||
|
disabled={dsbButton}
|
||||||
|
onPress={() => { setModalDetail(true) }}
|
||||||
|
>
|
||||||
|
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||||
|
</Pressable> */}
|
||||||
|
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||||
</View>
|
</View>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul Tugas"
|
label="Judul Tugas"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Tugas"
|
placeholder="Judul Tugas"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={title}
|
value={title}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
@@ -157,6 +229,14 @@ export default function CreateTaskAddTugas() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
<ModalAddDetailTugasTask
|
||||||
|
isVisible={modalDetail}
|
||||||
|
setVisible={setModalDetail}
|
||||||
|
dataTanggal={dataDetail}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
setDataDetail(data)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ColorsStatus } from "@/constants/ColorsStatus";
|
|||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetTask } from "@/lib/api";
|
import { apiGetTask } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import {
|
import {
|
||||||
AntDesign,
|
AntDesign,
|
||||||
Ionicons,
|
Ionicons,
|
||||||
@@ -20,6 +21,7 @@ import { router, useLocalSearchParams } from "expo-router";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,9 +33,22 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ListTask() {
|
export default function ListTask() {
|
||||||
const { id, status } = useLocalSearchParams<{ id: string; status: string }>()
|
const { colors } = useTheme()
|
||||||
|
const { id, status, year } = useLocalSearchParams<{ id: string; status: string; year: string }>()
|
||||||
const [isList, setList] = useState(false)
|
const [isList, setList] = useState(false)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AsyncStorage.getItem('division_view_mode').then((val) => {
|
||||||
|
if (val !== null) setList(val === 'list')
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function toggleView() {
|
||||||
|
const next = !isList
|
||||||
|
setList(next)
|
||||||
|
AsyncStorage.setItem('division_view_mode', next ? 'list' : 'grid')
|
||||||
|
}
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const update = useSelector((state: any) => state.taskUpdate)
|
const update = useSelector((state: any) => state.taskUpdate)
|
||||||
@@ -43,6 +58,8 @@ export default function ListTask() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [waiting, setWaiting] = useState(false)
|
const [waiting, setWaiting] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [isYear, setYear] = useState("")
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
async function handleLoad(loading: boolean, thisPage: number) {
|
||||||
try {
|
try {
|
||||||
@@ -55,8 +72,12 @@ export default function ListTask() {
|
|||||||
division: id,
|
division: id,
|
||||||
status: statusFix,
|
status: statusFix,
|
||||||
search,
|
search,
|
||||||
page: thisPage
|
page: thisPage,
|
||||||
|
year
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setYear(response.tahun)
|
||||||
|
|
||||||
if (thisPage == 1) {
|
if (thisPage == 1) {
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
} else if (thisPage > 1 && response.data.length > 0) {
|
||||||
@@ -104,9 +125,9 @@ export default function ListTask() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10]}>
|
<ScrollView horizontal style={[Styles.mb10]} showsHorizontalScrollIndicator={false}>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={statusFix}
|
active={statusFix}
|
||||||
value="0"
|
value="0"
|
||||||
@@ -115,7 +136,7 @@ export default function ListTask() {
|
|||||||
icon={
|
icon={
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="clock-alert-outline"
|
name="clock-alert-outline"
|
||||||
color={statusFix == "0" ? "white" : "black"}
|
color={statusFix == "0" ? "white" : colors.dimmed}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -129,7 +150,7 @@ export default function ListTask() {
|
|||||||
icon={
|
icon={
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="progress-check"
|
name="progress-check"
|
||||||
color={statusFix == "1" ? "white" : "black"}
|
color={statusFix == "1" ? "white" : colors.dimmed}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -143,7 +164,7 @@ export default function ListTask() {
|
|||||||
icon={
|
icon={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="checkmark-done-circle-outline"
|
name="checkmark-done-circle-outline"
|
||||||
color={statusFix == "2" ? "white" : "black"}
|
color={statusFix == "2" ? "white" : colors.dimmed}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -157,28 +178,30 @@ export default function ListTask() {
|
|||||||
icon={
|
icon={
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name="closecircleo"
|
name="closecircleo"
|
||||||
color={statusFix == "3" ? "white" : "black"}
|
color={statusFix == "3" ? "white" : colors.dimmed}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
n={4}
|
n={4}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||||
<InputSearch width={68} onChange={setSearch} />
|
<InputSearch width={68} onChange={setSearch} />
|
||||||
<Pressable
|
<Pressable onPress={toggleView}>
|
||||||
onPress={() => {
|
|
||||||
setList(!isList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||||
color={"black"}
|
color={colors.text}
|
||||||
size={30}
|
size={30}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[Styles.mv05]}>
|
||||||
|
<View style={[Styles.rowOnly]}>
|
||||||
|
<Text style={[Styles.mr05]}>Filter :</Text>
|
||||||
|
<LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<View style={[{ flex: 2 }]}>
|
<View style={[{ flex: 2 }]}>
|
||||||
{
|
{
|
||||||
loading ?
|
loading ?
|
||||||
@@ -205,9 +228,10 @@ export default function ListTask() {
|
|||||||
router.push(`./task/${item.id}`);
|
router.push(`./task/${item.id}`);
|
||||||
}}
|
}}
|
||||||
borderType="bottom"
|
borderType="bottom"
|
||||||
|
bgColor="transparent"
|
||||||
icon={
|
icon={
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
<View style={[Styles.iconContent]}>
|
||||||
<AntDesign name="areachart" size={25} color={"#384288"} />
|
<AntDesign name="areachart" size={25} color={"black"} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
@@ -221,6 +245,7 @@ export default function ListTask() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -262,11 +287,11 @@ export default function ListTask() {
|
|||||||
<LabelStatus
|
<LabelStatus
|
||||||
size="default"
|
size="default"
|
||||||
category={
|
category={
|
||||||
item.status === 0 ? 'primary' :
|
item.status === 0 ? 'secondary' :
|
||||||
item.status === 1 ? 'warning' :
|
item.status === 1 ? 'warning' :
|
||||||
item.status === 2 ? 'success' :
|
item.status === 2 ? 'success' :
|
||||||
item.status === 3 ? 'error' :
|
item.status === 3 ? 'error' :
|
||||||
'primary'
|
'secondary'
|
||||||
}
|
}
|
||||||
text={
|
text={
|
||||||
item.status === 0 ? 'SEGERA' :
|
item.status === 0 ? 'SEGERA' :
|
||||||
@@ -287,6 +312,7 @@ export default function ListTask() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -326,13 +352,13 @@ export default function ListTask() {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: "center" },]} >
|
<Text style={[Styles.textDefault, Styles.textCenter]} >
|
||||||
Tidak ada data
|
Tidak ada data
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
</View>
|
</View >
|
||||||
</View>
|
</View >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditTaskTugas, apiGetTaskTugas } from "@/lib/api";
|
import { apiEditTaskTugas, apiGetTaskTugas } from "@/lib/api";
|
||||||
|
import { formatDateOnly } from "@/lib/fun_formatDateOnly";
|
||||||
|
import { getDatesInRange } from "@/lib/fun_getDatesInRange";
|
||||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import dayjs from "dayjs";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import 'intl';
|
||||||
|
import 'intl/locale-data/jsonp/id';
|
||||||
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
@@ -21,6 +29,8 @@ import DateTimePicker, { DateType } from "react-native-ui-datepicker";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function UpdateProjectTaskDivision() {
|
export default function UpdateProjectTaskDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const { detail } = useLocalSearchParams<{ detail: string }>();
|
const { detail } = useLocalSearchParams<{ detail: string }>();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const update = useSelector((state: any) => state.taskUpdate);
|
const update = useSelector((state: any) => state.taskUpdate);
|
||||||
@@ -29,10 +39,14 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
startDate: DateType;
|
startDate: DateType;
|
||||||
endDate: DateType;
|
endDate: DateType;
|
||||||
}>({ startDate: undefined, endDate: undefined });
|
}>({ startDate: undefined, endDate: undefined });
|
||||||
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
const [month, setMonth] = useState<any>();
|
const [month, setMonth] = useState<any>();
|
||||||
const [year, setYear] = useState<any>();
|
const [year, setYear] = useState<any>();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [disableBtn, setDisableBtn] = useState(false);
|
const [disableBtn, setDisableBtn] = useState(false);
|
||||||
|
const [dataDetail, setDataDetail] = useState<any>([])
|
||||||
|
const [modalDetail, setModalDetail] = useState(false)
|
||||||
|
const [dsbButton, setDsbButton] = useState(true)
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
startDate: false,
|
startDate: false,
|
||||||
@@ -40,10 +54,8 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
title: false,
|
title: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const from = range.startDate
|
const from = formatDateOnly(range.startDate);
|
||||||
? dayjs(range.startDate).format("DD-MM-YYYY")
|
const to = formatDateOnly(range.endDate);
|
||||||
: "";
|
|
||||||
const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : "";
|
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
@@ -60,6 +72,22 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
});
|
});
|
||||||
setMonth(new Date(response.data.dateStart).getMonth());
|
setMonth(new Date(response.data.dateStart).getMonth());
|
||||||
setYear(new Date(response.data.dateStart).getFullYear());
|
setYear(new Date(response.data.dateStart).getFullYear());
|
||||||
|
|
||||||
|
const response2 = await apiGetTaskTugas({
|
||||||
|
user: hasil,
|
||||||
|
id: detail,
|
||||||
|
cat: "detailTask"
|
||||||
|
});
|
||||||
|
if (response2.data.length == 0) {
|
||||||
|
const datanya = getDatesInRange(response.data.dateStart, response.data.dateEnd)
|
||||||
|
setDataDetail(datanya.map((item: any) => ({
|
||||||
|
date: item,
|
||||||
|
timeStart: null,
|
||||||
|
timeEnd: null,
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
setDataDetail(response2.data)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -73,8 +101,23 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoadingSubmit(true)
|
||||||
|
const dataDetailFix = dataDetail.map((item: any) => ({
|
||||||
|
date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"),
|
||||||
|
timeStart: item.timeStart,
|
||||||
|
timeEnd: item.timeEnd,
|
||||||
|
}))
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiEditTaskTugas({ data: { title, dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), user: hasil }, id: detail });
|
const response = await apiEditTaskTugas({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"),
|
||||||
|
dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"),
|
||||||
|
user: hasil,
|
||||||
|
dataDetail: dataDetailFix
|
||||||
|
},
|
||||||
|
id: detail
|
||||||
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateTask({ ...update, task: !update.task }))
|
dispatch(setUpdateTask({ ...update, task: !update.task }))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
||||||
@@ -82,9 +125,13 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoadingSubmit(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,41 +162,80 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkButton() {
|
||||||
|
if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) {
|
||||||
|
setDsbButton(true)
|
||||||
|
setDataDetail([])
|
||||||
|
} else {
|
||||||
|
setDsbButton(false)
|
||||||
|
const awal = formatDateOnly(range.startDate, "YYYY-MM-DD")
|
||||||
|
const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD")
|
||||||
|
const datanya = getDatesInRange(awal, akhir)
|
||||||
|
setDataDetail(datanya.map((item: any) => ({
|
||||||
|
date: item,
|
||||||
|
timeStart: null,
|
||||||
|
timeEnd: null,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAll();
|
checkAll();
|
||||||
}, [from, to, title, error]);
|
}, [from, to, title, error]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkButton()
|
||||||
|
}, [range])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Edit Tanggal dan Tugas",
|
headerTitle: "Edit Tanggal dan Tugas",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={disableBtn}
|
// disable={disableBtn || loadingSubmit}
|
||||||
category="update"
|
// category="update"
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleEdit()
|
// handleEdit()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Edit Tanggal dan Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || loadingSubmit}
|
||||||
|
category="update"
|
||||||
|
onPress={() => {
|
||||||
|
handleEdit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
mode="range"
|
mode="range"
|
||||||
@@ -162,6 +248,15 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
selected: Styles.selectedDate,
|
selected: Styles.selectedDate,
|
||||||
selected_label: Styles.cWhite,
|
selected_label: Styles.cWhite,
|
||||||
range_fill: Styles.selectRangeDate,
|
range_fill: Styles.selectRangeDate,
|
||||||
|
month_label: { color: colors.text },
|
||||||
|
month_selector_label: { color: colors.text },
|
||||||
|
year_label: { color: colors.text },
|
||||||
|
year_selector_label: { color: colors.text },
|
||||||
|
day_label: { color: colors.text },
|
||||||
|
time_label: { color: colors.text },
|
||||||
|
weekday_label: { color: colors.text },
|
||||||
|
button_next_image: { tintColor: colors.text },
|
||||||
|
button_prev_image: { tintColor: colors.text },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -170,33 +265,41 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Mulai <Text style={Styles.cError}>*</Text>
|
Tanggal Mulai <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{from}</Text>
|
<Text style={{ textAlign: "center" }}>{from}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[{ width: "48%" }]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Berakhir <Text style={Styles.cError}>*</Text>
|
Tanggal Berakhir <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{to}</Text>
|
<Text style={{ textAlign: "center" }}>{to}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{(error.endDate || error.startDate) && (
|
{(error.endDate || error.startDate) && (
|
||||||
<Text style={[Styles.textInformation, Styles.cError, Styles.mt05]} >
|
<Text style={[Styles.textInformation, { color: colors.error }, Styles.mt05]} >
|
||||||
Tanggal tidak boleh kosong
|
Tanggal tidak boleh kosong
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{/* <Pressable
|
||||||
|
style={[Styles.btnTab, Styles.btnLainnya, dsbButton && Styles.btnDisabled]}
|
||||||
|
disabled={dsbButton}
|
||||||
|
onPress={() => { setModalDetail(true) }}
|
||||||
|
>
|
||||||
|
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||||
|
</Pressable> */}
|
||||||
|
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||||
</View>
|
</View>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul Tugas"
|
label="Judul Tugas"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Tugas"
|
placeholder="Judul Tugas"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={title}
|
value={title}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
@@ -207,7 +310,14 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
<ModalAddDetailTugasTask
|
||||||
|
isVisible={modalDetail}
|
||||||
|
setVisible={setModalDetail}
|
||||||
|
dataTanggal={dataDetail}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
setDataDetail(data)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiAddMemberDivision, apiGetDivisionOneDetail, apiGetUser } from "@/lib/api";
|
import { apiAddMemberDivision, apiGetDivisionOneDetail, apiGetUser } from "@/lib/api";
|
||||||
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddMemberDivision() {
|
export default function AddMemberDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
const [dataOld, setDataOld] = useState<Props[]>([])
|
||||||
@@ -31,9 +34,11 @@ export default function AddMemberDivision() {
|
|||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const update = useSelector((state: any) => state.divisionUpdate)
|
const update = useSelector((state: any) => state.divisionUpdate)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDivisionOneDetail({ user: hasil, id })
|
const response = await apiGetDivisionOneDetail({ user: hasil, id })
|
||||||
setDataOld(response.data.member)
|
setDataOld(response.data.member)
|
||||||
@@ -42,6 +47,8 @@ export default function AddMemberDivision() {
|
|||||||
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +79,7 @@ export default function AddMemberDivision() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiAddMemberDivision({ id: id, data: { user: hasil, member: selectMember } })
|
const response = await apiAddMemberDivision({ id: id, data: { user: hasil, member: selectMember } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -81,45 +89,65 @@ export default function AddMemberDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah Anggota',
|
headerTitle: 'Tambah Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
|
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
|
||||||
|
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -128,9 +156,11 @@ export default function AddMemberDivision() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -139,22 +169,22 @@ export default function AddMemberDivision() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderBottomColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.id, item.name, item.img)
|
!found && onChoose(item.id, item.name, item.img)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -165,6 +195,6 @@ export default function AddMemberDivision() {
|
|||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditDivision, apiGetDivisionOneDetail } from "@/lib/api";
|
import { apiEditDivision, apiGetDivisionOneDetail } from "@/lib/api";
|
||||||
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
@@ -12,10 +13,12 @@ import Toast from "react-native-toast-message";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function EditDivision() {
|
export default function EditDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.divisionUpdate)
|
const update = useSelector((state: any) => state.divisionUpdate)
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [data, setData] = useState({
|
const [data, setData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
@@ -43,6 +46,7 @@ export default function EditDivision() {
|
|||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiEditDivision({ user: hasil, name: data.name, desc: data.desc }, id)
|
const response = await apiEditDivision({ user: hasil, name: data.name, desc: data.desc }, id)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -52,35 +56,53 @@ export default function EditDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Edit Divisi",
|
headerTitle: "Edit Divisi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={error.name}
|
// disable={error.name || loading ? true : false}
|
||||||
category="update"
|
// category="update"
|
||||||
onPress={() => { handleEdit() }}
|
// onPress={() => { handleEdit() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Edit Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={error.name || loading ? true : false}
|
||||||
|
category="update"
|
||||||
|
onPress={() => { handleEdit() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView style={{ backgroundColor: colors.background }}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Nama Divisi"
|
label="Nama Divisi"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import DiscussionDivisionDetail from "@/components/division/discussionDivisionDetail"
|
import DiscussionDivisionDetail from "@/components/division/discussionDivisionDetail"
|
||||||
import FileDivisionDetail from "@/components/division/fileDivisionDetail"
|
import FileDivisionDetail from "@/components/division/fileDivisionDetail"
|
||||||
import FiturDivisionDetail from "@/components/division/fiturDivisionDetail"
|
import FiturDivisionDetail from "@/components/division/fiturDivisionDetail"
|
||||||
@@ -8,9 +8,10 @@ import CaraouselHome from "@/components/home/carouselHome"
|
|||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiGetDivisionOneDetail } from "@/lib/api"
|
import { apiGetDivisionOneDetail } from "@/lib/api"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native"
|
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string,
|
id: string,
|
||||||
@@ -22,15 +23,17 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailDivisionFitur() {
|
export default function DetailDivisionFitur() {
|
||||||
|
const { colors } = useTheme()
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [data, setData] = useState<Props>()
|
const [data, setData] = useState<Props>()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDivisionOneDetail({ user: hasil, id })
|
const response = await apiGetDivisionOneDetail({ user: hasil, id })
|
||||||
setData(response.data.division)
|
setData(response.data.division)
|
||||||
@@ -42,26 +45,50 @@ export default function DetailDivisionFitur() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad()
|
handleLoad(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
handleLoad(false)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: loading ? 'Loading... ' : data?.name,
|
headerTitle: loading ? 'Loading... ' : data?.name,
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightDivisionDetail id={id} />,
|
// headerRight: () => <HeaderRightDivisionDetail id={id} />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title={loading ? 'Loading...' : data?.name || ''}
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<HeaderRightDivisionDetail id={id} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<CaraouselHome />
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<CaraouselHome refreshing={refreshing} />
|
||||||
<View style={[Styles.ph15, Styles.mb100]}>
|
<View style={[Styles.ph15, Styles.mb100]}>
|
||||||
<FiturDivisionDetail />
|
<FiturDivisionDetail refreshing={refreshing} />
|
||||||
<TaskDivisionDetail />
|
<TaskDivisionDetail refreshing={refreshing} />
|
||||||
<FileDivisionDetail />
|
<FileDivisionDetail refreshing={refreshing} />
|
||||||
<DiscussionDivisionDetail />
|
<DiscussionDivisionDetail refreshing={refreshing} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import BorderBottomItem from "@/components/borderBottomItem"
|
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
|
||||||
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
|
|
||||||
import DrawerBottom from "@/components/drawerBottom"
|
import DrawerBottom from "@/components/drawerBottom"
|
||||||
|
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
|
||||||
import ImageUser from "@/components/imageNew"
|
import ImageUser from "@/components/imageNew"
|
||||||
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||||
import SectionCancel from "@/components/sectionCancel"
|
import SectionCancel from "@/components/sectionCancel"
|
||||||
import Skeleton from "@/components/skeleton"
|
import Text from "@/components/Text"
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem"
|
import { ConstEnv } from "@/constants/ConstEnv"
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus"
|
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiDeleteMemberDivision, apiGetDivisionOneDetail, apiUpdateStatusAdminDivision } from "@/lib/api"
|
import { apiDeleteMemberDivision, apiGetDivisionOneDetail, apiGetDivisionOneFeature, apiUpdateStatusAdminDivision } from "@/lib/api"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Pressable, SafeAreaView, ScrollView, Text, View } from "react-native"
|
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
import { useSelector } from "react-redux"
|
import { useSelector } from "react-redux"
|
||||||
|
|
||||||
@@ -37,6 +37,9 @@ type PropsMember = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function InformationDivision() {
|
export default function InformationDivision() {
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -44,21 +47,22 @@ export default function InformationDivision() {
|
|||||||
const [dataMember, setDataMember] = useState<PropsMember[]>([])
|
const [dataMember, setDataMember] = useState<PropsMember[]>([])
|
||||||
const [refresh, setRefresh] = useState(false)
|
const [refresh, setRefresh] = useState(false)
|
||||||
const update = useSelector((state: any) => state.divisionUpdate)
|
const update = useSelector((state: any) => state.divisionUpdate)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const SKELETON_COUNT = 5
|
||||||
|
const [isMemberDivision, setIsMemberDivision] = useState(false)
|
||||||
|
const [isAdminDivision, setIsAdminDivision] = useState(false)
|
||||||
const [dataMemberChoose, setDataMemberChoose] = useState({
|
const [dataMemberChoose, setDataMemberChoose] = useState({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
})
|
})
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
|
||||||
function handleMemberOut() {
|
function handleMemberOut() {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => {
|
||||||
title: 'Konfirmasi',
|
setShowDeleteModal(true)
|
||||||
desc: 'Apakah anda yakin ingin mengeluarkan anggota?',
|
}, 600)
|
||||||
onPress: () => { memberOut() }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function memberOut() {
|
async function memberOut() {
|
||||||
@@ -71,9 +75,11 @@ export default function InformationDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengeluarkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
@@ -89,9 +95,11 @@ export default function InformationDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah status admin"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
@@ -111,12 +119,42 @@ export default function InformationDivision() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
handleLoad(false)
|
||||||
|
handleCheckMember()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleCheckMember() {
|
||||||
|
try {
|
||||||
|
const hasil = await decryptToken(String(token?.current));
|
||||||
|
const response = await apiGetDivisionOneFeature({
|
||||||
|
id,
|
||||||
|
user: hasil,
|
||||||
|
cat: "check-member",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response2 = await apiGetDivisionOneFeature({
|
||||||
|
id,
|
||||||
|
user: hasil,
|
||||||
|
cat: "check-admin",
|
||||||
|
});
|
||||||
|
setIsMemberDivision(response.data);
|
||||||
|
setIsAdminDivision(response2.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false)
|
handleLoad(false)
|
||||||
}, [refresh, update])
|
}, [refresh, update])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(true)
|
handleLoad(true)
|
||||||
|
handleCheckMember()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function handleChooseMember(item: PropsMember) {
|
function handleChooseMember(item: PropsMember) {
|
||||||
@@ -125,112 +163,158 @@ export default function InformationDivision() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Informasi Divisi',
|
headerTitle: 'Informasi Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <HeaderRightDivisionInfo id={id} active={dataDetail?.isActive} />,
|
// headerRight: () => ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && <HeaderRightDivisionInfo id={id} active={dataDetail?.isActive} />,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Informasi Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && <HeaderRightDivisionInfo id={id} active={dataDetail?.isActive} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView style={[Styles.h100]}>
|
<ScrollView
|
||||||
<View style={[Styles.p15]}>
|
showsVerticalScrollIndicator={false}
|
||||||
{
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||||
dataDetail?.isActive == false && (
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
<SectionCancel title={'Divisi nonaktif'} />
|
>
|
||||||
)
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
}
|
|
||||||
<View style={[Styles.mb15]}>
|
{dataDetail?.isActive === false && <SectionCancel title="Divisi nonaktif" />}
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Deskripsi Divisi</Text>
|
|
||||||
<View style={[Styles.wrapPaper]}>
|
{/* Deskripsi */}
|
||||||
{loading ?
|
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow, Styles.mb15,
|
||||||
arrSkeleton.map((item, index) => {
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<View style={[Styles.sectionActionRow, { marginBottom: 12 }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialIcons name="info-outline" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Deskripsi</Text>
|
||||||
|
</View>
|
||||||
|
{loading
|
||||||
|
? Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<View key={i} style={{ height: 13, borderRadius: 6, marginBottom: 8, backgroundColor: colors.icon + '20', width: i === 2 ? '60%' : '100%' }} />
|
||||||
|
))
|
||||||
|
: <Text style={[Styles.textDefault, { color: colors.text, lineHeight: 22 }]}>{dataDetail?.desc}</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Tombol tambah anggota */}
|
||||||
|
{((entityUser.role !== "user" && entityUser.role !== "coadmin") || isAdminDivision) && dataDetail?.isActive && (
|
||||||
|
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.mb15,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push(`/division/${id}/add-member`)}
|
||||||
|
style={Styles.sectionActionRow}
|
||||||
|
>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '18' }]}>
|
||||||
|
<MaterialCommunityIcons name="account-plus-outline" size={18} color={colors.icon} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Tambah Anggota</Text>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Daftar anggota */}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.sectionCard,
|
||||||
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||||
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||||
|
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||||
|
{!loading && (
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{dataMember.length} anggota</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{loading
|
||||||
|
? Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
||||||
|
<View key={i} style={[Styles.rowItemsCenter, Styles.ph15,
|
||||||
|
{ paddingVertical: 14, gap: 14, borderBottomWidth: i < SKELETON_COUNT - 1 ? 1 : 0, borderBottomColor: colors.icon + '14' }]}>
|
||||||
|
<View style={[Styles.userProfileExtraSmall, { backgroundColor: colors.icon + '20', borderRadius: 100 }]} />
|
||||||
|
<View style={{ height: 13, borderRadius: 6, flex: 1, backgroundColor: colors.icon + '20', maxWidth: 140 + (i % 3) * 30 }} />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
: dataMember.length === 0
|
||||||
|
? (
|
||||||
|
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||||
|
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||||
|
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
: dataMember.map((item, index) => {
|
||||||
|
const canPress = dataDetail?.isActive && (isAdminDivision || (entityUser.role !== "user" && entityUser.role !== "coadmin"))
|
||||||
return (
|
return (
|
||||||
<Skeleton key={index} width={100} height={10} widthType="percent" borderRadius={10} />
|
<Pressable
|
||||||
|
key={index}
|
||||||
|
onPress={() => canPress && handleChooseMember(item)}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
Styles.rowItemsCenter, Styles.ph15,
|
||||||
|
{
|
||||||
|
paddingVertical: 13, gap: 14,
|
||||||
|
borderBottomWidth: index < dataMember.length - 1 ? 1 : 0,
|
||||||
|
borderBottomColor: colors.icon + '14',
|
||||||
|
backgroundColor: pressed && canPress ? colors.icon + '0E' : 'transparent',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: item.isAdmin ? colors.tabActive : colors.dimmed }]}>
|
||||||
|
{item.isAdmin ? 'Admin' : 'Anggota'}
|
||||||
|
</Text>
|
||||||
|
{canPress && <MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />}
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
}
|
||||||
<Text>{dataDetail?.desc}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.mb15]}>
|
|
||||||
<Text style={[Styles.textDefault, Styles.mv05]}>{dataMember.length} Anggota</Text>
|
|
||||||
<View style={[Styles.wrapPaper]}>
|
|
||||||
{
|
|
||||||
dataDetail?.isActive && (
|
|
||||||
<BorderBottomItem
|
|
||||||
onPress={() => { router.push(`/division/${id}/add-member`) }}
|
|
||||||
borderType="none"
|
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.gray]}>
|
|
||||||
<Feather name="user-plus" size={25} color={'#384288'} />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title="Tambah Anggota"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<SkeletonTwoItem key={index} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
dataMember.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<BorderBottomItem
|
|
||||||
width={55}
|
|
||||||
key={index}
|
|
||||||
borderType="bottom"
|
|
||||||
onPress={() => { dataDetail?.isActive && handleChooseMember(item) }}
|
|
||||||
icon={
|
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
|
|
||||||
}
|
|
||||||
title={item.name}
|
|
||||||
rightTopInfo={item.isAdmin ? "Admin" : "Anggota"}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title={dataMemberChoose.name}>
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title={dataMemberChoose.name}>
|
||||||
<View>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<Pressable style={[Styles.wrapItemBorderBottom]} onPress={() => { handleMemberAdmin() }}>
|
<MenuItemRow
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
icon={<MaterialIcons name="verified-user" color={colors.text} size={25} />}
|
||||||
<View style={[Styles.iconContent, ColorsStatus.info]}>
|
title={dataMemberChoose.isAdmin ? 'Berhentikan admin' : 'Jadikan admin'}
|
||||||
<MaterialIcons name="verified-user" size={25} color='#19345E' />
|
onPress={handleMemberAdmin}
|
||||||
</View>
|
/>
|
||||||
<View style={[Styles.rowSpaceBetween, { width: '88%' }]}>
|
<MenuItemRow
|
||||||
<View style={[Styles.ml10]}>
|
icon={<MaterialCommunityIcons name="account-remove" color={colors.text} size={25} />}
|
||||||
<Text style={[Styles.textDefault]}>{dataMemberChoose.isAdmin ? 'Memberhentikan sebagai admin' : 'Jadikan admin'}</Text>
|
title="Keluarkan"
|
||||||
</View>
|
onPress={handleMemberOut}
|
||||||
</View>
|
/>
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Pressable style={[Styles.wrapItemBorderBottom]} onPress={() => { handleMemberOut() }}>
|
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.info]}>
|
|
||||||
<MaterialCommunityIcons name="close-circle" size={25} color='#19345E' />
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.rowSpaceBetween, { width: '88%' }]}>
|
|
||||||
<View style={[Styles.ml10]}>
|
|
||||||
<Text style={[Styles.textDefault]}>Keluarkan dari divisi</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message="Apakah anda yakin ingin mengeluarkan anggota?"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
memberOut()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText="Keluar"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import ReportChartDocument from "@/components/division/reportChartDocument"
|
import ReportChartDocument from "@/components/division/reportChartDocument"
|
||||||
import ReportChartEvent from "@/components/division/reportChartEvent"
|
import ReportChartEvent from "@/components/division/reportChartEvent"
|
||||||
import ReportChartProgress from "@/components/division/reportChartProgress"
|
import ReportChartProgress from "@/components/division/reportChartProgress"
|
||||||
@@ -6,6 +6,7 @@ import { InputDate } from "@/components/inputDate"
|
|||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiGetDivisionReport } from "@/lib/api"
|
import { apiGetDivisionReport } from "@/lib/api"
|
||||||
import { stringToDate } from "@/lib/fun_stringToDate"
|
import { stringToDate } from "@/lib/fun_stringToDate"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
@@ -14,6 +15,7 @@ import { SafeAreaView, ScrollView, View } from "react-native"
|
|||||||
import Toast from "react-native-toast-message"
|
import Toast from "react-native-toast-message"
|
||||||
|
|
||||||
export default function ReportDivision() {
|
export default function ReportDivision() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [showReport, setShowReport] = useState(false);
|
const [showReport, setShowReport] = useState(false);
|
||||||
@@ -92,8 +94,11 @@ export default function ReportDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, });
|
Toast.show({ type: 'small', text1: response.message, });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,15 +109,22 @@ export default function ReportDivision() {
|
|||||||
}, [showReport]);
|
}, [showReport]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Laporan Divisi',
|
headerTitle: 'Laporan Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Laporan Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView style={{ backgroundColor: colors.background }}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<InputDate
|
<InputDate
|
||||||
onChange={(val) => validationForm("date", val)}
|
onChange={(val) => validationForm("date", val)}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonNextHeader from "@/components/buttonNextHeader";
|
import ButtonNextHeader from "@/components/buttonNextHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SelectForm from "@/components/selectForm";
|
import SelectForm from "@/components/selectForm";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
|
import { apiCheckDivisionName } from "@/lib/api";
|
||||||
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
||||||
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function CreateDivision() {
|
export default function CreateDivision() {
|
||||||
const [isSelect, setSelect] = useState(false);
|
const { colors } = useTheme();
|
||||||
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
const { token, decryptToken } = useAuthSession()
|
||||||
const dispatch = useDispatch();
|
const [isSelect, setSelect] = useState(false)
|
||||||
const update = useSelector((state: any) => state.divisionCreate);
|
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" })
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const update = useSelector((state: any) => state.divisionCreate)
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const userLogin = useSelector((state: any) => state.entities)
|
const userLogin = useSelector((state: any) => state.entities)
|
||||||
|
const [loadingBtn, setLoadingBtn] = useState(false)
|
||||||
|
const [showWarningModal, setShowWarningModal] = useState(false)
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
idGroup: false,
|
idGroup: false,
|
||||||
name: false,
|
name: false,
|
||||||
@@ -54,7 +63,31 @@ export default function CreateDivision() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSetData() {
|
async function handleCheckName() {
|
||||||
|
try {
|
||||||
|
setLoadingBtn(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiCheckDivisionName({ data: { ...dataForm }, user: hasil })
|
||||||
|
if (response.success) {
|
||||||
|
if (!response.available) {
|
||||||
|
setShowWarningModal(true)
|
||||||
|
} else {
|
||||||
|
handleSetData()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoadingBtn(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetData() {
|
||||||
dispatch(setFormCreateDivision({ ...update, data: dataForm }))
|
dispatch(setFormCreateDivision({ ...update, data: dataForm }))
|
||||||
router.push(`./create/add-member`)
|
router.push(`./create/add-member`)
|
||||||
}
|
}
|
||||||
@@ -66,28 +99,41 @@ export default function CreateDivision() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah Divisi",
|
headerTitle: "Tambah Divisi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonNextHeader
|
// <ButtonNextHeader
|
||||||
onPress={() => { handleSetData() }}
|
// onPress={() => { handleCheckName() }}
|
||||||
disable={error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
|
// disable={loadingBtn || error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Tambah Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<ButtonNextHeader
|
||||||
|
onPress={() => { handleCheckName() }}
|
||||||
|
disable={loadingBtn || error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
{
|
{
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||||
(
|
(
|
||||||
@@ -133,6 +179,15 @@ export default function CreateDivision() {
|
|||||||
open={isSelect}
|
open={isSelect}
|
||||||
valChoose={chooseGroup.val}
|
valChoose={chooseGroup.val}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showWarningModal}
|
||||||
|
title="Peringatan"
|
||||||
|
message="Nama divisi sudah ada. Tidak dapat membuat divisi dengan nama yang sama"
|
||||||
|
onConfirm={() => setShowWarningModal(false)}
|
||||||
|
onCancel={() => setShowWarningModal(false)}
|
||||||
|
confirmText="Oke"
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateDivision } from "@/lib/api";
|
import { apiCreateDivision } from "@/lib/api";
|
||||||
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
||||||
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
import { setUpdateDivision } from "@/lib/divisionUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
|
import { StackActions, useNavigation } from "@react-navigation/native";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -21,7 +24,9 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateDivisionAddAdmin() {
|
export default function CreateDivisionAddAdmin() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const navigation = useNavigation()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
const [dataOld, setDataOld] = useState<Props[]>([])
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
@@ -29,6 +34,7 @@ export default function CreateDivisionAddAdmin() {
|
|||||||
const update = useSelector((state: any) => state.divisionCreate)
|
const update = useSelector((state: any) => state.divisionCreate)
|
||||||
const updateDivision = useSelector((state: any) => state.divisionUpdate)
|
const updateDivision = useSelector((state: any) => state.divisionUpdate)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoadMember() {
|
async function handleLoadMember() {
|
||||||
setData(update.member)
|
setData(update.member)
|
||||||
@@ -48,43 +54,61 @@ export default function CreateDivisionAddAdmin() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
dispatch(setFormCreateDivision({ ...update, admin: selectMember }))
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiCreateDivision({ ...update, user: hasil })
|
const response = await apiCreateDivision({ ...update, admin: selectMember, user: hasil })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil membuat divisi', })
|
Toast.show({ type: 'small', text1: 'Berhasil membuat divisi', })
|
||||||
dispatch(setFormCreateDivision({ admin: [], member: [], data: { idGroup: '', name: '', desc: '' } }))
|
dispatch(setFormCreateDivision({ admin: [], member: [], data: { idGroup: '', name: '', desc: '' } }))
|
||||||
dispatch(setUpdateDivision(!updateDivision))
|
dispatch(setUpdateDivision(!updateDivision))
|
||||||
router.replace(`/division/`)
|
navigation.dispatch(StackActions.pop(3))
|
||||||
|
// router.replace(`/division/`)
|
||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal membuat divisi', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Pilih Admin Divisi',
|
headerTitle: 'Pilih Admin Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="create"
|
// category="create"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Pilih Admin Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<ButtonSaveHeader
|
||||||
|
category="create"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -93,22 +117,22 @@ export default function CreateDivisionAddAdmin() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderBottomColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.idUser)
|
!found && onChoose(item.idUser)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i == item.idUser) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i == item.idUser) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -119,6 +143,6 @@ export default function CreateDivisionAddAdmin() {
|
|||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonNextHeader from "@/components/buttonNextHeader";
|
import ButtonNextHeader from "@/components/buttonNextHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetUser } from "@/lib/api";
|
import { apiGetUser } from "@/lib/api";
|
||||||
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,6 +23,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateDivisionAddMember() {
|
export default function CreateDivisionAddMember() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
const [dataOld, setDataOld] = useState<Props[]>([])
|
||||||
@@ -59,34 +62,44 @@ export default function CreateDivisionAddMember() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Pilih Anggota',
|
headerTitle: 'Pilih Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonNextHeader
|
// <ButtonNextHeader
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length > 0 ? false : true}
|
||||||
onPress={() => { handleAddMember() }}
|
// onPress={() => { handleAddMember() }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Pilih Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={<ButtonNextHeader
|
||||||
|
disable={selectMember.length > 0 ? false : true}
|
||||||
|
onPress={() => { handleAddMember() }}
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
<InputSearch onChange={(val) => setSearch(val)} value={search} />
|
||||||
|
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -95,9 +108,11 @@ export default function CreateDivisionAddMember() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
@@ -106,22 +121,22 @@ export default function CreateDivisionAddMember() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={[Styles.itemSelectModal]}
|
style={[Styles.itemSelectModal, { borderBottomColor: colors.icon + '20' }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!found && onChoose(item.id, item.name, item.img)
|
!found && onChoose(item.id, item.name, item.img)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
<View style={[Styles.rowItemsCenter, Styles.w70]}>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.ml10]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{item.name}</Text>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} />
|
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -132,6 +147,6 @@ export default function CreateDivisionAddMember() {
|
|||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import PaperGridContent from "@/components/paperGridContent";
|
import PaperGridContent from "@/components/paperGridContent";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import WrapTab from "@/components/wrapTab";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDivision } from "@/lib/api";
|
import { apiGetDivision } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import {
|
import {
|
||||||
AntDesign,
|
AntDesign,
|
||||||
Feather,
|
Feather,
|
||||||
Ionicons,
|
Ionicons,
|
||||||
MaterialCommunityIcons,
|
MaterialCommunityIcons
|
||||||
MaterialIcons,
|
|
||||||
} from "@expo/vector-icons";
|
} from "@expo/vector-icons";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -35,25 +38,39 @@ export default function ListDivision() {
|
|||||||
cat?: string;
|
cat?: string;
|
||||||
}>();
|
}>();
|
||||||
const [isList, setList] = useState(false);
|
const [isList, setList] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AsyncStorage.getItem('division_view_mode').then((val) => {
|
||||||
|
if (val !== null) setList(val === 'list')
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function toggleView() {
|
||||||
|
const next = !isList
|
||||||
|
setList(next)
|
||||||
|
AsyncStorage.setItem('division_view_mode', next ? 'list' : 'grid')
|
||||||
|
}
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [nameGroup, setNameGroup] = useState("")
|
const queryClient = useQueryClient()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
|
const [category, setCategory] = useState<'divisi-saya' | 'semua'>(cat == 'semua' ? 'semua' : 'divisi-saya')
|
||||||
const update = useSelector((state: any) => state.divisionUpdate)
|
const update = useSelector((state: any) => state.divisionUpdate)
|
||||||
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [category, setCategory] = useState<'divisi-saya' | 'semua'>('divisi-saya')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Divisions with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['divisions', { status, search, group, category }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDivision({
|
const response = await apiGetDivision({
|
||||||
user: hasil,
|
user: hasil,
|
||||||
@@ -61,63 +78,61 @@ export default function ListDivision() {
|
|||||||
search: search,
|
search: search,
|
||||||
group: String(group),
|
group: String(group),
|
||||||
kategori: category,
|
kategori: category,
|
||||||
page: thisPage
|
page: pageParam
|
||||||
});
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
if (response.success) {
|
// Refetch when manual update state changes
|
||||||
if (thisPage == 1) {
|
|
||||||
setData(response.data);
|
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
|
||||||
setData([...data, ...response.data]);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setNameGroup(response.filter.name);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1);
|
refetch()
|
||||||
}, [update]);
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
// Flatten pages into a single data array
|
||||||
handleLoad(true, 1);
|
const flatData = useMemo(() => {
|
||||||
}, [status, search, group, category]);
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const loadMoreData = () => {
|
// Get nameGroup from the first available page
|
||||||
if (waiting) return
|
const nameGroup = useMemo(() => {
|
||||||
setTimeout(() => {
|
return data?.pages[0]?.filter?.name || "";
|
||||||
handleLoad(false, page + 1)
|
}, [data])
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['divisions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
name: data[index].name,
|
name: flatData[index]?.name,
|
||||||
desc: data[index].desc,
|
desc: flatData[index]?.desc,
|
||||||
jumlah_member: data[index].jumlah_member,
|
jumlah_member: flatData[index]?.jumlah_member,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<View>
|
<View>
|
||||||
{
|
{
|
||||||
entityUser.role != "user" && entityUser.role != "coadmin" ?
|
entityUser.role != "user" && entityUser.role != "coadmin" ?
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
@@ -126,7 +141,7 @@ export default function ListDivision() {
|
|||||||
icon={
|
icon={
|
||||||
<Feather
|
<Feather
|
||||||
name="check-circle"
|
name="check-circle"
|
||||||
color={status == "false" ? "black" : "white"}
|
color={status == "false" ? colors.dimmed : "white"}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -140,15 +155,15 @@ export default function ListDivision() {
|
|||||||
icon={
|
icon={
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name="closecircleo"
|
name="closecircleo"
|
||||||
color={status == "true" ? "black" : "white"}
|
color={status == "true" ? colors.dimmed : "white"}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
n={2}
|
n={2}
|
||||||
/>
|
/>
|
||||||
</View>
|
</WrapTab>
|
||||||
:
|
:
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={category == "semua" ? "false" : "true"}
|
active={category == "semua" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
@@ -157,7 +172,7 @@ export default function ListDivision() {
|
|||||||
icon={
|
icon={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="file-tray-outline"
|
name="file-tray-outline"
|
||||||
color={category == "semua" ? "black" : "white"}
|
color={category == "semua" ? colors.dimmed : "white"}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -171,38 +186,35 @@ export default function ListDivision() {
|
|||||||
icon={
|
icon={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="file-tray-stacked-outline"
|
name="file-tray-stacked-outline"
|
||||||
color={category == "semua" ? "white" : "black"}
|
color={category == "semua" ? "white" : colors.dimmed}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
n={2}
|
n={2}
|
||||||
/>
|
/>
|
||||||
</View>
|
</WrapTab>
|
||||||
}
|
}
|
||||||
|
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||||
<InputSearch width={68} onChange={setSearch} />
|
<InputSearch width={68} onChange={setSearch} />
|
||||||
<Pressable
|
<Pressable onPress={toggleView}>
|
||||||
onPress={() => {
|
|
||||||
setList(!isList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||||
color={"black"}
|
color={colors.text}
|
||||||
size={30}
|
size={30}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
|
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
|
||||||
<View style={[Styles.mv05]}>
|
<View style={[Styles.mt10, Styles.rowOnly]}>
|
||||||
<Text>Filter : {nameGroup}</Text>
|
<Text>Filter :</Text>
|
||||||
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
isList ?
|
isList ?
|
||||||
arrSkeleton.map((item, index) => (
|
arrSkeleton.map((item, index) => (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
@@ -212,17 +224,17 @@ export default function ListDivision() {
|
|||||||
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
||||||
))
|
))
|
||||||
:
|
:
|
||||||
data.length == 0 ? (
|
flatData.length == 0 ? (
|
||||||
<View style={[Styles.mt15]}>
|
<View style={[Styles.mt15]}>
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
isList ? (
|
isList ? (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
style={[{ paddingBottom: 100 }]}
|
style={[{ paddingBottom: 100 }]}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -230,13 +242,13 @@ export default function ListDivision() {
|
|||||||
key={index}
|
key={index}
|
||||||
onPress={() => { router.push(`/division/${item.id}`) }}
|
onPress={() => { router.push(`/division/${item.id}`) }}
|
||||||
borderType="bottom"
|
borderType="bottom"
|
||||||
|
bgColor="transparent"
|
||||||
icon={
|
icon={
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
<View style={[Styles.iconContent]}>
|
||||||
<MaterialIcons name="group" size={25} color={"#384288"} />
|
<Feather name="users" size={25} color={'black'} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
titleWeight="normal"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -248,6 +260,7 @@ export default function ListDivision() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -255,9 +268,9 @@ export default function ListDivision() {
|
|||||||
) : (
|
) : (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
style={[{ paddingBottom: 100 }]}
|
style={[{ paddingBottom: 100 }]}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -283,6 +296,7 @@ export default function ListDivision() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ReportChartDocument from "@/components/division/reportChartDocument";
|
import ReportChartDocument from "@/components/division/reportChartDocument";
|
||||||
import ReportChartEvent from "@/components/division/reportChartEvent";
|
import ReportChartEvent from "@/components/division/reportChartEvent";
|
||||||
import ReportChartProgress from "@/components/division/reportChartProgress";
|
import ReportChartProgress from "@/components/division/reportChartProgress";
|
||||||
@@ -9,6 +9,7 @@ import Styles from "@/constants/Styles";
|
|||||||
import { apiGetDivisionReport } from "@/lib/api";
|
import { apiGetDivisionReport } from "@/lib/api";
|
||||||
import { stringToDate } from "@/lib/fun_stringToDate";
|
import { stringToDate } from "@/lib/fun_stringToDate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -16,6 +17,7 @@ import { SafeAreaView, ScrollView, View } from "react-native";
|
|||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
export default function Report() {
|
export default function Report() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
||||||
const [showReport, setShowReport] = useState(false);
|
const [showReport, setShowReport] = useState(false);
|
||||||
@@ -110,8 +112,11 @@ export default function Report() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,24 +127,33 @@ export default function Report() {
|
|||||||
}, [showReport]);
|
}, [showReport]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Laporan Divisi",
|
headerTitle: "Laporan Divisi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Laporan Divisi"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.p15, Styles.mb50]}>
|
||||||
<SelectForm
|
<SelectForm
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
label="Lembaga Desa"
|
label="Lembaga Desa"
|
||||||
placeholder="Pilih Lembaga Desa"
|
placeholder="Pilih Lembaga Desa"
|
||||||
value={chooseGroup.label}
|
value={chooseGroup.label}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SelectForm from "@/components/selectForm";
|
import SelectForm from "@/components/selectForm";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditProfile, apiGetProfile } from "@/lib/api";
|
import { apiEditProfile, apiGetProfile } from "@/lib/api";
|
||||||
import { setEntities } from "@/lib/entitiesSlice";
|
import { setEntities } from "@/lib/entitiesSlice";
|
||||||
|
import { validateName } from "@/lib/fun_validateName";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useHeaderHeight } from "@react-navigation/elements";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -35,10 +42,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EditProfile() {
|
export default function EditProfile() {
|
||||||
|
const headerHeight = useHeaderHeight()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const { colors } = useTheme();
|
||||||
const entities = useSelector((state: any) => state.entities)
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [errorImg, setErrorImg] = useState(false)
|
const [errorImg, setErrorImg] = useState(false)
|
||||||
|
// ... keeping state same ...
|
||||||
const [selectedImage, setSelectedImage] = useState<string | undefined | { uri: string }>(undefined);
|
const [selectedImage, setSelectedImage] = useState<string | undefined | { uri: string }>(undefined);
|
||||||
const [choosePosition, setChoosePosition] = useState({ val: entities.idPosition, label: entities.position });
|
const [choosePosition, setChoosePosition] = useState({ val: entities.idPosition, label: entities.position });
|
||||||
const [chooseGender, setChooseGender] = useState({ val: entities.gender, label: entities.gender == "F" ? 'Perempuan' : 'Laki-laki' });
|
const [chooseGender, setChooseGender] = useState({ val: entities.gender, label: entities.gender == "F" ? 'Perempuan' : 'Laki-laki' });
|
||||||
@@ -46,7 +56,8 @@ export default function EditProfile() {
|
|||||||
const [isSelect, setSelect] = useState(false);
|
const [isSelect, setSelect] = useState(false);
|
||||||
const [disableBtn, setDisableBtn] = useState(false)
|
const [disableBtn, setDisableBtn] = useState(false)
|
||||||
const [valChoose, setValChoose] = useState("")
|
const [valChoose, setValChoose] = useState("")
|
||||||
const [imgForm, setImgForm] = useState<any>();
|
const [imgForm, setImgForm] = useState<any>()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [data, setData] = useState<Props>({
|
const [data, setData] = useState<Props>({
|
||||||
id: entities.id,
|
id: entities.id,
|
||||||
name: entities.name,
|
name: entities.name,
|
||||||
@@ -102,7 +113,7 @@ export default function EditProfile() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "name") {
|
} else if (cat == "name") {
|
||||||
setData({ ...data, name: val });
|
setData({ ...data, name: val });
|
||||||
if (val == "") {
|
if (!validateName(val)) {
|
||||||
setError({ ...error, name: true });
|
setError({ ...error, name: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, name: false });
|
setError({ ...error, name: false });
|
||||||
@@ -119,7 +130,7 @@ export default function EditProfile() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "phone") {
|
} else if (cat == "phone") {
|
||||||
setData({ ...data, phone: val });
|
setData({ ...data, phone: val });
|
||||||
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
|
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
|
||||||
setError({ ...error, phone: true });
|
setError({ ...error, phone: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, phone: false });
|
setError({ ...error, phone: false });
|
||||||
@@ -150,14 +161,15 @@ export default function EditProfile() {
|
|||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|
||||||
if (imgForm != undefined) {
|
if (imgForm != undefined) {
|
||||||
fd.append("file", {
|
fd.append("file", {
|
||||||
uri: imgForm.uri,
|
uri: imgForm.uri,
|
||||||
type: imgForm.mimeType,
|
type: imgForm.mimeType || "image/jpeg",
|
||||||
name: imgForm.fileName,
|
name: imgForm.fileName || "image.jpg",
|
||||||
} as any);
|
} as any);
|
||||||
} else {
|
} else {
|
||||||
fd.append("file", "undefined",);
|
fd.append("file", "undefined",);
|
||||||
@@ -176,9 +188,16 @@ export default function EditProfile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
type: 'small',
|
||||||
|
text1: message
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +206,9 @@ export default function EditProfile() {
|
|||||||
const pickImageAsync = async () => {
|
const pickImageAsync = async () => {
|
||||||
let result = await ImagePicker.launchImageLibraryAsync({
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: ["images"],
|
mediaTypes: ["images"],
|
||||||
allowsEditing: false,
|
allowsEditing: true,
|
||||||
quality: 1,
|
quality: 0.9,
|
||||||
aspect: [1, 1],
|
aspect: [1, 1]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
@@ -197,141 +216,166 @@ export default function EditProfile() {
|
|||||||
setSelectedImage(result.assets[0].uri);
|
setSelectedImage(result.assets[0].uri);
|
||||||
setImgForm(result.assets[0]);
|
setImgForm(result.assets[0]);
|
||||||
} else {
|
} else {
|
||||||
alert("Tidak ada gambar yang dipilih");
|
|
||||||
setErrorImg(false)
|
setErrorImg(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Edit Profile",
|
headerTitle: "Edit Profile",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
header: () => (
|
||||||
<ButtonSaveHeader
|
<AppHeader
|
||||||
disable={disableBtn}
|
title="Edit Profile"
|
||||||
category="update"
|
showBack={true}
|
||||||
onPress={() => {
|
onPressLeft={() => router.back()}
|
||||||
handleEdit()
|
right={
|
||||||
}}
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || loading ? true : false}
|
||||||
|
category="update"
|
||||||
|
onPress={() => {
|
||||||
|
handleEdit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
|
// headerRight: () => (
|
||||||
|
// <ButtonSaveHeader
|
||||||
|
// disable={disableBtn || loading ? true : false}
|
||||||
|
// category="update"
|
||||||
|
// onPress={() => {
|
||||||
|
// handleEdit()
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<KeyboardAvoidingView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
style={[Styles.h100]}
|
||||||
<View style={{ justifyContent: "center", alignItems: "center" }}>
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
{
|
keyboardVerticalOffset={headerHeight}
|
||||||
selectedImage != undefined ? (
|
>
|
||||||
<Pressable onPress={pickImageAsync}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
<Image
|
<View style={[Styles.p15]}>
|
||||||
src={
|
<View style={[Styles.contentItemCenter]}>
|
||||||
typeof selectedImage === "string"
|
{
|
||||||
? selectedImage
|
selectedImage != undefined ? (
|
||||||
: selectedImage.uri
|
<Pressable onPress={pickImageAsync}>
|
||||||
}
|
|
||||||
style={[Styles.userProfileBig]}
|
|
||||||
onError={() => { setErrorImg(true) }}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
) : (
|
|
||||||
<Pressable onPress={pickImageAsync}>
|
|
||||||
{
|
|
||||||
<Image
|
<Image
|
||||||
source={errorImg ? require("../../assets/images/user.jpg") : { uri: `https://wibu-storage.wibudev.com/api/files/${data?.img}` }}
|
src={
|
||||||
|
typeof selectedImage === "string"
|
||||||
|
? selectedImage
|
||||||
|
: selectedImage.uri
|
||||||
|
}
|
||||||
style={[Styles.userProfileBig]}
|
style={[Styles.userProfileBig]}
|
||||||
onError={() => { setErrorImg(true) }}
|
onError={() => { setErrorImg(true) }}
|
||||||
/>
|
/>
|
||||||
}
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
</Pressable>
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
)
|
</View>
|
||||||
}
|
</Pressable>
|
||||||
|
) : (
|
||||||
|
<Pressable onPress={pickImageAsync}>
|
||||||
|
<Image
|
||||||
|
source={errorImg ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${data?.img}` }}
|
||||||
|
style={[Styles.userProfileBig]}
|
||||||
|
onError={() => { setErrorImg(true) }}
|
||||||
|
/>
|
||||||
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
<SelectForm
|
||||||
|
label="Jabatan"
|
||||||
|
placeholder="Pilih Jabatan"
|
||||||
|
value={choosePosition.label}
|
||||||
|
required
|
||||||
|
onPress={() => {
|
||||||
|
setValChoose(choosePosition.val);
|
||||||
|
setValSelect("position");
|
||||||
|
setSelect(true);
|
||||||
|
}}
|
||||||
|
error={error.position}
|
||||||
|
errorText="Jabatan tidak boleh kosong"
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="NIK"
|
||||||
|
type="numeric"
|
||||||
|
placeholder="NIK"
|
||||||
|
required
|
||||||
|
value={data?.nik}
|
||||||
|
error={error.nik}
|
||||||
|
errorText="NIK Harus 16 Karakter"
|
||||||
|
onChange={val => {
|
||||||
|
validationForm("nik", val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Nama"
|
||||||
|
type="default"
|
||||||
|
placeholder="Nama"
|
||||||
|
required
|
||||||
|
value={data?.name}
|
||||||
|
error={error.name}
|
||||||
|
errorText="Nama harus 3–50 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
|
||||||
|
onChange={val => {
|
||||||
|
validationForm("name", val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Email"
|
||||||
|
type="default"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
value={data?.email}
|
||||||
|
error={error.email}
|
||||||
|
errorText="Email tidak valid"
|
||||||
|
onChange={val => {
|
||||||
|
validationForm("email", val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Nomor Telepon"
|
||||||
|
type="numeric"
|
||||||
|
placeholder="8XX-XXX-XXX"
|
||||||
|
required
|
||||||
|
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02]}>+62</Text>}
|
||||||
|
value={data?.phone}
|
||||||
|
error={error.phone}
|
||||||
|
errorText="Nomor Telepon tidak valid"
|
||||||
|
onChange={val => {
|
||||||
|
validationForm("phone", val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SelectForm
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
placeholder="Pilih Jenis Kelamin"
|
||||||
|
value={chooseGender.label}
|
||||||
|
required
|
||||||
|
onPress={() => {
|
||||||
|
setValChoose(chooseGender.val);
|
||||||
|
setValSelect("gender");
|
||||||
|
setSelect(true);
|
||||||
|
}}
|
||||||
|
error={error.gender}
|
||||||
|
errorText="Jenis Kelamin tidak boleh kosong"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<SelectForm
|
</ScrollView>
|
||||||
label="Jabatan"
|
</KeyboardAvoidingView>
|
||||||
placeholder="Pilih Jabatan"
|
|
||||||
value={choosePosition.label}
|
|
||||||
required
|
|
||||||
onPress={() => {
|
|
||||||
setValChoose(choosePosition.val);
|
|
||||||
setValSelect("position");
|
|
||||||
setSelect(true);
|
|
||||||
}}
|
|
||||||
error={error.position}
|
|
||||||
errorText="Jabatan tidak boleh kosong"
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="NIK"
|
|
||||||
type="numeric"
|
|
||||||
placeholder="NIK"
|
|
||||||
required
|
|
||||||
value={data?.nik}
|
|
||||||
error={error.nik}
|
|
||||||
errorText="NIK Harus 16 Karakter"
|
|
||||||
onChange={val => {
|
|
||||||
validationForm("nik", val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Nama"
|
|
||||||
type="default"
|
|
||||||
placeholder="Nama"
|
|
||||||
required
|
|
||||||
value={data?.name}
|
|
||||||
error={error.name}
|
|
||||||
errorText="Nama tidak boleh kosong"
|
|
||||||
onChange={val => {
|
|
||||||
validationForm("name", val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Email"
|
|
||||||
type="default"
|
|
||||||
placeholder="Email"
|
|
||||||
required
|
|
||||||
value={data?.email}
|
|
||||||
error={error.email}
|
|
||||||
errorText="Email tidak valid"
|
|
||||||
onChange={val => {
|
|
||||||
validationForm("email", val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Nomor Telepon"
|
|
||||||
type="numeric"
|
|
||||||
placeholder="8XX-XXX-XXX"
|
|
||||||
required
|
|
||||||
itemLeft={<Text>+62</Text>}
|
|
||||||
value={data?.phone}
|
|
||||||
error={error.phone}
|
|
||||||
errorText="Nomor Telepon tidak valid"
|
|
||||||
onChange={val => {
|
|
||||||
validationForm("phone", val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SelectForm
|
|
||||||
label="Jenis Kelamin"
|
|
||||||
placeholder="Pilih Jenis Kelamin"
|
|
||||||
value={chooseGender.label}
|
|
||||||
required
|
|
||||||
onPress={() => {
|
|
||||||
setValChoose(chooseGender.val);
|
|
||||||
setValSelect("gender");
|
|
||||||
setSelect(true);
|
|
||||||
}}
|
|
||||||
error={error.gender}
|
|
||||||
errorText="Jenis Kelamin tidak boleh kosong"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<ModalSelect
|
<ModalSelect
|
||||||
category={valSelect}
|
category={valSelect}
|
||||||
|
|||||||
@@ -1,51 +1,47 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import { ButtonFiturMenu } from "@/components/buttonFiturMenu";
|
import { ButtonFiturMenu } from "@/components/buttonFiturMenu";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { AntDesign, Entypo, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { AntDesign, Entypo, Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { SafeAreaView, View } from "react-native";
|
import { SafeAreaView, View } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function Feature() {
|
export default function Feature() {
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Fitur',
|
headerTitle: 'Fitur',
|
||||||
headerTitleAlign: 'center'
|
headerTitleAlign: 'center',
|
||||||
|
header: () => (
|
||||||
|
<AppHeader title="Fitur" showBack={true} onPressLeft={() => router.back()} />
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mb15]}>
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', rowGap: 20 }}>
|
||||||
<ButtonFiturMenu icon={<MaterialIcons name="group" size={35} color="black" />} text="Divisi" onPress={() => { router.push('/division?active=true') }} />
|
<ButtonFiturMenu icon={<Feather name="users" size={30} color={colors.icon} />} text="Divisi" onPress={() => { router.push('/division?active=true') }} />
|
||||||
<ButtonFiturMenu icon={<AntDesign name="areachart" size={35} color="black" />} text="Kegiatan" onPress={() => { router.push('/project?status=0') }} />
|
<ButtonFiturMenu icon={<Feather name="bar-chart" size={30} color={colors.icon} />} text="Kegiatan" onPress={() => { router.push('/project?status=0') }} />
|
||||||
<ButtonFiturMenu icon={<MaterialIcons name="campaign" size={35} color="black" />} text="Pengumuman" onPress={() => { router.push('/announcement') }} />
|
<ButtonFiturMenu icon={<Ionicons name="megaphone-outline" size={30} color={colors.icon} />} text="Pengumuman" onPress={() => { router.push('/announcement') }} />
|
||||||
<ButtonFiturMenu icon={<Ionicons name="chatbubbles-sharp" size={35} color="black" />} text="Diskusi" onPress={() => { router.push('/discussion?active=true') }} />
|
<ButtonFiturMenu icon={<Ionicons name="chatbubbles-outline" size={30} color={colors.icon} />} text="Diskusi" onPress={() => { router.push('/discussion?active=true') }} />
|
||||||
</View>
|
<ButtonFiturMenu icon={<Feather name="calendar" size={30} color={colors.icon} />} text="Kalender" onPress={() => { router.push('/village-calendar') }} />
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mb15, (entityUser.role == 'cosupadmin' ? Styles.w70 : entityUser.role == 'supadmin' || entityUser.role == 'developer' ? Styles.w100 : Styles.w40)]}>
|
<ButtonFiturMenu icon={<MaterialCommunityIcons name="account-group-outline" size={30} color={colors.icon} />} text="Anggota" onPress={() => { router.push('/member') }} />
|
||||||
<ButtonFiturMenu icon={<MaterialIcons name="groups" size={35} color="black" />} text="Anggota" onPress={() => { router.push('/member') }} />
|
<ButtonFiturMenu icon={<MaterialCommunityIcons name="account-tie-outline" size={30} color={colors.icon} />} text="Jabatan" onPress={() => { router.push('/position') }} />
|
||||||
<ButtonFiturMenu icon={<MaterialCommunityIcons name="account-tie" size={35} color="black" />} text="Jabatan" onPress={() => { router.push('/position') }} />
|
|
||||||
{
|
{
|
||||||
entityUser.role == "cosupadmin" && <ButtonFiturMenu icon={<Entypo name="image-inverted" size={35} color="black" />} text="Banner" onPress={() => { router.push('/banner') }} />
|
entityUser.role == "cosupadmin" && <ButtonFiturMenu icon={<Ionicons name="images-outline" size={30} color={colors.icon} />} text="Banner" onPress={() => { router.push('/banner') }} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||||
<>
|
<>
|
||||||
<ButtonFiturMenu icon={<AntDesign name="tags" size={35} color="black" />} text="Lembaga Desa" onPress={() => { router.push('/group') }} />
|
<ButtonFiturMenu icon={<Ionicons name="bookmarks-outline" size={30} color={colors.icon} />} text="Lembaga Desa" onPress={() => { router.push('/group') }} />
|
||||||
{/* <ButtonFiturMenu icon={<Ionicons name="color-palette-sharp" size={35} color="black" />} text="Tema" onPress={() => { }} /> */}
|
<ButtonFiturMenu icon={<Ionicons name="images-outline" size={30} color={colors.icon} />} text="Banner" onPress={() => { router.push('/banner') }} />
|
||||||
<ButtonFiturMenu icon={<Entypo name="image-inverted" size={35} color="black" />} text="Banner" onPress={() => { router.push('/banner') }} />
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
{/* {
|
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.mb15]}>
|
|
||||||
<ButtonFiturMenu icon={<Entypo name="image-inverted" size={35} color="black" />} text="Banner" onPress={() => { router.push('/banner') }} />
|
|
||||||
</View>
|
|
||||||
} */}
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import { ButtonForm } from "@/components/buttonForm";
|
import { ButtonForm } from "@/components/buttonForm";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
@@ -8,14 +9,18 @@ import InputSearch from "@/components/inputSearch";
|
|||||||
import MenuItemRow from "@/components/menuItemRow";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import WrapTab from "@/components/wrapTab";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiDeleteGroup, apiEditGroup, apiGetGroup } from "@/lib/api";
|
import { apiDeleteGroup, apiEditGroup, apiGetGroup } from "@/lib/api";
|
||||||
import { setUpdateGroup } from "@/lib/groupSlice";
|
import { setUpdateGroup } from "@/lib/groupSlice";
|
||||||
|
import { GUIDE_GROUP } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { useEffect, useState } from "react";
|
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -27,39 +32,63 @@ type Props = {
|
|||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
const [status, setStatus] = useState<'true' | 'false'>('true')
|
||||||
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
const [idChoose, setIdChoose] = useState('')
|
const [idChoose, setIdChoose] = useState('')
|
||||||
const [activeChoose, setActiveChoose] = useState(true)
|
const [activeChoose, setActiveChoose] = useState(true)
|
||||||
const [titleChoose, setTitleChoose] = useState('')
|
const [titleChoose, setTitleChoose] = useState('')
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('group')
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.groupUpdate)
|
const update = useSelector((state: any) => state.groupUpdate)
|
||||||
|
const [error, setError] = useState({
|
||||||
|
title: false,
|
||||||
|
});
|
||||||
|
|
||||||
const [data11, setData1] = useState(Array.from({ length: 20 }, (_, i) => `Item ${i}`));
|
// TanStack Query for Groups
|
||||||
|
const {
|
||||||
|
data: queryData,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['groups', { status, search }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiGetGroup({
|
||||||
|
user: hasil,
|
||||||
|
active: status,
|
||||||
|
search: search
|
||||||
|
})
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: string }) => (
|
const data = useMemo(() => queryData?.data || [], [queryData])
|
||||||
<View style={{ padding: 20, borderBottomWidth: 1, borderColor: '#ccc' }}>
|
|
||||||
<Text>{item}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch()
|
||||||
|
}, [update, refetch])
|
||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoadingSubmit(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
|
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
dispatch(setUpdateGroup(!update))
|
dispatch(setUpdateGroup(!update))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
|
setLoadingSubmit(false)
|
||||||
setVisibleEdit(false)
|
setVisibleEdit(false)
|
||||||
setModal(false)
|
setModal(false)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
||||||
@@ -71,6 +100,7 @@ export default function Index() {
|
|||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose)
|
const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
dispatch(setUpdateGroup(!update))
|
dispatch(setUpdateGroup(!update))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -80,119 +110,119 @@ export default function Index() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
|
||||||
try {
|
|
||||||
setLoading(loading)
|
|
||||||
const hasil = await decryptToken(String(token?.current))
|
|
||||||
const response = await apiGetGroup({ user: hasil, active: status, search: search })
|
|
||||||
setData(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(false)
|
|
||||||
}, [update])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, [status, search])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false)
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function validationForm(val: any, cat: 'title') {
|
||||||
|
if (cat === 'title') {
|
||||||
|
setTitleChoose(val)
|
||||||
|
if (val == "" || val.length < 3) {
|
||||||
|
setError((prev) => ({ ...prev, title: true }))
|
||||||
|
} else {
|
||||||
|
setError((prev) => ({ ...prev, title: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
|
id: data[index].id,
|
||||||
|
name: data[index].name,
|
||||||
|
isActive: data[index].isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<GuideOverlay visible={guideVisible} steps={GUIDE_GROUP} onDismiss={dismissGuide} />
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
<View style={[Styles.mb10]}>
|
||||||
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
onPress={() => { setStatus("true") }}
|
onPress={() => { setStatus("true") }}
|
||||||
label="Aktif"
|
label="Aktif"
|
||||||
icon={<Feather name="check-circle" color={status == "true" ? 'white' : 'black'} size={20} />}
|
icon={<Feather name="check-circle" color={status == "true" ? 'white' : colors.dimmed} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="false"
|
value="false"
|
||||||
onPress={() => { setStatus("false") }}
|
onPress={() => { setStatus("false") }}
|
||||||
label="Tidak Aktif"
|
label="Tidak Aktif"
|
||||||
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : 'black'} size={20} />}
|
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : colors.dimmed} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
</View>
|
</WrapTab>
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
<ScrollView
|
</View>
|
||||||
refreshControl={
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
<RefreshControl
|
{
|
||||||
refreshing={refreshing}
|
isLoading ?
|
||||||
onRefresh={handleRefresh}
|
arrSkeleton.map((item, index) => {
|
||||||
/>
|
return (
|
||||||
}
|
<SkeletonTwoItem key={index} />
|
||||||
style={[Styles.h100]}
|
)
|
||||||
>
|
})
|
||||||
<View>
|
:
|
||||||
{
|
data.length > 0 ?
|
||||||
|
<VirtualizedList
|
||||||
loading ?
|
data={data}
|
||||||
arrSkeleton.map((item, index) => {
|
getItemCount={() => data.length}
|
||||||
|
getItem={getItem}
|
||||||
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<BorderBottomItem
|
||||||
|
key={index}
|
||||||
|
onPress={() => {
|
||||||
|
setIdChoose(item.id)
|
||||||
|
setActiveChoose(item.isActive)
|
||||||
|
setTitleChoose(item.name)
|
||||||
|
setModal(true)
|
||||||
|
}}
|
||||||
|
borderType="all"
|
||||||
|
icon={
|
||||||
|
<View style={[Styles.iconContent]}>
|
||||||
|
<Ionicons name="bookmark-outline" size={25} color={'black'} />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={item.name}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})
|
}}
|
||||||
:
|
keyExtractor={(item, index) => String(index)}
|
||||||
data.length > 0 ?
|
showsVerticalScrollIndicator={false}
|
||||||
data.map((item, index) => {
|
refreshControl={
|
||||||
return (
|
<RefreshControl
|
||||||
<BorderBottomItem
|
refreshing={refreshing}
|
||||||
key={index}
|
onRefresh={handleRefresh}
|
||||||
onPress={() => {
|
tintColor={colors.icon}
|
||||||
setIdChoose(item.id)
|
/>
|
||||||
setActiveChoose(item.isActive)
|
}
|
||||||
setTitleChoose(item.name)
|
/>
|
||||||
setModal(true)
|
:
|
||||||
}}
|
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
|
||||||
borderType="all"
|
}
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
|
||||||
<MaterialCommunityIcons name="office-building-outline" size={25} color={'#384288'} />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.name}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title={titleChoose}>
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title={titleChoose}>
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color={colors.text} size={25} />}
|
||||||
title={activeChoose ? "Non Aktifkan" : "Aktifkan"}
|
title={activeChoose ? "Non Aktifkan" : "Aktifkan"}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => {
|
||||||
title: 'Konfirmasi',
|
setShowDeleteModal(true)
|
||||||
desc: activeChoose ? 'Apakah anda yakin ingin menonaktifkan data?' : 'Apakah anda yakin ingin mengaktifkan data?',
|
}, 600)
|
||||||
onPress: () => { handleDelete() }
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||||
title="Edit"
|
title="Edit"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
@@ -207,15 +237,36 @@ export default function Index() {
|
|||||||
<DrawerBottom animation="none" keyboard height={30} isVisible={isVisibleEdit} setVisible={() => setVisibleEdit(false)} title="Edit Lembaga Desa">
|
<DrawerBottom animation="none" keyboard height={30} isVisible={isVisibleEdit} setVisible={() => setVisibleEdit(false)} title="Edit Lembaga Desa">
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<View>
|
<View>
|
||||||
<InputForm type="default" placeholder="Nama Lembaga Desa" required label="Lembaga Desa" value={titleChoose} onChange={setTitleChoose} />
|
<InputForm
|
||||||
|
type="default"
|
||||||
|
placeholder="Nama Lembaga Desa"
|
||||||
|
required
|
||||||
|
label="Lembaga Desa"
|
||||||
|
value={titleChoose}
|
||||||
|
error={error.title}
|
||||||
|
bg={"transparent"}
|
||||||
|
errorText="Lembaga Desa tidak boleh kosong & minimal 3 karakter"
|
||||||
|
onChange={(val) => { validationForm(val, 'title') }} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<ButtonForm text="SIMPAN" onPress={() => { handleEdit() }} />
|
<ButtonForm text="SIMPAN" disabled={Object.values(error).some((v) => v == true) || titleChoose == "" || loadingSubmit} onPress={() => { handleEdit() }} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message={activeChoose ? 'Apakah anda yakin ingin menonaktifkan data?' : 'Apakah anda yakin ingin mengaktifkan data?'}
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDelete()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText={activeChoose ? "Nonaktifkan" : "Aktifkan"}
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
|
</View >
|
||||||
|
|
||||||
</SafeAreaView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import CaraouselHome from "@/components/home/carouselHome";
|
import CaraouselHome2 from "@/components/home/carouselHome2";
|
||||||
import ChartDokumenHome from "@/components/home/chartDokumenHome";
|
import ChartDokumenHome from "@/components/home/chartDokumenHome";
|
||||||
import ChartProgresHome from "@/components/home/chartProgresHome";
|
import ChartProgresHome from "@/components/home/chartProgresHome";
|
||||||
import DisccussionHome from "@/components/home/discussionHome";
|
import DisccussionHome from "@/components/home/discussionHome";
|
||||||
import DivisionHome from "@/components/home/divisionHome";
|
import DivisionHome from "@/components/home/divisionHome";
|
||||||
import EventHome from "@/components/home/eventHome";
|
import EventHome from "@/components/home/eventHome";
|
||||||
import FiturHome from "@/components/home/fiturHome";
|
|
||||||
import { HeaderRightHome } from "@/components/home/headerRightHome";
|
import { HeaderRightHome } from "@/components/home/headerRightHome";
|
||||||
import ProjectHome from "@/components/home/projectHome";
|
import ProjectHome from "@/components/home/projectHome";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
@@ -12,9 +11,12 @@ import Styles from "@/constants/Styles";
|
|||||||
import { apiGetProfile } from "@/lib/api";
|
import { apiGetProfile } from "@/lib/api";
|
||||||
import { setEntities } from "@/lib/entitiesSlice";
|
import { setEntities } from "@/lib/entitiesSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Platform, SafeAreaView, ScrollView, View } from "react-native";
|
import { Alert, Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -22,42 +24,118 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const entities = useSelector((state: any) => state.entities)
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { token, decryptToken } = useAuthSession()
|
const queryClient = useQueryClient()
|
||||||
const insets = useSafeAreaInsets();
|
const { token, decryptToken, signOut } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
|
const { data: profile, isError } = useQuery({
|
||||||
|
queryKey: ['profile'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const data = await apiGetProfile({ id: hasil })
|
||||||
|
return data.data
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0, // Ensure it refetches every time the component mounts
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync to Redux for global usage
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile) {
|
||||||
|
dispatch(setEntities(profile))
|
||||||
|
}
|
||||||
|
}, [profile, dispatch])
|
||||||
|
|
||||||
|
// Auto Sign Out if profile fetch fails (e.g. invalid/expired token)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
signOut()
|
||||||
|
}
|
||||||
|
}, [isError, signOut])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleUserLogin()
|
if (profile && profile.isActive === false) {
|
||||||
}, [dispatch]);
|
Alert.alert(
|
||||||
|
'Akun Dinonaktifkan',
|
||||||
|
'Akun kamu telah dinonaktifkan. Silahkan hubungi admin untuk informasi lebih lanjut.',
|
||||||
|
[{ text: 'OK', onPress: signOut }]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [profile, signOut])
|
||||||
|
|
||||||
async function handleUserLogin() {
|
useEffect(() => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
if (profile && profile.villageIsActive === false) {
|
||||||
apiGetProfile({ id: hasil }).then((data) => dispatch(setEntities(data.data)));
|
Alert.alert(
|
||||||
}
|
'Desa Dinonaktifkan',
|
||||||
|
'Desa kamu saat ini telah dinonaktifkan. Silahkan hubungi pengelola sistem untuk informasi lebih lanjut.',
|
||||||
|
[{ text: 'OK', onPress: signOut }]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [profile, signOut])
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
// Invalidate all queries related to the home screen
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['profile'] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['homeData'] })
|
||||||
|
|
||||||
|
// Artificial delay to show refresh indicator if sync is too fast
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
headerTitle: entities.village,
|
headerTitle: entities.village,
|
||||||
header: () => (
|
header: () => (
|
||||||
<View style={[Styles.rowItemsCenter, Styles.ph20, Platform.OS === 'ios' ? Styles.pb07 : Styles.pb13, { backgroundColor: '#19345E', paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
|
<View style={[Styles.rowItemsCenter, Styles.ph20, Platform.OS === 'ios' ? Styles.pb07 : Styles.pb13, { backgroundColor: colors.header, paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
|
||||||
<Text style={Styles.textHeaderHome}>{entities.village}</Text>
|
<Text style={Styles.textHeaderHome}>{entities.village}</Text>
|
||||||
<HeaderRightHome />
|
<HeaderRightHome />
|
||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<CaraouselHome />
|
refreshControl={
|
||||||
<View style={[Styles.ph15, Styles.mb100]}>
|
<RefreshControl
|
||||||
<FiturHome />
|
refreshing={refreshing}
|
||||||
<ProjectHome />
|
onRefresh={handleRefresh}
|
||||||
<DivisionHome />
|
tintColor={colors.icon}
|
||||||
<ChartProgresHome />
|
/>
|
||||||
<ChartDokumenHome />
|
}
|
||||||
<EventHome />
|
showsVerticalScrollIndicator={false}
|
||||||
<DisccussionHome />
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.header, colors.header, colors.header, colors.header, colors.homeGradient]}
|
||||||
|
style={[
|
||||||
|
Styles.posAbsolute,
|
||||||
|
Styles.zIndexMinus1,
|
||||||
|
{
|
||||||
|
width: Dimensions.get('window').width * 1.5,
|
||||||
|
height: Dimensions.get('window').width * 1.5,
|
||||||
|
borderRadius: Dimensions.get('window').width * 0.5,
|
||||||
|
top: -Dimensions.get('window').width * 1, // Positioned to show the bottom part of the circle as an arc
|
||||||
|
left: -Dimensions.get('window').width * 0.25,
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{/* <CaraouselHome refreshing={refreshing} /> */}
|
||||||
|
<View style={[Styles.ph15]}>
|
||||||
|
<CaraouselHome2 refreshing={refreshing} />
|
||||||
|
{/* <FiturHome /> */}
|
||||||
|
<ProjectHome refreshing={refreshing} />
|
||||||
|
<DivisionHome refreshing={refreshing} />
|
||||||
|
<ChartProgresHome refreshing={refreshing} />
|
||||||
|
<ChartDokumenHome refreshing={refreshing} />
|
||||||
|
<EventHome refreshing={refreshing} />
|
||||||
|
<DisccussionHome refreshing={refreshing} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ItemDetailMember from "@/components/itemDetailMember";
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
|
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { assetUserImage } from "@/constants/AssetsError";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import { valueRoleUser } from "@/constants/RoleUser";
|
import { valueRoleUser } from "@/constants/RoleUser";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetProfile } from "@/lib/api";
|
import { apiGetProfile } from "@/lib/api";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { AntDesign, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
|
import ImageViewing from 'react-native-image-viewing';
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -23,27 +30,37 @@ type Props = {
|
|||||||
group: string,
|
group: string,
|
||||||
img: string,
|
img: string,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
|
isApprover: boolean,
|
||||||
role: string
|
role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MemberDetail() {
|
export default function MemberDetail() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const { colors } = useTheme();
|
||||||
const [data, setData] = useState<Props>()
|
const [data, setData] = useState<Props>()
|
||||||
const [error, setError] = useState(false)
|
const [errorImg, setErrorImg] = useState(false)
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const [isEdit, setEdit] = useState(true)
|
const [isEdit, setEdit] = useState(true)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [preview, setPreview] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const response = await apiGetProfile({ id: id })
|
const response = await apiGetProfile({ id: id })
|
||||||
setData(response.data)
|
if (response.success) {
|
||||||
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
|
setData(response.data)
|
||||||
} catch (error) {
|
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
|
||||||
console.error(error)
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
|
}
|
||||||
|
} catch (error : any ) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -63,66 +80,111 @@ export default function MemberDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Anggota',
|
headerTitle: 'Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>,
|
header: () => (
|
||||||
headerShadowVisible: false
|
<AppHeader title="Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
(entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} isApprover={data?.isApprover ?? false} /> : <></>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[Styles.h100]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View style={[Styles.wrapHeadViewMember,]}>
|
<LinearGradient
|
||||||
{
|
colors={[colors.header, colors.homeGradient]}
|
||||||
loading ?
|
style={[Styles.wrapHeadViewMember]}
|
||||||
<>
|
>
|
||||||
<Skeleton width={100} height={100} borderRadius={100} />
|
{loading ? (
|
||||||
<Skeleton width={200} height={10} borderRadius={5} />
|
<>
|
||||||
<Skeleton width={150} height={10} borderRadius={5} />
|
<Skeleton width={100} height={100} borderRadius={100} />
|
||||||
</>
|
<Skeleton width={200} height={10} borderRadius={5} />
|
||||||
:
|
<Skeleton width={150} height={10} borderRadius={5} />
|
||||||
<>
|
</>
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${data?.img}`} size="lg" />
|
) : (
|
||||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{data?.name}</Text>
|
<>
|
||||||
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text>
|
<Pressable onPress={() => setPreview(true)}>
|
||||||
</>
|
<View style={[Styles.memberAvatarRing]}>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" onError={setErrorImg} />
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{data?.name}</Text>
|
||||||
|
<Text style={[Styles.textMediumNormal, Styles.cWhiteDimmed]}>{data?.role}</Text>
|
||||||
|
<View style={[Styles.memberBadgeRow]}>
|
||||||
|
{data?.isApprover && (
|
||||||
|
<View style={[Styles.memberBadgeApprover]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>APPROVER</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={[Styles.memberBadgePill, { backgroundColor: data?.isActive ? colors.success : colors.error }]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>{data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<Text style={[Styles.textDefaultSemiBold, Styles.mb08, { color: colors.dimmed }]}>Informasi</Text>
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>Informasi</Text>
|
<View>
|
||||||
|
{loading ? (
|
||||||
|
arrSkeleton.map((_, index) => (
|
||||||
|
<View key={index} style={[Styles.pv14, { borderBottomWidth: index < arrSkeleton.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}>
|
||||||
|
<Skeleton width={80} height={8} borderRadius={4} />
|
||||||
|
<View style={[Styles.mt05]}>
|
||||||
|
<Skeleton width={60} widthType="percent" height={10} borderRadius={4} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
[
|
||||||
|
{ icon: <MaterialCommunityIcons name="card-account-details" size={20} color={colors.icon} />, label: 'NIK', value: data?.nik },
|
||||||
|
{ icon: <MaterialCommunityIcons name="office-building-outline" size={20} color={colors.icon} />, label: 'Lembaga Desa', value: data?.group },
|
||||||
|
{ icon: <MaterialCommunityIcons name="account-tie" size={20} color={colors.icon} />, label: 'Jabatan', value: data?.position },
|
||||||
|
{ icon: <MaterialIcons name="phone" size={20} color={colors.icon} />, label: 'No Telepon', value: `+62${data?.phone}` },
|
||||||
|
{ icon: <MaterialIcons name="email" size={20} color={colors.icon} />, label: 'Email', value: data?.email },
|
||||||
|
{ icon: <MaterialCommunityIcons name="gender-male-female" size={20} color={colors.icon} />, label: 'Jenis Kelamin', value: data?.gender == "F" ? "Perempuan" : "Laki-Laki" },
|
||||||
|
].map((item, index, arr) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[Styles.memberInfoRow, { borderBottomWidth: index < arr.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.memberInfoIcon]}>
|
||||||
|
{item.icon}
|
||||||
|
</View>
|
||||||
|
<View style={[Styles.memberInfoContent]}>
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{item.label}</Text>
|
||||||
|
<Text style={[Styles.textDefault, Styles.mt02, { color: colors.text }]} numberOfLines={1}>{item.value ?? '-'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<Skeleton key={index} width={100} widthType="percent" height={25} borderRadius={5} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<ItemDetailMember category="nik" value={data?.nik} />
|
|
||||||
<ItemDetailMember category="group" value={data?.group} />
|
|
||||||
<ItemDetailMember category="position" value={data?.position} />
|
|
||||||
<ItemDetailMember category="phone" value={`+62${data?.phone}`} />
|
|
||||||
<ItemDetailMember category="email" value={data?.email} />
|
|
||||||
<ItemDetailMember category="gender" value={data?.gender == "F" ? "Perempuan" : "Laki-Laki"} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<ImageViewing
|
||||||
|
images={[{ uri: errorImg ? assetUserImage.uri : `${ConstEnv.url_storage}/files/${data?.img}` }]}
|
||||||
|
imageIndex={0}
|
||||||
|
visible={preview}
|
||||||
|
onRequestClose={() => setPreview(false)}
|
||||||
|
doubleTapToZoomEnabled
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SelectForm from "@/components/selectForm";
|
import SelectForm from "@/components/selectForm";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import { ColorsStatus } from "@/constants/ColorsStatus";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateUser } from "@/lib/api";
|
import { apiCreateUser } from "@/lib/api";
|
||||||
|
import { validateName } from "@/lib/fun_validateName";
|
||||||
import { setUpdateMember } from "@/lib/memberSlice";
|
import { setUpdateMember } from "@/lib/memberSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -26,9 +30,11 @@ import Toast from "react-native-toast-message";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function CreateMember() {
|
export default function CreateMember() {
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.memberUpdate)
|
const update = useSelector((state: any) => state.memberUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
const { colors } = useTheme();
|
||||||
const [valSelect, setValSelect] = useState<"group" | "position" | "role" | "gender">("group");
|
const [valSelect, setValSelect] = useState<"group" | "position" | "role" | "gender">("group");
|
||||||
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
||||||
const [choosePosition, setChoosePosition] = useState({ val: "", label: "" });
|
const [choosePosition, setChoosePosition] = useState({ val: "", label: "" });
|
||||||
@@ -41,6 +47,7 @@ export default function CreateMember() {
|
|||||||
const [disableBtn, setDisableBtn] = useState(true)
|
const [disableBtn, setDisableBtn] = useState(true)
|
||||||
const [valChoose, setValChoose] = useState("")
|
const [valChoose, setValChoose] = useState("")
|
||||||
const [imgForm, setImgForm] = useState<any>()
|
const [imgForm, setImgForm] = useState<any>()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
group: false,
|
group: false,
|
||||||
position: false,
|
position: false,
|
||||||
@@ -97,7 +104,7 @@ export default function CreateMember() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "name") {
|
} else if (cat == "name") {
|
||||||
setDataForm({ ...dataForm, name: val });
|
setDataForm({ ...dataForm, name: val });
|
||||||
if (val == "") {
|
if (!validateName(val)) {
|
||||||
setError({ ...error, name: true });
|
setError({ ...error, name: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, name: false });
|
setError({ ...error, name: false });
|
||||||
@@ -114,7 +121,7 @@ export default function CreateMember() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "phone") {
|
} else if (cat == "phone") {
|
||||||
setDataForm({ ...dataForm, phone: val });
|
setDataForm({ ...dataForm, phone: val });
|
||||||
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
|
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
|
||||||
setError({ ...error, phone: true });
|
setError({ ...error, phone: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, phone: false });
|
setError({ ...error, phone: false });
|
||||||
@@ -144,7 +151,7 @@ export default function CreateMember() {
|
|||||||
}, [error, dataForm])
|
}, [error, dataForm])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(entityUser.role !="supadmin" && entityUser.role != "developer"){
|
if (entityUser.role != "supadmin" && entityUser.role != "developer") {
|
||||||
validationForm("group", entities.idGroup, entities.group)
|
validationForm("group", entities.idGroup, entities.group)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
@@ -152,6 +159,7 @@ export default function CreateMember() {
|
|||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|
||||||
@@ -162,14 +170,14 @@ export default function CreateMember() {
|
|||||||
if (imgForm != undefined) {
|
if (imgForm != undefined) {
|
||||||
fd.append("file", {
|
fd.append("file", {
|
||||||
uri: imgForm.uri,
|
uri: imgForm.uri,
|
||||||
type: imgForm.mimeType,
|
type: imgForm.mimeType || "image/jpeg",
|
||||||
name: imgForm.fileName,
|
name: imgForm.fileName || "image.jpg",
|
||||||
} as any)
|
} as any)
|
||||||
} else {
|
} else {
|
||||||
fd.append("file", "undefined")
|
fd.append("file", "undefined")
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiCreateUser({data: fd})
|
const response = await apiCreateUser({ data: fd })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
||||||
dispatch(setUpdateMember(!update))
|
dispatch(setUpdateMember(!update))
|
||||||
@@ -177,8 +185,13 @@ export default function CreateMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,64 +199,60 @@ export default function CreateMember() {
|
|||||||
const pickImageAsync = async () => {
|
const pickImageAsync = async () => {
|
||||||
let result = await ImagePicker.launchImageLibraryAsync({
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: ["images"],
|
mediaTypes: ["images"],
|
||||||
allowsEditing: false,
|
allowsEditing: true,
|
||||||
quality: 1,
|
quality: 0.9,
|
||||||
aspect: [1, 1],
|
aspect: [1, 1]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
setSelectedImage(result.assets[0].uri);
|
setSelectedImage(result.assets[0].uri);
|
||||||
setImgForm(result.assets[0]);
|
setImgForm(result.assets[0]);
|
||||||
|
|
||||||
} else {
|
|
||||||
alert("Tidak ada gambar yang dipilih");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
|
||||||
<ButtonBackHeader
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerTitle: "Tambah Anggota",
|
headerTitle: "Tambah Anggota",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
header: () => (
|
||||||
<ButtonSaveHeader
|
<AppHeader title="Anggota"
|
||||||
disable={disableBtn}
|
showBack={true}
|
||||||
category="create"
|
onPressLeft={() => router.back()}
|
||||||
onPress={() => { handleCreate() }}
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || loading}
|
||||||
|
category="create"
|
||||||
|
onPress={() => { handleCreate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{loading && <LoadingCenter />}
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={[Styles.h100]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={{ justifyContent: "center", alignItems: "center" }}>
|
<View style={[Styles.contentItemCenter]}>
|
||||||
{selectedImage != undefined ? (
|
{selectedImage != undefined ? (
|
||||||
<Pressable onPress={pickImageAsync}>
|
<Pressable onPress={pickImageAsync}>
|
||||||
<Image src={selectedImage} style={[Styles.userProfileBig]} />
|
<Image src={selectedImage} style={[Styles.userProfileBig]} />
|
||||||
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<Pressable
|
<Pressable onPress={pickImageAsync} style={[Styles.iconContent, ColorsStatus.gray]} >
|
||||||
onPress={pickImageAsync}
|
<MaterialCommunityIcons name="account-tie" size={85} color={"gray"} />
|
||||||
style={[Styles.iconContent, ColorsStatus.gray]}
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
>
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
<MaterialCommunityIcons
|
</View>
|
||||||
name="account-tie"
|
|
||||||
size={100}
|
|
||||||
color={"gray"}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -254,6 +263,7 @@ export default function CreateMember() {
|
|||||||
placeholder="Pilih Lembaga Desa"
|
placeholder="Pilih Lembaga Desa"
|
||||||
value={chooseGroup.label}
|
value={chooseGroup.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(chooseGroup.val);
|
setValChoose(chooseGroup.val);
|
||||||
setValSelect("group");
|
setValSelect("group");
|
||||||
@@ -268,6 +278,7 @@ export default function CreateMember() {
|
|||||||
placeholder="Pilih Jabatan"
|
placeholder="Pilih Jabatan"
|
||||||
value={choosePosition.label}
|
value={choosePosition.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(choosePosition.val);
|
setValChoose(choosePosition.val);
|
||||||
setValSelect("position");
|
setValSelect("position");
|
||||||
@@ -281,6 +292,7 @@ export default function CreateMember() {
|
|||||||
placeholder="Pilih Role"
|
placeholder="Pilih Role"
|
||||||
value={chooseRole.label}
|
value={chooseRole.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(chooseRole.val);
|
setValChoose(chooseRole.val);
|
||||||
setValSelect("role");
|
setValSelect("role");
|
||||||
@@ -294,6 +306,7 @@ export default function CreateMember() {
|
|||||||
type="numeric"
|
type="numeric"
|
||||||
placeholder="NIK"
|
placeholder="NIK"
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
error={error.nik}
|
error={error.nik}
|
||||||
errorText="NIK Harus 16 Karakter"
|
errorText="NIK Harus 16 Karakter"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -305,8 +318,9 @@ export default function CreateMember() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Nama"
|
placeholder="Nama"
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
error={error.name}
|
error={error.name}
|
||||||
errorText="Nama tidak boleh kosong"
|
errorText="Nama harus 3–50 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
validationForm("name", val)
|
validationForm("name", val)
|
||||||
}}
|
}}
|
||||||
@@ -316,6 +330,7 @@ export default function CreateMember() {
|
|||||||
type="default"
|
type="default"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
error={error.email}
|
error={error.email}
|
||||||
errorText="Email tidak valid"
|
errorText="Email tidak valid"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -327,7 +342,8 @@ export default function CreateMember() {
|
|||||||
type="numeric"
|
type="numeric"
|
||||||
placeholder="8XX-XXX-XXX"
|
placeholder="8XX-XXX-XXX"
|
||||||
required
|
required
|
||||||
itemLeft={<Text>+62</Text>}
|
bg={colors.card}
|
||||||
|
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02, { color: colors.text }]}>+62</Text>}
|
||||||
error={error.phone}
|
error={error.phone}
|
||||||
errorText="Nomor Telepon tidak valid"
|
errorText="Nomor Telepon tidak valid"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -339,6 +355,7 @@ export default function CreateMember() {
|
|||||||
placeholder="Pilih Jenis Kelamin"
|
placeholder="Pilih Jenis Kelamin"
|
||||||
value={chooseGender.label}
|
value={chooseGender.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(chooseGender.val);
|
setValChoose(chooseGender.val);
|
||||||
setValSelect("gender");
|
setValSelect("gender");
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import LoadingCenter from "@/components/loadingCenter";
|
||||||
import ModalSelect from "@/components/modalSelect";
|
import ModalSelect from "@/components/modalSelect";
|
||||||
import SelectForm from "@/components/selectForm";
|
import SelectForm from "@/components/selectForm";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiEditUser, apiGetProfile } from "@/lib/api";
|
import { apiEditUser, apiGetProfile } from "@/lib/api";
|
||||||
|
import { validateName } from "@/lib/fun_validateName";
|
||||||
import { setUpdateMember } from "@/lib/memberSlice";
|
import { setUpdateMember } from "@/lib/memberSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -38,10 +44,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EditMember() {
|
export default function EditMember() {
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.memberUpdate)
|
const update = useSelector((state: any) => state.memberUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const { colors } = useTheme();
|
||||||
const [errorImg, setErrorImg] = useState(false)
|
const [errorImg, setErrorImg] = useState(false)
|
||||||
const [selectedImage, setSelectedImage] = useState<string | undefined | { uri: string }>(undefined);
|
const [selectedImage, setSelectedImage] = useState<string | undefined | { uri: string }>(undefined);
|
||||||
const [choosePosition, setChoosePosition] = useState({ val: "", label: "" });
|
const [choosePosition, setChoosePosition] = useState({ val: "", label: "" });
|
||||||
@@ -52,6 +60,7 @@ export default function EditMember() {
|
|||||||
const [disableBtn, setDisableBtn] = useState(false)
|
const [disableBtn, setDisableBtn] = useState(false)
|
||||||
const [valChoose, setValChoose] = useState("")
|
const [valChoose, setValChoose] = useState("")
|
||||||
const [imgForm, setImgForm] = useState<any>();
|
const [imgForm, setImgForm] = useState<any>();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [data, setData] = useState<Props>({
|
const [data, setData] = useState<Props>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -79,7 +88,7 @@ export default function EditMember() {
|
|||||||
try {
|
try {
|
||||||
const response = await apiGetProfile({ id: id });
|
const response = await apiGetProfile({ id: id });
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
setSelectedImage({ uri: `https://wibu-storage.wibudev.com/api/files/${response.data.img}`, });
|
setSelectedImage({ uri: `${ConstEnv.url_storage}/files/${response.data.img}`, });
|
||||||
setChoosePosition({
|
setChoosePosition({
|
||||||
val: response.data.idPosition,
|
val: response.data.idPosition,
|
||||||
label: response.data.position,
|
label: response.data.position,
|
||||||
@@ -127,7 +136,7 @@ export default function EditMember() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "name") {
|
} else if (cat == "name") {
|
||||||
setData({ ...data, name: val });
|
setData({ ...data, name: val });
|
||||||
if (val == "") {
|
if (!validateName(val)) {
|
||||||
setError({ ...error, name: true });
|
setError({ ...error, name: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, name: false });
|
setError({ ...error, name: false });
|
||||||
@@ -144,7 +153,7 @@ export default function EditMember() {
|
|||||||
}
|
}
|
||||||
} else if (cat == "phone") {
|
} else if (cat == "phone") {
|
||||||
setData({ ...data, phone: val });
|
setData({ ...data, phone: val });
|
||||||
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
|
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
|
||||||
setError({ ...error, phone: true });
|
setError({ ...error, phone: true });
|
||||||
} else {
|
} else {
|
||||||
setError({ ...error, phone: false });
|
setError({ ...error, phone: false });
|
||||||
@@ -162,11 +171,9 @@ export default function EditMember() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (Object.values(error).some((v) => v == true) || Object.values(data).some((v) => v == "")) {
|
const requiredFields: (keyof Props)[] = ["idPosition", "idUserRole", "nik", "name", "email", "phone", "gender"];
|
||||||
setDisableBtn(true)
|
const hasEmpty = requiredFields.some((key) => data[key] === "");
|
||||||
} else {
|
setDisableBtn(Object.values(error).some((v) => v === true) || hasEmpty);
|
||||||
setDisableBtn(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -175,14 +182,15 @@ export default function EditMember() {
|
|||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|
||||||
if (imgForm != undefined) {
|
if (imgForm != undefined) {
|
||||||
fd.append("file", {
|
fd.append("file", {
|
||||||
uri: imgForm.uri,
|
uri: imgForm.uri,
|
||||||
type: imgForm.mimeType,
|
type: imgForm.mimeType || "image/jpeg",
|
||||||
name: imgForm.fileName,
|
name: imgForm.fileName || "image.jpg",
|
||||||
} as any);
|
} as any);
|
||||||
} else {
|
} else {
|
||||||
fd.append("file", "undefined",);
|
fd.append("file", "undefined",);
|
||||||
@@ -201,9 +209,13 @@ export default function EditMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,9 +224,9 @@ export default function EditMember() {
|
|||||||
const pickImageAsync = async () => {
|
const pickImageAsync = async () => {
|
||||||
let result = await ImagePicker.launchImageLibraryAsync({
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: ["images"],
|
mediaTypes: ["images"],
|
||||||
allowsEditing: false,
|
allowsEditing: true,
|
||||||
quality: 1,
|
quality: 0.9,
|
||||||
aspect: [1, 1],
|
aspect: [1, 1]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
@@ -222,54 +234,54 @@ export default function EditMember() {
|
|||||||
setSelectedImage(result.assets[0].uri);
|
setSelectedImage(result.assets[0].uri);
|
||||||
setImgForm(result.assets[0]);
|
setImgForm(result.assets[0]);
|
||||||
} else {
|
} else {
|
||||||
alert("Tidak ada gambar yang dipilih");
|
|
||||||
setErrorImg(false)
|
setErrorImg(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
|
||||||
<ButtonBackHeader
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerTitle: "Edit Anggota",
|
headerTitle: "Edit Anggota",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
header: () => (
|
||||||
<ButtonSaveHeader
|
<AppHeader
|
||||||
disable={disableBtn}
|
title="Edit Anggota"
|
||||||
category="update"
|
showBack={true}
|
||||||
onPress={() => {
|
onPressLeft={() => router.back()}
|
||||||
handleEdit()
|
right={
|
||||||
}}
|
<ButtonSaveHeader
|
||||||
|
disable={disableBtn || loading}
|
||||||
|
category="update"
|
||||||
|
onPress={() => {
|
||||||
|
handleEdit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{loading && <LoadingCenter />}
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={[Styles.h100]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={{ justifyContent: "center", alignItems: "center" }}>
|
<View style={[Styles.contentItemCenter]}>
|
||||||
{
|
{
|
||||||
errorImg ?
|
errorImg ?
|
||||||
<Pressable onPress={pickImageAsync}>
|
<Pressable onPress={pickImageAsync}>
|
||||||
{
|
<Image
|
||||||
<Image
|
source={errorImg ? require("../../../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${data?.img}` }}
|
||||||
source={errorImg ? require("../../../../assets/images/user.jpg") : { uri: `https://wibu-storage.wibudev.com/api/files/${data?.img}` }}
|
style={[Styles.userProfileBig]}
|
||||||
style={[Styles.userProfileBig]}
|
onError={() => { setErrorImg(true) }}
|
||||||
onError={() => { setErrorImg(true) }}
|
/>
|
||||||
/>
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
}
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
:
|
:
|
||||||
selectedImage != undefined ? (
|
selectedImage != undefined ? (
|
||||||
@@ -283,6 +295,9 @@ export default function EditMember() {
|
|||||||
style={[Styles.userProfileBig]}
|
style={[Styles.userProfileBig]}
|
||||||
onError={() => { setErrorImg(true) }}
|
onError={() => { setErrorImg(true) }}
|
||||||
/>
|
/>
|
||||||
|
<View style={[Styles.absoluteIconPicker]}>
|
||||||
|
<MaterialCommunityIcons name="image" color={'white'} size={15} />
|
||||||
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
@@ -297,6 +312,7 @@ export default function EditMember() {
|
|||||||
placeholder="Pilih Jabatan"
|
placeholder="Pilih Jabatan"
|
||||||
value={choosePosition.label}
|
value={choosePosition.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(choosePosition.val);
|
setValChoose(choosePosition.val);
|
||||||
setValSelect("position");
|
setValSelect("position");
|
||||||
@@ -310,6 +326,7 @@ export default function EditMember() {
|
|||||||
placeholder="Pilih Role"
|
placeholder="Pilih Role"
|
||||||
value={chooseRole.label}
|
value={chooseRole.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(chooseRole.val);
|
setValChoose(chooseRole.val);
|
||||||
setValSelect("role");
|
setValSelect("role");
|
||||||
@@ -324,6 +341,7 @@ export default function EditMember() {
|
|||||||
placeholder="NIK"
|
placeholder="NIK"
|
||||||
required
|
required
|
||||||
value={data?.nik}
|
value={data?.nik}
|
||||||
|
bg={colors.card}
|
||||||
error={error.nik}
|
error={error.nik}
|
||||||
errorText="NIK Harus 16 Karakter"
|
errorText="NIK Harus 16 Karakter"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -336,8 +354,9 @@ export default function EditMember() {
|
|||||||
placeholder="Nama"
|
placeholder="Nama"
|
||||||
required
|
required
|
||||||
value={data?.name}
|
value={data?.name}
|
||||||
|
bg={colors.card}
|
||||||
error={error.name}
|
error={error.name}
|
||||||
errorText="Nama tidak boleh kosong"
|
errorText="Nama harus 3–50 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
validationForm("name", val)
|
validationForm("name", val)
|
||||||
}}
|
}}
|
||||||
@@ -348,6 +367,7 @@ export default function EditMember() {
|
|||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
required
|
required
|
||||||
value={data?.email}
|
value={data?.email}
|
||||||
|
bg={colors.card}
|
||||||
error={error.email}
|
error={error.email}
|
||||||
errorText="Email tidak valid"
|
errorText="Email tidak valid"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -359,8 +379,9 @@ export default function EditMember() {
|
|||||||
type="numeric"
|
type="numeric"
|
||||||
placeholder="8XX-XXX-XXX"
|
placeholder="8XX-XXX-XXX"
|
||||||
required
|
required
|
||||||
itemLeft={<Text>+62</Text>}
|
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02, { color: colors.text }]}>+62</Text>}
|
||||||
value={data?.phone}
|
value={data?.phone}
|
||||||
|
bg={colors.card}
|
||||||
error={error.phone}
|
error={error.phone}
|
||||||
errorText="Nomor Telepon tidak valid"
|
errorText="Nomor Telepon tidak valid"
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
@@ -372,6 +393,7 @@ export default function EditMember() {
|
|||||||
placeholder="Pilih Jenis Kelamin"
|
placeholder="Pilih Jenis Kelamin"
|
||||||
value={chooseGender.label}
|
value={chooseGender.label}
|
||||||
required
|
required
|
||||||
|
bg={colors.card}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setValChoose(chooseGender.val);
|
setValChoose(chooseGender.val);
|
||||||
setValSelect("gender");
|
setValSelect("gender");
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import WrapTab from "@/components/wrapTab";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetUser } from "@/lib/api";
|
import { apiGetUser } from "@/lib/api";
|
||||||
|
import { GUIDE_MEMBER } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather } from "@expo/vector-icons";
|
import { AntDesign, Feather } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -31,117 +39,129 @@ export default function Index() {
|
|||||||
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
|
const { colors } = useTheme();
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const update = useSelector((state: any) => state.memberUpdate)
|
const update = useSelector((state: any) => state.memberUpdate)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const queryClient = useQueryClient()
|
||||||
const [loading, setLoading] = useState(true)
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('member')
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Members with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['members', { status, search, group }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetUser({ user: hasil, active: status, search, group: String(group), page: thisPage })
|
const response = await apiGetUser({
|
||||||
if (thisPage == 1) {
|
user: hasil,
|
||||||
setData(response.data)
|
active: status,
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
search,
|
||||||
setData([...data, ...response.data])
|
group: String(group),
|
||||||
} else {
|
page: pageParam
|
||||||
return;
|
})
|
||||||
}
|
return response;
|
||||||
setNameGroup(response.filter.name)
|
},
|
||||||
} catch (error) {
|
initialPageParam: 1,
|
||||||
console.error(error)
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} finally {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
setLoading(false)
|
},
|
||||||
setWaiting(false)
|
enabled: !!token?.current,
|
||||||
}
|
staleTime: 0,
|
||||||
}
|
})
|
||||||
|
|
||||||
const loadMoreData = () => {
|
// Flatten pages into a single data array
|
||||||
if (waiting) return
|
const flatData = useMemo(() => {
|
||||||
setTimeout(() => {
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
handleLoad(false, page + 1)
|
}, [data])
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Get nameGroup from the first available page
|
||||||
|
const nameGroup = useMemo(() => {
|
||||||
|
return data?.pages[0]?.filter?.name || "";
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
// Refetch when manual update state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [group, search, status])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['members'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
name: data[index].name,
|
name: flatData[index]?.name,
|
||||||
nik: data[index].nik,
|
nik: flatData[index]?.nik,
|
||||||
email: data[index].email,
|
email: flatData[index]?.email,
|
||||||
phone: data[index].phone,
|
phone: flatData[index]?.phone,
|
||||||
gender: data[index].gender,
|
gender: flatData[index]?.gender,
|
||||||
position: data[index].position,
|
position: flatData[index]?.position,
|
||||||
group: data[index].group,
|
group: flatData[index]?.group,
|
||||||
img: data[index].img,
|
img: flatData[index]?.img,
|
||||||
isActive: data[index].isActive,
|
isActive: flatData[index]?.isActive,
|
||||||
role: data[index].role,
|
role: flatData[index]?.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1 }]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
|
<GuideOverlay visible={guideVisible} steps={GUIDE_MEMBER} onDismiss={dismissGuide} />
|
||||||
<View>
|
<View>
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
onPress={() => { setStatus("true") }}
|
onPress={() => { setStatus("true") }}
|
||||||
label="Aktif"
|
label="Aktif"
|
||||||
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="false"
|
value="false"
|
||||||
onPress={() => { setStatus("false") }}
|
onPress={() => { setStatus("false") }}
|
||||||
label="Tidak Aktif"
|
label="Tidak Aktif"
|
||||||
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : 'black'} size={20} />}
|
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : colors.dimmed} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
</View>
|
</WrapTab>
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
{
|
{
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||||
<View style={[Styles.mv05]}>
|
<View style={[Styles.mt10, Styles.rowOnly]}>
|
||||||
<Text>Filter : {nameGroup}</Text>
|
<Text>Filter :</Text>
|
||||||
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
data.length > 0
|
flatData.length > 0
|
||||||
?
|
?
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -150,7 +170,7 @@ export default function Index() {
|
|||||||
onPress={() => { router.push(`/member/${item.id}`) }}
|
onPress={() => { router.push(`/member/${item.id}`) }}
|
||||||
borderType="all"
|
borderType="all"
|
||||||
icon={
|
icon={
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />
|
||||||
}
|
}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
subtitle={`${item.group} - ${item.position}`}
|
subtitle={`${item.group} - ${item.position}`}
|
||||||
@@ -165,11 +185,12 @@ export default function Index() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetNotification, apiReadOneNotification } from "@/lib/api";
|
import { apiGetNotification, apiReadAllNotification, apiReadOneNotification } from "@/lib/api";
|
||||||
import { setUpdateNotification } from "@/lib/notificationSlice";
|
import { setUpdateNotification } from "@/lib/notificationSlice";
|
||||||
import { pushToPage } from "@/lib/pushToPage";
|
import { pushToPage } from "@/lib/pushToPage";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { SafeAreaView, View, VirtualizedList } from "react-native";
|
import { router, Stack } from "expo-router";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { FlatList, Pressable, RefreshControl, SafeAreaView, View } from "react-native";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -22,65 +25,121 @@ type Props = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HeaderRow = { _type: 'header'; date: string }
|
||||||
|
type ItemRow = Props & { _type: 'item' }
|
||||||
|
type ListRow = HeaderRow | ItemRow
|
||||||
|
|
||||||
|
function getNotifStyle(category: string): { icon: keyof typeof Feather.glyphMap; color: string } {
|
||||||
|
if (category === 'announcement') return { icon: 'volume-2', color: '#3B82F6' }
|
||||||
|
if (category === 'project') return { icon: 'activity', color: '#10B981' }
|
||||||
|
if (category.includes('/task')) return { icon: 'clipboard', color: '#8B5CF6' }
|
||||||
|
if (category === 'division') return { icon: 'users', color: '#3B82F6' }
|
||||||
|
if (category.includes('/discussion') || category === 'discussion-general') return { icon: 'message-square', color: '#06B6D4' }
|
||||||
|
if (category.includes('/calendar')) return { icon: 'calendar', color: '#F59E0B' }
|
||||||
|
if (category.includes('/document')) return { icon: 'file-text', color: '#FBBF24' }
|
||||||
|
if (category === 'member') return { icon: 'user', color: '#1F3C88' }
|
||||||
|
return { icon: 'bell', color: '#6B7280' }
|
||||||
|
}
|
||||||
|
|
||||||
export default function Notification() {
|
export default function Notification() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [loading, setLoading] = useState(false)
|
const { colors } = useTheme();
|
||||||
const [data, setData] = useState<Props[]>([])
|
const queryClient = useQueryClient()
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [markingAll, setMarkingAll] = useState(false)
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
const {
|
||||||
try {
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
setWaiting(true)
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['notifications'],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetNotification({ user: hasil, page: thisPage })
|
const response = await apiGetNotification({ user: hasil, page: pageParam })
|
||||||
if (thisPage == 1) {
|
return response;
|
||||||
setData(response.data)
|
},
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
initialPageParam: 1,
|
||||||
setData([...data, ...response.data])
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} else {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
return;
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const flatData = useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const listData = useMemo<ListRow[]>(() => {
|
||||||
|
const BULAN: Record<string, number> = {
|
||||||
|
'JAN': 0, 'FEB': 1, 'MAR': 2, 'APR': 3, 'MEI': 4, 'JUN': 5,
|
||||||
|
'JUL': 6, 'AGU': 7, 'SEP': 8, 'OKT': 9, 'NOV': 10, 'DES': 11,
|
||||||
|
}
|
||||||
|
const parseDate = (str: string) => {
|
||||||
|
const [d, m, y] = str.split(' ')
|
||||||
|
return new Date(Number(y), BULAN[m] ?? 0, Number(d)).getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: Record<string, Props[]> = {}
|
||||||
|
const dateOrder: string[] = []
|
||||||
|
|
||||||
|
flatData.forEach((item) => {
|
||||||
|
if (!groups[item.createdAt]) {
|
||||||
|
groups[item.createdAt] = []
|
||||||
|
dateOrder.push(item.createdAt)
|
||||||
}
|
}
|
||||||
|
groups[item.createdAt].push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
dateOrder.sort((a, b) => parseDate(b) - parseDate(a))
|
||||||
|
|
||||||
|
const result: ListRow[] = []
|
||||||
|
dateOrder.forEach((date) => {
|
||||||
|
result.push({ _type: 'header', date })
|
||||||
|
const sorted = [...groups[date]].sort((a, b) => Number(a.isRead) - Number(b.isRead))
|
||||||
|
sorted.forEach((item) => result.push({ ...item, _type: 'item' }))
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}, [flatData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch()
|
||||||
|
}, [updateNotification, refetch])
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUnread = flatData.some((item) => !item.isRead)
|
||||||
|
|
||||||
|
async function handleReadAll() {
|
||||||
|
try {
|
||||||
|
setMarkingAll(true)
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
await apiReadAllNotification({ user: hasil })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
|
dispatch(setUpdateNotification(!updateNotification))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setMarkingAll(false)
|
||||||
setWaiting(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const loadMoreData = () => {
|
|
||||||
if (waiting) return
|
|
||||||
setTimeout(() => {
|
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
|
||||||
id: data[index].id,
|
|
||||||
title: data[index].title,
|
|
||||||
desc: data[index].desc,
|
|
||||||
category: data[index].category,
|
|
||||||
idContent: data[index].idContent,
|
|
||||||
isRead: data[index].isRead,
|
|
||||||
createdAt: data[index].createdAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleReadNotification(id: string, category: string, idContent: string) {
|
async function handleReadNotification(id: string, category: string, idContent: string) {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiReadOneNotification({ user: hasil, id: id })
|
await apiReadOneNotification({ user: hasil, id: id })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
pushToPage(category, idContent)
|
pushToPage(category, idContent)
|
||||||
dispatch(setUpdateNotification(!updateNotification))
|
dispatch(setUpdateNotification(!updateNotification))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,80 +147,150 @@ export default function Notification() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function pushToPage(category: string, idContent: string) {
|
async function handleMarkOneRead(id: string) {
|
||||||
// const cat = category.split('/')
|
try {
|
||||||
// if (cat.length > 1) {
|
const hasil = await decryptToken(String(token?.current))
|
||||||
// if (cat[2] == 'calendar') {
|
await apiReadOneNotification({ user: hasil, id: id })
|
||||||
// router.push(`/division/${cat[1]}/calendar/${idContent}`)
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
// } else if (cat[2] == 'discussion') {
|
dispatch(setUpdateNotification(!updateNotification))
|
||||||
// router.push(`/division/${cat[1]}/discussion/${idContent}`)
|
} catch (error) {
|
||||||
// } else if (cat[2] == 'document') {
|
console.error(error)
|
||||||
// router.push(`/division/${cat[1]}/document/${idContent}`)
|
}
|
||||||
// } else if (cat[2] == 'task') {
|
}
|
||||||
// router.push(`/division/${cat[1]}/task/${idContent}`)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// if (cat[0] == 'announcement') {
|
|
||||||
// router.push(`/announcement/${idContent}`)
|
|
||||||
// } else if (cat[0] == 'discussion-general') {
|
|
||||||
// router.push(`/discussion/${idContent}`)
|
|
||||||
// } else if (cat[0] == 'division') {
|
|
||||||
// router.push(`/division/${idContent}`)
|
|
||||||
// } else if (cat[0] == 'member') {
|
|
||||||
// router.push(`/member/${idContent}`)
|
|
||||||
// } else if (cat[0] == 'project') {
|
|
||||||
// router.push(`/project/${idContent}`)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<Stack.Screen
|
||||||
{
|
options={{
|
||||||
loading ?
|
header: () => (
|
||||||
arrSkeleton.map((item, index) => {
|
<AppHeader
|
||||||
return (
|
title="Notifikasi"
|
||||||
<SkeletonTwoItem key={index} />
|
showBack={true}
|
||||||
)
|
onPressLeft={() => router.back()}
|
||||||
})
|
right={
|
||||||
:
|
hasUnread ? (
|
||||||
data.length > 0 ?
|
<Pressable
|
||||||
<VirtualizedList
|
onPress={() => setShowConfirm(true)}
|
||||||
data={data}
|
disabled={markingAll}
|
||||||
getItemCount={() => data.length}
|
style={{ opacity: markingAll ? 0.5 : 1, padding: 4 }}
|
||||||
getItem={getItem}
|
>
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
<Feather name="check-square" size={20} color="white" />
|
||||||
return (
|
</Pressable>
|
||||||
<BorderBottomItem
|
) : undefined
|
||||||
borderType="bottom"
|
}
|
||||||
width={55}
|
/>
|
||||||
icon={
|
)
|
||||||
<View style={[Styles.iconContent, item.isRead ? ColorsStatus.secondary : ColorsStatus.primary]}>
|
}}
|
||||||
<Feather name="bell" size={25} color="white" />
|
/>
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.title}
|
|
||||||
rightTopInfo={item.createdAt}
|
|
||||||
desc={item.desc}
|
|
||||||
textColor={item.isRead ? 'gray' : 'black'}
|
|
||||||
onPress={() => {
|
|
||||||
handleReadNotification(item.id, item.category, item.idContent)
|
|
||||||
|
|
||||||
}}
|
<ModalConfirmation
|
||||||
/>
|
visible={showConfirm}
|
||||||
)
|
title="Tandai Semua Dibaca"
|
||||||
}}
|
message="Semua notifikasi akan ditandai sebagai telah dibaca."
|
||||||
keyExtractor={(item, index) => String(index)}
|
confirmText="Tandai"
|
||||||
onEndReached={loadMoreData}
|
cancelText="Batal"
|
||||||
onEndReachedThreshold={0.5}
|
onConfirm={() => {
|
||||||
showsVerticalScrollIndicator={false}
|
setShowConfirm(false)
|
||||||
|
handleReadAll()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowConfirm(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<View style={[Styles.flex1, Styles.ph15, Styles.notifContainer]}>
|
||||||
|
{isLoading ? (
|
||||||
|
[0, 1, 2, 3, 4].map((_, i) => <SkeletonTwoItem key={i} />)
|
||||||
|
) : flatData.length === 0 ? (
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
|
<Feather name="bell-off" size={42} color={colors.icon + '40'} />
|
||||||
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
|
Tidak ada notifikasi
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={listData}
|
||||||
|
keyExtractor={(item, index) => String(index)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onEndReached={() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
||||||
|
}}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
:
|
}
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
renderItem={({ item }) => {
|
||||||
}
|
if (item._type === 'header') {
|
||||||
|
return (
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.notifHeaderRow]}>
|
||||||
|
<Text style={[Styles.notifDateText, { color: colors.dimmed }]}>
|
||||||
|
{item.date}
|
||||||
|
</Text>
|
||||||
|
<View style={[Styles.notifDateSeparator, { backgroundColor: colors.icon + '20' }]} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icon, color } = getNotifStyle(item.category)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => handleReadNotification(item.id, item.category, item.idContent)}
|
||||||
|
style={({ pressed }) => [Styles.notifItemRow, {
|
||||||
|
borderColor: colors.icon + '20',
|
||||||
|
backgroundColor: pressed
|
||||||
|
? colors.icon + '10'
|
||||||
|
: item.isRead
|
||||||
|
? colors.icon + '10'
|
||||||
|
: colors.card,
|
||||||
|
}]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.notifIconContainer, { backgroundColor: color + '20' }]}>
|
||||||
|
<Feather name={icon} size={20} color={color} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[Styles.flex1, Styles.notifContent]}>
|
||||||
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter]}>
|
||||||
|
<View style={[Styles.flex1, Styles.mr10]}>
|
||||||
|
<Text
|
||||||
|
style={[Styles.textDefaultSemiBold, { color: item.isRead ? colors.dimmed : colors.text }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{!item.isRead && (
|
||||||
|
<Pressable
|
||||||
|
onPress={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleMarkOneRead(item.id)
|
||||||
|
}}
|
||||||
|
hitSlop={8}
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1, flexShrink: 0 })}
|
||||||
|
>
|
||||||
|
<Text style={Styles.notifMarkReadText}>
|
||||||
|
Tandai dibaca
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[Styles.textMediumNormal, { color: item.isRead ? colors.dimmed : colors.text, opacity: item.isRead ? 0.7 : 1 }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.desc}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi";
|
import GuideOverlay from "@/components/GuideOverlay";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import { ButtonForm } from "@/components/buttonForm";
|
import { ButtonForm } from "@/components/buttonForm";
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import MenuItemRow from "@/components/menuItemRow";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
import WrapTab from "@/components/wrapTab";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiDeletePosition, apiEditPosition, apiGetPosition } from "@/lib/api";
|
import { apiDeletePosition, apiEditPosition, apiGetPosition } from "@/lib/api";
|
||||||
import { setUpdatePosition } from "@/lib/positionSlice";
|
import { setUpdatePosition } from "@/lib/positionSlice";
|
||||||
|
import { GUIDE_POSITION } from "@/lib/guideSteps";
|
||||||
|
import { useGuide } from "@/lib/useGuide";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -29,48 +35,54 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
const { colors } = useTheme()
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
|
||||||
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
||||||
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
|
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
name: false,
|
name: false,
|
||||||
});
|
});
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('position')
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.positionUpdate)
|
const update = useSelector((state: any) => state.positionUpdate)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
// TanStack Query for Positions
|
||||||
try {
|
const {
|
||||||
setLoading(loading)
|
data: queryData,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['positions', { status, search, group }],
|
||||||
|
queryFn: async () => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetPosition({ user: hasil, active: status, search: search, group: String(group) })
|
const response = await apiGetPosition({
|
||||||
setData(response.data)
|
user: hasil,
|
||||||
setNameGroup(response.filter.name)
|
active: status,
|
||||||
} catch (error) {
|
search: search,
|
||||||
console.error(error)
|
group: String(group)
|
||||||
} finally {
|
})
|
||||||
setLoading(false)
|
return response;
|
||||||
}
|
},
|
||||||
}
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = useMemo(() => queryData?.data || [], [queryData])
|
||||||
|
const nameGroup = useMemo(() => queryData?.filter?.name || "", [queryData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, [status, search, group])
|
|
||||||
|
|
||||||
|
|
||||||
function handleChooseData(id: string, name: string, active: boolean, group: string) {
|
function handleChooseData(id: string, name: string, active: boolean, group: string) {
|
||||||
@@ -83,8 +95,11 @@ export default function Index() {
|
|||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id)
|
const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id)
|
||||||
dispatch(setUpdatePosition(!update))
|
dispatch(setUpdatePosition(!update))
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
||||||
@@ -94,15 +109,23 @@ export default function Index() {
|
|||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
|
setLoadingSubmit(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiEditPosition({ user: hasil, name: chooseData.name, idGroup: chooseData.idGroup }, chooseData.id)
|
const response = await apiEditPosition({ user: hasil, name: chooseData.name, idGroup: chooseData.idGroup }, chooseData.id)
|
||||||
dispatch(setUpdatePosition(!update))
|
if (response.success) {
|
||||||
} catch (error) {
|
dispatch(setUpdatePosition(!update))
|
||||||
console.error(error)
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
|
setLoadingSubmit(false)
|
||||||
setVisibleEdit(false)
|
setVisibleEdit(false)
|
||||||
setModal(false)
|
setModal(false)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -122,96 +145,113 @@ export default function Index() {
|
|||||||
handleEdit()
|
handleEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false)
|
await queryClient.invalidateQueries({ queryKey: ['positions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
|
id: data[index].id,
|
||||||
|
name: data[index].name,
|
||||||
|
idGroup: data[index].idGroup,
|
||||||
|
group: data[index].group,
|
||||||
|
isActive: data[index].isActive,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<GuideOverlay visible={guideVisible} steps={GUIDE_POSITION} onDismiss={dismissGuide} />
|
||||||
<View style={[Styles.wrapBtnTab]}>
|
<View>
|
||||||
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
onPress={() => { setStatus("true") }}
|
onPress={() => { setStatus("true") }}
|
||||||
label="Aktif"
|
label="Aktif"
|
||||||
icon={<Feather name="check-circle" color={status == "true" ? 'white' : 'black'} size={20} />}
|
icon={<Feather name="check-circle" color={status == "true" ? 'white' : colors.dimmed} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="false"
|
value="false"
|
||||||
onPress={() => { setStatus("false") }}
|
onPress={() => { setStatus("false") }}
|
||||||
label="Tidak Aktif"
|
label="Tidak Aktif"
|
||||||
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : 'black'} size={20} />}
|
icon={<AntDesign name="closecircleo" color={status == "false" ? 'white' : colors.dimmed} size={20} />}
|
||||||
n={2} />
|
n={2} />
|
||||||
</View>
|
</WrapTab>
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
{
|
{
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||||
<View style={[Styles.mv05]}>
|
<View style={[Styles.mt10, Styles.rowOnly]}>
|
||||||
<Text>Filter : {nameGroup}</Text>
|
<Text>Filter :</Text>
|
||||||
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
<ScrollView
|
|
||||||
style={[Styles.h100]}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
/>
|
|
||||||
}>
|
|
||||||
<View>
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<SkeletonTwoItem key={index} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
data.length > 0 ?
|
|
||||||
data.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
onPress={() => { handleChooseData(item.id, item.name, item.isActive, item.idGroup) }}
|
|
||||||
borderType="all"
|
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
|
||||||
<MaterialCommunityIcons name="account-tie" size={25} color={'#384288'} />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.name}
|
|
||||||
subtitle={item.group}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[Styles.flex2, Styles.mt10]}>
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
arrSkeleton.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<SkeletonTwoItem key={index} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
:
|
||||||
|
data.length > 0 ?
|
||||||
|
<VirtualizedList
|
||||||
|
data={data}
|
||||||
|
getItemCount={() => data.length}
|
||||||
|
getItem={getItem}
|
||||||
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
|
return (
|
||||||
|
<BorderBottomItem
|
||||||
|
key={index}
|
||||||
|
onPress={() => {
|
||||||
|
entityUser.role != "user" &&
|
||||||
|
handleChooseData(item.id, item.name, item.isActive, item.idGroup)
|
||||||
|
}}
|
||||||
|
borderType="all"
|
||||||
|
icon={
|
||||||
|
<View style={[Styles.iconContent]}>
|
||||||
|
<MaterialCommunityIcons name="account-tie-outline" size={25} color={'black'} />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={item.name}
|
||||||
|
subtitle={item.group}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
keyExtractor={(item, index) => String(index)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<Text style={[Styles.textDefault, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada data</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title={chooseData.name}>
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title={chooseData.name}>
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color={colors.text} size={25} />}
|
||||||
title={chooseData.active ? 'Non Aktifkan' : "Aktifkan"}
|
title={chooseData.active ? 'Non Aktifkan' : "Aktifkan"}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
AlertKonfirmasi({
|
setTimeout(() => {
|
||||||
title: 'Konfirmasi',
|
setShowDeleteModal(true)
|
||||||
desc: chooseData.active ? 'Apakah anda yakin ingin menonaktifkan data?' : 'Apakah anda yakin ingin mengaktifkan data?',
|
}, 600)
|
||||||
onPress: () => { handleDelete() }
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
|
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||||
title="Edit"
|
title="Edit"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
@@ -225,13 +265,14 @@ export default function Index() {
|
|||||||
|
|
||||||
|
|
||||||
<DrawerBottom animation="none" keyboard height={30} backdropPressable={false} isVisible={isVisibleEdit} setVisible={() => setVisibleEdit(false)} title="Edit Jabatan">
|
<DrawerBottom animation="none" keyboard height={30} backdropPressable={false} isVisible={isVisibleEdit} setVisible={() => setVisibleEdit(false)} title="Edit Jabatan">
|
||||||
<View style={{ justifyContent: 'space-between', flex: 1 }}>
|
<View style={[Styles.justifySpaceBetween, Styles.flex1]}>
|
||||||
<View>
|
<View>
|
||||||
<InputForm
|
<InputForm
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Nama Jabatan"
|
placeholder="Nama Jabatan"
|
||||||
required
|
required
|
||||||
label="Jabatan"
|
label="Jabatan"
|
||||||
|
bg={"transparent"}
|
||||||
value={chooseData.name}
|
value={chooseData.name}
|
||||||
onChange={(val) => { validationForm(val) }}
|
onChange={(val) => { validationForm(val) }}
|
||||||
error={error.name}
|
error={error.name}
|
||||||
@@ -239,11 +280,23 @@ export default function Index() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={Styles.mb30}>
|
<View style={Styles.mb30}>
|
||||||
<ButtonForm text="SIMPAN" onPress={() => { checkForm() }} />
|
<ButtonForm text="SIMPAN" onPress={() => { handleEdit() }} disabled={Object.values(error).some((v) => v == true) || chooseData.name == "" || loadingSubmit} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
</SafeAreaView>
|
<ModalConfirmation
|
||||||
|
visible={showDeleteModal}
|
||||||
|
title="Konfirmasi"
|
||||||
|
message={chooseData.active ? 'Apakah anda yakin ingin menonaktifkan data?' : 'Apakah anda yakin ingin mengaktifkan data?'}
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
handleDelete()
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
confirmText={chooseData.active ? "Nonaktifkan" : "Aktifkan"}
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,69 +1,138 @@
|
|||||||
import AlertKonfirmasi from "@/components/alertKonfirmasi";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
|
||||||
import { ButtonHeader } from "@/components/buttonHeader";
|
import { ButtonHeader } from "@/components/buttonHeader";
|
||||||
import ItemDetailMember from "@/components/itemDetailMember";
|
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { assetUserImage } from "@/constants/AssetsError";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
|
import { apiGetProfile } from "@/lib/api";
|
||||||
|
import { setEntities } from "@/lib/entitiesSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Image, SafeAreaView, ScrollView, View } from "react-native";
|
import { Image, Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import { useSelector } from 'react-redux';
|
import ImageViewing from 'react-native-image-viewing';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const { signOut } = useAuthSession()
|
const { colors } = useTheme();
|
||||||
const entities = useSelector((state: any) => state.entities)
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
const [preview, setPreview] = useState(false)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const { token, decryptToken } = useAuthSession()
|
||||||
|
|
||||||
|
async function handleUserLogin() {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
apiGetProfile({ id: hasil })
|
||||||
|
.then((data) => dispatch(setEntities(data.data)))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
handleUserLogin()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
setRefreshing(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const infoRows = [
|
||||||
|
{ icon: <MaterialCommunityIcons name="card-account-details" size={20} color={colors.icon} />, label: 'NIK', value: entities.nik },
|
||||||
|
{ icon: <MaterialCommunityIcons name="office-building-outline" size={20} color={colors.icon} />, label: 'Lembaga Desa', value: entities.group },
|
||||||
|
{ icon: <MaterialCommunityIcons name="account-tie" size={20} color={colors.icon} />, label: 'Jabatan', value: entities.position },
|
||||||
|
{ icon: <MaterialIcons name="phone" size={20} color={colors.icon} />, label: 'No Telepon', value: `0${entities.phone}` },
|
||||||
|
{ icon: <MaterialIcons name="email" size={20} color={colors.icon} />, label: 'Email', value: entities.email },
|
||||||
|
{ icon: <MaterialCommunityIcons name="gender-male-female" size={20} color={colors.icon} />, label: 'Jenis Kelamin', value: entities.gender == "F" ? 'Perempuan' : 'Laki-laki' },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Profile',
|
headerTitle: 'Profile',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerShadowVisible: false,
|
header: () => (
|
||||||
headerRight: () => <ButtonHeader
|
<AppHeader
|
||||||
item={<AntDesign name="logout" size={20} color="white" />}
|
title="Profile"
|
||||||
onPress={() => {
|
showBack={true}
|
||||||
AlertKonfirmasi({
|
onPressLeft={() => router.back()}
|
||||||
title: 'Keluar',
|
right={
|
||||||
desc: 'Apakah anda yakin ingin keluar?',
|
<ButtonHeader
|
||||||
onPress: () => { signOut() }
|
item={<Feather name="settings" size={20} color="white" />}
|
||||||
})
|
onPress={() => router.push('/setting')}
|
||||||
}}
|
/>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={{ flexDirection: 'column' }}>
|
refreshControl={
|
||||||
<View style={[Styles.wrapHeadViewMember]}>
|
<RefreshControl
|
||||||
<Image
|
refreshing={refreshing}
|
||||||
source={error ? require("../../assets/images/user.jpg") : { uri: `https://wibu-storage.wibudev.com/api/files/${entities.img}` }}
|
onRefresh={handleRefresh}
|
||||||
onError={() => { setError(true) }}
|
tintColor={colors.icon}
|
||||||
style={[Styles.userProfileBig]}
|
/>
|
||||||
/>
|
}
|
||||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{entities.name}</Text>
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{entities.role}</Text>
|
>
|
||||||
</View>
|
<LinearGradient
|
||||||
<View style={[Styles.p15]}>
|
colors={[colors.header, colors.homeGradient]}
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
style={[Styles.wrapHeadViewMember]}
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>Informasi</Text>
|
>
|
||||||
{
|
<Pressable onPress={() => setPreview(true)}>
|
||||||
entities.idUserRole != "developer" && <Text onPress={() => { router.push('/edit-profile') }} style={[Styles.textLink]}>Edit</Text>
|
<View style={[Styles.memberAvatarRing]}>
|
||||||
}
|
<Image
|
||||||
|
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
|
||||||
|
onError={() => setError(true)}
|
||||||
|
style={[Styles.userProfileBig]}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ItemDetailMember category="nik" value={entities.nik} />
|
</Pressable>
|
||||||
<ItemDetailMember category="group" value={entities.group} />
|
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{entities.name}</Text>
|
||||||
<ItemDetailMember category="position" value={entities.position} />
|
<Text style={[Styles.textMediumNormal, Styles.cWhiteDimmed]}>{entities.role}</Text>
|
||||||
<ItemDetailMember category="phone" value={`0${entities.phone}`} />
|
{entities.isApprover && (
|
||||||
<ItemDetailMember category="email" value={entities.email} />
|
<View style={[Styles.memberBadgeRow, { justifyContent: 'center' }]}>
|
||||||
<ItemDetailMember category="gender" value={entities.gender == "F" ? 'Perempuan' : 'Laki-laki'} />
|
<View style={[Styles.memberBadgeApprover]}>
|
||||||
|
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>APPROVER</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.mb08, { color: colors.dimmed }]}>Informasi</Text>
|
||||||
|
<View>
|
||||||
|
{infoRows.map((item, index, arr) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[Styles.memberInfoRow, { borderBottomWidth: index < arr.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.memberInfoIcon]}>
|
||||||
|
{item.icon}
|
||||||
|
</View>
|
||||||
|
<View style={[Styles.memberInfoContent]}>
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{item.label}</Text>
|
||||||
|
<Text style={[Styles.textDefault, Styles.mt02, { color: colors.text }]} numberOfLines={1}>{item.value ?? '-'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<ImageViewing
|
||||||
|
images={[{ uri: error ? assetUserImage.uri : `${ConstEnv.url_storage}/files/${entities.img}` }]}
|
||||||
|
imageIndex={0}
|
||||||
|
visible={preview}
|
||||||
|
onRequestClose={() => setPreview(false)}
|
||||||
|
doubleTapToZoomEnabled
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import AppHeader from "@/components/AppHeader"
|
||||||
import BorderBottomItem from "@/components/borderBottomItem"
|
import BorderBottomItem from "@/components/borderBottomItem"
|
||||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
import ButtonSaveHeader from "@/components/buttonSaveHeader"
|
||||||
import ButtonSelect from "@/components/buttonSelect"
|
import ButtonSelect from "@/components/buttonSelect"
|
||||||
import DrawerBottom from "@/components/drawerBottom"
|
import DrawerBottom from "@/components/drawerBottom"
|
||||||
|
import LoadingCenter from "@/components/loadingCenter"
|
||||||
import MenuItemRow from "@/components/menuItemRow"
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiAddFileProject, apiCheckFileProject } from "@/lib/api"
|
import { apiAddFileProject, apiCheckFileProject } from "@/lib/api"
|
||||||
import { setUpdateProject } from "@/lib/projectUpdate"
|
import { setUpdateProject } from "@/lib/projectUpdate"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
||||||
import * as DocumentPicker from "expo-document-picker"
|
import * as DocumentPicker from "expo-document-picker"
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||||
@@ -17,6 +19,7 @@ import Toast from "react-native-toast-message"
|
|||||||
import { useDispatch, useSelector } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
|
|
||||||
export default function ProjectAddFile() {
|
export default function ProjectAddFile() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>()
|
const { id } = useLocalSearchParams<{ id: string }>()
|
||||||
const [fileForm, setFileForm] = useState<any[]>([])
|
const [fileForm, setFileForm] = useState<any[]>([])
|
||||||
const [listFile, setListFile] = useState<any[]>([])
|
const [listFile, setListFile] = useState<any[]>([])
|
||||||
@@ -26,6 +29,7 @@ export default function ProjectAddFile() {
|
|||||||
const [loadingCheck, setLoadingCheck] = useState(false)
|
const [loadingCheck, setLoadingCheck] = useState(false)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const pickDocumentAsync = async () => {
|
const pickDocumentAsync = async () => {
|
||||||
let result = await DocumentPicker.getDocumentAsync({
|
let result = await DocumentPicker.getDocumentAsync({
|
||||||
@@ -86,6 +90,7 @@ export default function ProjectAddFile() {
|
|||||||
|
|
||||||
async function handleAddFile() {
|
async function handleAddFile() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
|
|
||||||
@@ -113,9 +118,13 @@ export default function ProjectAddFile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,32 +132,49 @@ export default function ProjectAddFile() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah File',
|
headerTitle: 'Tambah File',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => <ButtonSaveHeader
|
// headerRight: () => <ButtonSaveHeader
|
||||||
disable={fileForm.length == 0 ? true : false}
|
// disable={fileForm.length == 0 || loading ? true : false}
|
||||||
category="create"
|
// category="create"
|
||||||
onPress={() => { handleAddFile() }} />
|
// onPress={() => { handleAddFile() }} />
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah File"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={fileForm.length == 0 || loading ? true : false}
|
||||||
|
category="create"
|
||||||
|
onPress={() => { handleAddFile() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
{
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
loading && <LoadingCenter size="large" />
|
||||||
|
}
|
||||||
|
<ScrollView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
|
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
|
||||||
{
|
{
|
||||||
listFile.length > 0 && (
|
listFile.length > 0 && (
|
||||||
<View style={[Styles.mb15]}>
|
<View style={[Styles.mb15]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
<Text style={[Styles.textDefaultSemiBold, Styles.mv05, { color: colors.text }]}>File</Text>
|
||||||
<View style={[Styles.wrapPaper]}>
|
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
{
|
{
|
||||||
listFile.map((item, index) => (
|
listFile.map((item, index) => (
|
||||||
<BorderBottomItem
|
<BorderBottomItem
|
||||||
key={index}
|
key={index}
|
||||||
borderType="all"
|
borderType="all"
|
||||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
|
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||||
title={item}
|
title={item}
|
||||||
titleWeight="normal"
|
titleWeight="normal"
|
||||||
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
onPress={() => { setIndexDelFile(index); setModal(true) }}
|
||||||
@@ -167,7 +193,7 @@ export default function ProjectAddFile() {
|
|||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<Ionicons name="trash" color="black" size={25} />}
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||||
title="Hapus"
|
title="Hapus"
|
||||||
onPress={() => { deleteFile(indexDelFile) }}
|
onPress={() => { deleteFile(indexDelFile) }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ImageWithLabel from "@/components/imageWithLabel";
|
import ImageWithLabel from "@/components/imageWithLabel";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiAddMemberProject, apiGetProjectOne, apiGetUser } from "@/lib/api";
|
import { apiAddMemberProject, apiGetProjectOne, apiGetUser } from "@/lib/api";
|
||||||
import { setUpdateProject } from "@/lib/projectUpdate";
|
import { setUpdateProject } from "@/lib/projectUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddMemberProject() {
|
export default function AddMemberProject() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
@@ -31,6 +34,7 @@ export default function AddMemberProject() {
|
|||||||
const [idGroup, setIdGroup] = useState('')
|
const [idGroup, setIdGroup] = useState('')
|
||||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleLoad() {
|
async function handleLoad() {
|
||||||
try {
|
try {
|
||||||
@@ -41,8 +45,11 @@ export default function AddMemberProject() {
|
|||||||
setIdGroup(responseGroup.data.idGroup)
|
setIdGroup(responseGroup.data.idGroup)
|
||||||
const responsemember = await apiGetUser({ user: hasil, active: "true", search: search, group: String(responseGroup.data.idGroup) })
|
const responsemember = await apiGetUser({ user: hasil, active: "true", search: search, group: String(responseGroup.data.idGroup) })
|
||||||
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +80,7 @@ export default function AddMemberProject() {
|
|||||||
|
|
||||||
async function handleAddMember() {
|
async function handleAddMember() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiAddMemberProject({ id: id, data: { user: hasil, member: selectMember } })
|
const response = await apiAddMemberProject({ id: id, data: { user: hasil, member: selectMember } })
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -80,43 +88,64 @@ export default function AddMemberProject() {
|
|||||||
dispatch(setUpdateProject({ ...update, member: !update.member }))
|
dispatch(setUpdateProject({ ...update, member: !update.member }))
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||||
headerTitle: 'Tambah Anggota Kegiatan',
|
headerTitle: 'Tambah Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
category="update"
|
// category="update"
|
||||||
disable={selectMember.length > 0 ? false : true}
|
// disable={selectMember.length == 0 || loading ? true : false}
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleAddMember()
|
// handleAddMember()
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Anggota"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
category="update"
|
||||||
|
disable={selectMember.length == 0 || loading ? true : false}
|
||||||
|
onPress={() => {
|
||||||
|
handleAddMember()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
|
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
|
||||||
{
|
{
|
||||||
selectMember.length > 0
|
selectMember.length > 0
|
||||||
?
|
?
|
||||||
<View>
|
<View>
|
||||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]}>
|
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||||
{
|
{
|
||||||
selectMember.map((item: any, index: any) => (
|
selectMember.map((item: any, index: any) => (
|
||||||
<ImageWithLabel
|
<ImageWithLabel
|
||||||
key={index}
|
key={index}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`}
|
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -125,42 +154,50 @@ export default function AddMemberProject() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
|
<Text style={[Styles.textDefault, Styles.pv05, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||||
}
|
}
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
data.length > 0 ?
|
data.length > 0 ?
|
||||||
data.map((item: any, index: any) => {
|
<View style={[Styles.mb100]}>
|
||||||
const found = dataOld.some((i: any) => i.idUser == item.id)
|
{
|
||||||
return (
|
data.map((item: any, index: any) => {
|
||||||
<Pressable
|
const found = dataOld.some((i: any) => i.idUser == item.id)
|
||||||
key={index}
|
return (
|
||||||
style={[Styles.itemSelectModal]}
|
<Pressable
|
||||||
onPress={() => {
|
key={index}
|
||||||
!found && onChoose(item.id, item.name, item.img)
|
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||||
}}
|
onPress={() => {
|
||||||
>
|
!found && onChoose(item.id, item.name, item.img)
|
||||||
<View style={[Styles.rowItemsCenter, Styles.w80]}>
|
}}
|
||||||
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} border />
|
>
|
||||||
<View style={[Styles.ml10]}>
|
<View style={[Styles.rowItemsCenter, Styles.w80]}>
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1}>{item.name}</Text>
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||||
|
<View style={[Styles.ml10]}>
|
||||||
|
<Text style={[Styles.textDefault]} numberOfLines={1}>{item.name}</Text>
|
||||||
|
{
|
||||||
|
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
{
|
{
|
||||||
found && <Text style={[Styles.textInformation, Styles.cGray]}>sudah menjadi anggota</Text>
|
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={colors.text} />
|
||||||
}
|
}
|
||||||
</View>
|
</Pressable>
|
||||||
</View>
|
)
|
||||||
{
|
}
|
||||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} />
|
)
|
||||||
}
|
}
|
||||||
</Pressable>
|
</View>
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
:
|
:
|
||||||
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>Tidak ada data</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>Tidak ada data</Text>
|
||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
|
import ButtonSelect from "@/components/buttonSelect";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
|
import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCreateProjectTask } from "@/lib/api";
|
import { apiCreateProjectTask } from "@/lib/api";
|
||||||
|
import { formatDateOnly } from "@/lib/fun_formatDateOnly";
|
||||||
|
import { getDatesInRange } from "@/lib/fun_getDatesInRange";
|
||||||
import { setUpdateProject } from "@/lib/projectUpdate";
|
import { setUpdateProject } from "@/lib/projectUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import dayjs from "dayjs";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import 'intl';
|
||||||
|
import 'intl/locale-data/jsonp/id';
|
||||||
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
@@ -23,9 +31,13 @@ import DateTimePicker, {
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function ProjectAddTask() {
|
export default function ProjectAddTask() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const headerHeight = useHeaderHeight();
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
|
const [dataDetail, setDataDetail] = useState<any>([])
|
||||||
|
const [modalDetail, setModalDetail] = useState(false)
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const [disable, setDisable] = useState(true);
|
const [disable, setDisable] = useState(true);
|
||||||
const [range, setRange] = useState<{
|
const [range, setRange] = useState<{
|
||||||
@@ -38,11 +50,11 @@ export default function ProjectAddTask() {
|
|||||||
title: false,
|
title: false,
|
||||||
})
|
})
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [dsbButton, setDsbButton] = useState(true)
|
||||||
|
|
||||||
const from = range.startDate
|
const from = formatDateOnly(range.startDate);
|
||||||
? dayjs(range.startDate).format("DD-MM-YYYY")
|
const to = formatDateOnly(range.endDate);
|
||||||
: "";
|
|
||||||
const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : "";
|
|
||||||
|
|
||||||
function checkAll() {
|
function checkAll() {
|
||||||
if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) {
|
if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) {
|
||||||
@@ -63,94 +75,168 @@ export default function ProjectAddTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkButton() {
|
||||||
|
if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) {
|
||||||
|
setDsbButton(true)
|
||||||
|
setDataDetail([])
|
||||||
|
} else {
|
||||||
|
setDsbButton(false)
|
||||||
|
const awal = formatDateOnly(range.startDate, "YYYY-MM-DD")
|
||||||
|
const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD")
|
||||||
|
const datanya = getDatesInRange(awal, akhir)
|
||||||
|
setDataDetail(datanya.map((item: any) => ({
|
||||||
|
date: item,
|
||||||
|
timeStart: null,
|
||||||
|
timeEnd: null,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAll()
|
checkAll()
|
||||||
}, [from, to, title, error])
|
}, [from, to, title, error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkButton()
|
||||||
|
}, [range])
|
||||||
|
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const dataDetailFix = dataDetail.map((item: any) => ({
|
||||||
|
date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"),
|
||||||
|
timeStart: item.timeStart,
|
||||||
|
timeEnd: item.timeEnd,
|
||||||
|
}))
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiCreateProjectTask({ data: { name: title, dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), user: hasil }, id });
|
const response = await apiCreateProjectTask({
|
||||||
|
data: {
|
||||||
|
name: title,
|
||||||
|
dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"),
|
||||||
|
dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"),
|
||||||
|
user: hasil,
|
||||||
|
dataDetail: dataDetailFix
|
||||||
|
}, id
|
||||||
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateProject({ ...update, task: !update.task, progress: !update.progress }))
|
dispatch(setUpdateProject({ ...update, task: !update.task, progress: !update.progress }))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menambah data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambah data', })
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Tambah Tugas",
|
headerTitle: "Tambah Tugas",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={disable}
|
// disable={disable || loading}
|
||||||
category="create"
|
// category="create"
|
||||||
onPress={() => { handleCreate() }}
|
// onPress={() => { handleCreate() }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Tambah Tugas"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disable || loading}
|
||||||
|
category="create"
|
||||||
|
onPress={() => { handleCreate() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={110}
|
keyboardVerticalOffset={headerHeight}
|
||||||
>
|
>
|
||||||
<ScrollView>
|
<ScrollView style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
mode="range"
|
mode="range"
|
||||||
startDate={range.startDate}
|
startDate={range.startDate}
|
||||||
endDate={range.endDate}
|
endDate={range.endDate}
|
||||||
onChange={(param) => setRange(param)}
|
onChange={(param) => { setRange(param) }}
|
||||||
styles={{
|
styles={{
|
||||||
selected: Styles.selectedDate,
|
selected: Styles.selectedDate,
|
||||||
selected_label: Styles.cWhite,
|
selected_label: Styles.cWhite,
|
||||||
range_fill: Styles.selectRangeDate,
|
range_fill: Styles.selectRangeDate,
|
||||||
|
month_label: { color: colors.text },
|
||||||
|
month_selector_label: { color: colors.text },
|
||||||
|
year_label: { color: colors.text },
|
||||||
|
year_selector_label: { color: colors.text },
|
||||||
|
day_label: { color: colors.text },
|
||||||
|
time_label: { color: colors.text },
|
||||||
|
weekday_label: { color: colors.text },
|
||||||
|
button_next_image: { tintColor: colors.text },
|
||||||
|
button_prev_image: { tintColor: colors.text },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.mv10]}>
|
<View style={[Styles.mv10]}>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween, Styles.mb10]}>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[Styles.w48]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Mulai <Text style={Styles.cError}>*</Text>
|
Tanggal Mulai <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{from}</Text>
|
<Text style={Styles.textCenter}>{from}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[{ width: "48%" }]}>
|
<View style={[Styles.w48]}>
|
||||||
<Text style={[Styles.mb05]}>
|
<Text style={[Styles.mb05]}>
|
||||||
Tanggal Berakhir <Text style={Styles.cError}>*</Text>
|
Tanggal Berakhir <Text style={{ color: colors.error }}>*</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[Styles.wrapPaper, Styles.p10]}>
|
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.borderAll, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<Text style={{ textAlign: "center" }}>{to}</Text>
|
<Text style={Styles.textCenter}>{to}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, Styles.cError, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, { color: colors.error }, Styles.mt05]}>Tanggal tidak boleh kosong</Text>
|
||||||
}
|
}
|
||||||
|
{/* <Pressable
|
||||||
|
style={[Styles.btnTab, Styles.btnLainnya, dsbButton && Styles.btnDisabled]}
|
||||||
|
disabled={dsbButton}
|
||||||
|
onPress={() => { setModalDetail(true) }}
|
||||||
|
>
|
||||||
|
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||||
|
</Pressable> */}
|
||||||
|
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||||
</View>
|
</View>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Judul Tugas"
|
label="Judul Tugas"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Judul Tugas"
|
placeholder="Judul Tugas"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
value={title}
|
value={title}
|
||||||
error={error.title}
|
error={error.title}
|
||||||
errorText="Judul tidak boleh kosong"
|
errorText="Judul tidak boleh kosong"
|
||||||
@@ -161,6 +247,14 @@ export default function ProjectAddTask() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
<ModalAddDetailTugasProject
|
||||||
|
isVisible={modalDetail}
|
||||||
|
setVisible={setModalDetail}
|
||||||
|
dataTanggal={dataDetail}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
setDataDetail(data)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ButtonBackHeader from "@/components/buttonBackHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiCancelProject } from "@/lib/api";
|
import { apiCancelProject } from "@/lib/api";
|
||||||
import { setUpdateProject } from "@/lib/projectUpdate";
|
import { setUpdateProject } from "@/lib/projectUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default function ProjectCancel() {
|
export default function ProjectCancel() {
|
||||||
|
const { colors } = useTheme();
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -19,6 +21,7 @@ export default function ProjectCancel() {
|
|||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [disable, setDisable] = useState(false);
|
const [disable, setDisable] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
function onValidation(val: string) {
|
function onValidation(val: string) {
|
||||||
@@ -44,6 +47,7 @@ export default function ProjectCancel() {
|
|||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiCancelProject({
|
const response = await apiCancelProject({
|
||||||
reason: reason,
|
reason: reason,
|
||||||
@@ -54,46 +58,71 @@ export default function ProjectCancel() {
|
|||||||
Toast.show({ type: 'small', text1: 'Berhasil membatalkan kegiatan', })
|
Toast.show({ type: 'small', text1: 'Berhasil membatalkan kegiatan', })
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal membatalkan kegiatan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
headerLeft: () => (
|
// headerLeft: () => (
|
||||||
<ButtonBackHeader
|
// <ButtonBackHeader
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
router.back();
|
// router.back();
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
),
|
// ),
|
||||||
headerTitle: "Pembatalan Kegiatan",
|
headerTitle: "Pembatalan Kegiatan",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
headerRight: () => (
|
// headerRight: () => (
|
||||||
<ButtonSaveHeader
|
// <ButtonSaveHeader
|
||||||
disable={disable}
|
// disable={disable || loading}
|
||||||
category="cancel"
|
// category="cancel"
|
||||||
onPress={() => {
|
// onPress={() => {
|
||||||
handleCancel();
|
// handleCancel();
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Pembatalan Kegiatan"
|
||||||
|
showBack={true}
|
||||||
|
onPressLeft={() => router.back()}
|
||||||
|
right={
|
||||||
|
<ButtonSaveHeader
|
||||||
|
disable={disable || loading}
|
||||||
|
category="cancel"
|
||||||
|
onPress={() => {
|
||||||
|
handleCancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.p15]}>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Alasan Pembatalan"
|
label="Alasan Pembatalan"
|
||||||
type="default"
|
type="default"
|
||||||
placeholder="Alasan Pembatalan"
|
placeholder="Alasan Pembatalan"
|
||||||
required
|
required
|
||||||
bg="white"
|
bg={colors.card}
|
||||||
error={error}
|
error={error}
|
||||||
errorText="Alasan pembatalan harus diisi"
|
errorText="Alasan pembatalan harus diisi"
|
||||||
onChange={(val) => onValidation(val)}
|
onChange={(val) => onValidation(val)}
|
||||||
|
multiline
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||