Compare commits

..

56 Commits

Author SHA1 Message Date
54a12669e9 Merge pull request 'join' (#19) from join into v1
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/19
2026-02-09 11:18:02 +08:00
e254cf8ed2 Merge pull request 'upd: panduan' (#18) from amalia/06-feb-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/18
2026-02-06 17:48:37 +08:00
ddfee00410 upd: panduan 2026-02-06 17:47:22 +08:00
1aa51adab0 Merge pull request 'amalia/04-feb-26' (#17) from amalia/04-feb-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/17
2026-02-04 17:32:49 +08:00
ebe8380829 upd: panduan penggunaan by QWEN AI 2026-02-04 13:58:15 +08:00
c421d267b9 upd: warna select pada filter 2026-02-04 11:41:18 +08:00
bbacd40ae9 upd: view file
Deskripsi:
- view file pada pengumuman, diskusi divisi dan diskusi umum

No Issues
2026-02-04 11:37:57 +08:00
9bab420f91 Merge pull request 'amalia/03-feb-26' (#16) from amalia/03-feb-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/16
2026-02-03 17:37:07 +08:00
7d8b72fdfa upd: modal view pengumuman
Deskripsi:
- modal view image pada detail pengumuman

NO Issues
2026-02-03 17:34:14 +08:00
e9c11a889d fix: tampilan pengumuman
Deskripsi:
- jarak bawah pada detail pengumuman

No Issues
2026-02-03 16:46:06 +08:00
10b74ccde9 Merge pull request 'revisi: tahun' (#15) from amalia/03-feb-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/15
2026-02-03 12:26:12 +08:00
73c6a19880 revisi: tahun
Deskripsi:
- pengaplikasian api filter tahun pada fitur tugas divisi
No Issues
2026-02-03 12:21:34 +08:00
225ed63027 Merge pull request 'rev: filter tahun' (#14) from amalia/02-feb-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/14
2026-02-02 17:43:21 +08:00
f34df12b2f rev: filter tahun
Deskripsi:
- tampilan modal filter
- tampilan filter disemua fitur yg ada filter nya
- pengaplikasian api

No Issues
2026-02-02 17:29:24 +08:00
a24a698f86 Merge pull request 'amalia/29-jan-26' (#13) from amalia/29-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/13
2026-01-29 17:23:13 +08:00
6b55433c02 upd: header 2026-01-29 17:21:50 +08:00
13cf7ef9c5 upd: header
Deskripsi:
- update padding pada header

No Issues
2026-01-29 14:15:17 +08:00
febb56f6e9 fix: document division
Deskripsi:
- update menu bottom pada saat select file atau dokumen

No Issues
2026-01-29 11:57:58 +08:00
8da7c598a7 Merge pull request 'fix : button header' (#12) from amalia/28-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/12
2026-01-28 17:43:54 +08:00
6cc5d07017 fix : button header
Deskripsi:
- semua udh custom button header untuk ios 26

NO Issues
2026-01-28 17:42:37 +08:00
7a589e11e4 Merge pull request 'upd: custom header' (#11) from amalia/27-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/11
2026-01-27 17:41:11 +08:00
c230e0b18b upd: custom header
Deskripsi:
- update custom button header

- yg blm : fitur divisi dan yg ada di divisi

No Issues
2026-01-27 17:39:54 +08:00
2433cf4bc9 Merge pull request 'amalia/23-jan-26' (#10) from amalia/23-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/10
2026-01-23 17:33:31 +08:00
013589b9f7 upd: modal img
Deskripsi:
- view foto profile
- view foto detail member
- view image banner

No Issues
2026-01-23 17:18:08 +08:00
b99476a593 upd: tampilan detail pengumuman
Deskripisi :
- align item pada judul pengumuman

NO Issues
2026-01-23 14:13:07 +08:00
e1b2cd3790 fix input nomer
Deskripsi:
- pada ios item left input terlalu keatas

No Issues
2026-01-23 12:13:42 +08:00
03d0836111 Merge pull request 'amalia/19-jan-26' (#9) from amalia/19-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/9
2026-01-19 17:27:16 +08:00
588df062f1 upd: loading overlay
Deskripsi:
- update loading saat aksi tambah dan edit pada fitur pengumuman, diskusi umum dan diskusi divisi

No Issues
2026-01-19 16:36:37 +08:00
e61fb83bfd upd: revisi diskusi divisi
Deskripsi:
- attachment file pada tambah diskusi divisi
- attachment file pada edit diskus divisi
- attachment file pada detail diskusi divisi

No Issues
2026-01-19 15:07:14 +08:00
e68f8957ad Merge pull request 'upd: diskusi umum' (#8) from amalia/17-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/8
2026-01-19 10:24:06 +08:00
eee3691aca upd: diskusi umum
Deskripsi:
- detail open file pada halaman detaul diskusi umum
- upload dan hapus file pada halaman edit diskusi umum

- refresh halaman detail diskusi ummum

No Issues
2026-01-17 11:19:24 +08:00
d9508ba978 Merge pull request 'upd: diskusi umum' (#7) from amalia/15-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/7
2026-01-15 17:36:18 +08:00
6f3514c80c upd: diskusi umum
deskripsi :
- integrasi api tambah data diskusi umum
- integrasi api detail data diskusi umum file

No Issues
2026-01-15 17:33:05 +08:00
bd31a2f993 Merge pull request 'req: pengumuman' (#6) from amalia/14-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/6
2026-01-14 17:45:05 +08:00
3fbb230302 req: pengumuman
Deskripsi:
- pengaplikasian api tambah, detail dan edit pengumuman

No Issues
2026-01-14 15:01:32 +08:00
57a24b699a Merge pull request 'tambahan:' (#5) from amalia/13-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/5
2026-01-13 17:14:05 +08:00
1a4ccc4f66 tambahan:
- Deskripsi:
- tampilan attach file pada halaman tambah diskusi umum
- tampilan attach file pada halaman update diskusi umum
- tampilan attach file pada halaman detail diskusi umum
- tampilan attach file pada halaman tambah diskusi divisi
- tampilan attach file pada halaman update diskusi divisi
- tampilan attach file pada halaman detail diskusi divisi

No Issues
2026-01-13 14:03:05 +08:00
2ba3675b3a Merge pull request 'amalia/12-jan-26' (#4) from amalia/12-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/4
2026-01-12 17:49:27 +08:00
ca3d0d9d19 upd: req client
Deskripsi:

 - tampilan tambah file saat tambah data pengumuman
- tampilan list file pada halaman detail pengumuman
- tampilan tambah file saat edit data pengumuman

No Issues
2026-01-12 15:52:48 +08:00
42f245f37c fix: kode otp
Deskripsi:
- fix ganti wa jenna untuk mengirim kode otp

No Issues
2026-01-12 14:12:24 +08:00
6acfcf9a54 Merge pull request 'amalia/12-nov-25' (#3) from amalia/12-nov-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/3
2025-11-12 17:51:57 +08:00
2b7022ee3d upd: version app 2025-11-12 17:51:01 +08:00
ec24cb70cb fix: tampilan dtail dvisi
Deskripsi:
- jarak mb pada list tugas hari ini di halaman detail divisi

No Issues
2025-11-12 16:29:37 +08:00
5451dc092f Merge pull request 'upd: tolak nama divisi yg sama' (#2) from amalia/27-okt-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/2
2025-10-27 17:45:59 +08:00
74ba2641ca upd: tolak nama divisi yg sama
Deskripsi:
- akan ditolak jika input nama divisi udah ada
- alert konfirmasi custom 1 tombol

No Issues
2025-10-27 11:31:47 +08:00
fb8a140a31 Merge pull request 'amalia/23-okt-25' (#1) from amalia/23-okt-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/1
2025-10-27 11:19:43 +08:00
f341bd01c6 update 2025-10-27 10:48:50 +08:00
c458504da2 upd: tampilan
Deskripsi:
- list diskusi pda dashboard dan detail divisi home
- ukuran banner dan stretch
- detail anggota > nama
- detail diskusi umum > judul kepotong
- detail pengumuman > deskripsi tidak keliatan warna putih

No Issues
2025-10-23 13:17:42 +08:00
a0d1b90662 upd: version update 2025-10-22 17:29:17 +08:00
bd9fe8676d upd : package ga terpakai 2025-10-22 17:26:33 +08:00
56856a96fd upd version android 2025-10-16 17:42:58 +08:00
049f6c63cc fix: detail pengumuman jika data kosong 2025-10-16 15:20:53 +08:00
2ea92d3e9a upd: version app ios dan android 2025-10-15 16:52:32 +08:00
04f7bda40f upd: tombol style
Deskripsi:
- bgcolor saat long press

No Issues
2025-10-15 15:41:42 +08:00
863871aae5 Merge pull request 'upd: aksi komentar diskusi' (#51) from amalia/14-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#51
2025-10-14 17:35:47 +08:00
0ce1f270ef upd: aksi komentar diskusi
Deskripsi:
- api hapus komentar diskusi umum dan diskusi divisi
- api edit komentar diskusi umum dan diskusi divisi
- layout edit komentar diskusi umum dan diskusi divisi
- pengaplikasian edit komentar pada diskusi umum dan diskusi divisi
- pengaplikasian hapus komentar pada diskusi umum dan diskudi divisi

No Issues
2025-10-14 14:58:54 +08:00
95 changed files with 4179 additions and 982 deletions

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
npm install
```
2. Start the app
```bash
npx expo start
3. Konfigurasi environment variables
Buat file `.env` di root direktori dan tambahkan variabel berikut:
```
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/)
- [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
## Struktur Proyek
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
├── app/ # File-file halaman utama
├── components/ # Komponen reusable
│ ├── announcement/ # Komponen pengumuman
│ ├── auth/ # Komponen otentikasi
│ ├── discussion/ # Komponen forum diskusi
│ ├── document/ # Komponen dokumentasi
│ ├── 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).
- [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.
Untuk menjalankan aplikasi di masing-masing platform:
## 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.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
### Build Production
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

@@ -4,7 +4,7 @@ export default {
expo: {
name: "Desa+",
slug: "mobile-darmasaba",
version: "2.0.2", // Versi aplikasi (App Store)
version: "2.0.5", // Versi aplikasi (App Store)
jsEngine: "jsc",
orientation: "portrait",
icon: "./assets/images/logo-icon-small.png",
@@ -14,7 +14,7 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "mobiledarmasaba.app",
buildNumber: "4",
buildNumber: "7",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
@@ -23,7 +23,7 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 11,
versionCode: 15,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"
@@ -77,7 +77,8 @@ export default {
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
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 ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import HeaderDiscussionGeneral from "@/components/discussion_general/headerDiscussionGeneral";
import HeaderRightDivisionList from "@/components/division/headerDivisionList";
import HeaderRightGroupList from "@/components/group/headerGroupList";
@@ -8,7 +8,6 @@ import HeaderRightPositionList from "@/components/position/headerRightPositionLi
import HeaderRightProjectList from "@/components/project/headerProjectList";
import Text from "@/components/Text";
import ToastCustom from "@/components/toastCustom";
import { Headers } from "@/constants/Headers";
import { apiReadOneNotification } from "@/lib/api";
import { pushToPage } from "@/lib/pushToPage";
import store from "@/lib/store";
@@ -19,7 +18,6 @@ import { getMessaging, onMessage } from "@react-native-firebase/messaging";
import { Redirect, router, Stack, usePathname } from "expo-router";
import { StatusBar } from 'expo-status-bar';
import { useEffect } from "react";
import { Platform } from "react-native";
import { Easing, Notifier } from 'react-native-notifier';
import { Provider } from "react-redux";
@@ -92,60 +90,121 @@ export default function RootLayout() {
return (
<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="feature" options={{ title: 'Fitur' }} />
<Stack.Screen name="search" options={{ title: 'Pencarian' }} />
<Stack.Screen name="notification" options={{
title: 'Notifikasi',
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
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="member/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Anggota',
headerTitleAlign: 'center',
headerRight: () => <HeaderMemberList />
// headerRight: () => <HeaderMemberList />
header: () => (
<AppHeader title="Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderMemberList />}
/>
)
}} />
<Stack.Screen name="discussion/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Diskusi Umum',
headerTitleAlign: 'center',
headerRight: () => <HeaderDiscussionGeneral />
// headerRight: () => <HeaderDiscussionGeneral />
header: () => (
<AppHeader
title="Diskusi Umum"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderDiscussionGeneral />}
/>
)
}} />
<Stack.Screen name="project/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Kegiatan',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightProjectList />
// headerRight: () => <HeaderRightProjectList />
header: () => (
<AppHeader title="Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderRightProjectList />}
/>
)
}} />
<Stack.Screen name="division/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Divisi',
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="group/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Lembaga Desa',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightGroupList />
// headerRight: () => <HeaderRightGroupList />
header: () => (
<AppHeader title="Lembaga Desa"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderRightGroupList />}
/>
)
}} />
<Stack.Screen name="position/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Jabatan',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightPositionList />
// headerRight: () => <HeaderRightPositionList />
header: () => (
<AppHeader title="Jabatan"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderRightPositionList />}
/>
)
}} />
<Stack.Screen name="announcement/index"
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightAnnouncementList />
// headerRight: () => <HeaderRightAnnouncementList />
header: () => (
<AppHeader title="Pengumuman"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderRightAnnouncementList />}
/>
)
}}
/>
</Stack>

View File

@@ -1,44 +1,95 @@
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 Text from '@/components/Text';
import { ConstEnv } from "@/constants/ConstEnv";
import { isImageFile } from "@/constants/FileExtensions";
import Styles from "@/constants/Styles";
import { apiGetAnnouncementOne } from "@/lib/api";
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 * as Sharing from 'expo-sharing';
import React, { useEffect, useState } from "react";
import { Dimensions, RefreshControl, 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 Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
id: string
title: string
desc: string
// Define TypeScript interfaces for better type safety
interface AnnouncementData {
id: 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() {
const { id } = useLocalSearchParams<{ id: string }>();
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props>({ id: '', title: '', desc: '' })
const [dataMember, setDataMember] = useState<any>({})
const [data, setData] = useState<AnnouncementData>({ id: '', title: '', desc: '' })
const [dataMember, setDataMember] = useState<Record<string, MemberData[]>>({})
const [dataFile, setDataFile] = useState<FileData[]>([])
const update = useSelector((state: any) => state.announcementUpdate)
const entityUser = useSelector((state: any) => state.user)
const contentWidth = Dimensions.get('window').width
const [loading, setLoading] = useState(true)
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index)
const [refreshing, setRefreshing] = useState(false)
const [loadingOpen, setLoadingOpen] = useState(false)
const [preview, setPreview] = useState(false)
const [chooseFile, setChooseFile] = useState<FileData>()
/**
* 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) {
try {
setLoading(loading)
const hasil = await decryptToken(String(token?.current))
const response = await apiGetAnnouncementOne({ id: id, user: hasil })
setData(response.data)
setDataMember(response.member)
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
if (response.success) {
setData(response.data)
setDataMember(response.member)
setDataFile(response.file)
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally {
setLoading(false)
}
@@ -52,26 +103,92 @@ export default function DetailAnnouncement() {
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) {
const htmlRegex = /<[a-z][\s\S]*>/i;
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 (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman',
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
@@ -84,7 +201,7 @@ export default function DetailAnnouncement() {
/>
}
>
<View style={[Styles.p15]}>
<View style={[Styles.p15, Styles.mb50]}>
<View style={[Styles.wrapPaper]}>
{
loading ?
@@ -102,8 +219,8 @@ export default function DetailAnnouncement() {
:
<>
<View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefaultSemiBold, Styles.w90]}>{data?.title}</Text>
<MaterialIcons name="campaign" size={25} color="black" style={[Styles.mr05]} />
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
</View>
<View style={[Styles.mt10]}>
{
@@ -111,6 +228,7 @@ export default function DetailAnnouncement() {
<RenderHTML
contentWidth={contentWidth}
source={{ html: data?.desc }}
baseStyle={{ color: 'black' }}
/>
:
<Text>{data?.desc}</Text>
@@ -120,7 +238,34 @@ export default function DetailAnnouncement() {
}
</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 ?
arrSkeleton.map((item, index) => {
@@ -155,6 +300,45 @@ export default function DetailAnnouncement() {
</View>
</View>
</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>
)
}

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 ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
import MenuItemRow from "@/components/menuItemRow";
import ModalSelectMultiple from "@/components/modalSelectMultiple";
import Text from "@/components/Text";
import Styles from "@/constants/Styles";
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
import { apiCreateAnnouncement } from "@/lib/api";
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 { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
@@ -23,6 +28,9 @@ export default function CreateAnnouncement() {
const [modalDivisi, setModalDivisi] = useState(false);
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({
title: "",
desc: "",
@@ -69,9 +77,26 @@ export default function CreateAnnouncement() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateAnnouncement({
data: { ...dataForm, user: hasil, groups: divisionMember },
});
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, groups: divisionMember, ...dataForm }
))
const response = await apiCreateAnnouncement(fd)
// const response = await apiCreateAnnouncement({
// data: { ...dataForm, user: hasil, groups: divisionMember },
// });
if (response.success) {
dispatch(setUpdateAnnouncement(!update))
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
@@ -84,32 +109,70 @@ export default function CreateAnnouncement() {
}
}
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 (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Pengumuman",
headerTitleAlign: "center",
headerRight: () => (
<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();
}}
// headerRight: () => (
// <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();
// }}
// />
// ),
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();
}}
/>
}
/>
),
)
}}
/>
<LoadingOverlay visible={loading} />
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
@@ -134,6 +197,27 @@ export default function CreateAnnouncement() {
onChange={(val) => validationForm("desc", val)}
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
value="Pilih divisi penerima pengumuman"
onPress={() => {
@@ -178,6 +262,16 @@ export default function CreateAnnouncement() {
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>
);
}

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 ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
import MenuItemRow from "@/components/menuItemRow";
import ModalSelectMultiple from "@/components/modalSelectMultiple";
import Text from '@/components/Text';
import Styles from "@/constants/Styles";
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api";
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 { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -33,6 +38,10 @@ export default function EditAnnouncement() {
const [modalDivisi, setModalDivisi] = useState(false);
const [disableBtn, setDisableBtn] = useState(true);
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({
title: "",
@@ -66,6 +75,7 @@ export default function EditAnnouncement() {
arrNew.push(newObject)
})
setDataMember(arrNew);
setDataFile(response.file);
} catch (error) {
console.error(error);
}
@@ -112,9 +122,22 @@ export default function EditAnnouncement() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiEditAnnouncement({
...dataForm, user: hasil, groups: dataMember,
}, 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(
{
...dataForm, user: hasil, groups: dataMember, oldFile: dataFile
}
))
const response = await apiEditAnnouncement(fd, id);
if (response.success) {
dispatch(setUpdateAnnouncement(!update))
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
@@ -127,32 +150,80 @@ export default function EditAnnouncement() {
}
}
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 (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Pengumuman",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
category="update"
onPress={() => {
dataMember.length == 0
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
: handleEdit();
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loading ? true : false}
// category="update"
// onPress={() => {
// dataMember.length == 0
// ? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
// : 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();
}}
/>
}
/>
),
)
}}
/>
<LoadingOverlay visible={loading} />
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
@@ -179,6 +250,38 @@ export default function EditAnnouncement() {
value={dataForm.desc}
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
value="Pilih divisi penerima pengumuman"
onPress={() => {
@@ -223,6 +326,16 @@ export default function EditAnnouncement() {
}}
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>
);
}

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Text from "@/components/Text";
@@ -115,19 +115,32 @@ export default function EditBanner() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Banner",
headerTitleAlign: "center",
headerRight: () => <ButtonSaveHeader
disable={title == "" || error || loading ? true : false}
onPress={() => { handleUpdateEntity() }}
category="update" />,
// headerRight: () => <ButtonSaveHeader
// disable={title == "" || error || loading ? true : false}
// onPress={() => { handleUpdateEntity() }}
// 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 showsVerticalScrollIndicator={false} style={[Styles.h100]}>
@@ -154,7 +167,7 @@ export default function EditBanner() {
>
<Entypo name="image" size={50} color={"#aeaeae"} />
<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
</Text>
</View>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Text from "@/components/Text";
@@ -97,24 +97,40 @@ export default function CreateBanner() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Banner",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={title == "" || selectedImage == undefined || error || loading ? true : false}
category="create"
onPress={() => {
handleCreateEntity();
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={title == "" || selectedImage == undefined || error || loading ? true : false}
// category="create"
// onPress={() => {
// 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 showsVerticalScrollIndicator={false} style={[Styles.h100]}>
@@ -137,7 +153,7 @@ export default function CreateBanner() {
>
<Entypo name="image" size={50} color={"#aeaeae"} />
<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
</Text>
</View>

View File

@@ -1,7 +1,7 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
import HeaderRightBannerList from "@/components/banner/headerBannerList"
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import DrawerBottom from "@/components/drawerBottom"
import MenuItemRow from "@/components/menuItemRow"
import ModalLoading from "@/components/modalLoading"
@@ -18,6 +18,7 @@ import { router, Stack } from "expo-router"
import * as Sharing from 'expo-sharing'
import { useState } from "react"
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 Toast from "react-native-toast-message"
import { useDispatch, useSelector } from "react-redux"
@@ -38,6 +39,7 @@ export default function BannerList() {
const dispatch = useDispatch()
const [refreshing, setRefreshing] = useState(false)
const [loadingOpen, setLoadingOpen] = useState(false)
const [viewImg, setViewImg] = useState(false)
const handleDeleteEntity = async () => {
try {
@@ -106,10 +108,20 @@ export default function BannerList() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Banner',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightBannerList />
// headerRight: () => <HeaderRightBannerList />
header: () => (
<AppHeader
title="Banner"
showBack={true}
onPressLeft={() => router.back()}
right={
<HeaderRightBannerList />
}
/>
)
}}
/>
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
@@ -167,8 +179,14 @@ export default function BannerList() {
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="file-eye" color="black" size={25} />}
title="Lihat / Share"
onPress={() => { openFile() }}
title="Lihat"
onPress={() => {
setModal(false)
setTimeout(() => {
setViewImg(true);
}, 1000);
// openFile()
}}
/>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
@@ -184,6 +202,14 @@ export default function BannerList() {
/>
</View>
</DrawerBottom>
<ImageViewing
images={[{ uri: `${ConstEnv.url_storage}/files/${selectFile?.image}` }]}
imageIndex={0}
visible={viewImg}
onRequestClose={() => setViewImg(false)}
doubleTapToZoomEnabled
/>
</SafeAreaView>
)
}

View File

@@ -1,6 +1,7 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import BorderBottomItem2 from "@/components/borderBottomItem2";
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
@@ -14,15 +15,15 @@ import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
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 { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { ref } from '@react-native-firebase/database';
import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, ScrollView, View } from "react-native";
import React, { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
@@ -46,13 +47,22 @@ type PropsKomentar = {
updatedAt: string
}
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function DetailDiscussionGeneral() {
const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user)
const entities = useSelector((state: any) => state.entities)
const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState<Props>()
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
const [memberDiscussion, setMemberDiscussion] = useState(false)
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
const [komentar, setKomentar] = useState('')
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
const [loading, setLoading] = useState(true)
@@ -63,11 +73,12 @@ export default function DetailDiscussionGeneral() {
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(() => {
const onValueChange = reference.on('value', snapshot => {
@@ -89,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 {
if (cat == "detail") {
setLoading(loading)
@@ -106,6 +117,8 @@ export default function DetailDiscussionGeneral() {
setDataKomentar(response.data)
} else if (cat == 'cek-anggota') {
setMemberDiscussion(response.data)
} else if (cat == 'file') {
setFileDiscussion(response.data)
}
} catch (error) {
@@ -120,12 +133,14 @@ export default function DetailDiscussionGeneral() {
handleLoad('detail', false)
handleLoad('komentar', false)
handleLoad('cek-anggota', false)
handleLoad('file', false)
}, [update]);
useEffect(() => {
handleLoad('detail', true)
handleLoad('komentar', true)
handleLoad('cek-anggota', true)
handleLoad('file', true)
}, []);
async function handleKomentar() {
@@ -138,7 +153,7 @@ export default function DetailDiscussionGeneral() {
setKomentar('')
updateTrigger()
} else {
Toast.show({ type: 'small', text1: 'Gagal menambahkan komentar' })
Toast.show({ type: 'small', text1: response.message })
}
}
} catch (error) {
@@ -148,29 +163,98 @@ export default function DetailDiscussionGeneral() {
}
}
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 (
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Diskusi',
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 }}>
<ScrollView showsVerticalScrollIndicator={false}>
<View style={[Styles.p15, Styles.mb100]}>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => handleRefresh()}
/>
}
>
<View style={[Styles.p15]}>
{
loading ?
<SkeletonContent />
:
<BorderBottomItem
<BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false}
borderType="bottom"
icon={
@@ -178,7 +262,8 @@ export default function DetailDiscussionGeneral() {
<MaterialIcons name="chat" size={25} color={'#384288'} />
</View>
}
title={data?.title + " " + data?.createdAt}
title={data?.title}
titleShowAll={true}
subtitle={
!data?.isActive ?
<LabelStatus category='warning' text='ARSIP' size="small" />
@@ -213,6 +298,7 @@ export default function DetailDiscussionGeneral() {
<BorderBottomItem
key={i}
borderType="bottom"
colorPress
icon={
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
}
@@ -231,7 +317,7 @@ export default function DetailDiscussionGeneral() {
})
}}
onLongPress={() => {
handleMenuKomentar(item.id, item.comment)
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/>
)
@@ -248,40 +334,78 @@ export default function DetailDiscussionGeneral() {
Styles.contentItemCenter,
Styles.w100,
{ backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]}>
{
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
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} />
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</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"
</View>
<InputForm
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
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>
}
</Text>
</View>
/>
</>
:
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>
@@ -293,9 +417,7 @@ export default function DetailDiscussionGeneral() {
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit"
onPress={() => {
setVisible(false)
}}
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
@@ -304,7 +426,9 @@ export default function DetailDiscussionGeneral() {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => { setVisible(false) }
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -95,16 +95,32 @@ export default function AddMemberDiscussionDetail() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Anggota Diskusi',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// handleAddMember()
// }}
// />
// )
header: () => (
<AppHeader
title="Tambah Anggota Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
/>
}
/>
)
}}

View File

@@ -1,9 +1,12 @@
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
import MenuItemRow from "@/components/menuItemRow";
import ModalSelect from "@/components/modalSelect";
import SelectForm from "@/components/selectForm";
import Text from '@/components/Text';
@@ -13,6 +16,8 @@ import { apiCreateDiscussionGeneral } from "@/lib/api";
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
import { setMemberChoose } from "@/lib/memberChoose";
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 { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -33,6 +38,9 @@ export default function CreateDiscussionGeneral() {
const entitiesMember = useSelector((state: any) => state.memberChoose)
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({
idGroup: "",
title: "",
@@ -95,13 +103,49 @@ export default function CreateDiscussionGeneral() {
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() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateDiscussionGeneral({
data: { ...dataForm, user: hasil, member: entitiesMember },
})
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(
{ ...dataForm, user: hasil, member: entitiesMember }
))
const response = await apiCreateDiscussionGeneral(fd)
// const response = await apiCreateDiscussionGeneral({
// data: { ...dataForm, user: hasil, member: entitiesMember },
// })
if (response.success) {
dispatch(setMemberChoose([]))
dispatch(setUpdateDiscussionGeneralDetail(!update))
@@ -122,26 +166,45 @@ export default function CreateDiscussionGeneral() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => { handleBack() }}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => { handleBack() }}
// />
// ),
headerTitle: "Tambah Diskusi",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={disableBtn || loading ? true : false}
onPress={() => {
entitiesMember.length == 0
? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', })
: handleCreate()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={disableBtn || loading ? true : false}
// onPress={() => {
// entitiesMember.length == 0
// ? Toast.show({ type: 'small', text1: 'Anda belum memilih anggota', })
// : 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()
}}
/>
}
/>
),
)
}}
/>
<LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15, Styles.mb100]}>
{
@@ -181,6 +244,26 @@ export default function CreateDiscussionGeneral() {
onChange={(val) => { validationForm("desc", val) }}
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
value="Pilih Anggota"
onPress={() => {
@@ -240,6 +323,16 @@ export default function CreateDiscussionGeneral() {
idParent={valSelect == "member" ? chooseGroup.val : ""}
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>
);
}

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 ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import LoadingOverlay from "@/components/loadingOverlay";
import MenuItemRow from "@/components/menuItemRow";
import Styles from "@/constants/Styles";
import { apiEditDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
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 { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -17,6 +25,10 @@ export default function EditDiscussionGeneral() {
const [disableBtn, setDisableBtn] = useState(false)
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 [dataForm, setDataForm] = useState({
title: "",
@@ -35,9 +47,17 @@ export default function EditDiscussionGeneral() {
user: hasil,
cat: "detail",
});
const responseFile = await apiGetDiscussionGeneralOne({
id: id,
user: hasil,
cat: "file",
});
if (response.success) {
setDataForm(response.data);
}
if (responseFile.success) {
setDataFile(responseFile.data);
}
} catch (error) {
console.error(error);
}
@@ -78,12 +98,56 @@ export default function EditDiscussionGeneral() {
checkForm()
}, [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() {
try {
setLoading(true)
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) {
dispatch(setUpdateDiscussionGeneralDetail(!update))
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
@@ -101,24 +165,39 @@ export default function EditDiscussionGeneral() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Diskusi",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
category="update"
onPress={() => { handleEdit() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loading ? true : false}
// category="update"
// onPress={() => { handleEdit() }}
// />
// ),
header: () => (
<AppHeader
title="Edit Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
category="update"
onPress={() => { handleEdit() }}
/>
}
/>
),
)
}}
/>
<LoadingOverlay visible={loading} />
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100]}>
<View style={[Styles.p15]}>
<InputForm
@@ -142,8 +221,50 @@ export default function EditDiscussionGeneral() {
onChange={(val) => validationForm("desc", val)}
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>
</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>
);
}

View File

@@ -121,8 +121,9 @@ export default function Discussion() {
<InputSearch onChange={setSearch} />
{
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
<View style={[Styles.mv05]}>
<Text>Filter : {nameGroup}</Text>
<View style={[Styles.mv05, Styles.rowOnly]}>
<Text>Filter :</Text>
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
</View>
}
</View>

View File

@@ -1,6 +1,6 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import MenuItemRow from "@/components/menuItemRow";
@@ -74,9 +74,16 @@ export default function MemberDiscussionDetail() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Anggota Diskusi',
headerTitleAlign: 'center',
header: () => (
<AppHeader
title="Anggota Diskusi"
showBack={true}
onPressLeft={() => router.back()}
/>
)
}}
/>
<ScrollView>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader"
import AppHeader from "@/components/AppHeader"
import HeaderRightDiscussionList from "@/components/discussion/headerDiscussionList"
import HeaderRightTaskList from "@/components/task/headerTaskList"
import { Headers } from "@/constants/Headers"
@@ -9,22 +9,49 @@ export default function RootLayout() {
<>
<Stack screenOptions={Headers.shadow}>
<Stack.Screen name="task/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Tugas Divisi',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightTaskList />
// headerRight: () => <HeaderRightTaskList />
header: () => (
<AppHeader
title="Tugas Divisi"
showBack={true}
onPressLeft={() => router.back()}
right={
<HeaderRightTaskList />
}
/>
)
}} />
<Stack.Screen name="discussion/index" options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Diskusi Divisi',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightDiscussionList />
// headerRight: () => <HeaderRightDiscussionList />
header: () => (
<AppHeader
title="Diskusi Divisi"
showBack={true}
onPressLeft={() => router.back()}
right={
<HeaderRightDiscussionList />
}
/>
)
}} />
<Stack.Screen name="calendar/history"
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Riwayat Acara',
headerTitleAlign: 'center',
header: () => (
<AppHeader
title="Riwayat Acara"
showBack={true}
onPressLeft={() => router.back()}
/>
)
}}
/>
</Stack>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -103,16 +103,32 @@ export default function AddMemberCalendarEvent() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader"
import AppHeader from "@/components/AppHeader"
import ButtonSaveHeader from "@/components/buttonSaveHeader"
import { InputDate } from "@/components/inputDate"
import { InputForm } from "@/components/inputForm"
@@ -165,17 +165,33 @@ export default function EditEventCalendar() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Edit Acara',
headerTitleAlign: 'center',
headerRight: () =>
<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()
}}
// headerRight: () =>
// <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()
// }}
// />
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()
}}
/>
}
/>
)
}}
/>
<KeyboardAvoidingView

View File

@@ -1,4 +1,5 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
@@ -154,10 +155,20 @@ export default function DetailEventCalendar() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Detail Acara',
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

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -93,14 +93,28 @@ export default function CreateCalendarAddMember() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => { handleAddMember() }}
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={selectMember.length == 0 || loading ? true : false}
// 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() }}
/>
}
/>
)
}}

View File

@@ -1,3 +1,4 @@
import AppHeader from "@/components/AppHeader";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonNextHeader from "@/components/buttonNextHeader";
import { InputDate } from "@/components/inputDate";
@@ -7,6 +8,7 @@ import SelectForm from "@/components/selectForm";
import Styles from "@/constants/Styles";
import { setFormCreateCalendar } from "@/lib/calendarCreate";
import { stringToDateTime } from "@/lib/fun_stringToDate";
import { useHeaderHeight } from '@react-navigation/elements';
import { Stack, router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import {
@@ -17,7 +19,6 @@ import {
View
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { useHeaderHeight } from '@react-navigation/elements';
export default function CalendarDivisionCreate() {
const { id } = useLocalSearchParams<{ id: string }>()
@@ -128,28 +129,44 @@ export default function CalendarDivisionCreate() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Acara",
headerTitleAlign: "center",
headerRight: () => (
<ButtonNextHeader
onPress={() => { handleSetData() }}
disable={Object.values(error).some((val) => val == true) || data.title == "" || data.dateStart == "" || data.timeStart == "" || data.timeEnd == "" || data.repeatEventType == ""}
// headerRight: () => (
// <ButtonNextHeader
// onPress={() => { handleSetData() }}
// 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
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight}
>
<ScrollView>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
<InputForm
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 ItemDateCalendar from "@/components/calendar/itemDateCalendar";
import EventItem from "@/components/eventItem";
@@ -128,16 +128,24 @@ export default function CalendarDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Kalender",
headerTitleAlign: "center",
headerRight: () => <HeaderRightCalendarList />,
// headerRight: () => <HeaderRightCalendarList />,
header: () => (
<AppHeader
title="Kalender"
showBack={true}
onPressLeft={() => router.back()}
right={<HeaderRightCalendarList />}
/>
)
}}
/>
<ScrollView

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 ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
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 { apiEditDiscussion, apiGetDiscussionOne } from "@/lib/api";
import { setUpdateDiscussion } from "@/lib/discussionUpdate";
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 { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -18,6 +26,11 @@ export default function DiscussionDivisionEdit() {
const update = useSelector((state: any) => state.discussionUpdate);
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() {
try {
@@ -27,6 +40,12 @@ export default function DiscussionDivisionEdit() {
user: hasil,
cat: "data",
});
const response2 = await apiGetDiscussionOne({
id: detail,
user: hasil,
cat: "file",
});
setDataFile(response2.data);
setData(response.data.desc);
} catch (error) {
console.error(error);
@@ -41,15 +60,31 @@ export default function DiscussionDivisionEdit() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current));
const response = await apiEditDiscussion({
data: { user: hasil, desc: data },
id: detail,
});
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: data, oldFile: dataFile
}
))
const response = await apiEditDiscussion(fd, detail);
// const response = await apiEditDiscussion({
// data: { user: hasil, desc: data },
// id: detail,
// });
if (response.success) {
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
router.back();
}else{
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
@@ -60,31 +95,79 @@ export default function DiscussionDivisionEdit() {
}
}
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 (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Diskusi",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={data == "" || loading}
category="update"
onPress={() => {
handleUpdate();
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={data == "" || loading}
// category="update"
// onPress={() => {
// 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]}>
<InputForm
label="Diskusi"
@@ -95,8 +178,53 @@ export default function DiscussionDivisionEdit() {
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>
</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>
);
}

View File

@@ -1,9 +1,13 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import BorderBottomItem2 from "@/components/borderBottomItem2";
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
import Skeleton from "@/components/skeleton";
import SkeletonContent from "@/components/skeletonContent";
import Text from "@/components/Text";
@@ -11,18 +15,21 @@ import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles";
import {
apiDeleteDiscussionCommentar,
apiEditDiscussionCommentar,
apiGetDiscussionOne,
apiGetDivisionOneFeature,
apiSendDiscussionCommentar,
} from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { ref } from "@react-native-firebase/database";
import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
@@ -44,12 +51,23 @@ type PropsComment = {
createdAt: string;
username: string;
img: string;
idUser: string;
isEdited: boolean;
updatedAt: string;
};
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function DiscussionDetail() {
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
const [data, setData] = useState<Props>();
const [dataComment, setDataComment] = useState<PropsComment[]>([]);
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
const { token, decryptToken } = useAuthSession();
const [komentar, setKomentar] = useState("");
const [loadingSend, setLoadingSend] = useState(false);
@@ -65,6 +83,14 @@ export default function DiscussionDetail() {
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(() => {
@@ -96,7 +122,15 @@ export default function DiscussionDetail() {
user: hasil,
cat: "data",
});
const responseFile = await apiGetDiscussionOne({
id: detail,
user: hasil,
cat: "file",
});
setData(response.data);
setFileDiscussion(responseFile.data)
setIsCreator(response.data.createdBy == hasil);
} catch (error) {
console.error(error);
@@ -172,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 () => {
setRefreshing(true)
@@ -185,23 +272,38 @@ export default function DiscussionDetail() {
<>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Diskusi",
headerTitleAlign: "center",
headerRight: () =>
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
<HeaderRightDiscussionDetail
id={detail}
status={data?.status}
isActive={data?.isActive}
/> : (<></>)
,
// headerRight: () =>
// (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
// <HeaderRightDiscussionDetail
// id={detail}
// status={data?.status}
// 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 }}>
@@ -218,7 +320,8 @@ export default function DiscussionDetail() {
loading ?
<SkeletonContent />
:
<BorderBottomItem
<BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false}
borderType="bottom"
icon={
@@ -268,6 +371,7 @@ export default function DiscussionDetail() {
<BorderBottomItem
key={index}
borderType="bottom"
colorPress
icon={
<ImageUser
src={`${ConstEnv.url_storage}/files/${item.img}`}
@@ -277,6 +381,7 @@ export default function DiscussionDetail() {
title={item.username}
rightTopInfo={item.createdAt}
desc={item.comment}
rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => {
setDetailMore((prev: any) => {
@@ -287,6 +392,9 @@ export default function DiscussionDetail() {
}
})
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/>
))
}
@@ -303,73 +411,145 @@ export default function DiscussionDetail() {
Styles.contentItemCenter,
Styles.w100,
{ backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]}
>
{
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") ||
isMemberDivision)
?
<InputForm
// disable={
// data?.status == 2 ||
// data?.isActive == false ||
// ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
// }
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,
]
}
/>
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</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"
</View>
<InputForm
bg="white"
type="default"
round
multiline
placeholder="Kirim Komentar"
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
value={selectKomentar.comment}
itemRight={
<Pressable
onPress={() => {
selectKomentar.comment != "" &&
!regexOnlySpacesOrEnter.test(selectKomentar.comment) &&
!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") &&
handleEditKomentar();
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons
name="send"
size={25}
style={
[selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
}
</Text>
</View>
/>
</>
:
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>
</KeyboardAvoidingView>
</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,16 +1,25 @@
import ButtonBackHeader from "@/components/buttonBackHeader"
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonSaveHeader from "@/components/buttonSaveHeader"
import ButtonSelect from "@/components/buttonSelect"
import DrawerBottom from "@/components/drawerBottom"
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 { apiCreateDiscussion } from "@/lib/api"
import { setUpdateDiscussion } from "@/lib/discussionUpdate"
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 { 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 CreateDiscussionDivision() {
const { id } = useLocalSearchParams<{ id: string }>()
const [desc, setDesc] = useState('')
@@ -18,12 +27,51 @@ export default function CreateDiscussionDivision() {
const update = useSelector((state: any) => state.discussionUpdate)
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() {
try {
setLoading(true)
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) {
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
@@ -43,18 +91,34 @@ export default function CreateDiscussionDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Diskusi',
headerTitleAlign: 'center',
headerRight: () => <ButtonSaveHeader
disable={desc == "" || loading}
category="create"
onPress={() => {
handleCreate()
}} />
// headerRight: () => <ButtonSaveHeader
// disable={desc == "" || loading}
// category="create"
// onPress={() => {
// 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]}>
<InputForm
label="Diskusi"
@@ -64,8 +128,38 @@ export default function CreateDiscussionDivision() {
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>
</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>
)
}

View File

@@ -1,5 +1,5 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import { ButtonHeader } from "@/components/buttonHeader";
import HeaderRightDocument from "@/components/document/headerDocument";
import ItemFile from "@/components/document/itemFile";
@@ -334,47 +334,81 @@ export default function DocumentDivision() {
}, [path]);
return (
<SafeAreaView>
<SafeAreaView style={{ flex: 1 }}>
<Stack.Screen
options={{
headerLeft: () =>
selectedFiles.length > 0 || dariSelectAll ? (
<ButtonHeader
item={<MaterialIcons name="close" size={20} color="white" />}
onPress={() => {
handleBatal();
}}
/>
) : (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () =>
// selectedFiles.length > 0 || dariSelectAll ? (
// <ButtonHeader
// item={<MaterialIcons name="close" size={20} color="white" />}
// onPress={() => {
// handleBatal();
// }}
// />
// ) : (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle:
selectedFiles.length > 0 || dariSelectAll
? `${selectedFiles.length} item terpilih`
: "Dokumen Divisi",
headerTitleAlign: "center",
headerRight: () =>
selectedFiles.length > 0 || dariSelectAll ? (
<ButtonHeader
item={
<MaterialIcons name="checklist-rtl" size={20} color="white" />
}
onPress={() => {
handleSelectAll();
}}
/>
) : (
<HeaderRightDocument path={path} isMember={isMemberDivision} />
),
// headerRight: () =>
// selectedFiles.length > 0 || dariSelectAll ? (
// <ButtonHeader
// item={
// <MaterialIcons name="checklist-rtl" size={20} color="white" />
// }
// onPress={() => {
// handleSelectAll();
// }}
// />
// ) : (
// <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} />
<ScrollView
style={[Styles.h100]}
refreshControl={
<RefreshControl
refreshing={refreshing}

View File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
@@ -130,22 +130,36 @@ export default function TaskDivisionAddFile() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah File",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={fileForm.length == 0 || loading ? true : false}
onPress={() => { handleAddFile() }}
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={fileForm.length == 0 || loading ? true : false}
// 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>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -97,16 +97,32 @@ export default function AddMemberTask() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Anggota Kegiatan',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
@@ -141,24 +141,40 @@ export default function TaskDivisionAddTask() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={disable || loading}
onPress={() => {
handleCreate();
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={disable || loading}
// onPress={() => {
// handleCreate();
// }}
// />
// ),
header: () => (
<AppHeader
title="Tambah Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
category="create"
disable={disable || loading}
onPress={() => {
handleCreate();
}}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -72,24 +72,40 @@ export default function TaskDivisionCancel() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Pembatalan Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable || loading}
category="cancel"
onPress={() => {
handleCancel();
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable || loading}
// category="cancel"
// onPress={() => {
// handleCancel();
// }}
// />
// ),
header: () => (
<AppHeader
title="Pembatalan Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="cancel"
onPress={() => {
handleCancel();
}}
/>
}
/>
),
)
}}
/>
<ScrollView>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -90,22 +90,35 @@ export default function TaskDivisionEdit() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Judul",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={disable || loading}
onPress={() => { handleUpdate() }}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={disable || loading}
// onPress={() => { handleUpdate() }}
// />
// ),
header: () => (
<AppHeader title="Tambah Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
category="update"
disable={disable || loading}
onPress={() => { handleUpdate() }}
/>
}
/>
),
)
}}
/>
<ScrollView>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import SectionCancel from "@/components/sectionCancel";
import SectionProgress from "@/components/sectionProgress";
import HeaderRightTaskDetail from "@/components/task/headerTaskDetail";
@@ -100,12 +100,24 @@ export default function DetailTaskDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: loading ? 'Loading... ' : data?.title,
headerTitleAlign: 'center',
headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision
? <></>
: <HeaderRightTaskDetail id={detail} division={id} status={data?.status} isAdminDivision={isAdminDivision} />,
// 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

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -90,22 +90,35 @@ export default function TaskDivisionReport() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Laporan Kegiatan",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={disable || loading}
onPress={() => { handleUpdate() }}
// 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 File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
@@ -116,22 +116,36 @@ export default function CreateTaskDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
handleBack();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// handleBack();
// }}
// />
// ),
headerTitle: `Tambah Tugas`,
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={title == "" || entitiesMember.length == 0 || taskCreate.length == 0 || loading}
category="create"
onPress={() => { handleCreate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={title == "" || entitiesMember.length == 0 || taskCreate.length == 0 || loading}
// category="create"
// 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>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -67,16 +67,32 @@ export default function AddMemberCreateTask() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={selectMember.length > 0 ? false : true}
onPress={() => {
handleAddMember()
}}
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
@@ -121,22 +121,35 @@ export default function CreateTaskAddTugas() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable}
category="create"
onPress={() => { handleCreate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable}
// category="create"
// onPress={() => { handleCreate() }}
// />
// ),
header: () => (
<AppHeader title="Tambah Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable}
category="create"
onPress={() => { handleCreate() }}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView

View File

@@ -31,7 +31,7 @@ type Props = {
};
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 { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props[]>([])
@@ -43,6 +43,8 @@ export default function ListTask() {
const [page, setPage] = useState(1)
const [waiting, setWaiting] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [isYear, setYear] = useState("")
async function handleLoad(loading: boolean, thisPage: number) {
try {
@@ -55,8 +57,12 @@ export default function ListTask() {
division: id,
status: statusFix,
search,
page: thisPage
page: thisPage,
year
});
setYear(response.tahun)
if (thisPage == 1) {
setData(response.data);
} else if (thisPage > 1 && response.data.length > 0) {
@@ -179,7 +185,13 @@ export default function ListTask() {
</Pressable>
</View>
</View>
<View style={[{ flex: 2 }, Styles.mt05]}>
<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 }]}>
{
loading ?
isList ?

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask";
@@ -189,24 +189,40 @@ export default function UpdateProjectTaskDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Tanggal dan Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loadingSubmit}
category="update"
onPress={() => {
handleEdit()
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loadingSubmit}
// category="update"
// onPress={() => {
// handleEdit()
// }}
// />
// ),
header: () => (
<AppHeader
title="Edit Tanggal dan Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loadingSubmit}
category="update"
onPress={() => {
handleEdit()
}}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -12,7 +12,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
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 { useDispatch, useSelector } from "react-redux";
@@ -100,16 +100,32 @@ export default function AddMemberDivision() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -66,22 +66,36 @@ export default function EditDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Divisi",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={error.name || loading ? true : false}
category="update"
onPress={() => { handleEdit() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={error.name || loading ? true : false}
// category="update"
// 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>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader"
import AppHeader from "@/components/AppHeader"
import DiscussionDivisionDetail from "@/components/division/discussionDivisionDetail"
import FileDivisionDetail from "@/components/division/fileDivisionDetail"
import FiturDivisionDetail from "@/components/division/fiturDivisionDetail"
@@ -57,10 +57,18 @@ export default function DetailDivisionFitur() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: loading ? 'Loading... ' : data?.name,
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
@@ -74,10 +82,10 @@ export default function DetailDivisionFitur() {
>
<CaraouselHome refreshing={refreshing} />
<View style={[Styles.ph15, Styles.mb100]}>
<FiturDivisionDetail refreshing={refreshing}/>
<TaskDivisionDetail refreshing={refreshing}/>
<FileDivisionDetail refreshing={refreshing}/>
<DiscussionDivisionDetail refreshing={refreshing}/>
<FiturDivisionDetail refreshing={refreshing} />
<TaskDivisionDetail refreshing={refreshing} />
<FileDivisionDetail refreshing={refreshing} />
<DiscussionDivisionDetail refreshing={refreshing} />
</View>
</ScrollView>
</SafeAreaView>

View File

@@ -1,6 +1,6 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi"
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
import DrawerBottom from "@/components/drawerBottom"
import ImageUser from "@/components/imageNew"
@@ -164,10 +164,20 @@ export default function InformationDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Informasi Divisi',
headerTitleAlign: 'center',
headerRight: () => ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && <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

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader"
import AppHeader from "@/components/AppHeader"
import ReportChartDocument from "@/components/division/reportChartDocument"
import ReportChartEvent from "@/components/division/reportChartEvent"
import ReportChartProgress from "@/components/division/reportChartProgress"
@@ -107,9 +107,16 @@ export default function ReportDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Laporan Divisi',
headerTitleAlign: 'center',
header: () => (
<AppHeader
title="Laporan Divisi"
showBack={true}
onPressLeft={() => router.back()}
/>
)
}}
/>
<ScrollView>

View File

@@ -1,5 +1,5 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonNextHeader from "@/components/buttonNextHeader";
import { InputForm } from "@/components/inputForm";
import ModalSelect from "@/components/modalSelect";
@@ -69,10 +69,9 @@ export default function CreateDivision() {
if (!response.available) {
AlertKonfirmasi({
title: 'Peringatan',
desc: 'Nama divisi sudah ada. Apakah anda yakin ingin membuat divisi dengan nama yang sama?',
onPress: () => {
handleSetData()
}
category: 'warning',
desc: 'Nama divisi sudah ada. Tidak dapat membuat divisi dengan nama yang sama',
onPress: () => { }
})
} else {
handleSetData()
@@ -103,25 +102,38 @@ export default function CreateDivision() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Divisi",
headerTitleAlign: "center",
headerRight: () => (
<ButtonNextHeader
onPress={() => { handleCheckName() }}
disable={loadingBtn || error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
// headerRight: () => (
// <ButtonNextHeader
// onPress={() => { handleCheckName() }}
// 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>
<View style={[Styles.p15, Styles.mb100]}>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
{
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
(

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import Text from "@/components/Text";
@@ -12,7 +12,7 @@ import { AntDesign } from "@expo/vector-icons";
import { StackActions, useNavigation } from "@react-navigation/native";
import { router, Stack, useLocalSearchParams } from "expo-router";
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 { useDispatch, useSelector } from "react-redux";
@@ -77,16 +77,29 @@ export default function CreateDivisionAddAdmin() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Admin Divisi',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="create"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonNextHeader from "@/components/buttonNextHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -63,13 +63,23 @@ export default function CreateDivisionAddMember() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonNextHeader
disable={selectMember.length > 0 ? false : true}
onPress={() => { handleAddMember() }}
// headerRight: () => (
// <ButtonNextHeader
// disable={selectMember.length > 0 ? false : true}
// onPress={() => { handleAddMember() }}
// />
// )
header: () => (
<AppHeader title="Pilih Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={<ButtonNextHeader
disable={selectMember.length > 0 ? false : true}
onPress={() => { handleAddMember() }}
/>}
/>
)
}}

View File

@@ -1,6 +1,7 @@
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonTab from "@/components/buttonTab";
import InputSearch from "@/components/inputSearch";
import LabelStatus from "@/components/labelStatus";
import PaperGridContent from "@/components/paperGridContent";
import Skeleton from "@/components/skeleton";
import SkeletonTwoItem from "@/components/skeletonTwoItem";
@@ -195,8 +196,9 @@ export default function ListDivision() {
</Pressable>
</View>
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
<View style={[Styles.mv05]}>
<Text>Filter : {nameGroup}</Text>
<View style={[Styles.mv05, Styles.rowOnly]}>
<Text>Filter :</Text>
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
</View>
)}
</View>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ReportChartDocument from "@/components/division/reportChartDocument";
import ReportChartEvent from "@/components/division/reportChartEvent";
import ReportChartProgress from "@/components/division/reportChartProgress";
@@ -125,19 +125,28 @@ export default function Report() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Laporan Divisi",
headerTitleAlign: "center",
header: () => (
<AppHeader title="Laporan Divisi"
showBack={true}
onPressLeft={() => router.back()}
/>
)
}}
/>
<ScrollView>
<View style={[Styles.p15, Styles.mb100]}>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
>
<View style={[Styles.p15, Styles.mb50]}>
<SelectForm
bg="white"
label="Lembaga Desa"

View File

@@ -328,7 +328,7 @@ export default function EditProfile() {
type="numeric"
placeholder="8XX-XXX-XXX"
required
itemLeft={<Text>+62</Text>}
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02]}>+62</Text>}
value={data?.phone}
error={error.phone}
errorText="Nomor Telepon tidak valid"

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import { ButtonFiturMenu } from "@/components/buttonFiturMenu";
import Styles from "@/constants/Styles";
import { AntDesign, Entypo, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
@@ -13,9 +13,11 @@ export default function Feature() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Fitur',
headerTitleAlign: 'center'
headerTitleAlign: 'center',
header: () => (
<AppHeader title="Fitur" showBack={true} onPressLeft={() => router.back()} />
)
}}
/>
<View style={[Styles.p15]}>

View File

@@ -1,17 +1,20 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ImageUser from "@/components/imageNew";
import ItemDetailMember from "@/components/itemDetailMember";
import LabelStatus from "@/components/labelStatus";
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
import Skeleton from "@/components/skeleton";
import Text from "@/components/Text";
import { assetUserImage } from "@/constants/AssetsError";
import { ConstEnv } from "@/constants/ConstEnv";
import { valueRoleUser } from "@/constants/RoleUser";
import Styles from "@/constants/Styles";
import { apiGetProfile } from "@/lib/api";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import React, { useEffect, useState } from "react";
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";
type Props = {
@@ -31,21 +34,27 @@ type Props = {
export default function MemberDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState<Props>()
const [error, setError] = useState(false)
const [errorImg, setErrorImg] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const [isEdit, setEdit] = useState(true)
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [preview, setPreview] = useState(false)
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
const response = await apiGetProfile({ id: id })
setData(response.data)
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
if (response.success) {
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) {
console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally {
setLoading(false)
}
@@ -68,11 +77,19 @@ export default function MemberDetail() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Anggota',
headerTitleAlign: 'center',
headerRight: () => (entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>,
headerShadowVisible: false
// headerRight: () => (entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>,
header: () => (
<AppHeader title="Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
(entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>
}
/>
)
}}
/>
<ScrollView
@@ -84,7 +101,7 @@ export default function MemberDetail() {
/>
}
>
<View style={[Styles.wrapHeadViewMember,]}>
<View style={[Styles.wrapHeadViewMember]}>
{
loading ?
<>
@@ -94,7 +111,9 @@ export default function MemberDetail() {
</>
:
<>
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" />
<Pressable onPress={() => setPreview(true)}>
<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>
</>
@@ -130,6 +149,14 @@ export default function MemberDetail() {
</View>
</ScrollView>
<ImageViewing
images={[{ uri: errorImg ? assetUserImage.uri : `${ConstEnv.url_storage}/files/${data?.img}` }]}
imageIndex={0}
visible={preview}
onRequestClose={() => setPreview(false)}
doubleTapToZoomEnabled
/>
</SafeAreaView>
)
}

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalSelect from "@/components/modalSelect";
@@ -209,22 +209,35 @@ export default function CreateMember() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Anggota",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => { handleCreate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loading}
// category="create"
// onPress={() => { handleCreate() }}
// />
// ),
header: () => (
<AppHeader title="Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => { handleCreate() }}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView
@@ -331,7 +344,7 @@ export default function CreateMember() {
type="numeric"
placeholder="8XX-XXX-XXX"
required
itemLeft={<Text>+62</Text>}
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02]}>+62</Text>}
error={error.phone}
errorText="Nomor Telepon tidak valid"
onChange={val => {

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalSelect from "@/components/modalSelect";
@@ -239,24 +239,40 @@ export default function EditMember() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Anggota",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading}
category="update"
onPress={() => {
handleEdit()
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loading}
// category="update"
// onPress={() => {
// handleEdit()
// }}
// />
// ),
header: () => (
<AppHeader
title="Edit Anggota"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="update"
onPress={() => {
handleEdit()
}}
/>
}
/>
),
)
}}
/>
@@ -371,7 +387,7 @@ export default function EditMember() {
type="numeric"
placeholder="8XX-XXX-XXX"
required
itemLeft={<Text>+62</Text>}
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02]}>+62</Text>}
value={data?.phone}
error={error.phone}
errorText="Nomor Telepon tidak valid"

View File

@@ -2,6 +2,7 @@ import BorderBottomItem from "@/components/borderBottomItem";
import ButtonTab from "@/components/buttonTab";
import ImageUser from "@/components/imageNew";
import InputSearch from "@/components/inputSearch";
import LabelStatus from "@/components/labelStatus";
import SkeletonTwoItem from "@/components/skeletonTwoItem";
import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
@@ -124,8 +125,9 @@ export default function Index() {
<InputSearch onChange={setSearch} />
{
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
<View style={[Styles.mv05]}>
<Text>Filter : {nameGroup}</Text>
<View style={[Styles.mv05, Styles.rowOnly]}>
<Text>Filter :</Text>
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
</View>
}
</View>

View File

@@ -5,6 +5,7 @@ import ButtonTab from "@/components/buttonTab";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import InputSearch from "@/components/inputSearch";
import LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
import SkeletonTwoItem from "@/components/skeletonTwoItem";
import Text from "@/components/Text";
@@ -166,8 +167,9 @@ export default function Index() {
<InputSearch onChange={setSearch} />
{
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
<View style={[Styles.mv05]}>
<Text>Filter : {nameGroup}</Text>
<View style={[Styles.mv05, Styles.rowOnly]}>
<Text>Filter :</Text>
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
</View>
}
</View>

View File

@@ -1,51 +1,73 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import { ButtonHeader } from "@/components/buttonHeader";
import ItemDetailMember from "@/components/itemDetailMember";
import Text from "@/components/Text";
import { assetUserImage } from "@/constants/AssetsError";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
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';
export default function Profile() {
const { signOut } = useAuthSession()
const entities = useSelector((state: any) => state.entities)
const [error, setError] = useState(false)
const [preview, setPreview] = useState(false)
return (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Profile',
headerTitleAlign: 'center',
headerShadowVisible: false,
headerRight: () => <ButtonHeader
item={<AntDesign name="logout" size={20} color="white" />}
onPress={() => {
AlertKonfirmasi({
title: 'Keluar',
desc: 'Apakah anda yakin ingin keluar?',
onPress: () => { signOut() }
})
}}
/>
header: () => (
<AppHeader
title="Profile"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonHeader
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 style={[Styles.h100]}>
<View style={{ flexDirection: 'column' }}>
<View style={[Styles.wrapHeadViewMember]}>
<Image
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
onError={() => { setError(true) }}
style={[Styles.userProfileBig]}
/>
<Pressable onPress={() => setPreview(true)}>
<Image
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
onError={() => { setError(true) }}
style={[Styles.userProfileBig]}
/>
</Pressable>
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{entities.name}</Text>
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{entities.role}</Text>
</View>
@@ -65,6 +87,13 @@ export default function Profile() {
</View>
</View>
</ScrollView>
<ImageViewing
images={[{ uri: error ? assetUserImage.uri : `${ConstEnv.url_storage}/files/${entities.img}` }]}
imageIndex={0}
visible={preview}
onRequestClose={() => setPreview(false)}
doubleTapToZoomEnabled
/>
</SafeAreaView>
)
}

View File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/AppHeader"
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import ButtonSaveHeader from "@/components/buttonSaveHeader"
import ButtonSelect from "@/components/buttonSelect"
import DrawerBottom from "@/components/drawerBottom"
@@ -130,13 +130,27 @@ export default function ProjectAddFile() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah File',
headerTitleAlign: 'center',
headerRight: () => <ButtonSaveHeader
disable={fileForm.length == 0 || loading ? true : false}
category="create"
onPress={() => { handleAddFile() }} />
// headerRight: () => <ButtonSaveHeader
// disable={fileForm.length == 0 || loading ? true : false}
// category="create"
// onPress={() => { handleAddFile() }} />
header: () => (
<AppHeader
title="Tambah File"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={fileForm.length == 0 || loading ? true : false}
category="create"
onPress={() => { handleAddFile() }}
/>
}
/>
)
}}
/>
<ScrollView>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -97,16 +97,32 @@ export default function AddMemberProject() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Anggota Kegiatan',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="update"
disable={selectMember.length == 0 || loading ? true : false}
onPress={() => {
handleAddMember()
}}
// headerRight: () => (
// <ButtonSaveHeader
// category="update"
// disable={selectMember.length == 0 || loading ? true : false}
// onPress={() => {
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject";
@@ -136,22 +136,36 @@ export default function ProjectAddTask() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable || loading}
category="create"
onPress={() => { handleCreate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable || loading}
// category="create"
// onPress={() => { handleCreate() }}
// />
// ),
header: () => (
<AppHeader
title="Tambah Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="create"
onPress={() => { handleCreate() }}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -6,7 +6,7 @@ import { apiCancelProject } 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 React, { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
@@ -68,24 +68,40 @@ export default function ProjectCancel() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Pembatalan Kegiatan",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable || loading}
category="cancel"
onPress={() => {
handleCancel();
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable || loading}
// category="cancel"
// onPress={() => {
// handleCancel();
// }}
// />
// ),
header: () => (
<AppHeader
title="Pembatalan Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="cancel"
onPress={() => {
handleCancel();
}}
/>
}
/>
),
)
}}
/>
<ScrollView

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -43,7 +43,7 @@ export default function EditProject() {
setJudul(val)
if (val == "" || val == "null") {
setError(true)
}else{
} else {
setError(false)
}
}
@@ -89,22 +89,36 @@ export default function EditProject() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Edit Judul Kegiatan",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable || loading}
category="update"
onPress={() => { handleUpdate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable || loading}
// category="update"
// onPress={() => { handleUpdate() }}
// />
// ),
header: () => (
<AppHeader
title="Edit Judul Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable || loading}
category="update"
onPress={() => { handleUpdate() }}
/>
}
/>
),
)
}}
/>
<ScrollView>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import HeaderRightProjectDetail from "@/components/project/headerProjectDetail";
import SectionFile from "@/components/project/sectionFile";
import SectionLink from "@/components/project/sectionLink";
@@ -11,7 +11,7 @@ import Styles from "@/constants/Styles";
import { apiGetProjectOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
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 { useSelector } from "react-redux";
@@ -94,10 +94,20 @@ export default function DetailProject() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: loading ? 'Loading... ' : data?.title,
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

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import Styles from "@/constants/Styles";
@@ -89,22 +89,36 @@ export default function ReportProject() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Laporan Kegiatan",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable || loading}
category="update"
onPress={() => { handleUpdate() }}
// 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

View File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
@@ -193,24 +193,39 @@ export default function CreateProject() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
handleBack();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// handleBack();
// }}
// />
// ),
headerTitle: "Tambah Kegiatan",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => {
handleCreate()
}}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disableBtn || loading}
// category="create"
// onPress={() => {
// handleCreate()
// }}
// />
// ),
header: () => (
<AppHeader title="Tambah Kegiatan"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loading}
category="create"
onPress={() => {
handleCreate()
}}
/>
}
/>
),
)
}}
/>
<ScrollView

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ImageUser from "@/components/imageNew";
import ImageWithLabel from "@/components/imageWithLabel";
@@ -11,8 +11,8 @@ import { setMemberChoose } from "@/lib/memberChoose";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
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";
@@ -74,16 +74,31 @@ export default function AddMemberCreateProject() {
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pilih Anggota',
headerTitleAlign: 'center',
headerRight: () => (
<ButtonSaveHeader
category="create"
disable={selectMember.length > 0 ? false : true}
onPress={() => {
handleAddMember()
}}
// 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 File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject";
@@ -12,7 +12,7 @@ import { router, Stack } from "expo-router";
import 'intl';
import 'intl/locale-data/jsonp/id';
import moment from "moment";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import {
KeyboardAvoidingView,
Platform,
@@ -122,22 +122,36 @@ export default function CreateProjectAddTask() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
// headerLeft: () => (
// <ButtonBackHeader
// onPress={() => {
// router.back();
// }}
// />
// ),
headerTitle: "Tambah Tugas",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disable}
category="create"
onPress={() => { handleCreate() }}
// headerRight: () => (
// <ButtonSaveHeader
// disable={disable}
// category="create"
// onPress={() => { handleCreate() }}
// />
// ),
header: () => (
<AppHeader
title="Tambah Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disable}
category="create"
onPress={() => { handleCreate() }}
/>
}
/>
),
)
}}
/>
<KeyboardAvoidingView

View File

@@ -32,16 +32,18 @@ type Props = {
};
export default function ListProject() {
const { status, group, cat } = useLocalSearchParams<{
const { status, group, cat, year } = useLocalSearchParams<{
status?: string;
group?: string;
cat?: string;
year?: string;
}>();
const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>('0')
const { token, decryptToken } = useAuthSession();
const entityUser = useSelector((state: any) => state.user)
const [search, setSearch] = useState("")
const [nameGroup, setNameGroup] = useState("")
const [isYear, setYear] = useState("")
const [data, setData] = useState<Props[]>([])
const [isList, setList] = useState(false)
const update = useSelector((state: any) => state.projectUpdate)
@@ -63,11 +65,13 @@ export default function ListProject() {
search: search,
group: String(group),
kategori: String(cat),
page: thisPage
page: thisPage,
year: String(year)
});
if (response.success) {
setNameGroup(response.filter.name);
setYear(response.tahun)
if (thisPage == 1) {
setData(response.data);
} else if (thisPage > 1 && response.data.length > 0) {
@@ -91,7 +95,7 @@ export default function ListProject() {
useEffect(() => {
handleLoad(true, 1);
}, [statusFix, search, group, cat]);
}, [statusFix, search, group, cat, year]);
const loadMoreData = () => {
if (waiting) return
@@ -194,17 +198,25 @@ export default function ListProject() {
</View>
<View style={[Styles.mv05]}>
{
entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
<Text>Filter :
// entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
<View style={[Styles.rowOnly]}>
<Text style={[Styles.mr05]}>Filter :</Text>
{
(entityUser.role == "supadmin" || entityUser.role == "developer") && nameGroup
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
<LabelStatus size="small" category="secondary" text={nameGroup} style={{ marginRight: 5 }} />
}
{
(entityUser.role == 'user' || entityUser.role == 'coadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? 'Kegiatan Saya' : 'Semua Kegiatan'
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? <LabelStatus size="small" category="secondary" text="Kegiatan Saya" style={{ marginRight: 5 }} /> : <LabelStatus size="small" category="secondary" text="Semua Kegiatan" style={{ marginRight: 5 }} />
: ''
}
</Text>
<LabelStatus size="small" category="secondary" text={isYear} style={{ marginRight: 5 }} />
{/* {
(entityUser.role == 'user' || entityUser.role == 'coadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? <LabelStatus size="small" category="primary" text="Kegiatan Saya" /> : <LabelStatus size="small" category="primary" text="Semua Kegiatan" />
: ''
} */}
</View>
}
</View>
</View>

View File

@@ -1,4 +1,4 @@
import ButtonBackHeader from "@/components/buttonBackHeader";
import AppHeader from "@/components/AppHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import { InputForm } from "@/components/inputForm";
import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject";
@@ -14,7 +14,7 @@ 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 React, { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, SafeAreaView, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import DateTimePicker, { DateType } from "react-native-ui-datepicker";
@@ -172,14 +172,28 @@ export default function UpdateProjectTask() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Edit Tanggal dan Tugas',
headerTitleAlign: 'center',
headerRight: () => <ButtonSaveHeader
disable={disableBtn || loadingSubmit}
category="update"
onPress={() => { handleEdit() }}
/>
// headerRight: () => <ButtonSaveHeader
// disable={disableBtn || loadingSubmit}
// category="update"
// onPress={() => { handleEdit() }}
// />
header: () => (
<AppHeader
title="Edit Tanggal dan Tugas"
showBack={true}
onPressLeft={() => router.back()}
right={
<ButtonSaveHeader
disable={disableBtn || loadingSubmit}
category="update"
onPress={() => { handleEdit() }}
/>
}
/>
)
}}
/>
<KeyboardAvoidingView

View File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ImageUser from "@/components/imageNew";
import InputSearch from "@/components/inputSearch";
import Text from '@/components/Text';
@@ -82,9 +82,11 @@ export default function Search() {
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pencarian',
headerTitleAlign: 'center'
headerTitleAlign: 'center',
header: () => (
<AppHeader title="Pencarian" showBack={true} onPressLeft={() => router.back()} />
)
}}
/>
<View style={[Styles.p15]}>

BIN
bun.lockb

Binary file not shown.

36
components/AppHeader.tsx Normal file
View File

@@ -0,0 +1,36 @@
import Styles from '@/constants/Styles';
import { useRouter } from 'expo-router';
import { Platform, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ButtonBackHeader from './buttonBackHeader';
type Props = {
title: string;
right?: React.ReactNode;
showBack?: boolean;
onPressLeft?: () => void
left?: React.ReactNode
};
export default function AppHeader({ title, right, showBack = true, onPressLeft, left }: Props) {
const insets = useSafeAreaInsets();
const router = useRouter();
return (
<View style={[Styles.headerContainer, Platform.OS === 'ios' ? Styles.pb05 : Styles.pb13, { paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
<View style={Styles.headerApp}>
{showBack ? (
<ButtonBackHeader onPress={onPressLeft} />
) :
left ? left :
(
<View style={Styles.headerSide} />
)}
<Text style={Styles.headerTitle}>{title}</Text>
<View style={Styles.headerSide}>{right}</View>
</View>
</View>
);
}

View File

@@ -5,17 +5,27 @@ type Props = {
title: string,
desc: string
onPress: () => void
category?: string
}
export default function AlertKonfirmasi({ title, desc, onPress }: Props) {
Alert.alert(title, desc, [
{
text: 'Tidak',
style: 'cancel',
},
{
text: 'Ya',
onPress: () => { onPress() }
},
]);
export default function AlertKonfirmasi({ title, desc, onPress, category }: Props) {
if (category == "warning") {
Alert.alert(title, desc, [
{
text: 'Oke',
style: 'cancel',
},
]);
} else {
Alert.alert(title, desc, [
{
text: 'Tidak',
style: 'cancel',
},
{
text: 'Ya',
onPress: () => { onPress() }
},
]);
}
}

View File

@@ -39,10 +39,10 @@ export default function ViewLogin({ onValidate }: Props) {
}
}
} else {
return Toast.show({ type: 'small', text1: response.message, position: 'top' })
return Toast.show({ type: 'small', text1: response.message, position: 'bottom' })
}
} catch (error) {
return Toast.show({ type: 'small', text1: `Terjadi kesalahan, coba lagi`, position: 'top' })
return Toast.show({ type: 'small', text1: `Terjadi kesalahan, coba lagi`, position: 'bottom' })
} finally {
setLoadingLogin(false)
}
@@ -70,7 +70,7 @@ export default function ViewLogin({ onValidate }: Props) {
type="numeric"
placeholder="XXX-XXX-XXXX"
round
itemLeft={<Text>+62</Text>}
itemLeft={<Text style={[Platform.OS === 'ios' && Styles.mt02]}>+62</Text>}
info="Kami akan mengirim kode verifikasi melalui WhatsApp, guna mengonfirmasikan nomor Anda." />
<ButtonForm
text="MASUK"

View File

@@ -28,7 +28,7 @@ export default function ViewVerification({ phone, otp }: Props) {
const encrypted = await encryptToken(valueUser);
signIn(encrypted);
} else {
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', position: 'top' })
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', position: 'bottom' })
}
}
@@ -36,7 +36,7 @@ export default function ViewVerification({ phone, otp }: Props) {
if (value === otpFix.toString()) {
login()
} else {
return Toast.show({ type: 'small', text1: 'Kode OTP tidak sesuai', position: 'top' });
return Toast.show({ type: 'small', text1: 'Kode OTP tidak sesuai', position: 'bottom' });
}
}

View File

@@ -1,6 +1,6 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import React from "react";
import React, { useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import Text from "./Text";
@@ -19,21 +19,38 @@ type Props = {
bgColor?: 'white' | 'transparent'
width?: number
descEllipsize?: boolean
textColor?: string
textColor?: string,
colorPress?: boolean
titleShowAll?: boolean
}
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor }: Props) {
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll }: Props) {
const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false);
return (
<Pressable onLongPress={onLongPress} style={[borderType == 'bottom' ? Styles.wrapItemBorderBottom : borderType == 'all' ? Styles.wrapItemBorderAll : Styles.wrapItemBorderNone, bgColor && bgColor == 'white' && ColorsStatus.white]} onPress={onPress}>
<Pressable onLongPress={onLongPress} onPress={onPress}
onPressIn={() => setIsTap(true)}
onPressOut={() => setIsTap(false)}
style={({ pressed }) => [
borderType == 'bottom'
? Styles.wrapItemBorderBottom
: borderType == 'all'
? Styles.wrapItemBorderAll
: Styles.wrapItemBorderNone,
bgColor && bgColor == 'white' && ColorsStatus.white,
// efek warna saat ditekan (sementara)
isTap && colorPress && ColorsStatus.pressedGray,
]}
>
<View style={[Styles.rowItemsCenter]}>
{icon}
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>
<View style={[Styles.ml10, rightTopInfo ? { width: '70%' } : { width: '90%' }]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={1} ellipsizeMode='tail'>{title}</Text>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
{
subtitle &&
typeof subtitle == "string"

View File

@@ -0,0 +1,232 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv";
import { isImageFile } from "@/constants/FileExtensions";
import Styles from "@/constants/Styles";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import * as FileSystem from 'expo-file-system';
import { startActivityAsync } from 'expo-intent-launcher';
import * as Sharing from 'expo-sharing';
import React, { useState } from "react";
import { Dimensions, Platform, Pressable, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import ImageViewing from "react-native-image-viewing";
import * as mime from 'react-native-mime-types';
import Toast from "react-native-toast-message";
import Text from "./Text";
type Props = {
title?: string
subtitle?: string | React.ReactNode
icon: React.ReactNode
desc?: string
rightTopInfo?: string
onPress?: () => void
onLongPress?: () => void
borderType: 'all' | 'bottom' | 'none'
leftBottomInfo?: React.ReactNode | string
rightBottomInfo?: React.ReactNode | string
titleWeight?: 'normal' | 'bold'
bgColor?: 'white' | 'transparent'
width?: number
descEllipsize?: boolean
textColor?: string,
colorPress?: boolean
titleShowAll?: boolean
dataFile: { id: string; idStorage: string; name: string; extension: string }[]
}
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll, dataFile }: Props) {
const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false);
const [loadingOpen, setLoadingOpen] = useState(false)
const [chooseFile, setChooseFile] = useState<PropsFile>()
const [preview, setPreview] = useState(false)
function handleChooseFile(item: PropsFile) {
setChooseFile(item)
setPreview(true)
}
const openFile = async (item: PropsFile) => {
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 (
<>
<Pressable onLongPress={onLongPress} onPress={onPress}
onPressIn={() => setIsTap(true)}
onPressOut={() => setIsTap(false)}
style={({ pressed }) => [
borderType == 'bottom'
? Styles.wrapItemBorderBottom
: borderType == 'all'
? Styles.wrapItemBorderAll
: Styles.wrapItemBorderNone,
bgColor && bgColor == 'white' && ColorsStatus.white,
// efek warna saat ditekan (sementara)
isTap && colorPress && ColorsStatus.pressedGray,
]}
>
<View style={[Styles.rowItemsCenter]}>
{icon}
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>
<View style={[Styles.ml10, rightTopInfo ? { width: '70%' } : { width: '90%' }]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
{
subtitle &&
typeof subtitle == "string"
? <Text style={[Styles.textMediumNormal, { lineHeight: 15, color: textColorFix }]}>{subtitle}</Text>
: <View style={{ alignItems: 'flex-start' }}>
{subtitle}
</View>
}
</View>
{
rightTopInfo && <Text style={[Styles.textInformation, Styles.mt05, { color: textColorFix }]}>{rightTopInfo}</Text>
}
</View>
</View>
{desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'left', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>}
{
dataFile.length > 0 && (
<ScrollView horizontal style={[Styles.mv05]} showsHorizontalScrollIndicator={false}>
{dataFile.map((item, index) => (
<Pressable
key={index}
style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}
onPress={() => {
isImageFile(item.extension) ?
handleChooseFile(item)
: openFile(item)
}}
>
<MaterialCommunityIcons
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={18}
color="grey" />
<Text style={[Styles.textInformation, Styles.cGray]}>{item.name}.{item.extension}</Text>
</Pressable>
))}
</ScrollView>
)
}
{
(leftBottomInfo || rightBottomInfo) &&
(
<View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}>
{
typeof leftBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text>
:
leftBottomInfo
}
{
typeof rightBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text>
:
rightBottomInfo
}
</View>
)
}
</Pressable>
<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>
)}
/>
</>
)
}

View File

@@ -15,16 +15,16 @@ export default function DiscussionItem({ title, user, date, onPress }: Props) {
<Pressable style={[Styles.wrapItemDiscussion]} onPress={onPress}>
<View style={[Styles.rowItemsCenter, Styles.mb10]}>
<Ionicons name="chatbox-ellipses-outline" size={22} color="black" style={Styles.mr10} />
<View style={[{flex:1}]}>
<View style={[{ flex: 1 }]}>
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title}</Text>
</View>
</View>
<View style={Styles.rowSpaceBetween}>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowSpaceBetween]}>
<View style={[Styles.rowItemsCenter, { flex: 1 }]}>
<Feather name="user" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation]}>{user}</Text>
<Text style={[Styles.textInformation]} numberOfLines={1} ellipsizeMode="tail">{user}</Text>
</View>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowItemsCenter, { flex: 1, justifyContent: 'flex-end' }]}>
<Feather name="clock" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation]}>{date}</Text>
</View>

View File

@@ -57,7 +57,7 @@ export default function TaskDivisionDetail({ refreshing }: { refreshing: boolean
:
data.length > 0 ?
data.map((item, index) => (
<Pressable key={index} style={[Styles.wrapPaper]} onPress={() => { router.push(`/division/${id}/task/${item.idProject}`) }}>
<Pressable key={index} style={[Styles.wrapPaper, Styles.mb05]} onPress={() => { router.push(`/division/${id}/task/${item.idProject}`) }}>
<Text style={[Styles.textDefaultSemiBold]} numberOfLines={1} ellipsizeMode="tail">{item.title} - {item.projectTitle}</Text>
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
<Feather name="clock" size={18} color="grey" style={Styles.mr05} />

View File

@@ -6,17 +6,19 @@ type Props = {
src: string,
size?: 'sm' | 'xs' | 'lg'
border?: boolean
onError?: (val:boolean) => void
}
export default function ImageUser({ src, size }: Props) {
export default function ImageUser({ src, size, onError }: Props) {
const [error, setError] = useState(false)
return (
<Image
source={error ? require('../assets/images/user.jpg') : { uri: src }}
style={[size == 'xs' ? Styles.userProfileExtraSmall : size == 'lg' ? Styles.userProfileBig : Styles.userProfileSmall, Styles.borderAll]}
onError={() =>
onError={() => {
setError(true)
}
onError?.(true)
}}
/>
)
}

View File

@@ -20,10 +20,11 @@ type Props = {
disable?: boolean
multiline?: boolean
mb?: boolean
focus?: boolean
};
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true }: Props) {
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true, focus }: Props) {
const lebar = Dimensions.get("window").width;
if (itemLeft != undefined || itemRight != undefined) {

View File

@@ -1,16 +1,23 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import { View } from "react-native";
import { View, StyleProp, ViewStyle } from "react-native";
import Text from "./Text";
type Props = {
category: 'error' | 'success' | 'warning' | 'primary' | 'secondary'
text: string
size: 'small' | 'default'
style?: StyleProp<ViewStyle>
}
export default function LabelStatus({ category, text, size }: Props) {
export default function LabelStatus({ category, text, size, style }: Props) {
return (
<View style={[size == "small" ? Styles.labelStatusSmall : Styles.labelStatus, ColorsStatus[category], Styles.round10, Styles.contentItemCenter]}>
<View style={[
size == "small" ? Styles.labelStatusSmall : Styles.labelStatus,
ColorsStatus[category],
Styles.round10,
Styles.contentItemCenter,
style
]}>
<Text style={[size == "small" ? Styles.textSmallSemiBold : Styles.textMediumSemiBold, Styles.cWhite, { textAlign: 'center' }]}>{text}</Text>
</View>
)

View File

@@ -0,0 +1,50 @@
import React from "react";
import { View, ActivityIndicator, StyleSheet, Text } from "react-native";
type Props = {
visible: boolean;
text?: string;
};
export default function LoadingOverlay({
visible,
text = "Loading...",
}: Props) {
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={styles.box}>
<ActivityIndicator size="small" color="#2c3e50" />
<Text style={styles.text}>{text}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0,0,0,0.35)",
justifyContent: "center",
alignItems: "center",
zIndex: 9999,
},
box: {
paddingVertical: 5,
paddingHorizontal: 15,
backgroundColor: "#fff",
borderRadius: 8,
alignItems: "center",
elevation: 6, // Android
shadowColor: "#000", // iOS
shadowOpacity: 0.25,
shadowRadius: 5,
shadowOffset: { width: 0, height: 4 },
},
text: {
marginTop: 5,
fontSize: 14,
color: "#2c3e50",
},
});

View File

@@ -1,25 +1,27 @@
import Styles from "@/constants/Styles"
import { apiGetGroup } from "@/lib/api"
import { apiGetGroup, apiGetTahunProject, apiGetTahunTask } from "@/lib/api"
import { setEntityFilterGroup } from "@/lib/filterSlice"
import { useAuthSession } from "@/providers/AuthProvider"
import { AntDesign } from "@expo/vector-icons"
import { router } from "expo-router"
import { useEffect, useState } from "react"
import { Pressable, ScrollView, View } from "react-native"
import Text from './Text';
import { ScrollView, TouchableOpacity, View } from "react-native"
import { useDispatch, useSelector } from "react-redux"
import { ButtonForm } from "./buttonForm"
import DrawerBottom from "./drawerBottom"
import Text from './Text'
type Props = {
open: boolean,
close: (value: boolean) => void
page: 'position' | 'member' | 'discussion' | 'project' | 'division'
category?: 'filter-group' | 'filter-data'
page: 'position' | 'member' | 'discussion' | 'project' | 'division' | 'division/task'
category?: 'filter-group' | 'filter-data' | 'year-only',
valueGroup?: string,
valueYear?: string,
dataPassing?: any
}
export default function ModalFilter({ open, close, page, category }: Props) {
export default function ModalFilter({ open, close, page, category, valueGroup, valueYear, dataPassing }: Props) {
const data = [
{
id: "data-saya",
@@ -34,60 +36,163 @@ export default function ModalFilter({ open, close, page, category }: Props) {
const dispatch = useDispatch()
const entities = useSelector((state: any) => state.filterGroup)
const update = useSelector((state: any) => state.groupUpdate)
const [chooseGroup, setChooseGroup] = useState('')
const [chooseGroup, setChooseGroup] = useState(valueGroup || '')
const [chooseYear, setChooseYear] = useState(valueYear || '')
const [dataTahun, setDataTahun] = useState<{ id: string, name: string }[]>([])
const [passingData, setPassingData] = useState(dataPassing)
async function handleLoad() {
const hasil = await decryptToken(String(token?.current))
const response = await apiGetGroup({ active: 'true', user: hasil, search: '' })
dispatch(setEntityFilterGroup(response.data))
if (page === 'project') {
const responseTahun = await apiGetTahunProject({ user: hasil })
setDataTahun(responseTahun.data)
} else if (page === 'division/task') {
const responseTahun = await apiGetTahunTask({ user: hasil, division: passingData })
setDataTahun(responseTahun.data)
}
}
useEffect(() => {
handleLoad()
handleLoad()
}, [dispatch, update]);
return (
<DrawerBottom animation="slide" isVisible={open} setVisible={close} title="Filter" height={75}>
<DrawerBottom animation="slide" isVisible={open} setVisible={close} title="Pilih Preferensi" height={75}>
<View style={{ justifyContent: 'space-between', flex: 1 }}>
<ScrollView>
<View>
{
category == "filter-data"
?
data.map((item: any, index: any) => (
<Pressable key={index} style={[Styles.itemSelectModal]} onPress={() => { setChooseGroup(item.id) }}>
<Text style={[chooseGroup == item.id ? Styles.textDefaultSemiBold : Styles.textDefault]}>{item.name}</Text>
{
chooseGroup == item.id && <AntDesign name="check" size={20} color={'black'}/>
}
</Pressable>
))
:
entities.map((item: any, index: any) => (
<Pressable key={index} style={[Styles.itemSelectModal]} onPress={() => { setChooseGroup(item.id) }}>
<Text style={[chooseGroup == item.id ? Styles.textDefaultSemiBold : Styles.textDefault]}>{item.name}</Text>
{
chooseGroup == item.id && <AntDesign name="check" size={20} color={'black'}/>
}
</Pressable>
))
}
</View>
{
category != "year-only" &&
<View>
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>{category == "filter-data" ? "Filter Data" : "Lembaga Desa"}</Text>
<View style={[Styles.rowOnly, { flexWrap: 'wrap' }]}>
{
category == "filter-data"
?
data.map((item: any, index: any) => (
<TouchableOpacity
key={index}
style={[
Styles.chip,
chooseGroup == item.id && Styles.chipSelected,
]}
activeOpacity={0.8}
onPress={() => { setChooseGroup(item.id) }}
>
{/* {chooseGroup == item.id && (
<View style={Styles.checkIcon}>
<Ionicons name="checkmark" size={14} color="white" />
</View>
)} */}
<Text
style={[
Styles.chipText,
chooseGroup == item.id && Styles.chipTextSelected,
]}
>
{item.name}
</Text>
</TouchableOpacity>
// <Pressable key={index} style={[Styles.itemSelectModal]} onPress={() => { setChooseGroup(item.id) }}>
// <Text style={[chooseGroup == item.id ? Styles.textDefaultSemiBold : Styles.textDefault]}>{item.name}</Text>
// {
// chooseGroup == item.id && <AntDesign name="check" size={20} color={'black'} />
// }
// </Pressable>
))
:
entities.map((item: any, index: any) => (
<TouchableOpacity
key={index}
style={[
Styles.chip,
chooseGroup == item.id && Styles.chipSelected,
]}
activeOpacity={0.8}
onPress={() => { setChooseGroup(item.id) }}
>
{/* {chooseGroup == item.id && (
<View style={Styles.checkIcon}>
<Ionicons name="checkmark" size={14} color="white" />
</View>
)} */}
<Text
style={[
Styles.chipText,
chooseGroup == item.id && Styles.chipTextSelected,
]}
>
{item.name}
</Text>
</TouchableOpacity>
// <Pressable key={index} style={[Styles.itemSelectModal]} onPress={() => { setChooseGroup(item.id) }}>
// <Text style={[chooseGroup == item.id ? Styles.textDefaultSemiBold : Styles.textDefault]}>{item.name}</Text>
// {
// chooseGroup == item.id && <AntDesign name="check" size={20} color={'black'}/>
// }
// </Pressable>
))
}
</View>
</View>
}
{(page == "project" || page == "division/task") && (
<View>
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Tahun</Text>
<View style={[Styles.rowOnly, { flexWrap: 'wrap' }]}>
{
dataTahun.map((item: { id: string, name: string }, index: number) => (
<TouchableOpacity
key={index}
style={[
Styles.chip,
chooseYear == item.id && Styles.chipSelected,
]}
activeOpacity={0.8}
onPress={() => { setChooseYear(item.id) }}
>
<Text
style={[
Styles.chipText,
chooseYear == item.id && Styles.chipTextSelected,
]}
>
{item.name}
</Text>
</TouchableOpacity>
))
}
</View>
</View>
)}
</ScrollView>
<View>
<ButtonForm text="Terapkan" onPress={() => {
close(false)
page == 'project' ?
category == "filter-data"
category == "year-only"
?
router.replace(`/${page}?status=0&cat=${chooseGroup}`)
router.replace(`/${page}?status=0&year=${chooseYear}`)
:
router.replace(`/${page}?status=0&group=${chooseGroup}`)
category == "filter-data"
?
router.replace(`/${page}?status=0&cat=${chooseGroup}&year=${chooseYear}`)
:
router.replace(`/${page}?status=0&group=${chooseGroup}&year=${chooseYear}`)
:
router.replace(`/${page}?active=true&group=${chooseGroup}`)
page == "division/task" ?
router.replace(`/division/${dataPassing}/task?status=0&year=${chooseYear}`)
:
router.replace(`/${page}?active=true&group=${chooseGroup}`)
}} />
</View>
</View>

View File

@@ -31,7 +31,7 @@ export default function HeaderRightProjectList() {
/>
}
{
(entityUser.role == "user" || entityUser.role == "coadmin" || entityUser.role == "supadmin" || entityUser.role == "developer") &&
// (entityUser.role == "user" || entityUser.role == "coadmin" || entityUser.role == "supadmin" || entityUser.role == "developer") &&
<MenuItemRow
icon={<AntDesign name="filter" color="black" size={25} />}
title="Filter"
@@ -50,7 +50,7 @@ export default function HeaderRightProjectList() {
close={() => { setFilter(false) }}
open={isFilter}
page="project"
category={entityUser.role == "supadmin" || entityUser.role == "developer" ? "filter-group" : "filter-data"}
category={entityUser.role == "admin" || entityUser.role == "cosupadmin" ? 'year-only' : entityUser.role == "supadmin" || entityUser.role == "developer" ? "filter-group" : "filter-data"}
/>
</>
)

View File

@@ -9,6 +9,7 @@ import { useSelector } from "react-redux"
import ButtonMenuHeader from "../buttonMenuHeader"
import DrawerBottom from "../drawerBottom"
import MenuItemRow from "../menuItemRow"
import ModalFilter from "../modalFilter"
export default function HeaderRightTaskList() {
const [isVisible, setVisible] = useState(false)
@@ -16,6 +17,8 @@ export default function HeaderRightTaskList() {
const { token, decryptToken } = useAuthSession()
const { id } = useLocalSearchParams<{ id: string }>();
const entityUser = useSelector((state: any) => state.user);
const [isFilter, setFilter] = useState(false)
async function handleCheckAdmin() {
try {
@@ -38,22 +41,44 @@ export default function HeaderRightTaskList() {
return (
<>
{
{/* {
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision
? <ButtonMenuHeader onPress={() => { setVisible(true) }} /> : <></>
}
} */}
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu">
<View style={Styles.rowItemsCenter}>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision
&&
<MenuItemRow
icon={<AntDesign name="pluscircle" color="black" size={25} />}
title="Tambah Tugas Divisi"
onPress={() => {
setVisible(false)
router.push('./task/create')
}}
/>
}
<MenuItemRow
icon={<AntDesign name="pluscircle" color="black" size={25} />}
title="Tambah Tugas Divisi"
icon={<AntDesign name="filter" color="black" size={25} />}
title="Filter"
onPress={() => {
setVisible(false)
router.push('./task/create')
setTimeout(() => {
setFilter(true)
}, 600)
}}
/>
</View>
</DrawerBottom>
<ModalFilter
close={() => { setFilter(false) }}
open={isFilter}
page="division/task"
category={"year-only"}
dataPassing={id}
/>
</>
)
}

5
constants/AssetsError.ts Normal file
View File

@@ -0,0 +1,5 @@
import { Image } from "react-native";
export const assetUserImage = Image.resolveAssetSource(
require('@/assets/images/user.jpg')
);

View File

@@ -31,5 +31,8 @@ export const ColorsStatus = {
},
lightRed: {
backgroundColor: '#ffcdcd'
},
pressedGray: {
backgroundColor: '#e4e4e4'
}
}

130
constants/FileExtensions.ts Normal file
View File

@@ -0,0 +1,130 @@
/**
* File Extensions Constants
* Categorizes common file extensions for use throughout the application
*/
// Image file extensions
export const IMAGE_EXTENSIONS = [
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg',
'tiff',
'ico'
];
// Document file extensions
export const DOCUMENT_EXTENSIONS = [
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'txt',
'rtf',
'odt',
'ods',
'odp',
'csv',
'xml',
'html',
'htm'
];
// Video file extensions
export const VIDEO_EXTENSIONS = [
'mp4',
'avi',
'mov',
'wmv',
'flv',
'webm',
'mkv',
'm4v',
'3gp',
'mpeg',
'mpg'
];
// Audio file extensions
export const AUDIO_EXTENSIONS = [
'mp3',
'wav',
'flac',
'aac',
'ogg',
'wma',
'm4a',
'opus'
];
// Archive file extensions
export const ARCHIVE_EXTENSIONS = [
'zip',
'rar',
'7z',
'tar',
'gz',
'bz2',
'xz',
'iso',
'dmg'
];
// Combined list of all extensions
export const ALL_EXTENSIONS = [
...IMAGE_EXTENSIONS,
...DOCUMENT_EXTENSIONS,
...VIDEO_EXTENSIONS,
...AUDIO_EXTENSIONS,
...ARCHIVE_EXTENSIONS
];
// Helper function to get file type category based on extension
export const getFileTypeCategory = (extension: string): string => {
const ext = extension.toLowerCase();
if (IMAGE_EXTENSIONS.includes(ext)) {
return 'image';
} else if (DOCUMENT_EXTENSIONS.includes(ext)) {
return 'document';
} else if (VIDEO_EXTENSIONS.includes(ext)) {
return 'video';
} else if (AUDIO_EXTENSIONS.includes(ext)) {
return 'audio';
} else if (ARCHIVE_EXTENSIONS.includes(ext)) {
return 'archive';
}
return 'unknown';
};
// Helper function to check if a file is an image
export const isImageFile = (extension: string): boolean => {
return IMAGE_EXTENSIONS.includes(extension.toLowerCase());
};
// Helper function to check if a file is a document
export const isDocumentFile = (extension: string): boolean => {
return DOCUMENT_EXTENSIONS.includes(extension.toLowerCase());
};
// Helper function to check if a file is a video
export const isVideoFile = (extension: string): boolean => {
return VIDEO_EXTENSIONS.includes(extension.toLowerCase());
};
// Helper function to check if a file is audio
export const isAudioFile = (extension: string): boolean => {
return AUDIO_EXTENSIONS.includes(extension.toLowerCase());
};
// Helper function to check if a file is an archive
export const isArchiveFile = (extension: string): boolean => {
return ARCHIVE_EXTENSIONS.includes(extension.toLowerCase());
};

View File

@@ -178,6 +178,9 @@ const Styles = StyleSheet.create({
ph20: {
paddingHorizontal: 20,
},
pv03: {
paddingVertical: 3
},
pv05: {
paddingVertical: 5
},
@@ -343,7 +346,9 @@ const Styles = StyleSheet.create({
borderRadius: 15,
backgroundColor: '#19345E',
display: 'flex',
width: '92%'
width: '92%',
resizeMode: 'stretch',
overflow: 'hidden',
},
wrapGridContent: {
shadowColor: '#171717',
@@ -473,6 +478,7 @@ const Styles = StyleSheet.create({
wrapHeadViewMember: {
backgroundColor: '#19345E',
paddingVertical: 30,
paddingHorizontal: 15,
alignItems: 'center',
borderBottomLeftRadius: 25,
borderBottomRightRadius: 25
@@ -564,7 +570,7 @@ const Styles = StyleSheet.create({
paddingVertical: 10,
position: 'absolute',
width: '100%',
bottom: 0
bottom: 0,
},
animatedView: {
width: '100%',
@@ -625,6 +631,61 @@ const Styles = StyleSheet.create({
position: 'absolute',
opacity: 0,
zIndex: -1,
},
headerContainer: {
backgroundColor: '#19345E',
},
headerApp: {
// height: 40,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
},
headerTitle: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
headerSide: {
width: 40,
alignItems: 'center',
},
chip: {
paddingVertical: 5,
paddingHorizontal: 15,
borderRadius: 5,
backgroundColor: "#F3F4F6",
borderWidth: 1,
borderColor: "transparent",
marginRight: 10,
marginBottom: 10,
},
chipSelected: {
backgroundColor: "#f2f6ffff",
borderColor: "#384288",
},
chipText: {
fontSize: 16,
color: "#222",
},
chipTextSelected: {
color: "#384288",
},
checkIcon: {
position: "absolute",
top: -6,
left: -6,
backgroundColor: "#384288",
borderRadius: 10,
padding: 2,
},
headerModalViewImg: {
paddingTop: 50,
paddingHorizontal: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}
})

View File

@@ -394,7 +394,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
PRODUCT_NAME = "Desa";
PRODUCT_NAME = Desa;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -429,7 +429,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
PRODUCT_NAME = "Desa";
PRODUCT_NAME = Desa;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.0.2</string>
<string>2.0.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>4</string>
<string>7</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -11,7 +11,20 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => {
}
export const apiSendOtp = async (body: { phone: string, otp: number }) => {
const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
const message = "Desa+\nMasukkan kode ini " + body.otp + " pada aplikasi Desa+ anda. Jangan berikan pada siapapun."
const textFix = encodeURIComponent(message)
// const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
const res = await fetch(
`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`,
},
}
);
return res.status
}
@@ -182,6 +195,16 @@ export const apiSendDiscussionGeneralCommentar = async ({ id, data }: { id: stri
return response.data;
};
export const apiDeleteDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { user: string } }) => {
const response = await api.delete(`/mobile/discussion-general/${id}/comment`, { data })
return response.data;
};
export const apiUpdateDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { desc: string, user: string } }) => {
const response = await api.put(`/mobile/discussion-general/${id}/comment`, data)
return response.data;
};
export const apiDeleteMemberDiscussionGeneral = async (data: { user: string, idUser: string }, id: string) => {
await api.delete(`mobile/discussion-general/${id}/member`, { data }).then(response => {
@@ -207,13 +230,32 @@ export const apiDeleteDiscussionGeneral = async (data: { user: string, active: b
});
};
export const apiEditDiscussionGeneral = async (data: { user: string, title: string, desc: string }, id: string) => {
const response = await api.put(`/mobile/discussion-general/${id}`, data)
// export const apiEditDiscussionGeneral = async (data: { user: string, title: string, desc: string }, id: string) => {
// const response = await api.put(`/mobile/discussion-general/${id}`, data)
// return response.data;
// };
export const apiEditDiscussionGeneral = async (data: FormData, id: string) => {
const response = await api.put(`/mobile/discussion-general/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiCreateDiscussionGeneral = async ({ data }: { data: { idGroup: string, title: string, desc: string, user: string, member: [] } }) => {
const response = await api.post(`/mobile/discussion-general`, data)
// export const apiCreateDiscussionGeneral = async ({ data }: { data: { idGroup: string, title: string, desc: string, user: string, member: [] } }) => {
// const response = await api.post(`/mobile/discussion-general`, data)
// return response.data;
// };
export const apiCreateDiscussionGeneral = async (data: FormData) => {
// const response = await api.post(`/mobile/discussion-general`, data)
const response = await api.post(`/mobile/discussion-general`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
@@ -233,28 +275,53 @@ export const apiGetDivisionGroup = async ({ user }: { user: string }) => {
return response.data;
};
export const apiCreateAnnouncement = async ({ data }: { data: { title: string, desc: string, user: string, groups: any[] } }) => {
const response = await api.post(`/mobile/announcement`, data)
// export const apiCreateAnnouncement = async ({ data }: { data: { title: string, desc: string, user: string, groups: any[] } }) => {
// const response = await api.post(`/mobile/announcement`, data)
// return response.data;
// };
export const apiCreateAnnouncement = async (data: FormData) => {
const response = await api.post(`/mobile/announcement`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiGetAnnouncementOne = async ({ user, id }: { user: string, id: string }) => {
const response = await api.get(`mobile/announcement/${id}?user=${user}`);
return response.data;
};
export const apiEditAnnouncement = async (data: { title: string, desc: string, user: string, groups: any[] }, id: string) => {
const response = await api.put(`/mobile/announcement/${id}`, data)
// export const apiEditAnnouncement = async (data: { title: string, desc: string, user: string, groups: any[], oldFile: any[] }, id: string) => {
// const response = await api.put(`/mobile/announcement/${id}`, data)
// return response.data;
// };
export const apiEditAnnouncement = async (data: FormData, id: string) => {
const response = await api.put(`/mobile/announcement/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiDeleteAnnouncement = async (data: { user: string }, id: string) => {
const response = await api.delete(`mobile/announcement/${id}`, { data })
return response.data
};
export const apiGetProject = async ({ user, status, search, group, kategori, page }: { user: string, status: string, search: string, group?: string, kategori?: string, page?: number }) => {
const response = await api.get(`mobile/project?user=${user}&status=${status}&group=${group}&search=${search}&cat=${kategori}&page=${page}`);
export const apiGetTahunProject = async ({ user }: { user: string }) => {
const response = await api.get(`mobile/project/tahun?user=${user}`);
return response.data;
};
export const apiGetProject = async ({ user, status, search, group, kategori, page, year }: { user: string, status: string, search: string, group?: string, kategori?: string, page?: number, year?: string }) => {
const response = await api.get(`mobile/project?user=${user}&status=${status}&group=${group}&search=${search}&cat=${kategori}&page=${page}&year=${year}`);
return response.data;
};
@@ -435,7 +502,7 @@ export const apiGetDiscussion = async ({ user, search, division, active, page }:
return response.data;
};
export const apiGetDiscussionOne = async ({ id, user, cat }: { id: string, user: string, cat: 'data' | 'comment' }) => {
export const apiGetDiscussionOne = async ({ id, user, cat }: { id: string, user: string, cat: 'data' | 'comment' | 'file' }) => {
const response = await api.get(`mobile/discussion/${id}?user=${user}&cat=${cat}`);
return response.data;
};
@@ -445,8 +512,27 @@ export const apiSendDiscussionCommentar = async ({ data, id }: { data: { user: s
return response.data;
};
export const apiEditDiscussion = async ({ data, id }: { data: { user: string, desc: string }, id: string }) => {
const response = await api.post(`/mobile/discussion/${id}`, data)
export const apiEditDiscussionCommentar = async ({ data, id }: { data: { user: string, comment: string }, id: string }) => {
const response = await api.put(`/mobile/discussion/${id}/comment`, data)
return response.data;
};
export const apiDeleteDiscussionCommentar = async ({ data, id }: { data: { user: string }, id: string }) => {
const response = await api.delete(`/mobile/discussion/${id}/comment`, { data })
return response.data;
};
// export const apiEditDiscussion = async ({ data, id }: { data: { user: string, desc: string }, id: string }) => {
// const response = await api.post(`/mobile/discussion/${id}`, data)
// return response.data;
// };
export const apiEditDiscussion = async (data: FormData, id: string) => {
const response = await api.post(`/mobile/discussion/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
@@ -460,8 +546,17 @@ export const apiOpenCloseDiscussion = async (data: { user: string, status: numbe
return response.data
};
export const apiCreateDiscussion = async ({ data }: { data: { user: string, desc: string, idDivision: string } }) => {
const response = await api.post(`/mobile/discussion`, data)
// export const apiCreateDiscussion = async ({ data }: { data: { user: string, desc: string, idDivision: string } }) => {
// const response = await api.post(`/mobile/discussion`, data)
// return response.data;
// };
export const apiCreateDiscussion = async (data: FormData) => {
const response = await api.post(`/mobile/discussion`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
@@ -510,8 +605,13 @@ export const apiUpdateCalendar = async ({ data, id }: { data: { title: string, d
return response.data;
};
export const apiGetTask = async ({ user, status, search, division, page }: { user: string, status: string, search: string, division: string, page?: number }) => {
const response = await api.get(`mobile/task?user=${user}&status=${status}&division=${division}&search=${search}&page=${page}`);
export const apiGetTask = async ({ user, status, search, division, page, year }: { user: string, status: string, search: string, division: string, page?: number, year?: string }) => {
const response = await api.get(`mobile/task?user=${user}&status=${status}&division=${division}&search=${search}&page=${page}&year=${year}`);
return response.data;
};
export const apiGetTahunTask = async ({ user, division }: { user: string, division: string }) => {
const response = await api.get(`mobile/task/tahun?user=${user}&division=${division}`);
return response.data;
};

View File

@@ -75,6 +75,7 @@
"react-native-gesture-handler": "~2.24.0",
"react-native-gifted-charts": "^1.4.57",
"react-native-image-picker": "^8.2.1",
"react-native-image-viewing": "^0.2.2",
"react-native-mime-types": "^2.5.0",
"react-native-modal": "^14.0.0-rc.1",
"react-native-modal-datetime-picker": "^18.0.0",