From eccfe293876e4f9a01fbf9b0ee2315cfb815f093 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Wed, 6 May 2026 12:32:33 +0800 Subject: [PATCH] feat: tambah fitur lampiran file pada tugas kegiatan dan tugas divisi --- .../task/[detail]/tugas-file/[taskId].tsx | 382 ++++++++++++++++++ .../project/[id]/tugas-file/[taskId].tsx | 377 +++++++++++++++++ components/itemSectionTanggalTugas.tsx | 163 +++++++- components/project/sectionTanggalTugas.tsx | 95 ++--- components/task/sectionTanggalTugasTask.tsx | 10 + lib/api.ts | 44 ++ 6 files changed, 996 insertions(+), 75 deletions(-) create mode 100644 app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx create mode 100644 app/(application)/project/[id]/tugas-file/[taskId].tsx diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx new file mode 100644 index 0000000..2112500 --- /dev/null +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx @@ -0,0 +1,382 @@ +import AppHeader from "@/components/AppHeader"; +import BorderBottomItem from "@/components/borderBottomItem"; +import { ButtonForm } from "@/components/buttonForm"; +import ButtonSelect from "@/components/buttonSelect"; +import DrawerBottom from "@/components/drawerBottom"; +import ModalConfirmation from "@/components/ModalConfirmation"; +import ModalLoading from "@/components/modalLoading"; +import MenuItemRow from "@/components/menuItemRow"; +import Skeleton from "@/components/skeleton"; +import Text from "@/components/Text"; +import { ConstEnv } from "@/constants/ConstEnv"; +import Styles from "@/constants/Styles"; +import { + apiAddTugasTaskFile, + apiDeleteTugasTaskFile, + apiGetTaskOne, + apiGetTugasTaskFile, + apiLinkTugasTaskFile, +} from "@/lib/api"; +import { setUpdateTask } from "@/lib/taskUpdate"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { useTheme } from "@/providers/ThemeProvider"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as DocumentPicker from "expo-document-picker"; +import * as FileSystem from "expo-file-system"; +import { startActivityAsync } from "expo-intent-launcher"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import * as Sharing from "expo-sharing"; +import { useEffect, useState } from "react"; +import { + ActivityIndicator, + Alert, + Platform, + SafeAreaView, + ScrollView, + View, +} from "react-native"; +import * as mime from "react-native-mime-types"; +import Toast from "react-native-toast-message"; +import { useDispatch, useSelector } from "react-redux"; + +type FileItem = { + id: string; // DivisionProjectTaskFile.id + idFile: string; // DivisionProjectFile.id + name: string; + extension: string; + idStorage: string; +}; + +type ProjectFile = { + id: string; + name: string; + extension: string; + idStorage: string; +}; + +export default function TugasFileScreen() { + const { colors } = useTheme(); + const { id, detail, taskId, member: memberParam } = useLocalSearchParams<{ + id: string; + detail: string; + taskId: string; + member: string; + }>(); + const { token, decryptToken } = useAuthSession(); + const dispatch = useDispatch(); + const update = useSelector((state: any) => state.taskUpdate); + const entityUser = useSelector((state: any) => state.user); + const isMember = memberParam === "true"; + const canEdit = isMember || (entityUser.role !== "user" && entityUser.role !== "coadmin"); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingOpen, setLoadingOpen] = useState(false); + const [loadingUpload, setLoadingUpload] = useState(false); + const [loadingLink, setLoadingLink] = useState(false); + + const [selectFile, setSelectFile] = useState(null); + const [isMenuModal, setMenuModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const [projectFiles, setProjectFiles] = useState([]); + const [isPickerModal, setPickerModal] = useState(false); + const [loadingProjectFiles, setLoadingProjectFiles] = useState(false); + const [selectedProjectFiles, setSelectedProjectFiles] = useState([]); + + const arrSkeleton = Array.from({ length: 4 }); + + async function loadFiles() { + try { + setLoading(true); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetTugasTaskFile({ user: hasil, id: taskId }); + setData(response.data ?? []); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + + async function loadProjectFiles() { + try { + setLoadingProjectFiles(true); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetTaskOne({ id: detail, user: hasil, cat: "file" }); + setProjectFiles(response.data ?? []); + } catch (error) { + console.error(error); + } finally { + setLoadingProjectFiles(false); + } + } + + useEffect(() => { + loadFiles(); + }, []); + + const openFile = () => { + setMenuModal(false); + setLoadingOpen(true); + const remoteUrl = ConstEnv.url_storage + "/files/" + selectFile?.idStorage; + const fileName = selectFile?.name + "." + selectFile?.extension; + const localPath = `${FileSystem.documentDirectory}/${fileName}`; + const mimeType = mime.lookup(fileName); + + FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => { + const contentURL = await FileSystem.getContentUriAsync(uri); + try { + if (Platform.OS === "android") { + await startActivityAsync("android.intent.action.VIEW", { + data: contentURL, + flags: 1, + type: mimeType as string, + }); + } else { + Sharing.shareAsync(localPath); + } + } catch { + Alert.alert("INFO", "Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini"); + } finally { + setLoadingOpen(false); + } + }); + }; + + async function handleDelete() { + try { + const hasil = await decryptToken(String(token?.current)); + const response = await apiDeleteTugasTaskFile({ user: hasil }, String(selectFile?.id)); + if (response.success) { + Toast.show({ type: "small", text1: "Berhasil menghapus file" }); + dispatch(setUpdateTask({ ...update, task: !update.task })); + loadFiles(); + } else { + Toast.show({ type: "small", text1: response.message }); + } + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menghapus file"; + Toast.show({ type: "small", text1: message }); + } finally { + setMenuModal(false); + } + } + + async function handleUpload() { + const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true }); + if (result.canceled) return; + + try { + setLoadingUpload(true); + const hasil = await decryptToken(String(token?.current)); + const fd = new FormData(); + + for (let i = 0; i < result.assets.length; i++) { + fd.append(`file${i}`, { + uri: result.assets[i].uri, + type: "application/octet-stream", + name: result.assets[i].name, + } as any); + } + fd.append("data", JSON.stringify({ user: hasil })); + + const response = await apiAddTugasTaskFile({ data: fd, id: taskId }); + if (response.success) { + Toast.show({ type: "small", text1: "Berhasil menambahkan file" }); + dispatch(setUpdateTask({ ...update, task: !update.task })); + loadFiles(); + } else { + Toast.show({ type: "small", text1: response.message }); + } + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menambahkan file"; + Toast.show({ type: "small", text1: message }); + } finally { + setLoadingUpload(false); + } + } + + function toggleProjectFileSelect(id: string) { + setSelectedProjectFiles((prev) => + prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] + ); + } + + async function handleLinkFiles() { + if (selectedProjectFiles.length === 0) return; + try { + setLoadingLink(true); + const hasil = await decryptToken(String(token?.current)); + for (const idFile of selectedProjectFiles) { + await apiLinkTugasTaskFile({ user: hasil, idFile, id: taskId }); + } + Toast.show({ type: "small", text1: "Berhasil menambahkan file" }); + dispatch(setUpdateTask({ ...update, task: !update.task })); + setPickerModal(false); + setSelectedProjectFiles([]); + loadFiles(); + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menambahkan file"; + Toast.show({ type: "small", text1: message }); + } finally { + setLoadingLink(false); + } + } + + const attachedFileIds = new Set(data.map((f) => f.idFile)); + + return ( + + ( + router.back()} + /> + ), + }} + /> + + + + + {canEdit && ( + <> + + { + setSelectedProjectFiles([]); + setPickerModal(true); + loadProjectFiles(); + }} + /> + + )} + + {loadingUpload && } + + + File Terlampir + + {loading ? ( + arrSkeleton.map((_, index) => ( + + )) + ) : data.length > 0 ? ( + data.map((item, index) => ( + } + title={item.name + "." + item.extension} + titleWeight="normal" + onPress={() => { + setSelectFile(item); + setMenuModal(true); + }} + /> + )) + ) : ( + + Tidak ada file + + )} + + + + + + {/* Menu per file */} + + + } + title="Lihat / Share" + onPress={openFile} + /> + {canEdit && ( + } + title="Hapus" + onPress={() => { + setMenuModal(false); + setTimeout(() => setShowDeleteModal(true), 600); + }} + /> + )} + + + + { + setShowDeleteModal(false); + handleDelete(); + }} + onCancel={() => setShowDeleteModal(false)} + confirmText="Hapus" + cancelText="Batal" + /> + + {/* Picker file dari proyek */} + + + {loadingProjectFiles ? ( + + ) : projectFiles.length > 0 ? ( + projectFiles.map((item, index) => { + const isAttached = attachedFileIds.has(item.id); + const isSelected = selectedProjectFiles.includes(item.id); + return ( + + + ) : ( + + ) + } + title={item.name + "." + item.extension} + titleWeight="normal" + onPress={() => !isAttached && toggleProjectFileSelect(item.id)} + bgColor="transparent" + /> + + ); + }) + ) : ( + + Tidak ada file tersedia + + )} + + {projectFiles.length > 0 && ( + + + + )} + + + ); +} diff --git a/app/(application)/project/[id]/tugas-file/[taskId].tsx b/app/(application)/project/[id]/tugas-file/[taskId].tsx new file mode 100644 index 0000000..3f47532 --- /dev/null +++ b/app/(application)/project/[id]/tugas-file/[taskId].tsx @@ -0,0 +1,377 @@ +import AppHeader from "@/components/AppHeader"; +import BorderBottomItem from "@/components/borderBottomItem"; +import { ButtonForm } from "@/components/buttonForm"; +import ButtonSelect from "@/components/buttonSelect"; +import DrawerBottom from "@/components/drawerBottom"; +import MenuItemRow from "@/components/menuItemRow"; +import ModalConfirmation from "@/components/ModalConfirmation"; +import ModalLoading from "@/components/modalLoading"; +import Skeleton from "@/components/skeleton"; +import Text from "@/components/Text"; +import { ConstEnv } from "@/constants/ConstEnv"; +import Styles from "@/constants/Styles"; +import { + apiAddProjectTaskFile, + apiDeleteProjectTaskFile, + apiGetProjectOne, + apiGetProjectTaskFile, + apiLinkProjectTaskFile, +} from "@/lib/api"; +import { setUpdateProject } from "@/lib/projectUpdate"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { useTheme } from "@/providers/ThemeProvider"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as DocumentPicker from "expo-document-picker"; +import * as FileSystem from "expo-file-system"; +import { startActivityAsync } from "expo-intent-launcher"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import * as Sharing from "expo-sharing"; +import { useEffect, useState } from "react"; +import { + ActivityIndicator, + Alert, + Platform, + SafeAreaView, + ScrollView, + View, +} from "react-native"; +import * as mime from "react-native-mime-types"; +import Toast from "react-native-toast-message"; +import { useDispatch, useSelector } from "react-redux"; + +type FileItem = { + id: string; // ProjectTaskFile.id + idFile: string; // ProjectFile.id + name: string; + extension: string; + idStorage: string; +}; + +type ProjectFile = { + id: string; + name: string; + extension: string; + idStorage: string; +}; + +export default function ProjectTugasFileScreen() { + const { colors } = useTheme(); + const { id, taskId, member: memberParam } = useLocalSearchParams<{ id: string; taskId: string; member: string }>(); + const { token, decryptToken } = useAuthSession(); + const dispatch = useDispatch(); + const update = useSelector((state: any) => state.projectUpdate); + const entityUser = useSelector((state: any) => state.user); + const isMember = memberParam === "true"; + const canEdit = isMember || (entityUser.role !== "user" && entityUser.role !== "coadmin"); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingOpen, setLoadingOpen] = useState(false); + const [loadingUpload, setLoadingUpload] = useState(false); + const [loadingLink, setLoadingLink] = useState(false); + + const [selectFile, setSelectFile] = useState(null); + const [isMenuModal, setMenuModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const [projectFiles, setProjectFiles] = useState([]); + const [isPickerModal, setPickerModal] = useState(false); + const [loadingProjectFiles, setLoadingProjectFiles] = useState(false); + const [selectedProjectFiles, setSelectedProjectFiles] = useState([]); + + const arrSkeleton = Array.from({ length: 4 }); + + async function loadFiles() { + try { + setLoading(true); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetProjectTaskFile({ user: hasil, id: taskId }); + setData(response.data ?? []); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + + async function loadProjectFiles() { + try { + setLoadingProjectFiles(true); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetProjectOne({ user: hasil, cat: "file", id }); + setProjectFiles(response.data ?? []); + } catch (error) { + console.error(error); + } finally { + setLoadingProjectFiles(false); + } + } + + useEffect(() => { + loadFiles(); + }, []); + + const openFile = () => { + setMenuModal(false); + setLoadingOpen(true); + const remoteUrl = ConstEnv.url_storage + "/files/" + selectFile?.idStorage; + const fileName = selectFile?.name + "." + selectFile?.extension; + const localPath = `${FileSystem.documentDirectory}/${fileName}`; + const mimeType = mime.lookup(fileName); + + FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => { + const contentURL = await FileSystem.getContentUriAsync(uri); + try { + if (Platform.OS === "android") { + await startActivityAsync("android.intent.action.VIEW", { + data: contentURL, + flags: 1, + type: mimeType as string, + }); + } else { + Sharing.shareAsync(localPath); + } + } catch { + Alert.alert("INFO", "Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini"); + } finally { + setLoadingOpen(false); + } + }); + }; + + async function handleDelete() { + try { + const hasil = await decryptToken(String(token?.current)); + const response = await apiDeleteProjectTaskFile({ user: hasil }, String(selectFile?.id)); + if (response.success) { + Toast.show({ type: "small", text1: "Berhasil menghapus file" }); + dispatch(setUpdateProject({ ...update, task: !update.task })); + loadFiles(); + } else { + Toast.show({ type: "small", text1: response.message }); + } + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menghapus file"; + Toast.show({ type: "small", text1: message }); + } finally { + setMenuModal(false); + } + } + + async function handleUpload() { + const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true }); + if (result.canceled) return; + + try { + setLoadingUpload(true); + const hasil = await decryptToken(String(token?.current)); + const fd = new FormData(); + + for (let i = 0; i < result.assets.length; i++) { + fd.append(`file${i}`, { + uri: result.assets[i].uri, + type: "application/octet-stream", + name: result.assets[i].name, + } as any); + } + fd.append("data", JSON.stringify({ user: hasil })); + + const response = await apiAddProjectTaskFile({ data: fd, id: taskId }); + if (response.success) { + Toast.show({ type: "small", text1: "Berhasil menambahkan file" }); + dispatch(setUpdateProject({ ...update, task: !update.task })); + loadFiles(); + } else { + Toast.show({ type: "small", text1: response.message }); + } + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menambahkan file"; + Toast.show({ type: "small", text1: message }); + } finally { + setLoadingUpload(false); + } + } + + function toggleProjectFileSelect(fileId: string) { + setSelectedProjectFiles((prev) => + prev.includes(fileId) ? prev.filter((v) => v !== fileId) : [...prev, fileId] + ); + } + + async function handleLinkFiles() { + if (selectedProjectFiles.length === 0) return; + try { + setLoadingLink(true); + const hasil = await decryptToken(String(token?.current)); + for (const idFile of selectedProjectFiles) { + await apiLinkProjectTaskFile({ user: hasil, idFile, id: taskId }); + } + Toast.show({ type: "small", text1: "Berhasil menambahkan file" }); + dispatch(setUpdateProject({ ...update, task: !update.task })); + setPickerModal(false); + setSelectedProjectFiles([]); + loadFiles(); + } catch (error: any) { + const message = error?.response?.data?.message || "Gagal menambahkan file"; + Toast.show({ type: "small", text1: message }); + } finally { + setLoadingLink(false); + } + } + + const attachedFileIds = new Set(data.map((f) => f.idFile)); + + return ( + + ( + router.back()} + /> + ), + }} + /> + + + + + {canEdit && ( + <> + + { + setSelectedProjectFiles([]); + setPickerModal(true); + loadProjectFiles(); + }} + /> + + )} + + {loadingUpload && } + + + File Terlampir + + {loading ? ( + arrSkeleton.map((_, index) => ( + + )) + ) : data.length > 0 ? ( + data.map((item, index) => ( + } + title={item.name + "." + item.extension} + titleWeight="normal" + onPress={() => { + setSelectFile(item); + setMenuModal(true); + }} + /> + )) + ) : ( + + Tidak ada file + + )} + + + + + + {/* Menu per file */} + + + } + title="Lihat / Share" + onPress={openFile} + /> + {canEdit && ( + } + title="Hapus" + onPress={() => { + setMenuModal(false); + setTimeout(() => setShowDeleteModal(true), 600); + }} + /> + )} + + + + { + setShowDeleteModal(false); + handleDelete(); + }} + onCancel={() => setShowDeleteModal(false)} + confirmText="Hapus" + cancelText="Batal" + /> + + {/* Picker file dari proyek */} + + + {loadingProjectFiles ? ( + + ) : projectFiles.length > 0 ? ( + projectFiles.map((item, index) => { + const isAttached = attachedFileIds.has(item.id); + const isSelected = selectedProjectFiles.includes(item.id); + return ( + + + ) : ( + + ) + } + title={item.name + "." + item.extension} + titleWeight="normal" + onPress={() => !isAttached && toggleProjectFileSelect(item.id)} + bgColor="transparent" + /> + + ); + }) + ) : ( + + Tidak ada file tersedia + + )} + + {projectFiles.length > 0 && ( + + + + )} + + + ); +} diff --git a/components/itemSectionTanggalTugas.tsx b/components/itemSectionTanggalTugas.tsx index 8030a9f..5cffd5e 100644 --- a/components/itemSectionTanggalTugas.tsx +++ b/components/itemSectionTanggalTugas.tsx @@ -1,48 +1,123 @@ import Styles from "@/constants/Styles"; import { useTheme } from "@/providers/ThemeProvider"; import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { Pressable, View } from "react-native"; +import { useState } from "react"; +import { LayoutChangeEvent, Pressable, View } from "react-native"; import Text from "./Text"; +type FileItem = { + name: string + extension: string +} + type Props = { done?: boolean title: string dateStart: string dateEnd: string + files?: FileItem[] onPress?: () => void } -export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEnd, onPress }: Props) { - const { colors } = useTheme(); +// estimasi lebar chip berdasarkan panjang teks +const CHAR_W = 6.5 // lebar rata-rata per karakter (font size 10) +const ICON_W = 17 // icon 13px + margin 4px +const PAD_H = 16 // paddingHorizontal 8 * 2 +const GAP = 6 +const PLUS_W = 72 // lebar chip "+X lainnya" + +function estimateChipWidth(label: string) { + return PAD_H + ICON_W + label.length * CHAR_W +} + +function getVisibleChips(files: FileItem[], containerWidth: number) { + if (containerWidth === 0) return { visible: [], extra: files.length } + + let used = 0 + const visible: FileItem[] = [] + + for (let i = 0; i < files.length; i++) { + const label = `${files[i].name}.${files[i].extension}` + const chipW = estimateChipWidth(label) + const isLast = i === files.length - 1 + const plusChipW = isLast ? 0 : PLUS_W + GAP + const gapW = visible.length > 0 ? GAP : 0 + + if (used + gapW + chipW + plusChipW <= containerWidth) { + visible.push(files[i]) + used += gapW + chipW + } else { + break + } + } + + return { visible, extra: files.length - visible.length } +} + +function getFileIcon(extension: string): keyof typeof MaterialCommunityIcons.glyphMap { + const ext = extension.toLowerCase() + if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline' + if (ext === 'pdf') return 'file-pdf-box' + if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline' + if (['doc', 'docx'].includes(ext)) return 'file-word-outline' + if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline' + if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline' + return 'file-outline' +} + +const chipStyle = (colors: any) => ({ + flexDirection: 'row' as const, + alignItems: 'center' as const, + backgroundColor: colors.dimmed + '5', + borderRadius: 6, + borderWidth: 0.5, + borderColor: colors.icon + '20', + paddingHorizontal: 8, + paddingVertical: 4, +}) + +export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEnd, files = [], onPress }: Props) { + const { colors } = useTheme() + const [containerWidth, setContainerWidth] = useState(0) + + const { visible, extra } = getVisibleChips(files, containerWidth) + + function onRowLayout(e: LayoutChangeEvent) { + const w = e.nativeEvent.layout.width + if (w !== containerWidth) setContainerWidth(w) + } return ( - - { - done != undefined ? - done ? - <> - - Selesai - - : - <> - - Belum Selesai - - : - <> - } + {/* Status */} + + {done != undefined && ( + done ? ( + <> + + Selesai + + ) : ( + <> + + Belum Selesai + + ) + )} + + {/* Judul tugas */} - + {title} + + {/* Tanggal */} Tanggal Mulai @@ -57,6 +132,52 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn + + {/* Lampiran file */} + {files.length > 0 && ( + + + + + {files.length} Lampiran + + + + {visible.map((file, index) => { + const label = `${file.name}.${file.extension}` + const chipW = Math.min(estimateChipWidth(label), containerWidth * 0.55) + return ( + + + + {label} + + + ) + })} + {extra > 0 && ( + + + +{extra} lainnya + + + )} + + + )} + ) -} \ No newline at end of file +} diff --git a/components/project/sectionTanggalTugas.tsx b/components/project/sectionTanggalTugas.tsx index 183adee..38c7a81 100644 --- a/components/project/sectionTanggalTugas.tsx +++ b/components/project/sectionTanggalTugas.tsx @@ -26,6 +26,7 @@ type Props = { dateStart: string; dateEnd: string; createdAt: string; + files?: { name: string; extension: string }[]; }; export default function SectionTanggalTugasProject({ status, member, refreshing }: { status: number | undefined, member: boolean, refreshing?: boolean }) { @@ -144,12 +145,9 @@ export default function SectionTanggalTugasProject({ status, member, refreshing title={item.title} dateStart={item.dateStart} dateEnd={item.dateEnd} + files={item.files ?? []} onPress={() => { - if (status == 3 || (!member && (entityUser.role == "user" || entityUser.role == "coadmin"))) return - setTugas({ - id: item.id, - status: item.status - }) + setTugas({ id: item.id, status: item.status }) setModal(true) }} /> @@ -169,65 +167,54 @@ export default function SectionTanggalTugasProject({ status, member, refreshing > - } - title="Update Status" - onPress={() => { - setModal(false); - setTimeout(() => { - setSelect(true); - }, 600) - }} - /> - - } - title="Edit Tugas" - onPress={() => { - setModal(false); - router.push(`/project/update/${tugas.id}`); - }} - /> - - - } + icon={} title="Detail Waktu" onPress={() => { setModal(false); - setTimeout(() => { - setModalDetail(true) - }, 600) + setTimeout(() => setModalDetail(true), 600) }} /> - - } - title="Hapus Tugas" + icon={} + title="File Tugas" onPress={() => { - setModal(false) - setTimeout(() => { - setShowDeleteModal(true) - }, 600) + setModal(false); + router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`); }} /> + {(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && ( + <> + } + title="Update Status" + onPress={() => { + setModal(false); + setTimeout(() => setSelect(true), 600) + }} + /> + } + title="Edit Tugas" + onPress={() => { + setModal(false); + router.push(`/project/update/${tugas.id}`); + }} + /> + + )} + {(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && ( + + } + title="Hapus Tugas" + onPress={() => { + setModal(false) + setTimeout(() => setShowDeleteModal(true), 600) + }} + /> + + )} { setTugas({ id: item.id, @@ -179,6 +181,14 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision } }, 600) }} /> + } + title="File Tugas" + onPress={() => { + setModal(false); + router.push(`/division/${id}/task/${detail}/tugas-file/${tugas.id}?member=${isMemberDivision}`) + }} + /> { (entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision ? diff --git a/lib/api.ts b/lib/api.ts index ffb7da1..a744165 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -357,6 +357,28 @@ export const apiDeleteProjectMember = async (data: { user: string, idUser: strin return response.data }; +export const apiGetProjectTaskFile = async ({ user, id }: { user: string, id: string }) => { + const response = await api.get(`/mobile/project/task/file/${id}`, { params: { user } }) + return response.data; +}; + +export const apiAddProjectTaskFile = async ({ data, id }: { data: FormData, id: string }) => { + const response = await api.post(`/mobile/project/task/file/${id}`, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + return response.data; +}; + +export const apiLinkProjectTaskFile = async ({ user, idFile, id }: { user: string, idFile: string, id: string }) => { + const response = await api.patch(`/mobile/project/task/file/${id}`, { user, idFile }) + return response.data; +}; + +export const apiDeleteProjectTaskFile = async (data: { user: string }, id: string) => { + const response = await api.delete(`/mobile/project/task/file/${id}`, { data }) + return response.data; +}; + export const apiAddMemberProject = async ({ data, id }: { data: { user: string, member: any[] }, id: string }) => { const response = await api.post(`/mobile/project/${id}/member`, data) @@ -664,6 +686,28 @@ export const apiAddFileTask = async ({ data, id }: { data: FormData, id: string return response.data; }; +export const apiGetTugasTaskFile = async ({ user, id }: { user: string, id: string }) => { + const response = await api.get(`/mobile/task/tugas/file/${id}`, { params: { user } }) + return response.data; +}; + +export const apiAddTugasTaskFile = async ({ data, id }: { data: FormData, id: string }) => { + const response = await api.post(`/mobile/task/tugas/file/${id}`, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + return response.data; +}; + +export const apiLinkTugasTaskFile = async ({ user, idFile, id }: { user: string, idFile: string, id: string }) => { + const response = await api.patch(`/mobile/task/tugas/file/${id}`, { user, idFile }) + return response.data; +}; + +export const apiDeleteTugasTaskFile = async (data: { user: string }, id: string) => { + const response = await api.delete(`/mobile/task/tugas/file/${id}`, { data }) + return response.data; +}; + export const apiEditTask = async (data: { title: string, user: string }, id: string) => { const response = await api.put(`/mobile/task/${id}`, data) return response.data;