Merge pull request 'join' (#19) from join into v1

Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/19
This commit is contained in:
2026-02-09 11:18:02 +08:00
226 changed files with 8990 additions and 2539 deletions

View File

@@ -39,5 +39,7 @@ app-example
x.ts x.ts
x.sh x.sh
/android
/ios .env
android/

6
.gitignore vendored
View File

@@ -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

View 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
View 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
View File

@@ -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+ 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.

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -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 6
versionName "1.0.0" versionName "1.0.2"
} }
signingConfigs { signingConfigs {
debug { debug {

View File

@@ -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"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<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"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -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>

View File

@@ -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()

84
app.config.js Normal file
View File

@@ -0,0 +1,84 @@
import 'dotenv/config';
export default {
expo: {
name: "Desa+",
slug: "mobile-darmasaba",
version: "2.0.5", // 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: "7",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
},
googleServicesFile: process.env.IOS_GOOGLE_SERVICES_FILE
},
android: {
package: "mobiledarmasaba.app",
versionCode: 15,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"
},
googleServicesFile: "./google-services.json",
permissions: [
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
"READ_MEDIA_IMAGES", // Android 13+
"READ_MEDIA_VIDEO", // Android 13+
"READ_MEDIA_AUDIO" // Android 13+
]
},
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",
[
"@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,
}
}
};

View File

@@ -1,5 +1,5 @@
import HeaderRightAnnouncementList from "@/components/announcement/headerAnnouncementList"; import HeaderRightAnnouncementList from "@/components/announcement/headerAnnouncementList";
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
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,13 +8,14 @@ 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 { apiReadOneNotification } from "@/lib/api"; import { 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 { getApp } from "@react-native-firebase/app";
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
import { Redirect, router, Stack, usePathname } from "expo-router";
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useEffect } from "react"; import { useEffect } from "react";
import { Easing, Notifier } from 'react-native-notifier'; import { Easing, Notifier } from 'react-native-notifier';
@@ -22,11 +23,14 @@ import { Provider } from "react-redux";
export default function RootLayout() { export default function RootLayout() {
const { token, decryptToken, isLoading } = useAuthSession() const { token, decryptToken, isLoading } = useAuthSession()
const pathname = usePathname()
async function handleReadNotification(id: string, category: string, idContent: string) { 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))
const response = await apiReadOneNotification({ user: hasil, id: id })
}
pushToPage(category, idContent) pushToPage(category, idContent)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -34,25 +38,46 @@ 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: remoteMessage.notification?.body,
}); duration: 3000,
animationDuration: 300,
showEasing: Easing.ease,
onPress: () => handleReadNotification(String(id), String(category), String(content), String(title)),
hideOnPress: true,
});
}
} }
}); });
return unsubscribe; return unsubscribe;
}, []); }, [pathname]);
if (isLoading) { if (isLoading) {
@@ -65,64 +90,125 @@ export default function RootLayout() {
return ( return (
<Provider store={store}> <Provider store={store}>
<Stack screenOptions={Headers.shadow} > <Stack screenOptions={{
headerShown: true,
animation: "slide_from_right",
// ⬇️ PENTING BANGET
animationTypeForReplace: "pop",
fullScreenGestureEnabled: true,
gestureEnabled: true,
}} >
<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', title: 'Notifikasi',
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // 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="member/index" options={{ <Stack.Screen name="member/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Anggota', title: 'Anggota',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderMemberList /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Diskusi Umum', title: 'Diskusi Umum',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderDiscussionGeneral /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Kegiatan', title: 'Kegiatan',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderRightProjectList /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Divisi', title: 'Divisi',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderRightDivisionList /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Lembaga Desa', headerTitle: 'Lembaga Desa',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderRightGroupList /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Jabatan', headerTitle: 'Jabatan',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderRightPositionList /> // 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() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman', headerTitle: 'Pengumuman',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <HeaderRightAnnouncementList /> // 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 />
</Provider> </Provider>
) )

View File

@@ -1,43 +1,95 @@
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail"; import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import Skeleton from "@/components/skeleton"; import Skeleton from "@/components/skeleton";
import Text from '@/components/Text'; import Text from '@/components/Text';
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 { Entypo, 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 = { // Define TypeScript interfaces for better type safety
id: string interface AnnouncementData {
title: string id: string;
desc: string title: 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 [data, setData] = useState<AnnouncementData>({ id: '', title: '', desc: '' })
const [dataMember, setDataMember] = useState<any>({}) 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
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index) 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>()
/**
* Opens the image preview modal for the selected image file
* @param item The file data object containing image information
*/
function handleChooseFile(item: FileData) {
setChooseFile(item)
setPreview(true)
}
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 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)
setDataMember(response.member)
setDataFile(response.file)
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -51,23 +103,105 @@ export default function DetailAnnouncement() {
handleLoad(true) handleLoad(true)
}, []) }, [])
/**
* Checks if a string contains HTML tags
* @param text The text to check for HTML tags
* @returns True if the text contains HTML tags, false otherwise
*/
function hasHtmlTags(text: string) { function hasHtmlTags(text: string) {
const htmlRegex = /<[a-z][\s\S]*>/i; const htmlRegex = /<[a-z][\s\S]*>/i;
return htmlRegex.test(text); return htmlRegex.test(text);
}
/**
* Handles pull-to-refresh functionality
* Reloads the announcement data without showing loading indicators
*/
const handleRefresh = async () => {
setRefreshing(true)
handleLoad(false)
// Simulate network request delay for better UX
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);
// Download the file
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 (openError) {
console.error('Error opening file:', openError);
Toast.show({
type: 'error',
text1: 'Tidak ada aplikasi yang dapat membuka file ini'
});
}
} catch (error) {
console.error('Error downloading or opening file:', error);
Toast.show({
type: 'error',
text1: 'Gagal membuka file',
text2: 'Silakan coba lagi nanti'
});
} finally {
setLoadingOpen(false);
}
}; };
return ( return (
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman', headerTitle: 'Pengumuman',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>, // headerRight: () => entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>,
header: () => (
<AppHeader title="Pengumuman"
showBack={true}
onPressLeft={() => router.back()}
right={entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>}
/>
)
}} }}
/> />
<ScrollView> <ScrollView
<View style={[Styles.p15]}> showsVerticalScrollIndicator={false}
style={[Styles.h100]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => handleRefresh()}
/>
}
>
<View style={[Styles.p15, Styles.mb50]}>
<View style={[Styles.wrapPaper]}> <View style={[Styles.wrapPaper]}>
{ {
loading ? loading ?
@@ -84,9 +218,9 @@ export default function DetailAnnouncement() {
</View> </View>
: :
<> <>
<View style={Styles.rowItemsCenter}> <View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} /> <MaterialIcons name="campaign" size={25} color="black" style={[Styles.mr05]} />
<Text style={[Styles.textDefaultSemiBold]}>{data?.title}</Text> <Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
</View> </View>
<View style={[Styles.mt10]}> <View style={[Styles.mt10]}>
{ {
@@ -94,6 +228,7 @@ export default function DetailAnnouncement() {
<RenderHTML <RenderHTML
contentWidth={contentWidth} contentWidth={contentWidth}
source={{ html: data?.desc }} source={{ html: data?.desc }}
baseStyle={{ color: 'black' }}
/> />
: :
<Text>{data?.desc}</Text> <Text>{data?.desc}</Text>
@@ -103,7 +238,34 @@ export default function DetailAnnouncement() {
} }
</View> </View>
<View style={[Styles.wrapPaper, Styles.mv15]}> {
dataFile.length > 0 && (
<View style={[Styles.wrapPaper, Styles.mt10]}>
<View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
</View>
{dataFile.map((item, index) => (
<BorderBottomItem
key={`${item.id}-${index}`}
borderType="bottom"
icon={<MaterialCommunityIcons
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={25}
color="black"
/>}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => {
isImageFile(item.extension) ?
handleChooseFile(item)
: openFile(item)
}}
/>
))}
</View>
)
}
<View style={[Styles.wrapPaper, Styles.mt10]}>
{ {
loading ? loading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
@@ -138,6 +300,45 @@ export default function DetailAnnouncement() {
</View> </View>
</View> </View>
</ScrollView> </ScrollView>
<ImageViewing
images={[{ uri: `${ConstEnv.url_storage}/files/${chooseFile?.idStorage}` }]}
imageIndex={0}
visible={preview}
onRequestClose={() => setPreview(false)}
doubleTapToZoomEnabled
HeaderComponent={({ imageIndex }) => (
<View style={[Styles.headerModalViewImg]}>
{/* CLOSE */}
<Pressable
onPress={() => setPreview(false)}
accessibilityRole="button"
accessibilityLabel="Close image viewer"
>
<Text style={{ color: 'white', fontSize: 26 }}></Text>
</Pressable>
{/* MENU */}
<Pressable
onPress={() => chooseFile && openFile(chooseFile)}
accessibilityRole="button"
accessibilityLabel="Download or share image"
disabled={loadingOpen}
>
<Text style={{ color: loadingOpen ? 'gray' : 'white', fontSize: 22 }}></Text>
</Pressable>
</View>
)}
FooterComponent={({ imageIndex }) => (
<View style={{
paddingBottom: 20,
paddingHorizontal: 16,
alignItems: 'center',
}}>
<Text style={{ color: 'white', fontSize: 16 }}>{chooseFile?.name}.{chooseFile?.extension}</Text>
</View>
)}
/>
</SafeAreaView> </SafeAreaView>
) )
} }

View File

@@ -1,16 +1,21 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
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 { Entypo, Ionicons, MaterialCommunityIcons } 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 { SafeAreaView, ScrollView, StyleSheet, 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 +26,11 @@ export default function CreateAnnouncement() {
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
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 [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
const [dataForm, setDataForm] = useState({ const [dataForm, setDataForm] = useState({
title: "", title: "",
desc: "", desc: "",
@@ -66,10 +75,28 @@ export default function CreateAnnouncement() {
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)
// const response = await apiCreateAnnouncement({
// data: { ...dataForm, user: hasil, groups: divisionMember },
// });
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', })
@@ -77,37 +104,80 @@ export default function CreateAnnouncement() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
setLoading(false)
} }
} }
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
return ( return (
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Tambah Pengumuman", headerTitle: "Tambah Pengumuman",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || divisionMember.length == 0 || loading ? true : false}
category="create" // category="create"
onPress={() => { // onPress={() => {
divisionMember.length == 0 // divisionMember.length == 0
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", }) // ? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
: handleCreate(); // : handleCreate();
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Tambah Pengumuman"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || divisionMember.length == 0 || loading ? true : false}
category="create"
onPress={() => {
divisionMember.length == 0
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
: handleCreate();
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<View style={[Styles.p15, Styles.mb100]}> <ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
<InputForm <InputForm
label="Judul" label="Judul"
type="default" type="default"
@@ -127,6 +197,27 @@ export default function CreateAnnouncement() {
onChange={(val) => validationForm("desc", val)} onChange={(val) => validationForm("desc", val)}
multiline multiline
/> />
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect <ButtonSelect
value="Pilih divisi penerima pengumuman" value="Pilih divisi penerima pengumuman"
onPress={() => { onPress={() => {
@@ -145,9 +236,9 @@ export default function CreateAnnouncement() {
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text> <Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
{ {
item.Division.map((division: any, i: any) => ( item.Division.map((division: any, i: any) => (
<View key={i} style={[Styles.rowItemsCenter, Styles.mv05]}> <View key={i} style={[Styles.rowItemsCenter, Styles.w90]}>
<Entypo name="dot-single" size={24} color="black" /> <Entypo name="dot-single" size={24} color="black" />
<Text style={[Styles.textDefault]}>{division.name}</Text> <Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{division.name}</Text>
</View> </View>
)) ))
} }
@@ -171,6 +262,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" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -1,14 +1,19 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
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 { Entypo, 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 { SafeAreaView, ScrollView, View } from "react-native";
@@ -33,6 +38,11 @@ export default function EditAnnouncement() {
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<any>([]);
const [fileForm, setFileForm] = useState<any[]>([])
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
const [isModalFile, setModalFile] = useState(false)
const [loading, setLoading] = useState(false)
const [dataForm, setDataForm] = useState({ const [dataForm, setDataForm] = useState({
title: "", title: "",
desc: "", desc: "",
@@ -65,6 +75,7 @@ export default function EditAnnouncement() {
arrNew.push(newObject) arrNew.push(newObject)
}) })
setDataMember(arrNew); setDataMember(arrNew);
setDataFile(response.file);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -109,10 +120,24 @@ export default function EditAnnouncement() {
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', })
@@ -120,37 +145,90 @@ export default function EditAnnouncement() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
setLoading(false)
} }
} }
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
if (cat == "newFile") {
setFileForm([...fileForm.filter((val, i) => i !== index)])
} else {
setDataFile(prev =>
prev.map(item =>
item.id === index
? { ...item, delete: true }
: item
)
);
}
setModalFile(false)
}
return ( return (
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Edit Pengumuman", headerTitle: "Edit Pengumuman",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || loading ? true : false}
category="update" // category="update"
onPress={() => { // onPress={() => {
dataMember.length == 0 // dataMember.length == 0
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", }) // ? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
: handleEdit(); // : handleEdit();
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Edit Pengumuman"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
category="update"
onPress={() => {
dataMember.length == 0
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
: handleEdit();
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<View style={[Styles.p15, Styles.mb100]}> <ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
<InputForm <InputForm
label="Judul" label="Judul"
type="default" type="default"
@@ -172,6 +250,38 @@ export default function EditAnnouncement() {
value={dataForm.desc} value={dataForm.desc}
multiline multiline
/> />
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
dataFile.filter((val) => !val.delete).map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
/>
))
}
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect <ButtonSelect
value="Pilih divisi penerima pengumuman" value="Pilih divisi penerima pengumuman"
onPress={() => { onPress={() => {
@@ -189,9 +299,9 @@ export default function EditAnnouncement() {
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text> <Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
{ {
item.Division.map((division: any, i: any) => ( item.Division.map((division: any, i: any) => (
<View key={i} style={[Styles.rowItemsCenter, Styles.mv05]}> <View key={i} style={[Styles.rowItemsCenter, Styles.w90]}>
<Entypo name="dot-single" size={24} color="black" /> <Entypo name="dot-single" size={24} color="black" />
<Text style={[Styles.textDefault]}>{division.name}</Text> <Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{division.name}</Text>
</View> </View>
)) ))
} }
@@ -216,6 +326,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" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -87,7 +87,7 @@ export default function Announcement() {
<View> <View>
<InputSearch onChange={setSearch} /> <InputSearch onChange={setSearch} />
</View> </View>
<View style={[{ flex: 2 }, Styles.mb50]}> <View style={[{ flex: 2 }, Styles.mt05]}>
{ {
loading ? loading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
@@ -114,7 +114,7 @@ export default function Announcement() {
</View> </View>
} }
title={item.title} title={item.title}
desc={item.desc.replace(/<[^>]*>?/gm, '')} desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
/> />
) )

View File

@@ -1,7 +1,8 @@
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 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";
@@ -30,6 +31,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 +44,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 +51,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 +71,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();
@@ -105,6 +106,8 @@ export default function EditBanner() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', }) Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
} finally {
setLoading(false)
} }
}; };
@@ -112,22 +115,35 @@ export default function EditBanner() {
<SafeAreaView> <SafeAreaView>
<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> <ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15, Styles.mb100]}> <View style={[Styles.p15, Styles.mb100]}>
<View style={[Styles.mb15]}> <View style={[Styles.mb15]}>
{selectedImage != undefined ? ( {selectedImage != undefined ? (
@@ -151,7 +167,7 @@ export default function EditBanner() {
> >
<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>

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Text from "@/components/Text"; import Text from "@/components/Text";
@@ -29,6 +29,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 +42,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,32 +56,40 @@ 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);
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) {
console.error(error);
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', }) Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
} finally {
setLoading(false)
} }
}; };
@@ -90,28 +97,44 @@ export default function CreateBanner() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Tambah Banner", headerTitle: "Tambah Banner",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={title == "" || selectedImage == undefined || error ? true : false} // disable={title == "" || selectedImage == undefined || error || loading ? true : false}
category="create" // category="create"
onPress={() => { // onPress={() => {
handleCreateEntity(); // handleCreateEntity();
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Fitur"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={title == "" || selectedImage == undefined || error || loading ? true : false}
category="create"
onPress={() => {
handleCreateEntity();
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15, Styles.mb100]}> <View style={[Styles.p15]}>
<View style={[Styles.mb15]}> <View style={[Styles.mb15]}>
{selectedImage != undefined ? ( {selectedImage != undefined ? (
<Pressable onPress={pickImageAsync}> <Pressable onPress={pickImageAsync}>
@@ -130,7 +153,7 @@ export default function CreateBanner() {
> >
<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 pixel untuk Mohon unggah gambar dalam resolusi 1650 x 720 pixel untuk
memastikan memastikan
</Text> </Text>
</View> </View>

View File

@@ -1,10 +1,12 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi" import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
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 ModalLoading from "@/components/modalLoading" import ModalLoading from "@/components/modalLoading"
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"
@@ -16,11 +18,11 @@ import { router, Stack } from "expo-router"
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import { useState } from "react" import { 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
@@ -37,6 +39,7 @@ 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 [viewImg, setViewImg] = useState(false)
const handleDeleteEntity = async () => { const handleDeleteEntity = async () => {
try { try {
@@ -71,7 +74,7 @@ export default function BannerList() {
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)
@@ -105,14 +108,24 @@ export default function BannerList() {
<SafeAreaView> <SafeAreaView>
<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 />
}
/>
)
}} }}
/> />
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} /> <ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<ScrollView <ScrollView
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
@@ -121,26 +134,37 @@ export default function BannerList() {
} }
style={[Styles.h100]} style={[Styles.h100]}
> >
<View style={[Styles.p15, Styles.mb100]}> {
{entities.map((index: any, key: number) => ( entities.length > 0
<BorderBottomItem ?
key={key} <View style={[Styles.p15, Styles.mb100]}>
onPress={() => { {entities.map((index: any, key: number) => (
setDataId(index.id) <BorderBottomItem
setSelectFile(index) key={key}
setModal(true) onPress={() => {
}} setDataId(index.id)
borderType="all" setSelectFile(index)
icon={ setModal(true)
<Image }}
source={{ uri: `https://wibu-storage.wibudev.com/api/files/${index.image}` }} borderType="all"
style={[Styles.imgListBanner]} icon={
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
style={[Styles.imgListBanner]}
/>
}
title={index.title}
width={65}
/> />
} ))}
title={index.title} </View>
/> :
))} <View style={[Styles.p15, Styles.mb100]}>
</View> <Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
</View>
}
</ScrollView> </ScrollView>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu"> <DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">
@@ -155,8 +179,14 @@ export default function BannerList() {
/> />
<MenuItemRow <MenuItemRow
icon={<MaterialCommunityIcons name="file-eye" color="black" size={25} />} icon={<MaterialCommunityIcons name="file-eye" color="black" 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" color="black" size={25} />}
@@ -172,6 +202,14 @@ export default function BannerList() {
/> />
</View> </View>
</DrawerBottom> </DrawerBottom>
<ImageViewing
images={[{ uri: `${ConstEnv.url_storage}/files/${selectFile?.image}` }]}
imageIndex={0}
visible={viewImg}
onRequestClose={() => setViewImg(false)}
doubleTapToZoomEnabled
/>
</SafeAreaView> </SafeAreaView>
) )
} }

View File

@@ -1,21 +1,30 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem from "@/components/borderBottomItem";
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 LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
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 { 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 { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { firebase } from '@react-native-firebase/database'; 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 +43,42 @@ 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 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)
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,49 +133,129 @@ 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) {
console.error(error) console.error(error)
} 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) {
console.error(error)
} 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) {
console.error(error)
} 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={{ flex: 1 }}>
<ScrollView> <ScrollView
<View style={[Styles.p15, Styles.mb100]}> showsVerticalScrollIndicator={false}
style={[Styles.h100]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => handleRefresh()}
/>
}
>
<View style={[Styles.p15]}>
{ {
loading ? loading ?
<SkeletonContent /> <SkeletonContent />
: :
<BorderBottomItem <BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false} descEllipsize={false}
width={55}
borderType="bottom" borderType="bottom"
icon={ icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}> <View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
@@ -151,13 +263,13 @@ export default function DetailDiscussionGeneral() {
</View> </View>
} }
title={data?.title} title={data?.title}
titleShowAll={true}
subtitle={ subtitle={
!data?.isActive ? !data?.isActive ?
<LabelStatus category='warning' text='ARSIP' size="small" /> <LabelStatus category='warning' text='ARSIP' size="small" />
: :
<LabelStatus category={data.status == 1 ? 'success' : 'error'} text={data.status == 1 ? 'BUKA' : 'TUTUP'} size="small" /> <LabelStatus category={data.status == 1 ? 'success' : 'error'} text={data.status == 1 ? 'BUKA' : 'TUTUP'} size="small" />
} }
rightTopInfo={data?.createdAt}
desc={data?.desc} desc={data?.desc}
leftBottomInfo={ leftBottomInfo={
<View style={[Styles.rowItemsCenter]}> <View style={[Styles.rowItemsCenter]}>
@@ -165,6 +277,11 @@ export default function DetailDiscussionGeneral() {
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{dataKomentar.length} Komentar</Text> <Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{dataKomentar.length} Komentar</Text>
</View> </View>
} }
rightBottomInfo={
<View style={[Styles.rowItemsCenter]}>
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{data?.createdAt}</Text>
</View>
}
/> />
} }
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
@@ -180,14 +297,28 @@ export default function DetailDiscussionGeneral() {
return ( return (
<BorderBottomItem <BorderBottomItem
key={i} key={i}
width={55}
borderType="bottom" borderType="bottom"
colorPress
icon={ icon={
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="xs" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
} }
title={item.username} title={item.username}
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
desc={item.comment} desc={item.comment}
rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => {
setDetailMore((prev: any) => {
if (prev.includes(item.id)) {
return prev.filter((id: string) => id !== item.id)
} else {
return [...prev, item.id]
}
})
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/> />
) )
}) })
@@ -197,33 +328,112 @@ export default function DetailDiscussionGeneral() {
</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: "#f4f4f4" },
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="black" 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="black" 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"
bg="white"
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"))) ? Styles.cGray : Styles.cDefault} />
</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"
bg="white"
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"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>
:
<View style={[Styles.pv20, { alignItems: 'center' }]}>
<Text style={[Styles.textInformation, Styles.cGray]}>
{
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="black" size={25} />}
title="Edit"
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
title="Hapus"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>
</View>
</DrawerBottom>
</> </>
) )
} }

View File

@@ -1,9 +1,10 @@
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";
@@ -31,6 +32,7 @@ export default function AddMemberDiscussionDetail() {
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,15 +72,21 @@ 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) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', })
} finally {
setLoading(false)
} }
} }
@@ -87,16 +95,32 @@ export default function AddMemberDiscussionDetail() {
<SafeAreaView> <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()
}}
/>
}
/> />
) )
}} }}
@@ -108,13 +132,13 @@ export default function AddMemberDiscussionDetail() {
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,7 +149,9 @@ export default function AddMemberDiscussionDetail() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -140,7 +166,7 @@ export default function AddMemberDiscussionDetail() {
}} }}
> >
<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>
{ {
@@ -149,7 +175,7 @@ export default function AddMemberDiscussionDetail() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )

View File

@@ -1,17 +1,23 @@
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 ImageUser from "@/components/imageNew"; import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
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 { Ionicons, MaterialCommunityIcons } 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 { SafeAreaView, ScrollView, View } from "react-native";
@@ -31,6 +37,10 @@ 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 [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
const [dataForm, setDataForm] = useState({ const [dataForm, setDataForm] = useState({
idGroup: "", idGroup: "",
title: "", title: "",
@@ -93,12 +103,49 @@ export default function CreateDiscussionGeneral() {
router.back() router.back()
} }
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, 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 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)
// const response = await apiCreateDiscussionGeneral({
// data: { ...dataForm, user: hasil, member: entitiesMember },
// })
if (response.success) { if (response.success) {
dispatch(setMemberChoose([])) dispatch(setMemberChoose([]))
dispatch(setUpdateDiscussionGeneralDetail(!update)) dispatch(setUpdateDiscussionGeneralDetail(!update))
@@ -110,6 +157,8 @@ export default function CreateDiscussionGeneral() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -117,27 +166,46 @@ export default function CreateDiscussionGeneral() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { handleBack() }} // onPress={() => { handleBack() }}
/> // />
), // ),
headerTitle: "Tambah Diskusi", headerTitle: "Tambah Diskusi",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
category="create" // category="create"
disable={disableBtn} // disable={disableBtn || loading ? true : false}
onPress={() => { // onPress={() => {
entitiesMember.length == 0 // entitiesMember.length == 0
? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', }) // ? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', })
: handleCreate() // : handleCreate()
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Tambah Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
category="create"
disable={disableBtn || loading ? true : false}
onPress={() => {
entitiesMember.length == 0
? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', })
: handleCreate()
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15, Styles.mb100]}> <View style={[Styles.p15, Styles.mb100]}>
{ {
(entityUser.role == "supadmin" || (entityUser.role == "supadmin" ||
@@ -176,6 +244,26 @@ export default function CreateDiscussionGeneral() {
onChange={(val) => { validationForm("desc", val) }} onChange={(val) => { validationForm("desc", val) }}
multiline multiline
/> />
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect <ButtonSelect
value="Pilih Anggota" value="Pilih Anggota"
onPress={() => { onPress={() => {
@@ -211,7 +299,7 @@ export default function CreateDiscussionGeneral() {
key={index} key={index}
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
} }
title={item.name} title={item.name}
/> />
@@ -235,6 +323,16 @@ export default function CreateDiscussionGeneral() {
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" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -1,10 +1,18 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import Text from "@/components/Text";
import BorderBottomItem from "@/components/borderBottomItem";
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 LoadingOverlay from "@/components/loadingOverlay";
import MenuItemRow from "@/components/menuItemRow";
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 { 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 { SafeAreaView, ScrollView, View } from "react-native";
@@ -16,6 +24,11 @@ export default function EditDiscussionGeneral() {
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: "", title: "",
@@ -34,9 +47,17 @@ export default function EditDiscussionGeneral() {
user: hasil, user: hasil,
cat: "detail", cat: "detail",
}); });
const responseFile = await apiGetDiscussionGeneralOne({
id: id,
user: hasil,
cat: "file",
});
if (response.success) { if (response.success) {
setDataForm(response.data); setDataForm(response.data);
} }
if (responseFile.success) {
setDataFile(responseFile.data);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -77,11 +98,56 @@ export default function EditDiscussionGeneral() {
checkForm() checkForm()
}, [error, dataForm]) }, [error, dataForm])
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
if (cat == "newFile") {
setFileForm([...fileForm.filter((val, 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', })
@@ -89,6 +155,9 @@ export default function EditDiscussionGeneral() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -96,25 +165,40 @@ export default function EditDiscussionGeneral() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Edit Diskusi", headerTitle: "Edit Diskusi",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || loading ? true : false}
category="update" // category="update"
onPress={() => { handleEdit() }} // onPress={() => { handleEdit() }}
// />
// ),
header: () => (
<AppHeader
title="Edit Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
category="update"
onPress={() => { handleEdit() }}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
<InputForm <InputForm
label="Judul" label="Judul"
@@ -137,8 +221,50 @@ export default function EditDiscussionGeneral() {
onChange={(val) => validationForm("desc", val)} onChange={(val) => validationForm("desc", val)}
multiline multiline
/> />
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
dataFile.filter((val) => !val.delete).map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
/>
))
}
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
</View>
}
</View> </View>
</ScrollView> </ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -98,31 +98,36 @@ export default function Discussion() {
return ( return (
<View style={[Styles.p15, { flex: 1 }]}> <View style={[Styles.p15, { flex: 1 }]}>
<View> <View>
<View style={[Styles.wrapBtnTab]}> {
<ButtonTab entityUser.role != "user" && entityUser.role != "coadmin" &&
active={status == "false" ? "false" : "true"} <View style={[Styles.wrapBtnTab]}>
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" ? 'black' : 'white'} size={20} />}
active={status == "false" ? "false" : "true"} n={2} />
value="false" <ButtonTab
onPress={() => { setStatus("false") }} active={status == "false" ? "false" : "true"}
label="Arsip" value="false"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />} onPress={() => { setStatus("false") }}
n={2} /> label="Arsip"
</View> icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
</View>
}
<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.mv05, 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.mt05]}>
{ {
loading ? loading ?
arrSkeleton.map((item: any, i: number) => { arrSkeleton.map((item: any, i: number) => {
@@ -153,7 +158,7 @@ export default function Discussion() {
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" /> status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
} }
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
desc={item.desc} desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
leftBottomInfo={ leftBottomInfo={
<View style={[Styles.rowItemsCenter]}> <View style={[Styles.rowItemsCenter]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} /> <Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
@@ -176,34 +181,6 @@ export default function Discussion() {
/> />
} }
/> />
// data.map((item: any, i: number) => {
// return (
// <BorderBottomItem
// key={i}
// 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`}
// />
// )
// })
: :
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text> <Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
} }

View File

@@ -1,12 +1,13 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"; import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
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 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 SkeletonTwoItem from "@/components/skeletonTwoItem";
import Text from '@/components/Text'; import Text from '@/components/Text';
import { ColorsStatus } from "@/constants/ColorsStatus"; 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";
@@ -73,9 +74,16 @@ export default function MemberDiscussionDetail() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Anggota Diskusi', headerTitle: 'Anggota Diskusi',
headerTitleAlign: 'center', headerTitleAlign: 'center',
header: () => (
<AppHeader
title="Anggota Diskusi"
showBack={true}
onPressLeft={() => router.back()}
/>
)
}} }}
/> />
<ScrollView> <ScrollView>
@@ -109,7 +117,7 @@ export default function MemberDiscussionDetail() {
key={index} key={index}
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
} }
title={item.name} title={item.name}
onPress={() => { onPress={() => {
@@ -134,22 +142,25 @@ export default function MemberDiscussionDetail() {
router.push(`/member/${chooseUser.idUser}`) router.push(`/member/${chooseUser.idUser}`)
}} }}
/> />
{
entityUser.role != "user" && entityUser.role != "coadmin" &&
<MenuItemRow
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />}
title="Keluarkan"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
onPress: () => {
handleDeleteUser()
}
})
<MenuItemRow }}
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />} />
title="Keluarkan" }
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
onPress: () => {
handleDeleteUser()
}
})
}}
/>
</View> </View>
</DrawerBottom> </DrawerBottom>
</SafeAreaView> </SafeAreaView>

View File

@@ -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>

View File

@@ -1,9 +1,10 @@
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";
@@ -31,6 +32,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 +80,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) {
@@ -90,6 +93,8 @@ export default function AddMemberCalendarEvent() {
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -98,16 +103,32 @@ export default function AddMemberCalendarEvent() {
<SafeAreaView> <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()
}}
/>
}
/> />
) )
}} }}
@@ -118,13 +139,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)}
/> />
)) ))
@@ -135,7 +156,10 @@ export default function AddMemberCalendarEvent() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>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) => {
@@ -146,11 +170,10 @@ export default function AddMemberCalendarEvent() {
style={[Styles.itemSelectModal]} style={[Styles.itemSelectModal]}
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>
{ {
@@ -159,7 +182,7 @@ export default function AddMemberCalendarEvent() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )

View File

@@ -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,10 +9,11 @@ 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 { 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() {
@@ -21,6 +22,8 @@ export default function EditEventCalendar() {
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,
@@ -140,6 +143,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) {
@@ -150,7 +154,9 @@ export default function EditEventCalendar() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengubah acara', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -159,107 +165,129 @@ export default function EditEventCalendar() {
<SafeAreaView> <SafeAreaView>
<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="white"
/> 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="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> </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"}

View File

@@ -1,4 +1,5 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi" import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem" import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader" import ButtonBackHeader from "@/components/buttonBackHeader"
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail" import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
@@ -7,14 +8,16 @@ import ImageUser from "@/components/imageNew"
import MenuItemRow from "@/components/menuItemRow" import MenuItemRow from "@/components/menuItemRow"
import Skeleton from "@/components/skeleton" import Skeleton from "@/components/skeleton"
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 { MaterialCommunityIcons } 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"
@@ -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));
@@ -147,10 +155,20 @@ export default function DetailEventCalendar() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Detail Acara', headerTitle: 'Detail Acara',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} /> // headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
header:()=>(
<AppHeader
title="Detail Acara"
showBack={true}
onPressLeft={() => router.back()}
right={
(entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
}
/>
)
}} }}
/> />
<ScrollView <ScrollView
@@ -164,12 +182,12 @@ export default function DetailEventCalendar() {
> >
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
<View style={[Styles.wrapPaper, Styles.mb15]}> <View style={[Styles.wrapPaper, Styles.mb15]}>
<View style={Styles.rowItemsCenter}> <View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
<MaterialCommunityIcons name="calendar-text" size={30} color="black" style={Styles.mr10} /> <MaterialCommunityIcons name="calendar-text" size={30} color="black" style={Styles.mr10} />
{ {
loading ? loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" /> <Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
: <Text style={[Styles.textDefault]}>{data?.title}</Text> : <Text style={[Styles.textDefault, Styles.w90]}>{data?.title}</Text>
} }
</View> </View>
@@ -215,16 +233,20 @@ export default function DetailEventCalendar() {
loading ? loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" /> <Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
: :
<Text style={[Styles.textDefault]}>{data?.linkMeet ? data.linkMeet : '-'}</Text> data?.linkMeet ?
<Pressable onPress={() => { handleCopy(data.linkMeet) }}>
<Text style={[Styles.textDefault]}>{data.linkMeet}</Text>
</Pressable>
: <Text style={[Styles.textDefault]}>-</Text>
} }
</View> </View>
<View style={[Styles.rowItemsCenter, Styles.mt10]}> <View style={[Styles.rowItemsCenter, Styles.mt10, { alignItems: 'flex-start' }]}>
<MaterialCommunityIcons name="card-text-outline" size={30} color="black" style={Styles.mr10} /> <MaterialCommunityIcons name="card-text-outline" size={30} color="black" style={Styles.mr10} />
{ {
loading ? loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" /> <Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
: :
<Text style={[Styles.textDefault]}>{data?.desc}</Text> <Text style={[Styles.textDefault, Styles.w90]}>{data?.desc}</Text>
} }
</View> </View>
</View> </View>
@@ -241,7 +263,7 @@ export default function DetailEventCalendar() {
<BorderBottomItem <BorderBottomItem
key={index} key={index}
borderType="bottom" borderType="bottom"
icon={<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} />} icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
title={item.name} title={item.name}
subtitle={item.email} subtitle={item.email}
onPress={() => { onPress={() => {

View File

@@ -1,9 +1,10 @@
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";
@@ -31,6 +32,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 +60,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) {
@@ -80,7 +83,9 @@ export default function CreateCalendarAddMember() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal membuat acara', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -88,14 +93,28 @@ export default function CreateCalendarAddMember() {
<SafeAreaView> <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 || 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 +126,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)}
/> />
)) ))
@@ -124,7 +143,10 @@ export default function CreateCalendarAddMember() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -136,13 +158,13 @@ export default function CreateCalendarAddMember() {
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={'black'} />
} }
</Pressable> </Pressable>
) )

