Compare commits

...

11 Commits

Author SHA1 Message Date
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
17 changed files with 1058 additions and 190 deletions

View File

@@ -0,0 +1,334 @@
# Panduan Penggunaan Aplikasi Desa+
## Daftar Isi
1. [Gambaran Umum Aplikasi](#gambaran-umum-aplikasi)
2. [Fitur-fitur Utama](#fitur-fitur-utama)
3. [User Roles dan Hak Akses](#user-roles-dan-hak-akses)
4. [Petunjuk Penggunaan](#petunjuk-penggunaan)
5. [Troubleshooting](#troubleshooting)
## Gambaran Umum Aplikasi
Aplikasi Desa+ adalah platform digital berbasis mobile yang dirancang 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.
### Teknologi yang Digunakan
- React Native dengan Expo
- Firebase (Authentication, Realtime Database, Cloud Messaging)
- Redux Toolkit untuk manajemen state
- TypeScript untuk type safety
## Fitur-fitur Utama
### 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
- **Cara menggunakan:**
1. Masukkan nomor telepon yang terdaftar
2. Klik "Kirim OTP"
3. Masukkan kode OTP yang diterima melalui WhatsApp
4. Klik "Verifikasi" untuk masuk ke aplikasi
### 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
- Grafik progres kegiatan
- Grafik dokumen
- Daftar kegiatan terbaru
- Daftar divisi aktif
- Daftar acara mendatang
- Diskusi terbaru
### 3. Pengumuman
**Deskripsi:** Fitur untuk membuat, melihat, dan mengelola pengumuman desa
- **Fungsi:** Menyebarkan informasi penting kepada seluruh warga
- **Siapa yang bisa mengakses:**
- Pembuatan/Edit/Hapus: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- **Cara menggunakan:**
1. Pilih menu "Pengumuman" dari fitur utama
2. Untuk membuat pengumuman baru, klik tombol "+" di kanan atas
3. Isi judul dan deskripsi pengumuman
4. Pilih grup yang akan menerima pengumuman (opsional)
5. Klik "Simpan" untuk menerbitkan
### 4. Diskusi Umum
**Deskripsi:** Forum diskusi untuk komunikasi antar warga dan pihak pengelola
- **Fungsi:** Tempat berdiskusi mengenai berbagai topik yang berkaitan dengan desa
- **Siapa yang bisa mengakses:**
- Pembuatan/Edit/Hapus: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- Berkomentar: Semua pengguna
- **Cara menggunakan:**
1. Pilih menu "Diskusi" dari fitur utama
2. Untuk membuat diskusi baru, klik tombol "+" di kanan atas
3. Isi judul dan deskripsi diskusi
4. Pilih anggota yang akan menjadi partisipan
5. Klik "Simpan" untuk membuat diskusi
6. Klik pada judul diskusi untuk membuka dan memberikan komentar
### 5. 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: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- Status (Buka/Tutup): Berdasarkan hak akses
- **Status Kegiatan:**
- Segera: Proyek yang akan segera dimulai
- Dikerjakan: Proyek yang sedang dalam proses pengerjaan
- Selesai: Proyek yang telah selesai
- Batal: Proyek yang dibatalkan
- **Cara menggunakan:**
1. Pilih menu "Kegiatan" dari fitur utama
2. Untuk membuat kegiatan baru, klik tombol "+" di kanan atas
3. Isi nama kegiatan dan deskripsi
4. Pilih tahun pelaksanaan
5. Tambahkan anggota yang terlibat
6. Klik "Simpan" untuk membuat kegiatan
### 6. Divisi
**Deskripsi:** Fitur untuk mengelola struktur organisasi desa berdasarkan divisi
- **Fungsi:** Mengorganisir warga dan tugas-tugas berdasarkan divisi-divisi tertentu
- **Siapa yang bisa mengakses:**
- Pembuatan/Edit/Hapus: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- Anggota: Berdasarkan divisi yang diikuti
- **Cara menggunakan:**
1. Pilih menu "Divisi" dari fitur utama
2. Untuk membuat divisi baru, klik tombol "+" di kanan atas
3. Isi nama dan deskripsi divisi
4. Pilih grup induk (jika ada)
5. Tambahkan anggota dan admin divisi
6. Klik "Simpan" untuk membuat divisi
7. Klik pada nama divisi untuk melihat detail dan fitur dalam divisi tersebut
### 7. Anggota
**Deskripsi:** Fitur untuk mengelola data warga atau anggota desa
- **Fungsi:** Menyimpan dan mengelola informasi tentang warga desa
- **Siapa yang bisa mengakses:**
- Pembuatan/Edit/Hapus: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- **Cara menggunakan:**
1. Pilih menu "Anggota" dari fitur utama
2. Gunakan fitur pencarian untuk menemukan anggota tertentu
3. Gunakan filter untuk menampilkan anggota aktif/tidak aktif
4. Klik pada nama anggota untuk melihat detail profil
5. Untuk menambah anggota baru, klik tombol "+" di kanan atas
### 8. Jabatan
**Deskripsi:** Fitur untuk mengelola posisi atau jabatan dalam organisasi desa
- **Fungsi:** Mendefinisikan struktur jabatan dalam lembaga desa
- **Siapa yang bisa mengakses:**
- Pembuatan/Edit/Hapus: Super Admin, Admin, Deputy Super Admin
- Melihat: Semua pengguna
- **Cara menggunakan:**
1. Pilih menu "Jabatan" dari fitur utama
2. Gunakan fitur pencarian untuk menemukan jabatan tertentu
3. Gunakan filter untuk menampilkan jabatan aktif/tidak aktif
4. Untuk menambah jabatan baru, klik tombol "+" di kanan atas
### 9. 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, Developer
- Melihat: Semua pengguna
- **Cara menggunakan:**
1. Pilih menu "Lembaga Desa" dari fitur utama
2. Gunakan fitur pencarian untuk menemukan lembaga tertentu
3. Gunakan filter untuk menampilkan lembaga aktif/tidak aktif
4. Untuk menambah lembaga baru, klik tombol "+" di kanan atas
### 10. Diskusi Divisi
**Deskripsi:** Forum diskusi khusus untuk masing-masing divisi
- **Fungsi:** Tempat berdiskusi secara internal dalam divisi
- **Siapa yang bisa mengakses:** Hanya anggota divisi yang bersangkutan
- **Cara menggunakan:**
1. Pilih sebuah divisi dari menu "Divisi"
2. Pilih submenu "Diskusi Divisi"
3. Klik tombol "+" untuk membuat diskusi baru
4. Isi judul dan deskripsi diskusi
5. Klik "Simpan" untuk membuat diskusi
### 11. 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:** Hanya anggota divisi yang bersangkutan
- **Cara menggunakan:**
1. Pilih sebuah divisi dari menu "Divisi"
2. Pilih submenu "Tugas Divisi"
3. Klik tombol "+" untuk membuat tugas baru
4. Isi nama tugas dan detail lainnya
5. Tambahkan anggota yang bertugas
6. Klik "Simpan" untuk membuat tugas
### 12. Dokumen
**Deskripsi:** Sistem manajemen dokumen untuk menyimpan dan mengelola file-file desa
- **Fungsi:** Menyimpan dokumen penting dalam struktur folder
- **Siapa yang bisa mengakses:** Akses berdasarkan divisi dan hak akses
- **Cara menggunakan:**
1. Pilih sebuah divisi dari menu "Divisi"
2. Pilih submenu "Dokumen"
3. Buat folder baru atau upload file
4. Gunakan fitur share untuk membagikan dokumen ke divisi lain
### 13. Kalender/Acara
**Deskripsi:** Fitur untuk menjadwalkan dan mengelola acara-acara desa
- **Fungsi:** Menjadwalkan kegiatan dan acara penting desa
- **Siapa yang bisa mengakses:** Hanya anggota divisi yang bersangkutan
- **Cara menggunakan:**
1. Pilih sebuah divisi dari menu "Divisi"
2. Pilih submenu "Kalender"
3. Klik tombol "+" untuk membuat acara baru
4. Isi detail acara (judul, deskripsi, tanggal, waktu)
5. Tambahkan anggota yang ikut serta
6. Klik "Simpan" untuk membuat acara
### 14. 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
- **Cara menggunakan:**
1. Klik ikon notifikasi di bagian atas layar
2. Lihat daftar notifikasi yang belum dibaca
3. Klik pada notifikasi untuk membuka konten terkait
### 15. Profil
**Deskripsi:** Fitur untuk melihat dan mengedit informasi pribadi pengguna
- **Fungsi:** Menampilkan dan mengelola informasi akun pengguna
- **Siapa yang bisa mengakses:** Pengguna yang bersangkutan
- **Cara menggunakan:**
1. Klik menu "Profil" dari navigasi bawah
2. Lihat informasi pribadi
3. Klik "Edit" untuk mengubah informasi (tidak tersedia untuk developer)
### 16. 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, Deputy Super Admin, Developer
- **Cara menggunakan:**
1. Pilih menu "Banner" dari fitur utama (jika tersedia)
2. Klik tombol "+" untuk membuat banner baru
3. Upload gambar banner dan isi deskripsi
4. Klik "Simpan" untuk menerbitkan banner
## User Roles dan Hak Akses
Aplikasi Desa+ memiliki sistem hierarki peran pengguna sebagai berikut:
### 1. Developer
- **Deskripsi:** Peran tertinggi dengan semua hak akses
- **Hak akses:**
- Semua fitur dan fungsi dalam aplikasi
- Manajemen semua data dan pengaturan sistem
### 2. Super Admin (Supadmin)
- **Deskripsi:** Administrator utama desa
- **Hak akses:**
- Semua fitur kecuali beberapa fungsi sistem tingkat tinggi
- Manajemen Deputy Super Admin, Admin, Co-Admin, dan User
- Akses ke semua data dan fungsi administratif
### 3. Deputy Super Admin (Cosupadmin)
- **Deskripsi:** Wakil administrator utama
- **Hak akses:**
- Manajemen Admin, Co-Admin, dan User
- Akses ke sebagian besar fitur administratif
- Dapat mengelola banner
### 4. Admin
- **Deskripsi:** Administrator tingkat menengah
- **Hak akses:**
- Manajemen Co-Admin dan User
- Akses ke fitur-fitur administratif dasar
- Tidak dapat mengelola Deputy Super Admin dan Super Admin
### 5. Deputy Admin (Coadmin)
- **Deskripsi:** Wakil administrator
- **Hak akses:**
- Manajemen User
- Akses terbatas ke fitur-fitur administratif
- Tidak dapat mengelola Admin ke atas
### 6. User
- **Deskripsi:** Pengguna biasa
- **Hak akses:**
- Akses ke fitur-fitur umum
- Tidak dapat mengelola pengguna lain
- Tidak dapat mengakses fungsi administratif
## Petunjuk Penggunaan
### Login ke Aplikasi
1. Buka aplikasi Desa+
2. Masukkan nomor telepon yang terdaftar
3. Klik "Kirim OTP"
4. Cek WhatsApp untuk menerima kode OTP
5. Masukkan kode OTP yang diterima
6. Klik "Verifikasi" untuk masuk ke aplikasi
### Navigasi Utama
- **Home:** Tampilan utama dengan informasi dan akses cepat
- **Fitur:** Menu utama untuk mengakses semua fitur aplikasi
- **Pencarian:** Mencari konten di seluruh aplikasi
- **Notifikasi:** Melihat pemberitahuan penting
- **Profil:** Informasi akun dan pengaturan pribadi
### Membuat Pengumuman Baru
1. Pilih menu "Fitur" dari navigasi bawah
2. Klik "Pengumuman"
3. Klik tombol "+" di kanan atas
4. Isi judul dan deskripsi pengumuman
5. Pilih grup sasaran (opsional)
6. Klik "Simpan"
### Bergabung dalam Diskusi
1. Pilih menu "Fitur" dari navigasi bawah
2. Klik "Diskusi"
3. Pilih diskusi yang ingin diikuti
4. Ketik komentar di kotak bawah
5. Klik "Kirim" untuk mengirim komentar
### Mengelola Divisi
1. Pilih menu "Fitur" dari navigasi bawah
2. Klik "Divisi"
3. Klik pada nama divisi untuk melihat detail
4. Di dalam detail divisi, Anda dapat mengakses:
- Tugas Divisi
- Dokumen Divisi
- Diskusi Divisi
- Kalender Divisi
## 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)
### Notifikasi Tidak Muncul
- Pastikan izin notifikasi diaktifkan di pengaturan aplikasi
- Pastikan aplikasi tetap berjalan di latar belakang
### Gambar Tidak Muncul
- Periksa koneksi internet
- Coba refresh halaman atau restart aplikasi
### 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 administrator desa atau tim pengembang aplikasi.
---
*Dokumen ini terakhir diperbarui pada 4 Februari 2026*

View File

@@ -1,10 +1,10 @@
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
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";
@@ -14,24 +14,46 @@ 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 { Alert, Dimensions, Platform, 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 [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string }[]>([])
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
@@ -39,13 +61,25 @@ export default function DetailAnnouncement() {
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 })
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
if (response.success) {
setData(response.data)
setDataMember(response.member)
@@ -69,30 +103,47 @@ 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 = (item: { idStorage: string; name: string; extension: string }) => {
if (Platform.OS == 'android') setLoadingOpen(true)
let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
const fileName = item.name + '.' + item.extension;
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
const mimeType = mime.lookup(fileName)
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);
FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => {
const contentURL = await FileSystem.getContentUriAsync(uri);
setLoadingOpen(false)
try {
if (Platform.OS == 'android') {
if (Platform.OS === 'android') {
await startActivityAsync(
'android.intent.action.VIEW',
{
@@ -101,15 +152,26 @@ export default function DetailAnnouncement() {
type: mimeType as string,
}
);
} else if (Platform.OS == 'ios') {
Sharing.shareAsync(localPath);
} else if (Platform.OS === 'ios') {
await Sharing.shareAsync(localPath);
}
} catch (error) {
Alert.alert('INFO', 'Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini');
} finally {
if (Platform.OS == 'android') setLoadingOpen(false)
} 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 (
@@ -139,7 +201,7 @@ export default function DetailAnnouncement() {
/>
}
>
<View style={[Styles.p15]}>
<View style={[Styles.p15, Styles.mb50]}>
<View style={[Styles.wrapPaper]}>
{
loading ?
@@ -184,12 +246,20 @@ export default function DetailAnnouncement() {
</View>
{dataFile.map((item, index) => (
<BorderBottomItem
key={index}
key={`${item.id}-${index}`}
borderType="bottom"
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
icon={<MaterialCommunityIcons
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={25}
color="black"
/>}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }}
onPress={() => {
isImageFile(item.extension) ?
handleChooseFile(item)
: openFile(item)
}}
/>
))}
</View>
@@ -230,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

@@ -80,8 +80,6 @@ export default function DetailDiscussionGeneral() {
})
const [viewEdit, setViewEdit] = useState(false)
useEffect(() => {
const onValueChange = reference.on('value', snapshot => {
if (snapshot.val() == null) {

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

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

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

@@ -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,14 +1,17 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv";
import { isImageFile } from "@/constants/FileExtensions";
import Styles from "@/constants/Styles";
import { Ionicons } from "@expo/vector-icons";
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 { Alert, Dimensions, Platform, Pressable, View } from "react-native";
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";
@@ -33,26 +36,47 @@ type Props = {
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 = (item: { idStorage: string; name: string; extension: string }) => {
if (Platform.OS == 'android') setLoadingOpen(true)
let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
const fileName = item.name + '.' + item.extension;
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
const mimeType = mime.lookup(fileName)
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);
FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => {
const contentURL = await FileSystem.getContentUriAsync(uri);
setLoadingOpen(false)
try {
if (Platform.OS == 'android') {
if (Platform.OS === 'android') {
await startActivityAsync(
'android.intent.action.VIEW',
{
@@ -61,89 +85,148 @@ export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress
type: mimeType as string,
}
);
} else if (Platform.OS == 'ios') {
Sharing.shareAsync(localPath);
} else if (Platform.OS === 'ios') {
await Sharing.shareAsync(localPath);
}
} catch (error) {
Alert.alert('INFO', 'Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini');
} finally {
if (Platform.OS == 'android') setLoadingOpen(false)
} 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>
<>
<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>
{
subtitle &&
typeof subtitle == "string"
? <Text style={[Styles.textMediumNormal, { lineHeight: 15, color: textColorFix }]}>{subtitle}</Text>
: <View style={{ alignItems: 'flex-start' }}>
{subtitle}
</View>
rightTopInfo && <Text style={[Styles.textInformation, Styles.mt05, { color: textColorFix }]}>{rightTopInfo}</Text>
}
</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={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }}
>
<Ionicons name="document-text" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{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>
{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>
)
}
</Pressable>
)}
FooterComponent={({ imageIndex }) => (
<View style={{
paddingBottom: 20,
paddingHorizontal: 16,
alignItems: 'center',
}}>
<Text style={{ color: 'white', fontSize: 16 }}>{chooseFile?.name}.{chooseFile?.extension}</Text>
</View>
)}
/>
</>
)
}

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

@@ -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}
/>
</>
)
}

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

@@ -651,6 +651,42 @@ const Styles = StyleSheet.create({
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',
}
})
export default Styles;

View File

@@ -315,8 +315,13 @@ export const apiDeleteAnnouncement = async (data: { user: string }, id: string)
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;
};
@@ -600,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;
};