Files
mobile-darmasaba/components/project/sectionTanggalTugas.tsx
amaliadwiy 85aca330e5 feat: filter approval berdasarkan group dan perbaikan tampilan riwayat
- Simpan idGroup user ke Redux saat login agar perbandingan group bisa dilakukan
- Filter button persetujuan project: isApprover hanya tampil jika group sama
- Filter button persetujuan division/task: isApprover hanya tampil jika group sama
- Pass idGroup ke SectionTanggalTugasProject dan SectionTanggalTugasTask dari parent
- Samakan warna icon, label, dan nama pada riwayat persetujuan
- Ubah bg alasan penolakan dari merah ke netral, label tetap merah
- Ekstrak inline styles ModalRiwayatApproval ke approval.styles.ts
2026-05-18 14:52:30 +08:00

352 lines
14 KiB
TypeScript

import Styles from "@/constants/Styles";
import { apiApproveRejectProjectTask, apiDeleteProjectTask, apiGetProjectOne, apiGetProjectTaskApprovals, apiSubmitProjectTask } 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 { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
import DrawerBottom from "../drawerBottom";
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
import MenuItemRow from "../menuItemRow";
import ModalConfirmation from "../ModalConfirmation";
import ModalRiwayatApproval from "../ModalRiwayatApproval";
import ModalTolakApproval from "../ModalTolakApproval";
import SkeletonTask from "../skeletonTask";
import Text from "../Text";
import ModalListDetailTugasProject from "./modalListDetailTugasProject";
type Props = {
id: string;
title: string;
desc: string;
status: number;
dateStart: string;
dateEnd: string;
createdAt: string;
files?: { name: string; extension: string }[];
};
type ApprovalRecord = {
id: string
status: number
note?: string
submitter: { name: string }
approver?: { name: string }
createdAt: string
}
export default function SectionTanggalTugasProject({ status, member, refreshing, idGroup }: { status: number | undefined, member: boolean, refreshing?: boolean, idGroup: string }) {
const { colors } = useTheme();
const entityUser = useSelector((state: any) => state.user)
const dispatch = useDispatch()
const update = useSelector((state: any) => state.projectUpdate)
const [isModal, setModal] = useState(false);
const { token, decryptToken } = useAuthSession();
const [modalDetail, setModalDetail] = useState(false)
const [modalRiwayat, setModalRiwayat] = useState(false)
const [modalTolak, setModalTolak] = useState(false)
const [modalKonfirmasiSetujui, setModalKonfirmasiSetujui] = useState(false)
const [modalPersetujuan, setModalPersetujuan] = useState(false)
const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState<Props[]>([]);
const [loading, setLoading] = useState(true)
const [loadingAction, setLoadingAction] = useState(false)
const [loadingRiwayat, setLoadingRiwayat] = useState(false)
const [riwayatData, setRiwayatData] = useState<ApprovalRecord[]>([])
const arrSkeleton = Array.from({ length: 5 });
const [tugas, setTugas] = useState({ id: '', status: 0 })
const [showDeleteModal, setShowDeleteModal] = useState(false)
const isApprover = (entityUser.isApprover && entityUser.idGroup === idGroup) || ['supadmin', 'developer'].includes(entityUser.role)
const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin'
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
const hasil = await decryptToken(String(token?.current));
const response = await apiGetProjectOne({ user: hasil, cat: "task", id: id });
setData(response.data);
} catch (error) {
console.error(error);
} finally {
setLoading(false)
}
}
useEffect(() => { handleLoad(false) }, [update.task]);
useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing]);
useEffect(() => { handleLoad(true) }, []);
async function handleLoadRiwayat() {
try {
setLoadingRiwayat(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiGetProjectTaskApprovals({ user: hasil, id: tugas.id })
setRiwayatData(response.data ?? [])
} catch (error) {
console.error(error)
} finally {
setLoadingRiwayat(false)
}
}
async function handleSubmitAjukan() {
try {
setLoadingAction(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiSubmitProjectTask({ user: hasil, id: tugas.id })
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
Toast.show({ type: 'small', text1: 'Berhasil mengajukan task untuk persetujuan' })
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error: any) {
const message = error?.response?.data?.message || "Gagal mengajukan persetujuan"
Toast.show({ type: 'small', text1: message })
} finally {
setLoadingAction(false)
setModal(false)
}
}
async function handleSetujui() {
try {
setLoadingAction(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiApproveRejectProjectTask({ user: hasil, id: tugas.id, action: 'approve' })
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
Toast.show({ type: 'small', text1: 'Tugas berhasil disetujui' })
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error: any) {
const message = error?.response?.data?.message || "Gagal menyetujui tugas"
Toast.show({ type: 'small', text1: message })
} finally {
setLoadingAction(false)
setModalKonfirmasiSetujui(false)
}
}
async function handleTolak(note: string) {
try {
setLoadingAction(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiApproveRejectProjectTask({ user: hasil, id: tugas.id, action: 'reject', note })
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
Toast.show({ type: 'small', text1: 'Tugas berhasil ditolak' })
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error: any) {
const message = error?.response?.data?.message || "Gagal menolak tugas"
Toast.show({ type: 'small', text1: message })
} finally {
setLoadingAction(false)
setModalTolak(false)
}
}
async function handleDelete() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiDeleteProjectTask({ user: hasil, idProject: id }, tugas.id);
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
}
} catch (error: any) {
const message = error?.response?.data?.message || "Gagal menghapus data"
Toast.show({ type: 'small', text1: message })
}
}
const canTakeAction = member || isAdmin
const showAjukan = (member || isApprover) && tugas.status === 0 && status !== 3
const showApproverActions = isApprover && tugas.status === 2
return (
<>
<View style={[Styles.mb15, Styles.mt10]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
<View>
{loading
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
: data.length > 0
? data.map((item, index) => (
<ItemSectionTanggalTugas
key={index}
status={item.status}
title={item.title}
dateStart={item.dateStart}
dateEnd={item.dateEnd}
files={item.files ?? []}
onPress={() => {
setTugas({ id: item.id, status: item.status })
setModal(true)
}}
/>
))
: <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
}
</View>
</View>
{/* Drawer menu */}
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu" height={40}>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{/* Baris 1 — selalu tampil */}
<MenuItemRow
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
title="File Tugas"
onPress={() => {
setModal(false)
router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`)
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
title="Detail Waktu"
onPress={() => {
setModal(false)
setTimeout(() => setModalDetail(true), 600)
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="history" color={colors.text} size={25} />}
title="Riwayat"
onPress={() => {
setModal(false)
handleLoadRiwayat()
setTimeout(() => setModalRiwayat(true), 600)
}}
/>
{/* Separator antar baris */}
{(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
<View style={{ width: '100%', height: 15 }} />
)}
{/* Baris 2 — semua aksi kondisional dalam satu baris */}
{showAjukan && (
<MenuItemRow
icon={<MaterialCommunityIcons name="check-circle-outline" color={colors.text} size={25} />}
title="Ajukan Selesai"
disabled={loadingAction}
onPress={() => {
setModal(false)
setTimeout(() => handleSubmitAjukan(), 600)
}}
/>
)}
{showApproverActions && (
<MenuItemRow
icon={<MaterialCommunityIcons name="shield-check-outline" color={colors.text} size={25} />}
title="Persetujuan"
disabled={loadingAction}
onPress={() => {
setModal(false)
setTimeout(() => setModalPersetujuan(true), 600)
}}
/>
)}
{canTakeAction && isAdmin && status !== 3 && (
<>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
title="Edit Tugas"
onPress={() => {
setModal(false)
router.push(`/project/update/${tugas.id}`)
}}
/>
<MenuItemRow
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
title="Hapus Tugas"
onPress={() => {
setModal(false)
setTimeout(() => setShowDeleteModal(true), 600)
}}
/>
</>
)}
</View>
</DrawerBottom>
{/* Drawer persetujuan */}
<DrawerBottom animation="slide" isVisible={modalPersetujuan} setVisible={setModalPersetujuan} title="Persetujuan Tugas" height={25}>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
<MenuItemRow
icon={<MaterialCommunityIcons name="check-circle" color={colors.success} size={25} />}
title="Setujui"
color={colors.success}
disabled={loadingAction}
onPress={() => {
setModalPersetujuan(false)
setTimeout(() => setModalKonfirmasiSetujui(true), 600)
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="close-circle-outline" color={colors.error} size={25} />}
title="Tolak"
color={colors.error}
disabled={loadingAction}
onPress={() => {
setModalPersetujuan(false)
setTimeout(() => setModalTolak(true), 600)
}}
/>
</View>
</DrawerBottom>
<ModalConfirmation
visible={showDeleteModal}
title="Konfirmasi"
message="Apakah anda yakin ingin menghapus data ini?"
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
<ModalConfirmation
visible={modalKonfirmasiSetujui}
title="Konfirmasi"
message="Apakah anda yakin ingin menyetujui tugas ini?"
onConfirm={handleSetujui}
onCancel={() => setModalKonfirmasiSetujui(false)}
confirmText="Setujui"
cancelText="Batal"
/>
<ModalRiwayatApproval
isVisible={modalRiwayat}
setVisible={setModalRiwayat}
data={riwayatData}
loading={loadingRiwayat}
/>
<ModalTolakApproval
isVisible={modalTolak}
setVisible={setModalTolak}
onTolak={handleTolak}
loading={loadingAction}
/>
<ModalListDetailTugasProject
isVisible={modalDetail}
setVisible={setModalDetail}
idTask={tugas.id}
/>
</>
);
}