View File

@@ -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,7 @@ 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 { Stack, router, useLocalSearchParams } from "expo-router"; import { Stack, router, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { import {
@@ -24,6 +26,7 @@ export default function CalendarDivisionCreate() {
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,
@@ -126,28 +129,44 @@ export default function CalendarDivisionCreate() {
<SafeAreaView> <SafeAreaView>
<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"

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
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";
@@ -8,7 +8,6 @@ import Styles from "@/constants/Styles";
import { apiGetCalendarByDateDivision, apiGetIndicatorCalendar } from "@/lib/api"; import { apiGetCalendarByDateDivision, apiGetIndicatorCalendar } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
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';
@@ -53,7 +52,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 +70,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,6 +112,7 @@ 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))}
/> />
); );
}, },
@@ -126,16 +128,24 @@ export default function CalendarDivision() {
<SafeAreaView> <SafeAreaView>
<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 />}
/>
)
}} }}
/> />
<ScrollView <ScrollView
@@ -154,7 +164,7 @@ export default function CalendarDivision() {
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: Styles.cBlack,

View File

@@ -1,10 +1,18 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
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 LoadingOverlay from "@/components/loadingOverlay";
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 { 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 { SafeAreaView, ScrollView, View } from "react-native";
@@ -17,6 +25,12 @@ export default function DiscussionDivisionEdit() {
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 }[]>([])
async function handleLoad() { async function handleLoad() {
try { try {
@@ -26,6 +40,12 @@ export default function DiscussionDivisionEdit() {
user: hasil, user: hasil,
cat: "data", cat: "data",
}); });
const response2 = await apiGetDiscussionOne({
id: detail,
user: hasil,
cat: "file",
});
setDataFile(response2.data);
setData(response.data.desc); setData(response.data.desc);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -38,46 +58,116 @@ export default function DiscussionDivisionEdit() {
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);
// const response = await apiEditDiscussion({
// data: { user: hasil, desc: data },
// id: 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) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
if (cat == "newFile") {
setFileForm([...fileForm.filter((val, i) => i !== index)])
} else {
setDataFile(prev =>
prev.map(item =>
item.id === index
? { ...item, delete: true }
: item
)
);
}
setModalFile(false)
}
return ( return (
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Edit Diskusi", headerTitle: "Edit Diskusi",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={data == ""} // disable={data == "" || loading}
category="update" // category="update"
onPress={() => { // onPress={() => {
handleUpdate(); // handleUpdate();
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Edit Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={data == "" || loading}
category="update"
onPress={() => {
handleUpdate();
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
<InputForm <InputForm
label="Diskusi" label="Diskusi"
@@ -86,9 +176,55 @@ export default function DiscussionDivisionEdit() {
required required
value={data} value={data}
onChange={setData} onChange={setData}
multiline
/> />
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
dataFile.filter((val) => !val.delete).map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
/>
))
}
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
</View>
}
</View> </View>
</ScrollView> </ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -1,24 +1,35 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem from "@/components/borderBottomItem";
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 LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
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 { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { firebase } from "@react-native-firebase/database"; 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 +51,23 @@ 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 { 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,8 +79,18 @@ 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)
useEffect(() => { useEffect(() => {
@@ -90,7 +122,15 @@ export default function DiscussionDetail() {
user: hasil, user: hasil,
cat: "data", cat: "data",
}); });
const responseFile = await apiGetDiscussionOne({
id: detail,
user: hasil,
cat: "file",
});
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);
@@ -166,6 +206,59 @@ export default function DiscussionDetail() {
} }
} }
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) {
console.error(error);
} 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) {
console.error(error);
} 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)
@@ -179,23 +272,38 @@ export default function DiscussionDetail() {
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Diskusi", headerTitle: "Diskusi",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => // headerRight: () =>
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ? // (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
<HeaderRightDiscussionDetail // <HeaderRightDiscussionDetail
id={detail} // id={detail}
status={data?.status} // status={data?.status}
isActive={data?.isActive} // isActive={data?.isActive}
/> : (<></>) // /> : (<></>)
, // ,
header: () => (
<AppHeader
title="Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
<HeaderRightDiscussionDetail
id={detail}
status={data?.status}
isActive={data?.isActive}
/> : (<></>)
}
/>
)
}} }}
/> />
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@@ -212,13 +320,13 @@ export default function DiscussionDetail() {
loading ? loading ?
<SkeletonContent /> <SkeletonContent />
: :
<BorderBottomItem <BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false} descEllipsize={false}
width={55}
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser <ImageUser
src={`https://wibu-storage.wibudev.com/api/files/${data?.user_img}`} src={`${ConstEnv.url_storage}/files/${data?.user_img}`}
size="sm" size="sm"
/> />
} }
@@ -262,18 +370,31 @@ export default function DiscussionDetail() {
dataComment.map((item, index) => ( dataComment.map((item, index) => (
<BorderBottomItem <BorderBottomItem
key={index} key={index}
width={55}
borderType="bottom" borderType="bottom"
colorPress
icon={ icon={
<ImageUser <ImageUser
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} src={`${ConstEnv.url_storage}/files/${item.img}`}
size="xs" size="xs"
/> />
} }
title={item.username} title={item.username}
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
desc={item.comment} desc={item.comment}
descEllipsize={false} rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => {
setDetailMore((prev: any) => {
if (prev.includes(item.id)) {
return prev.filter((id: string) => id !== item.id)
} else {
return [...prev, item.id]
}
})
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/> />
)) ))
} }
@@ -283,67 +404,152 @@ export default function DiscussionDetail() {
</ScrollView> </ScrollView>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={110} keyboardVerticalOffset={headerHeight}
> >
<View <View
style={[ style={[
Styles.contentItemCenter, Styles.contentItemCenter,
Styles.w100, Styles.w100,
{ backgroundColor: "#f4f4f4" }, { backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]} ]}
> >
<InputForm {
disable={ viewEdit ?
data?.status == 2 || <>
data?.isActive == false || <View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
((entityUser.role == "user" || entityUser.role == "coadmin") && <View style={[Styles.rowItemsCenter]}>
!isMemberDivision) <Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
} <Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
bg="white" </View>
type="default" <Pressable onPress={() => handleViewEditKomentar()}>
round <MaterialIcons name="close" color="black" size={22} />
placeholder="Kirim Komentar" </Pressable>
onChange={setKomentar} </View>
value={komentar} <InputForm
itemRight={ bg="white"
<Pressable type="default"
onPress={() => { round
komentar != "" && multiline
!loadingSend && placeholder="Kirim Komentar"
data?.status != 2 && onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
data?.isActive && value={selectKomentar.comment}
(((entityUser.role == "user" || itemRight={
entityUser.role == "coadmin") && <Pressable
isMemberDivision) || onPress={() => {
entityUser.role == "admin" || selectKomentar.comment != "" &&
entityUser.role == "supadmin" || !regexOnlySpacesOrEnter.test(selectKomentar.comment) &&
entityUser.role == "developer" || !loadingSend &&
entityUser.role == "cosupadmin") && data?.status != 2 &&
handleKomentar(); data?.isActive &&
}} (((entityUser.role == "user" ||
> entityUser.role == "coadmin") &&
<MaterialIcons isMemberDivision) ||
name="send" entityUser.role == "admin" ||
size={25} entityUser.role == "supadmin" ||
style={ entityUser.role == "developer" ||
komentar == "" || entityUser.role == "cosupadmin") &&
loadingSend || handleEditKomentar();
data?.status == 2 || }}
data?.isActive == false || style={[
((entityUser.role == "user" || Platform.OS == 'android' && Styles.mb12,
entityUser.role == "coadmin") && ]}
!isMemberDivision) >
? Styles.cGray <MaterialIcons
: Styles.cDefault name="send"
size={25}
style={
[selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
} }
/> />
</Pressable> </>
} :
/> data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") ||
isMemberDivision)
?
<InputForm
bg="white"
type="default"
round
multiline
placeholder="Kirim Komentar"
onChange={setKomentar}
value={komentar}
itemRight={
<Pressable
onPress={() => {
komentar != "" &&
!regexOnlySpacesOrEnter.test(komentar) &&
!loadingSend &&
data?.status != 2 &&
data?.isActive &&
(((entityUser.role == "user" ||
entityUser.role == "coadmin") &&
isMemberDivision) ||
entityUser.role == "admin" ||
entityUser.role == "supadmin" ||
entityUser.role == "developer" ||
entityUser.role == "cosupadmin") &&
handleKomentar();
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons
name="send"
size={25}
style={
[komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
}
/>
:
<View style={[Styles.pv20, { alignItems: 'center' }]}>
<Text style={[Styles.textInformation, Styles.cGray]}>
{
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="black" size={25} />}
title="Edit"
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
title="Hapus"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>
</View>
</DrawerBottom>
</> </>
); );
} }

View File

@@ -1,27 +1,77 @@
import ButtonBackHeader from "@/components/buttonBackHeader" import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem"
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 LoadingOverlay from "@/components/loadingOverlay"
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 { 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 { 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 CreateDiscussionDivision() { export default function CreateDiscussionDivision() {
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 () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, 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)
// const response = await apiCreateDiscussion({ data: { user: hasil, desc, idDivision: id } })
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 }));
@@ -32,6 +82,8 @@ export default function CreateDiscussionDivision() {
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -39,22 +91,75 @@ export default function CreateDiscussionDivision() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Diskusi', headerTitle: 'Tambah Diskusi',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => <ButtonSaveHeader // headerRight: () => <ButtonSaveHeader
disable={desc == ""} // disable={desc == "" || loading}
category="create" // category="create"
onPress={() => { // onPress={() => {
handleCreate() // handleCreate()
}} /> // }} />
header: () => (
<AppHeader
title="Tambah Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={desc == "" || loading}
category="create"
onPress={() => {
handleCreate()
}} />
}
/>
)
}} }}
/> />
<ScrollView> <LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<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
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
</View> </View>
</ScrollView> </ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView> </SafeAreaView>
) )
} }

