Compare commits
15 Commits
amalia/29-
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 54a12669e9 | |||
| e254cf8ed2 | |||
| ddfee00410 | |||
| 1aa51adab0 | |||
| ebe8380829 | |||
| c421d267b9 | |||
| bbacd40ae9 | |||
| 9bab420f91 | |||
| 7d8b72fdfa | |||
| e9c11a889d | |||
| 10b74ccde9 | |||
| 73c6a19880 | |||
| 225ed63027 | |||
| f34df12b2f | |||
| a24a698f86 |
249
Panduan-Penggunaan-Aplikasi.md
Normal file
249
Panduan-Penggunaan-Aplikasi.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Panduan Aplikasi Desa+
|
||||
|
||||
## Daftar Isi
|
||||
|
||||
1. [Gambaran Umum Aplikasi](#gambaran-umum-aplikasi)
|
||||
2. [User Roles dan Hak Akses](#user-roles-dan-hak-akses)
|
||||
3. [Fitur-fitur Aplikasi](#fitur-fitur-aplikasi)
|
||||
4. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Gambaran Umum Aplikasi
|
||||
|
||||
Aplikasi Desa+ adalah platform digital berbasis mobile yang dirancang untuk khusus untuk pegawai desa dalam mengelola data dan memantau progres kegiatan internal. Aplikasi ini menyediakan berbagai fitur seperti pengelolaan data per divisi, pemantauan kegiatan umum, forum diskusi, pengumuman, hingga manajemen folder dokumen, aplikasi ini membantu meningkatkan efisiensi kerja, koordinasi, serta transparansi di lingkungan desa.
|
||||
|
||||
### Teknologi yang Digunakan
|
||||
|
||||
- React Native dengan Expo
|
||||
- Firebase (Authentication, Realtime Database, Cloud Messaging)
|
||||
- Redux Toolkit untuk manajemen state
|
||||
- TypeScript untuk type safety
|
||||
|
||||
## User Roles dan Hak Akses
|
||||
|
||||
Aplikasi Desa+ memiliki sistem hierarki peran pengguna sebagai berikut:
|
||||
|
||||
### 1. Super Admin
|
||||
|
||||
- **Hak akses:**
|
||||
- Semua fitur dan fungsi dalam aplikasi
|
||||
- Manajemen pengguna dengan role Wakil Super Admin, Admin, Wakil Admin, dan User
|
||||
- Akses ke semua data dan fungsi administratif
|
||||
|
||||
### 2. Wakil Super Admin
|
||||
|
||||
- **Hak akses:**
|
||||
- Manajemen pengguna dengan role Admin, Wakil Admin, dan User
|
||||
- Akses ke sebagian besar fitur administratif
|
||||
- Dapat mengelola banner
|
||||
|
||||
### 3. Admin
|
||||
|
||||
- **Hak akses:**
|
||||
- Manajemen pengguna dengan role Wakil Admin dan User
|
||||
- Akses ke fitur-fitur administratif dasar
|
||||
- Tidak dapat mengelola Wakil Super Admin dan Super Admin
|
||||
|
||||
### 4. Wakil Admin
|
||||
|
||||
- **Hak akses:**
|
||||
- Manajemen pengguna dengan role User
|
||||
- Akses terbatas ke fitur-fitur administratif
|
||||
- Tidak dapat mengelola Admin ke atas
|
||||
|
||||
### 5. User
|
||||
|
||||
- **Hak akses:**
|
||||
- Akses ke fitur-fitur umum
|
||||
- Tidak dapat mengelola pengguna lain
|
||||
- Tidak dapat mengakses fungsi administratif (kecuali dalam divisi dimana pengguna tersebut adalah anggota)
|
||||
|
||||
## Fitur-fitur Aplikasi
|
||||
|
||||
### 1. Otentikasi (Login & Verifikasi)
|
||||
|
||||
**Deskripsi:** Sistem login menggunakan nomor telepon dan verifikasi OTP (One Time Password)
|
||||
|
||||
- **Fungsi:** Memverifikasi identitas pengguna sebelum mengakses aplikasi
|
||||
- **Siapa yang bisa mengakses:** Semua pengguna yang terdaftar
|
||||
|
||||
### 2. Dashboard/Home Screen
|
||||
|
||||
**Deskripsi:** Tampilan utama aplikasi yang menampilkan informasi dan akses cepat ke berbagai fitur
|
||||
|
||||
- **Fungsi:** Menyediakan ringkasan informasi desa dan akses cepat ke fitur-fitur utama
|
||||
- **Siapa yang bisa mengakses:** Semua pengguna yang telah login
|
||||
- **Komponen:**
|
||||
- Carousel banner untuk promosi atau informasi penting
|
||||
- Fitur untuk mengakses semua fitur aplikasi
|
||||
- Grafik progres kegiatan
|
||||
- Grafik jumlah dokumen
|
||||
- Daftar kegiatan terupdate
|
||||
- Daftar divisi aktif
|
||||
- Daftar acara mendatang
|
||||
- Diskusi terbaru
|
||||
|
||||
### 3. Pencarian
|
||||
|
||||
**Deskripsi:** Fitur untuk mencari anggota, kegiatan dan divisi
|
||||
|
||||
- **Fungsi:** Mencari anggota, kegiatan dan divisi
|
||||
- **Siapa yang bisa mengakses:** Semua pengguna
|
||||
|
||||
### 4. Notifikasi
|
||||
|
||||
**Deskripsi:** Sistem notifikasi untuk memberitahu pengguna tentang aktivitas penting
|
||||
|
||||
- **Fungsi:** Memberitahu pengguna tentang pengumuman, komentar, atau aktivitas lainnya
|
||||
- **Siapa yang bisa mengakses:** Semua pengguna
|
||||
|
||||
### 5. Profil
|
||||
|
||||
**Deskripsi:** Fitur untuk melihat dan mengedit informasi pribadi pengguna
|
||||
|
||||
- **Fungsi:** Menampilkan dan mengelola informasi akun pengguna
|
||||
- **Siapa yang bisa mengakses:** Pengguna yang bersangkutan
|
||||
|
||||
### 6. Banner
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola banner promosi atau informasi penting di halaman utama
|
||||
|
||||
- **Fungsi:** Menampilkan informasi atau promosi penting di tampilan awal
|
||||
- **Siapa yang bisa mengakses:** Super Admin, Wakil Super Admin
|
||||
|
||||
### 7. Lembaga Desa
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola berbagai lembaga dalam desa
|
||||
|
||||
- **Fungsi:** Mengorganisir struktur organisasi desa berdasarkan lembaga
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin
|
||||
- Melihat: Super Admin
|
||||
|
||||
### 8. Jabatan
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola posisi atau jabatan
|
||||
|
||||
- **Fungsi:** Mengelola data jabatan
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Wakil Admin
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 9. Anggota
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola data pengguna
|
||||
|
||||
- **Fungsi:** Menyimpan dan mengelola informasi tentang pengguna
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Wakil Admin
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 10. Diskusi Umum
|
||||
|
||||
**Deskripsi:** Forum diskusi untuk komunikasi anggota terpilih
|
||||
|
||||
- **Fungsi:** Tempat berdiskusi mengenai berbagai topik yang berkaitan dengan desa
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||
- Melihat: Semua pengguna
|
||||
- Berkomentar: Pengguna terpilih
|
||||
|
||||
### 11. Kegiatan/Proyek
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola dan melacak proyek atau kegiatan desa
|
||||
|
||||
- **Fungsi:** Mengelola dan memonitor kemajuan proyek-proyek desa
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus/Membatalkan/Mengelola anggota: Super Admin, Wakil Super Admin, Admin
|
||||
- Mengelola detail (file, task, link, laporan) : Super Admin, Wakil Super Admin, Admin, Anggota dari kegiatan
|
||||
- Melihat: Semua pengguna
|
||||
- **Status Kegiatan:**
|
||||
- Segera: Proyek yang akan segera dimulai
|
||||
- Dikerjakan: Proyek yang sedang dalam proses pengerjaan
|
||||
- Selesai: Proyek yang telah selesai
|
||||
- Batal: Proyek yang dibatalkan
|
||||
|
||||
### 12. Pengumuman
|
||||
|
||||
**Deskripsi:** Fitur untuk membuat, melihat, dan mengelola pengumuman desa
|
||||
|
||||
- **Fungsi:** Menyebarkan informasi penting kepada anggota divisi terpilih
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||
- Melihat:
|
||||
- Super admin: Semua pengumuman
|
||||
- Wakil super admin & admin : Pengumuman sesuai lembaga desa
|
||||
- Lainnya: Pengumuman yang ditujukan ke divisi mereka
|
||||
|
||||
### 13. Divisi
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola data desa berdasarkan divisi
|
||||
|
||||
- **Fungsi:** Mengorganisir tugas-tugas berdasarkan divisi-divisi tertentu
|
||||
- **Catatan:** Anggota divisi (role : Wakil Admin dan User) yg diangkat menjadi "Admin Divisi", mendapat akses khusus untuk mengelola divisi tersebut
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin
|
||||
- Edit Divisi / Non aktifkan Divisi tertentu / Mengelola Anggota divisi tertentu : Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||
- Laporan semua divisi : Super Admin, Wakil Super Admin
|
||||
- Laporan divisi tertentu : semua pengguna
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 14. Diskusi Divisi
|
||||
|
||||
**Deskripsi:** Forum diskusi khusus untuk masing-masing divisi
|
||||
|
||||
- **Fungsi:** Tempat berdiskusi secara internal dalam divisi
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||
- Memberi komentar : Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 15. Tugas Divisi
|
||||
|
||||
**Deskripsi:** Fitur untuk mengelola tugas-tugas dalam masing-masing divisi
|
||||
|
||||
- **Fungsi:** Menetapkan dan melacak tugas-tugas yang harus diselesaikan oleh anggota divisi
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Admin Divisi
|
||||
- Mengelola detail (file, task, link, laporan) : Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 16. Dokumen Divisi
|
||||
|
||||
**Deskripsi:** Sistem manajemen dokumen untuk menyimpan dan mengelola file-file disetiap divisi
|
||||
|
||||
- **Fungsi:** Menyimpan dokumen penting dalam struktur folder disetiap divisi
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||
- Melihat: Semua pengguna
|
||||
|
||||
### 17. Kalender/Acara Divisi
|
||||
|
||||
**Deskripsi:** Fitur untuk menjadwalkan dan mengelola acara-acara desa disetiap divisi
|
||||
|
||||
- **Fungsi:** Menjadwalkan kegiatan dan acara penting desa disetiap divisi
|
||||
- **Siapa yang bisa mengakses:**
|
||||
- Pembuatan/Edit/Hapus: Super Admin, Wakil Super Admin, Admin, Anggota divisi
|
||||
- Melihat: Semua pengguna
|
||||
- Riwayat: Semua pengguna
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Masalah Login
|
||||
|
||||
- Pastikan nomor telepon yang dimasukkan sudah benar dan terdaftar
|
||||
- Pastikan koneksi internet stabil saat menerima OTP
|
||||
- Jika tidak menerima OTP, coba kirim ulang setelah beberapa menit
|
||||
|
||||
### Tidak Bisa Mengakses Fitur Tertentu
|
||||
|
||||
- Pastikan peran Anda memiliki hak akses ke fitur tersebut
|
||||
- Beberapa fitur hanya tersedia untuk peran tertentu (misalnya Admin ke atas)
|
||||
|
||||
### Lupa Password
|
||||
|
||||
- Aplikasi ini menggunakan sistem login OTP, jadi tidak ada password yang disimpan
|
||||
- Cukup gunakan nomor telepon dan minta OTP kembali
|
||||
|
||||
## Dukungan dan Bantuan
|
||||
|
||||
Jika Anda mengalami masalah atau memiliki pertanyaan tentang penggunaan aplikasi, silakan hubungi tim pengembang aplikasi.
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -80,8 +80,6 @@ export default function DetailDiscussionGeneral() {
|
||||
})
|
||||
const [viewEdit, setViewEdit] = useState(false)
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const onValueChange = reference.on('value', snapshot => {
|
||||
if (snapshot.val() == null) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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
130
constants/FileExtensions.ts
Normal 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());
|
||||
};
|
||||
@@ -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;
|
||||
18
lib/api.ts
18
lib/api.ts
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user