View File

@@ -5,8 +5,9 @@ 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 { 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 { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons"; import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
@@ -40,6 +41,30 @@ 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)
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 {
@@ -79,6 +104,10 @@ export default function DiscussionDivision() {
}, 1000); }, 1000);
} }
useEffect(() => {
handleCheckMember()
}, [])
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false, 1) handleLoad(false, 1)
@@ -100,27 +129,31 @@ export default function DiscussionDivision() {
return ( return (
<View style={[Styles.p15, { flex: 1 }]}> <View style={[Styles.p15, { flex: 1 }]}>
<View> {
<View style={[Styles.wrapBtnTab]}> ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
<ButtonTab <View>
active={status == "false" ? "false" : "true"} <View style={[Styles.wrapBtnTab]}>
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" ? 'black' : 'white'} size={20} />}
active={status == "false" ? "false" : "true"} n={2} />
value="false" <ButtonTab
onPress={() => { setStatus("false") }} active={status == "false" ? "false" : "true"}
label="Arsip" value="false"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />} onPress={() => { setStatus("false") }}
n={2} /> label="Arsip"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
</View>
<InputSearch onChange={setSearch} />
</View> </View>
<InputSearch onChange={setSearch} /> }
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{ {
loading ? loading ?
arrSkeleton.map((item: any, i: number) => { arrSkeleton.map((item: any, i: number) => {
@@ -138,11 +171,10 @@ export default function DiscussionDivision() {
return ( return (
<BorderBottomItem <BorderBottomItem
key={index} key={index}
width={55}
onPress={() => { router.push(`./discussion/${item.id}`) }} onPress={() => { router.push(`./discussion/${item.id}`) }}
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
} }
title={item.user_name} title={item.user_name}
subtitle={ subtitle={
@@ -171,34 +203,8 @@ export default function DiscussionDivision() {
/> />
} }
/> />
// data.map((item, index) => (
// <BorderBottomItem
// key={index}
// width={55}
// onPress={() => { router.push(`./discussion/${item.id}`) }}
// borderType="bottom"
// icon={
// <ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" />
// }
// title={item.user_name}
// subtitle={
// active == "true" ? item.status == 1 ? <LabelStatus category='success' text='BUKA' size="small" /> : <LabelStatus category='error' text='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'}
// />
// ))
: :
( (<Text style={[Styles.textDefault, Styles.cGray, Styles.mv10, { textAlign: "center" }]}>Tidak ada diskusi</Text>)
<Text style={[Styles.textDefault, Styles.cGray, Styles.mv10, { textAlign: "center" }]}>Tidak ada diskusi</Text>
)
} }
</View> </View>
</View> </View>

View File

@@ -1,5 +1,5 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"; import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader"; 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";
@@ -13,10 +13,12 @@ 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 { 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";
@@ -64,6 +66,7 @@ type PropsPath = {
}; };
export default function DocumentDivision() { export default function DocumentDivision() {
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 +86,8 @@ 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 [bodyRename, setBodyRename] = useState({ const [bodyRename, setBodyRename] = useState({
id: "", id: "",
name: "", name: "",
@@ -91,6 +96,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 +223,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) {
@@ -213,7 +237,8 @@ export default function DocumentDivision() {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally { } finally {
setRename(false); setLoadingRename(false)
setRename(false)
} }
} }
@@ -268,7 +293,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,47 +334,81 @@ export default function DocumentDivision() {
}, [path]); }, [path]);
return ( return (
<SafeAreaView> <SafeAreaView style={{ flex: 1 }}>
<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={20} color="white" />}
onPress={() => {
handleBatal();
}}
/>
}
onPressLeft={() => {
(selectedFiles.length > 0 || dariSelectAll) ? handleBatal() : router.back();
}}
right={
selectedFiles.length > 0 || dariSelectAll ? (
<ButtonHeader
item={
<MaterialIcons name="checklist-rtl" size={20} color="white" />
}
onPress={() => {
handleSelectAll();
}}
/>
) : (
<HeaderRightDocument path={path} isMember={isMemberDivision} />
)
}
/>
)
}} }}
/> />
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} /> <ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<ScrollView <ScrollView
style={[Styles.h100]}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
@@ -359,25 +418,20 @@ export default function DocumentDivision() {
<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="black" />
setPath(item.id); )}
}} <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 +462,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);
}} }}
@@ -440,20 +495,6 @@ export default function DocumentDivision() {
{(selectedFiles.length > 0 || dariSelectAll) && ( {(selectedFiles.length > 0 || dariSelectAll) && (
<View style={[ColorsStatus.primary, Styles.bottomMenuSelectDocument]}> <View style={[ColorsStatus.primary, Styles.bottomMenuSelectDocument]}>
<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
@@ -551,7 +592,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

View File

@@ -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";
@@ -32,6 +32,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 +91,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();
@@ -119,6 +121,8 @@ export default function TaskDivisionAddFile() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -126,22 +130,36 @@ export default function TaskDivisionAddFile() {
<SafeAreaView> <SafeAreaView>
<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>
@@ -171,6 +189,9 @@ 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 File

@@ -1,9 +1,10 @@
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";
@@ -11,7 +12,7 @@ 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";
@@ -30,6 +31,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 +74,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) {
@@ -83,43 +86,61 @@ export default function AddMemberTask() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} 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 }]}>
<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)}
/> />
)) ))
@@ -130,7 +151,9 @@ export default function AddMemberTask() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -145,7 +168,7 @@ export default function AddMemberTask() {
}} }}
> >
<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>
{ {
@@ -154,7 +177,7 @@ export default function AddMemberTask() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )
@@ -165,6 +188,6 @@ export default function AddMemberTask() {
} }
</ScrollView> </ScrollView>
</View> </View>
</SafeAreaView> </>
) )
} }

View File

@@ -1,16 +1,22 @@
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 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 { 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, Pressable, SafeAreaView,
ScrollView, ScrollView,
View View
} from "react-native"; } from "react-native";
@@ -24,6 +30,8 @@ export default function TaskDivisionAddTask() {
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 +41,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 +76,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,
}); });
@@ -93,7 +131,9 @@ export default function TaskDivisionAddTask() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Gagal menambah data', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -101,29 +141,45 @@ export default function TaskDivisionAddTask() {
<SafeAreaView> <SafeAreaView>
<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]}>
@@ -137,6 +193,13 @@ 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: Styles.cBlack,
month_selector_label: Styles.cBlack,
year_label: Styles.cBlack,
year_selector_label: Styles.cBlack,
day_label: Styles.cBlack,
time_label: Styles.cBlack,
weekday_label: Styles.cBlack,
}} }}
/> />
</View> </View>
@@ -162,6 +225,13 @@ export default function TaskDivisionAddTask() {
{ {
(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, Styles.cError, 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>
</View> </View>
<InputForm <InputForm
label="Judul Tugas" label="Judul Tugas"
@@ -179,7 +249,14 @@ export default function TaskDivisionAddTask() {
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<ModalAddDetailTugasTask
isVisible={modalDetail}
setVisible={setModalDetail}
dataTanggal={dataDetail}
onSubmit={(data) => {
setDataDetail(data)
}}
/>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -19,6 +19,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 +44,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(
{ {
@@ -60,7 +62,9 @@ export default function TaskDivisionCancel() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Gagal membatalkan kegiatan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -68,24 +72,40 @@ export default function TaskDivisionCancel() {
<SafeAreaView> <SafeAreaView>
<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>

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -19,6 +19,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 +62,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(
{ {
@@ -79,6 +81,8 @@ export default function TaskDivisionEdit() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -86,22 +90,35 @@ export default function TaskDivisionEdit() {
<SafeAreaView> <SafeAreaView>
<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>

View File

@@ -1,12 +1,14 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
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 { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -30,6 +32,35 @@ export default function DetailTaskDivision() {
const [progress, setProgress] = useState(0) const [progress, setProgress] = useState(0)
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 [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 handleLoad(cat: 'data' | 'progress') { async function handleLoad(cat: 'data' | 'progress') {
@@ -69,10 +100,24 @@ export default function DetailTaskDivision() {
<SafeAreaView> <SafeAreaView>
<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} />
}
/>
)
}} }}
/> />
<ScrollView <ScrollView
@@ -88,9 +133,11 @@ 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 text={`Kemajuan Kegiatan ${progress}%`} progress={progress} />
<SectionTanggalTugasTask refreshing={refreshing}/> <SectionReportTask refreshing={refreshing} />
<SectionFileTask refreshing={refreshing}/> <SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionMemberTask refreshing={refreshing}/> <SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>

View File

@@ -0,0 +1,142 @@
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 { 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 { 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) {
console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
}
}
return (
<SafeAreaView>
<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="white"
value={laporan}
onChange={(val) => { onValidation(val) }}
error={error}
errorText="Laporan kegiatan harus diisi"
multiline
/>
</View>
</ScrollView>
</SafeAreaView>
);
}

View File

@@ -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 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";
@@ -38,6 +39,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 +79,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()
@@ -102,6 +105,9 @@ export default function CreateTaskDivision() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -110,22 +116,36 @@ export default function CreateTaskDivision() {
<SafeAreaView> <SafeAreaView>
<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>
@@ -184,7 +204,7 @@ export default function CreateTaskDivision() {
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser <ImageUser
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} src={`${ConstEnv.url_storage}/files/${item.img}`}
size="sm" size="sm"
/> />
} }

View File

@@ -1,9 +1,10 @@
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";
@@ -11,7 +12,7 @@ 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";
@@ -25,7 +26,6 @@ export default function AddMemberCreateTask() {
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 +64,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 }]}>
<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)}
/> />
)) ))
@@ -105,7 +121,9 @@ export default function AddMemberCreateTask() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -119,13 +137,13 @@ export default function AddMemberCreateTask() {
}} }}
> >
<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={'black'} />
} }
</Pressable> </Pressable>
) )
@@ -136,6 +154,6 @@ export default function AddMemberCreateTask() {
} }
</ScrollView> </ScrollView>
</View> </View>
</SafeAreaView> </>
) )
} }

View File

@@ -1,15 +1,22 @@
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 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 { 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,
Platform, Platform,
Pressable,
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
View View
@@ -20,6 +27,7 @@ import DateTimePicker, {
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
export default function CreateTaskAddTugas() { export default function CreateTaskAddTugas() {
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 +41,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 +67,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);
@@ -81,27 +121,40 @@ export default function CreateTaskAddTugas() {
<SafeAreaView> <SafeAreaView>
<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]}>
@@ -115,6 +168,13 @@ 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: Styles.cBlack,
month_selector_label: Styles.cBlack,
year_label: Styles.cBlack,
year_selector_label: Styles.cBlack,
day_label: Styles.cBlack,
time_label: Styles.cBlack,
weekday_label: Styles.cBlack,
}} }}
/> />
</View> </View>
@@ -140,6 +200,13 @@ export default function CreateTaskAddTugas() {
{ {
(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, Styles.cError, 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>
</View> </View>
<InputForm <InputForm
label="Judul Tugas" label="Judul Tugas"
@@ -157,6 +224,14 @@ export default function CreateTaskAddTugas() {
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<ModalAddDetailTugasTask
isVisible={modalDetail}
setVisible={setModalDetail}
dataTanggal={dataDetail}
onSubmit={(data) => {
setDataDetail(data)
}}
/>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -31,7 +31,7 @@ type Props = {
}; };
export default function ListTask() { export default function ListTask() {
const { id, status } = useLocalSearchParams<{ id: string; status: string }>() 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()
const [data, setData] = useState<Props[]>([]) const [data, setData] = useState<Props[]>([])
@@ -43,6 +43,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 +57,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) {
@@ -106,7 +112,7 @@ export default function ListTask() {
return ( return (
<View style={[Styles.p15, { flex: 1 }]}> <View style={[Styles.p15, { flex: 1 }]}>
<View> <View>
<ScrollView horizontal style={[Styles.mb10]}> <ScrollView horizontal style={[Styles.mb10]} showsHorizontalScrollIndicator={false}>
<ButtonTab <ButtonTab
active={statusFix} active={statusFix}
value="0" value="0"
@@ -179,6 +185,12 @@ export default function ListTask() {
</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={{ marginRight: 5 }} />
</View>
</View>
<View style={[{ flex: 2 }]}> <View style={[{ flex: 2 }]}>
{ {
loading ? loading ?

View File

@@ -1,17 +1,24 @@
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 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 { 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,
Platform, Platform,
Pressable,
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
View View
@@ -21,6 +28,7 @@ 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 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 +37,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 +52,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 +70,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 +99,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', })
@@ -84,7 +125,9 @@ export default function UpdateProjectTaskDivision() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Gagal mengubah data', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoadingSubmit(false)
} }
} }
@@ -115,37 +158,76 @@ 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>
<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]}>
@@ -162,6 +244,13 @@ 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: Styles.cBlack,
month_selector_label: Styles.cBlack,
year_label: Styles.cBlack,
year_selector_label: Styles.cBlack,
day_label: Styles.cBlack,
time_label: Styles.cBlack,
weekday_label: Styles.cBlack,
}} }}
/> />
)} )}
@@ -190,6 +279,13 @@ export default function UpdateProjectTaskDivision() {
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>
</View> </View>
<InputForm <InputForm
label="Judul Tugas" label="Judul Tugas"
@@ -207,7 +303,14 @@ export default function UpdateProjectTaskDivision() {
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<ModalAddDetailTugasTask
isVisible={modalDetail}
setVisible={setModalDetail}
dataTanggal={dataDetail}
onSubmit={(data) => {
setDataDetail(data)
}}
/>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -1,9 +1,10 @@
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";
@@ -11,7 +12,7 @@ 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";
@@ -31,9 +32,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 +45,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 +77,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) {
@@ -83,43 +89,61 @@ export default function AddMemberDivision() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} 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 }]}>
<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)}
/> />
)) ))
@@ -130,7 +154,9 @@ export default function AddMemberDivision() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -145,7 +171,7 @@ export default function AddMemberDivision() {
}} }}
> >
<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>
{ {
@@ -154,7 +180,7 @@ export default function AddMemberDivision() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )
@@ -165,6 +191,6 @@ export default function AddMemberDivision() {
} }
</ScrollView> </ScrollView>
</View> </View>
</SafeAreaView> </>
) )
} }

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -16,6 +16,7 @@ export default function EditDivision() {
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 +44,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) {
@@ -55,6 +57,8 @@ export default function EditDivision() {
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -62,22 +66,36 @@ export default function EditDivision() {
<SafeAreaView> <SafeAreaView>
<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>

View File

@@ -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"
@@ -10,7 +10,7 @@ import { apiGetDivisionOneDetail } from "@/lib/api"
import { useAuthSession } from "@/providers/AuthProvider" import { useAuthSession } from "@/providers/AuthProvider"
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,
@@ -26,11 +26,12 @@ export default function DetailDivisionFitur() {
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 +43,49 @@ 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>
<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}
/>
}
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>

View File

@@ -1,20 +1,22 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi" import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem" import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo" import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
import DrawerBottom from "@/components/drawerBottom" import DrawerBottom from "@/components/drawerBottom"
import ImageUser from "@/components/imageNew" import ImageUser from "@/components/imageNew"
import SectionCancel from "@/components/sectionCancel" import SectionCancel from "@/components/sectionCancel"
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 { ColorsStatus } from "@/constants/ColorsStatus" import { ColorsStatus } from "@/constants/ColorsStatus"
import { ConstEnv } from "@/constants/ConstEnv"
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 { Feather, 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 +39,8 @@ type PropsMember = {
} }
export default function InformationDivision() { export default function InformationDivision() {
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()
@@ -46,6 +50,8 @@ export default function InformationDivision() {
const update = useSelector((state: any) => state.divisionUpdate) const update = useSelector((state: any) => state.divisionUpdate)
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 [isMemberDivision, setIsMemberDivision] = useState(false)
const [isAdminDivision, setIsAdminDivision] = useState(false)
const [dataMemberChoose, setDataMemberChoose] = useState({ const [dataMemberChoose, setDataMemberChoose] = useState({
id: '', id: '',
name: '', name: '',
@@ -111,12 +117,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) {
@@ -128,13 +164,31 @@ export default function InformationDivision() {
<SafeAreaView> <SafeAreaView>
<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
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
style={[Styles.h100]}
>
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
{ {
dataDetail?.isActive == false && ( dataDetail?.isActive == false && (
@@ -159,6 +213,7 @@ export default function InformationDivision() {
<Text style={[Styles.textDefault, Styles.mv05]}>{dataMember.length} Anggota</Text> <Text style={[Styles.textDefault, Styles.mv05]}>{dataMember.length} Anggota</Text>
<View style={[Styles.wrapPaper]}> <View style={[Styles.wrapPaper]}>
{ {
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
dataDetail?.isActive && ( dataDetail?.isActive && (
<BorderBottomItem <BorderBottomItem
onPress={() => { router.push(`/division/${id}/add-member`) }} onPress={() => { router.push(`/division/${id}/add-member`) }}
@@ -184,12 +239,11 @@ export default function InformationDivision() {
dataMember.map((item, index) => { dataMember.map((item, index) => {
return ( return (
<BorderBottomItem <BorderBottomItem
width={55}
key={index} key={index}
borderType="bottom" borderType="bottom"
onPress={() => { dataDetail?.isActive && handleChooseMember(item) }} onPress={() => { dataDetail?.isActive && (isAdminDivision || (entityUser.role != "user" && entityUser.role != "coadmin")) && handleChooseMember(item) }}
icon={ icon={
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} size="sm" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
} }
title={item.name} title={item.name}
rightTopInfo={item.isAdmin ? "Admin" : "Anggota"} rightTopInfo={item.isAdmin ? "Admin" : "Anggota"}

View File

@@ -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"
@@ -107,9 +107,16 @@ export default function ReportDivision() {
<SafeAreaView> <SafeAreaView>
<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>

View File

@@ -1,22 +1,28 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AlertKonfirmasi from "@/components/alertKonfirmasi";
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 { 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 { token, decryptToken } = useAuthSession()
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" }); const [isSelect, setSelect] = useState(false)
const dispatch = useDispatch(); const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" })
const update = useSelector((state: any) => state.divisionCreate); 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 [error, setError] = useState({ const [error, setError] = useState({
idGroup: false, idGroup: false,
name: false, name: false,
@@ -54,7 +60,34 @@ 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) {
AlertKonfirmasi({
title: 'Peringatan',
category: 'warning',
desc: 'Nama divisi sudah ada. Tidak dapat membuat divisi dengan nama yang sama',
onPress: () => { }
})
} else {
handleSetData()
}
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} 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`)
} }
@@ -69,25 +102,38 @@ export default function CreateDivision() {
<SafeAreaView> <SafeAreaView>
<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]}
>
<View style={[Styles.p15]}>
{ {
(entityUser.role == "supadmin" || entityUser.role == "developer") && (entityUser.role == "supadmin" || entityUser.role == "developer") &&
( (

View File

@@ -1,16 +1,18 @@
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 { 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";
@@ -22,6 +24,7 @@ type Props = {
export default function CreateDivisionAddAdmin() { export default function CreateDivisionAddAdmin() {
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 +32,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 +52,59 @@ 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) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal membuat divisi', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} 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 }]}>
<ScrollView> <ScrollView>
{ {
data.length > 0 ? data.length > 0 ?
@@ -99,7 +119,7 @@ export default function CreateDivisionAddAdmin() {
}} }}
> >
<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>
{ {
@@ -108,7 +128,7 @@ export default function CreateDivisionAddAdmin() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )
@@ -119,6 +139,6 @@ export default function CreateDivisionAddAdmin() {
} }
</ScrollView> </ScrollView>
</View> </View>
</SafeAreaView> </>
) )
} }

View File

@@ -1,9 +1,10 @@
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";
@@ -11,7 +12,7 @@ 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 { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
type Props = { type Props = {
@@ -59,34 +60,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 }]}>
<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)}
/> />
)) ))
@@ -97,7 +108,9 @@ export default function CreateDivisionAddMember() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
} }
<ScrollView> <ScrollView
showsVerticalScrollIndicator={false}
>
{ {
data.length > 0 ? data.length > 0 ?
@@ -112,7 +125,7 @@ export default function CreateDivisionAddMember() {
}} }}
> >
<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>
{ {
@@ -121,7 +134,7 @@ export default function CreateDivisionAddMember() {
</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={'black'} />
} }
</Pressable> </Pressable>
) )
@@ -132,6 +145,6 @@ export default function CreateDivisionAddMember() {
} }
</ScrollView> </ScrollView>
</View> </View>
</SafeAreaView> </>
) )
} }

View File

@@ -1,6 +1,7 @@
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";
@@ -180,7 +181,7 @@ export default function ListDivision() {
</View> </View>
} }
<View style={[Styles.rowSpaceBetween]}> <View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<InputSearch width={68} onChange={setSearch} /> <InputSearch width={68} onChange={setSearch} />
<Pressable <Pressable
onPress={() => { onPress={() => {
@@ -195,12 +196,13 @@ export default function ListDivision() {
</Pressable> </Pressable>
</View> </View>
{(entityUser.role == "supadmin" || entityUser.role == "developer") && ( {(entityUser.role == "supadmin" || entityUser.role == "developer") && (
<View style={[Styles.mv05]}> <View style={[Styles.mv05, 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.mt05]}>
{ {
loading ? loading ?
isList ? isList ?

View File

@@ -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";
@@ -125,19 +125,28 @@ export default function Report() {
<SafeAreaView> <SafeAreaView>
<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]}
>
<View style={[Styles.p15, Styles.mb50]}>
<SelectForm <SelectForm
bg="white" bg="white"
label="Lembaga Desa" label="Lembaga Desa"

View File

@@ -4,15 +4,21 @@ 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 { 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,6 +41,7 @@ type Props = {
}; };
export default function EditProfile() { export default function EditProfile() {
const headerHeight = useHeaderHeight()
const dispatch = useDispatch() const dispatch = useDispatch()
const entities = useSelector((state: any) => state.entities) const entities = useSelector((state: any) => state.entities)
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
@@ -46,7 +53,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 +110,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 +127,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 +158,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",);
@@ -179,6 +188,8 @@ export default function EditProfile() {
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', }) Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
} finally {
setLoading(false)
} }
} }
@@ -187,9 +198,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,7 +208,6 @@ 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)
} }
}; };
@@ -217,7 +227,7 @@ export default function EditProfile() {
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( headerRight: () => (
<ButtonSaveHeader <ButtonSaveHeader
disable={disableBtn} disable={disableBtn || loading ? true : false}
category="update" category="update"
onPress={() => { onPress={() => {
handleEdit() handleEdit()
@@ -226,112 +236,122 @@ export default function EditProfile() {
), ),
}} }}
/> />
<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={{ justifyContent: "center", alignItems: "center" }}>
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 350 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}

View File

@@ -1,4 +1,4 @@
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 { AntDesign, Entypo, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
@@ -13,9 +13,11 @@ export default function Feature() {
<SafeAreaView> <SafeAreaView>
<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 File

@@ -15,7 +15,7 @@ import { setUpdateGroup } from "@/lib/groupSlice";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons"; import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react"; import { useEffect, 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";
@@ -34,7 +34,7 @@ export default function Index() {
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 [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('')
@@ -42,24 +42,21 @@ export default function Index() {
const dispatch = useDispatch() const dispatch = useDispatch()
const update = useSelector((state: any) => state.groupUpdate) const update = useSelector((state: any) => state.groupUpdate)
const [error, setError] = useState({
const [data11, setData1] = useState(Array.from({ length: 20 }, (_, i) => `Item ${i}`)); title: false,
});
const renderItem = ({ item }: { item: string }) => (
<View style={{ padding: 20, borderBottomWidth: 1, borderColor: '#ccc' }}>
<Text>{item}</Text>
</View>
);
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)
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', })
@@ -109,10 +106,29 @@ export default function Index() {
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,
});
return ( return (
<SafeAreaView> <View style={[Styles.p15, { flex: 1 }]}>
<View style={[Styles.p15]}> <View style={[Styles.mb10]}>
<View style={[Styles.wrapBtnTab]}> <View style={[Styles.wrapBtnTab]}>
<ButtonTab <ButtonTab
active={status == "false" ? "false" : "true"} active={status == "false" ? "false" : "true"}
@@ -130,51 +146,53 @@ export default function Index() {
n={2} /> n={2} />
</View> </View>
<InputSearch onChange={setSearch} /> <InputSearch onChange={setSearch} />
<ScrollView </View>
refreshControl={ <View style={[{ flex: 2 }, Styles.mt05]}>
<RefreshControl {
refreshing={refreshing} loading ?
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, ColorsStatus.lightGreen]}>
<MaterialCommunityIcons name="office-building-outline" size={25} color={'#384288'} />
</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={() => { />
setIdChoose(item.id) }
setActiveChoose(item.isActive) />
setTitleChoose(item.name) :
setModal(true) <Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>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}>
@@ -207,15 +225,22 @@ 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}
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>
</View >
</SafeAreaView>
) )
} }

View File

@@ -13,8 +13,8 @@ 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 { 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 { 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,8 +22,9 @@ 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 { token, decryptToken, signOut } = useAuthSession()
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets()
const [refreshing, setRefreshing] = useState(false)
useEffect(() => { useEffect(() => {
handleUserLogin() handleUserLogin()
@@ -31,9 +32,20 @@ export default function Home() {
async function handleUserLogin() { async function handleUserLogin() {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
apiGetProfile({ id: hasil }).then((data) => dispatch(setEntities(data.data))); apiGetProfile({ id: hasil })
.then((data) => dispatch(setEntities(data.data)))
.catch((error) => {
signOut()
});
} }
const handleRefresh = async () => {
setRefreshing(true)
handleUserLogin()
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false)
};
return ( return (
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
@@ -48,16 +60,24 @@ export default function Home() {
), ),
}} }}
/> />
<ScrollView> <ScrollView
<CaraouselHome /> refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
showsVerticalScrollIndicator={false}
>
<CaraouselHome refreshing={refreshing}/>
<View style={[Styles.ph15, Styles.mb100]}> <View style={[Styles.ph15, Styles.mb100]}>
<FiturHome /> <FiturHome />
<ProjectHome /> <ProjectHome refreshing={refreshing}/>
<DivisionHome /> <DivisionHome refreshing={refreshing}/>
<ChartProgresHome /> <ChartProgresHome refreshing={refreshing}/>
<ChartDokumenHome /> <ChartDokumenHome refreshing={refreshing}/>
<EventHome /> <EventHome refreshing={refreshing}/>
<DisccussionHome /> <DisccussionHome refreshing={refreshing}/>
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>

View File

@@ -1,15 +1,20 @@
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 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 { 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 = {
@@ -29,21 +34,27 @@ type Props = {
export default function MemberDetail() { export default function MemberDetail() {
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
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)
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -66,11 +77,19 @@ export default function MemberDetail() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />, // headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Anggota', headerTitle: 'Anggota',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerRight: () => (entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>, // headerRight: () => (entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>,
headerShadowVisible: false header: () => (
<AppHeader title="Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
(entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>
}
/>
)
}} }}
/> />
<ScrollView <ScrollView
@@ -82,7 +101,7 @@ export default function MemberDetail() {
/> />
} }
> >
<View style={[Styles.wrapHeadViewMember,]}> <View style={[Styles.wrapHeadViewMember]}>
{ {
loading ? loading ?
<> <>
@@ -92,8 +111,10 @@ export default function MemberDetail() {
</> </>
: :
<> <>
<ImageUser src={`https://wibu-storage.wibudev.com/api/files/${data?.img}`} size="lg" /> <Pressable onPress={() => setPreview(true)}>
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{data?.name}</Text> <ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" onError={setErrorImg} />
</Pressable>
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, { textAlign: 'center' }]}>{data?.name}</Text>
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text> <Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text>
</> </>
@@ -102,6 +123,11 @@ export default function MemberDetail() {
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
<View style={[Styles.rowSpaceBetween]}> <View style={[Styles.rowSpaceBetween]}>
<Text style={[Styles.textDefaultSemiBold]}>Informasi</Text> <Text style={[Styles.textDefaultSemiBold]}>Informasi</Text>
<LabelStatus
size="small"
category={data?.isActive ? 'success' : 'error'}
text={data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}
/>
</View> </View>
{ {
loading ? loading ?
@@ -123,6 +149,14 @@ export default function MemberDetail() {
</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>
) )
} }

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import ModalSelect from "@/components/modalSelect"; import ModalSelect from "@/components/modalSelect";
@@ -7,9 +7,11 @@ 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 { 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,6 +28,7 @@ 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()
@@ -41,6 +44,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 +101,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 +118,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 +148,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 +156,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 +167,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))
@@ -179,6 +184,9 @@ export default function CreateMember() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -186,17 +194,14 @@ 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");
} }
}; };
@@ -204,46 +209,58 @@ export default function CreateMember() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Tambah Anggota", headerTitle: "Tambah Anggota",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || loading}
category="create" // category="create"
onPress={() => { handleCreate() }} // onPress={() => { handleCreate() }}
// />
// ),
header: () => (
<AppHeader title="Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => { handleCreate() }}
/>
}
/> />
), )
}} }}
/> />
<KeyboardAvoidingView <KeyboardAvoidingView
style={[Styles.h100]} style={[Styles.h100]}
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={{ justifyContent: "center", alignItems: "center" }}>
{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>
@@ -306,7 +323,7 @@ export default function CreateMember() {
placeholder="Nama" placeholder="Nama"
required required
error={error.name} error={error.name}
errorText="Nama tidak boleh kosong" errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => { onChange={val => {
validationForm("name", val) validationForm("name", val)
}} }}
@@ -327,7 +344,7 @@ export default function CreateMember() {
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]}>+62</Text>}
error={error.phone} error={error.phone}
errorText="Nomor Telepon tidak valid" errorText="Nomor Telepon tidak valid"
onChange={val => { onChange={val => {

View File

@@ -1,13 +1,17 @@
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 { 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 { 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,6 +42,7 @@ 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()
@@ -52,6 +57,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 +85,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 +133,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 +150,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 });
@@ -175,14 +181,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",);
@@ -203,7 +210,9 @@ export default function EditMember() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -212,9 +221,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,7 +231,6 @@ 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)
} }
}; };
@@ -231,45 +239,62 @@ export default function EditMember() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Edit Anggota", headerTitle: "Edit Anggota",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || loading}
category="update" // category="update"
onPress={() => { // onPress={() => {
handleEdit() // handleEdit()
}} // }}
// />
// ),
header: () => (
<AppHeader
title="Edit Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="update"
onPress={() => {
handleEdit()
}}
/>
}
/> />
), )
}} }}
/> />
<KeyboardAvoidingView <KeyboardAvoidingView
style={[Styles.h100]} style={[Styles.h100]}
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={{ justifyContent: "center", alignItems: "center" }}>
{ {
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 +308,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
@@ -337,7 +365,7 @@ export default function EditMember() {
required required
value={data?.name} value={data?.name}
error={error.name} error={error.name}
errorText="Nama tidak boleh kosong" errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => { onChange={val => {
validationForm("name", val) validationForm("name", val)
}} }}
@@ -359,7 +387,7 @@ 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]}>+62</Text>}
value={data?.phone} value={data?.phone}
error={error.phone} error={error.phone}
errorText="Nomor Telepon tidak valid" errorText="Nomor Telepon tidak valid"

View File

@@ -2,8 +2,10 @@ 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 { 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 { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
@@ -123,12 +125,13 @@ export default function Index() {
<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.mv05, 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.mt05]}>
{ {
loading ? loading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
@@ -150,7 +153,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}`}

View File

@@ -9,7 +9,7 @@ import { pushToPage } from "@/lib/pushToPage";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SafeAreaView, View, VirtualizedList } from "react-native"; import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
type Props = { type Props = {
@@ -31,6 +31,7 @@ export default function Notification() {
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) 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)
async function handleLoad(loading: boolean, thisPage: number) { async function handleLoad(loading: boolean, thisPage: number) {
try { try {
@@ -88,33 +89,12 @@ export default function Notification() {
} }
} }
// function pushToPage(category: string, idContent: string) { const handleRefresh = async () => {
// const cat = category.split('/') setRefreshing(true)
// if (cat.length > 1) { handleLoad(false, 1)
// if (cat[2] == 'calendar') { await new Promise(resolve => setTimeout(resolve, 2000));
// router.push(`/division/${cat[1]}/calendar/${idContent}`) setRefreshing(false)
// } else if (cat[2] == 'discussion') { };
// router.push(`/division/${cat[1]}/discussion/${idContent}`)
// } else if (cat[2] == 'document') {
// 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>
@@ -136,7 +116,6 @@ export default function Notification() {
return ( return (
<BorderBottomItem <BorderBottomItem
borderType="bottom" borderType="bottom"
width={55}
icon={ icon={
<View style={[Styles.iconContent, item.isRead ? ColorsStatus.secondary : ColorsStatus.primary]}> <View style={[Styles.iconContent, item.isRead ? ColorsStatus.secondary : ColorsStatus.primary]}>
<Feather name="bell" size={25} color="white" /> <Feather name="bell" size={25} color="white" />
@@ -157,6 +136,12 @@ export default function Notification() {
onEndReached={loadMoreData} onEndReached={loadMoreData}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
/> />
: :
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text> <Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>

View File

@@ -5,6 +5,7 @@ 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 SkeletonTwoItem from "@/components/skeletonTwoItem"; import SkeletonTwoItem from "@/components/skeletonTwoItem";
import Text from "@/components/Text"; import Text from "@/components/Text";
@@ -16,7 +17,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons"; import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, 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";
@@ -40,6 +41,7 @@ export default function Index() {
const [data, setData] = useState<Props[]>([]) const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [nameGroup, setNameGroup] = 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,
@@ -94,15 +96,20 @@ 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) {
dispatch(setUpdatePosition(!update))
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} 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', })
} }
} }
@@ -129,9 +136,18 @@ export default function Index() {
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, { flex: 1 }]}>
<View style={[Styles.p15]}> <View>
<View style={[Styles.wrapBtnTab]}> <View style={[Styles.wrapBtnTab]}>
<ButtonTab <ButtonTab
active={status == "false" ? "false" : "true"} active={status == "false" ? "false" : "true"}
@@ -151,51 +167,58 @@ export default function Index() {
<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.mv05, 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={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
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, ColorsStatus.lightGreen]}>
<MaterialCommunityIcons name="account-tie" size={25} color={'#384288'} />
</View>
}
title={item.name}
subtitle={item.group}
/>
)
}}
keyExtractor={(item, index) => String(index)}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
/>
:
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>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
@@ -239,11 +262,10 @@ 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>
</View>
</SafeAreaView>
) )
} }

View File

@@ -1,50 +1,73 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"; import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import { ButtonHeader } from "@/components/buttonHeader"; import { ButtonHeader } from "@/components/buttonHeader";
import ItemDetailMember from "@/components/itemDetailMember"; 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 { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons"; import { AntDesign } from "@expo/vector-icons";
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, SafeAreaView, ScrollView, View } from "react-native";
import ImageViewing from 'react-native-image-viewing';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
export default function Profile() { export default function Profile() {
const { signOut } = useAuthSession() const { signOut } = useAuthSession()
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)
return ( return (
<SafeAreaView> <SafeAreaView>
<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={<AntDesign name="logout" size={20} color="white" />}
}) onPress={() => {
}} AlertKonfirmasi({
/> title: 'Keluar',
desc: 'Apakah anda yakin ingin keluar?',
onPress: () => { signOut() }
})
}}
/>
}
/>
)
// headerRight: () => <ButtonHeader
// item={<AntDesign name="logout" size={20} color="white" />}
// onPress={() => {
// AlertKonfirmasi({
// title: 'Keluar',
// desc: 'Apakah anda yakin ingin keluar?',
// onPress: () => { signOut() }
// })
// }}
// />
}} }}
/> />
<ScrollView> <ScrollView style={[Styles.h100]}>
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column' }}>
<View style={[Styles.wrapHeadViewMember]}> <View style={[Styles.wrapHeadViewMember]}>
<Image <Pressable onPress={() => setPreview(true)}>
source={error ? require("../../assets/images/user.jpg") : { uri: `https://wibu-storage.wibudev.com/api/files/${entities.img}` }} <Image
onError={() => { setError(true) }} source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
style={[Styles.userProfileBig]} onError={() => { setError(true) }}
/> style={[Styles.userProfileBig]}
/>
</Pressable>
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{entities.name}</Text> <Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{entities.name}</Text>
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{entities.role}</Text> <Text style={[Styles.textMediumNormal, Styles.cWhite]}>{entities.role}</Text>
</View> </View>
@@ -64,6 +87,13 @@ export default function Profile() {
</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>
) )
} }

View File

@@ -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"
@@ -26,6 +26,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 +87,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();
@@ -116,6 +118,8 @@ export default function ProjectAddFile() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', }) Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -126,13 +130,27 @@ export default function ProjectAddFile() {
<SafeAreaView> <SafeAreaView>
<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> <ScrollView>
@@ -162,6 +180,9 @@ export default function ProjectAddFile() {
{ {
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 File

@@ -1,9 +1,10 @@
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";
@@ -11,7 +12,7 @@ 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";
@@ -31,6 +32,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 {
@@ -43,6 +45,7 @@ export default function AddMemberProject() {
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)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} }
} }
@@ -73,6 +76,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) {
@@ -82,41 +86,60 @@ export default function AddMemberProject() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} 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 }]}>
<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)}
/> />
)) ))
@@ -127,40 +150,48 @@ export default function AddMemberProject() {
: :
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text> <Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>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) => { <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]}
}} 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, Styles.cGray]}>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={'black'} />
} }
</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> </>
) )
} }

View File

@@ -1,17 +1,24 @@
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 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 { 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,
Platform, Platform,
Pressable,
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
View View
@@ -23,9 +30,12 @@ import DateTimePicker, {
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
export default function ProjectAddTask() { export default function ProjectAddTask() {
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 +48,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,21 +73,62 @@ 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) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -85,27 +136,41 @@ export default function ProjectAddTask() {
<SafeAreaView> <SafeAreaView>
<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>
<View style={[Styles.p15, Styles.mb100]}> <View style={[Styles.p15, Styles.mb100]}>
@@ -114,11 +179,18 @@ export default function ProjectAddTask() {
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: Styles.cBlack,
month_selector_label: Styles.cBlack,
year_label: Styles.cBlack,
year_selector_label: Styles.cBlack,
day_label: Styles.cBlack,
time_label: Styles.cBlack,
weekday_label: Styles.cBlack,
}} }}
/> />
</View> </View>
@@ -144,6 +216,13 @@ export default function ProjectAddTask() {
{ {
(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, Styles.cError, 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>
</View> </View>
<InputForm <InputForm
label="Judul Tugas" label="Judul Tugas"
@@ -161,6 +240,14 @@ export default function ProjectAddTask() {
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<ModalAddDetailTugasProject
isVisible={modalDetail}
setVisible={setModalDetail}
dataTanggal={dataDetail}
onSubmit={(data) => {
setDataDetail(data)
}}
/>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -6,7 +6,7 @@ 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 { 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";
@@ -19,6 +19,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 +45,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,
@@ -56,6 +58,9 @@ export default function ProjectCancel() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -63,28 +68,47 @@ export default function ProjectCancel() {
<SafeAreaView> <SafeAreaView>
<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]}
>
<View style={[Styles.p15]}>
<InputForm <InputForm
label="Alasan Pembatalan" label="Alasan Pembatalan"
type="default" type="default"
@@ -94,6 +118,7 @@ export default function ProjectCancel() {
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>

View File

@@ -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 { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -19,6 +19,7 @@ export default function EditProject() {
const [judul, setJudul] = useState(""); const [judul, setJudul] = 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)
async function handleLoad() { async function handleLoad() {
try { try {
@@ -42,6 +43,8 @@ export default function EditProject() {
setJudul(val) setJudul(val)
if (val == "" || val == "null") { if (val == "" || val == "null") {
setError(true) setError(true)
} else {
setError(false)
} }
} }
@@ -59,6 +62,7 @@ export default function EditProject() {
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 apiEditProject({ const response = await apiEditProject({
name: judul, name: judul,
@@ -68,9 +72,14 @@ export default function EditProject() {
dispatch(setUpdateProject({ ...update, data: !update.data })) dispatch(setUpdateProject({ ...update, data: !update.data }))
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: response.message, })
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -80,22 +89,36 @@ export default function EditProject() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
router.back(); // router.back();
}} // }}
/> // />
), // ),
headerTitle: "Edit Judul Kegiatan", headerTitle: "Edit Judul Kegiatan",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disable} // disable={disable || loading}
category="update" // category="update"
onPress={() => { handleUpdate() }} // onPress={() => { handleUpdate() }}
// />
// ),
header: () => (
<AppHeader
title="Edit Judul Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="update"
onPress={() => { handleUpdate() }}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <ScrollView>

View File

@@ -1,7 +1,9 @@
import ButtonBackHeader from "@/components/buttonBackHeader"; import AppHeader from "@/components/AppHeader";
import HeaderRightProjectDetail from "@/components/project/headerProjectDetail"; import HeaderRightProjectDetail from "@/components/project/headerProjectDetail";
import SectionFile from "@/components/project/sectionFile"; import SectionFile from "@/components/project/sectionFile";
import SectionLink from "@/components/project/sectionLink";
import SectionMember from "@/components/project/sectionMember"; import SectionMember from "@/components/project/sectionMember";
import SectionReportProject from "@/components/project/sectionReportProject";
import SectionTanggalTugasProject from "@/components/project/sectionTanggalTugas"; import SectionTanggalTugasProject from "@/components/project/sectionTanggalTugas";
import SectionCancel from "@/components/sectionCancel"; import SectionCancel from "@/components/sectionCancel";
import SectionProgress from "@/components/sectionProgress"; import SectionProgress from "@/components/sectionProgress";
@@ -9,7 +11,7 @@ import Styles from "@/constants/Styles";
import { apiGetProjectOne } from "@/lib/api"; import { apiGetProjectOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
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 { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -92,10 +94,20 @@ export default function DetailProject() {
<SafeAreaView> <SafeAreaView>
<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: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMember ? null : <HeaderRightProjectDetail id={id} status={data?.status} />, // headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMember ? null : <HeaderRightProjectDetail id={id} status={data?.status} />,
header: () => (
<AppHeader
title={loading ? 'Loading...' : data && data?.title || ''}
showBack={true}
onPressLeft={() => router.back()}
right={
(entityUser.role == "user" || entityUser.role == "coadmin") && !isMember ? null : <HeaderRightProjectDetail id={id} status={data?.status} />
}
/>
)
}} }}
/> />
<ScrollView <ScrollView
@@ -111,8 +123,10 @@ export default function DetailProject() {
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 text={`Kemajuan Kegiatan ${progress}%`} progress={progress} />
<SectionTanggalTugasProject status={data?.status} member={isMember} refreshing={refreshing}/> <SectionReportProject refreshing={refreshing} />
<SectionTanggalTugasProject status={data?.status} member={isMember} refreshing={refreshing} />
<SectionFile status={data?.status} member={isMember} refreshing={refreshing} /> <SectionFile status={data?.status} member={isMember} refreshing={refreshing} />
<SectionLink status={data?.status} member={isMember} refreshing={refreshing} />
<SectionMember status={data?.status} refreshing={refreshing} /> <SectionMember status={data?.status} refreshing={refreshing} />
</View> </View>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,145 @@
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
import { apiGetProjectOne, apiReportProject } from "@/lib/api";
import { setUpdateProject } from "@/lib/projectUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
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 ReportProject() {
const { token, decryptToken } = useAuthSession();
const { id } = useLocalSearchParams<{ id: string }>();
const dispatch = useDispatch()
const update = useSelector((state: any) => state.projectUpdate)
const [laporan, setLaporan] = useState("");
const [error, setError] = useState(false);
const [disable, setDisable] = useState(false);
const [loading, setLoading] = useState(false)
async function handleLoad() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetProjectOne({
user: hasil,
cat: "data",
id: id,
});
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 == null || laporan == undefined || 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 apiReportProject({
report: laporan,
user: hasil,
}, id);
if (response.success) {
dispatch(setUpdateProject({ ...update, report: !update.report }))
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
router.back();
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
}
}
return (
<SafeAreaView>
<Stack.Screen
options={{
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Laporan Kegiatan",
headerTitleAlign: "center",
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable || loading}
// category="update"
// onPress={() => { handleUpdate() }}
// />
// ),
header: () => (
<AppHeader
title="Laporan Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="update"
onPress={() => { handleUpdate() }}
/>
}
/>
)
}}
/>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
<InputForm
label="Laporan Kegiatan"
type="default"
placeholder="Laporan Kegiatan"
required
bg="white"
value={laporan}
onChange={(val) => { onValidation(val) }}
error={error}
errorText="Judul Kegiatan harus diisi"
multiline
/>
</View>
</ScrollView>
</SafeAreaView>
);
}

View File

@@ -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";
@@ -10,8 +10,10 @@ import ModalSelect from "@/components/modalSelect";
import SectionListAddTask from "@/components/project/sectionListAddTask"; import SectionListAddTask from "@/components/project/sectionListAddTask";
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 { apiCreateProject } from "@/lib/api"; import { apiCreateProject } from "@/lib/api";
import { setGroupChoose } from "@/lib/groupChoose";
import { setMemberChoose } from "@/lib/memberChoose"; import { setMemberChoose } from "@/lib/memberChoose";
import { setUpdateProject } from "@/lib/projectUpdate"; import { setUpdateProject } from "@/lib/projectUpdate";
import { setTaskCreate } from "@/lib/taskCreate"; import { setTaskCreate } from "@/lib/taskCreate";
@@ -29,6 +31,7 @@ import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
export default function CreateProject() { export default function CreateProject() {
const [loading, setLoading] = useState(false)
const { token, decryptToken } = useAuthSession(); const { token, decryptToken } = useAuthSession();
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" }); const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -55,10 +58,21 @@ export default function CreateProject() {
member: false, member: false,
}); });
const [hitung, setHitung] = useState(0) const [hitung, setHitung] = useState(0)
let hitungRefresh = 0;
useEffect(() => {
if (hitungRefresh == 0) {
dispatch(setGroupChoose(''));
dispatch(setTaskCreate([]));
dispatch(setMemberChoose([]));
}
hitungRefresh++;
}, []);
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(setGroupChoose(val));
dispatch(setMemberChoose([])); dispatch(setMemberChoose([]));
setDataForm({ ...dataForm, idGroup: val }); setDataForm({ ...dataForm, idGroup: val });
if (val == "" || val == "null") { if (val == "" || val == "null") {
@@ -73,7 +87,7 @@ export default function CreateProject() {
} else { } else {
setError(error => ({ ...error, title: false })) setError(error => ({ ...error, title: false }))
} }
} else if (cat == "task" && hitung > 1) { } else if (cat == "task" && hitung > 2) {
if (taskCreate.length == 0) { if (taskCreate.length == 0) {
setError(error => ({ ...error, task: true })) setError(error => ({ ...error, task: true }))
} else { } else {
@@ -91,6 +105,7 @@ export default function CreateProject() {
} }
function handleBack() { function handleBack() {
dispatch(setGroupChoose(''));
dispatch(setTaskCreate([])); dispatch(setTaskCreate([]));
dispatch(setMemberChoose([])); dispatch(setMemberChoose([]));
router.back(); router.back();
@@ -100,8 +115,16 @@ export default function CreateProject() {
validationForm('task', ''); validationForm('task', '');
}, [taskCreate]); }, [taskCreate]);
useEffect(() => {
if (entityUser.role != "supadmin" && entityUser.role != "developer") {
validationForm('group', userLogin.idGroup, userLogin.group);
}
}, []);
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()
@@ -127,6 +150,9 @@ export default function CreateProject() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoading(false)
} }
} }
@@ -167,28 +193,46 @@ export default function CreateProject() {
<SafeAreaView> <SafeAreaView>
<Stack.Screen <Stack.Screen
options={{ options={{
headerLeft: () => ( // headerLeft: () => (
<ButtonBackHeader // <ButtonBackHeader
onPress={() => { // onPress={() => {
handleBack(); // handleBack();
}} // }}
/> // />
), // ),
headerTitle: "Tambah Kegiatan", headerTitle: "Tambah Kegiatan",
headerTitleAlign: "center", headerTitleAlign: "center",
headerRight: () => ( // headerRight: () => (
<ButtonSaveHeader // <ButtonSaveHeader
disable={disableBtn} // disable={disableBtn || loading}
category="create" // category="create"
onPress={() => { // onPress={() => {
handleCreate() // handleCreate()
}} // }}
// />
// ),
header: () => (
<AppHeader title="Tambah Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => {
handleCreate()
}}
/>
}
/> />
), )
}} }}
/> />
<ScrollView> <ScrollView
<View style={[Styles.p15, Styles.mb100]}> showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
{ {
(entityUser.role == "supadmin" || entityUser.role == "developer") (entityUser.role == "supadmin" || entityUser.role == "developer")
&& &&
@@ -234,16 +278,12 @@ export default function CreateProject() {
onPress={() => { onPress={() => {
if (entityUser.role == "supadmin" || entityUser.role == "developer") { if (entityUser.role == "supadmin" || entityUser.role == "developer") {
if (chooseGroup.val != "") { if (chooseGroup.val != "") {
setSelect(true); router.push(`/project/create/member`);
setValSelect("member");
} else { } else {
Toast.show({ type: 'small', text1: "Pilih Lembaga Desa terlebih dahulu", }) Toast.show({ type: 'small', text1: "Pilih Lembaga Desa terlebih dahulu", })
} }
} else { } else {
validationForm('group', userLogin.idGroup, userLogin.group); router.push(`/project/create/member`);
setValChoose(userLogin.idGroup)
setSelect(true);
setValSelect("member");
} }
}} }}
error={error.member} error={error.member}
@@ -287,7 +327,7 @@ export default function CreateProject() {
borderType="bottom" borderType="bottom"
icon={ icon={
<ImageUser <ImageUser
src={`https://wibu-storage.wibudev.com/api/files/${item.img}`} src={`${ConstEnv.url_storage}/files/${item.img}`}
size="sm" size="sm"
/> />
} }

View File

@@ -0,0 +1,165 @@
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
import InputSearch from "@/components/inputSearch";
import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiGetUser } from "@/lib/api";
import { setMemberChoose } from "@/lib/memberChoose";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import React, { useEffect, useState } from "react";
import { Pressable, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
type Props = {
idUser: string,
name: string,
img: string
}
export default function AddMemberCreateProject() {
const dispatch = useDispatch()
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props[]>([])
const [selectMember, setSelectMember] = useState<any[]>([])
const [search, setSearch] = useState('')
const entitiesMember = useSelector((state: any) => state.memberChoose)
const entitiesGroup = useSelector((state: any) => state.groupChoose)
const entityUser = useSelector((state: any) => state.user)
const userLogin = useSelector((state: any) => state.entities)
async function handleLoadMember() {
const hasil = await decryptToken(String(token?.current))
let groupFix = userLogin.idGroup
if (entityUser.role == 'supadmin' || entityUser.role == 'developer') {
groupFix = entitiesGroup
}
const responMemberDivision = await apiGetUser({ user: hasil, active: "true", search: search, group: groupFix })
setData(responMemberDivision.data.filter((i: any) => i.idUserRole != 'supadmin'))
if (entitiesMember.length > 0) {
setSelectMember(entitiesMember)
}
}
useEffect(() => {
handleLoadMember()
}, [search]);
function onChoose(val: string, label: string, img?: string) {
if (selectMember.some((i: any) => i.idUser == val)) {
setSelectMember(selectMember.filter((i: any) => i.idUser != val))
} else {
setSelectMember([...selectMember, { idUser: val, name: label, img }])
}
}
async function handleAddMember() {
try {
dispatch(setMemberChoose(selectMember))
router.back()
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', })
}
}
return (
<>
<Stack.Screen
options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Anggota',
headerTitleAlign: 'center',
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={selectMember.length > 0 ? false : true}
// onPress={() => {
// 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, { flex: 1 }]}>
<InputSearch onChange={(val) => setSearch(val)} value={search} />
{
selectMember.length > 0
?
<View>
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
{
selectMember.map((item: any, index: any) => (
<ImageWithLabel
key={index}
label={item.name}
src={`${ConstEnv.url_storage}/files/${item.img}`}
onClick={() => onChoose(item.idUser, item.name, item.img)}
/>
))
}
</ScrollView>
</View>
:
<Text style={[Styles.textDefault, Styles.cGray, Styles.pv05, { textAlign: 'center' }]}>Tidak ada member yang dipilih</Text>
}
<ScrollView
showsVerticalScrollIndicator={false}
>
{
data.length > 0 ?
data.map((item: any, index: any) => {
return (
<Pressable
key={index}
style={[Styles.itemSelectModal]}
onPress={() => {
onChoose(item.id, item.name, item.img)
}}
>
<View style={[Styles.rowItemsCenter]}>
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
<View style={[Styles.ml10]}>
<Text style={[Styles.textDefault]}>{item.name}</Text>
</View>
</View>
{
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={'black'} />
}
</Pressable>
)
}
)
:
<Text style={[Styles.textDefault, { textAlign: 'center' }]}>Tidak ada data</Text>
}
</ScrollView>
</View>
</>
)
}

Some files were not shown because too many files have changed in this diff Show More