feat: tambah fitur approval task pada project dan divisi
- tambah komponen ModalRiwayatApproval dan ModalTolakApproval - update itemSectionTanggalTugas untuk mendukung status menunggu persetujuan - update sectionTanggalTugas (project) dan sectionTanggalTugasTask (divisi) dengan alur approval lengkap - tambah API approval project task dan division task di lib/api.ts - tambah toggle approver di headerMemberDetail dan tampilkan badge approver di detail member - update carouselHome untuk dispatch isApprover ke Redux - update drawerBottom untuk mendukung scroll pada modal - ganti label 'Belum dimulai' menjadi 'Belum ada tugas yang diselesaikan'
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import Styles from "@/constants/Styles";
|
||||
import { apiDeleteTaskTugas, apiGetTaskOne, apiUpdateStatusTaskDivision } from "@/lib/api";
|
||||
import { apiApproveRejectTaskTugas, apiDeleteTaskTugas, apiGetTaskOne, apiGetTaskTugasApprovals, apiSubmitTaskTugas } from "@/lib/api";
|
||||
import { setUpdateTask } from "@/lib/taskUpdate";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
@@ -13,7 +13,8 @@ import DrawerBottom from "../drawerBottom";
|
||||
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
|
||||
import MenuItemRow from "../menuItemRow";
|
||||
import ModalConfirmation from "../ModalConfirmation";
|
||||
import ModalSelect from "../modalSelect";
|
||||
import ModalRiwayatApproval from "../ModalRiwayatApproval";
|
||||
import ModalTolakApproval from "../ModalTolakApproval";
|
||||
import SkeletonTask from "../skeletonTask";
|
||||
import Text from "../Text";
|
||||
import ModalListDetailTugasTask from "./modalListDetailTugasTask";
|
||||
@@ -28,25 +29,41 @@ type Props = {
|
||||
files?: { name: string; extension: string }[];
|
||||
}
|
||||
|
||||
export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
|
||||
type ApprovalRecord = {
|
||||
id: string
|
||||
status: number
|
||||
note?: string
|
||||
submitter: { name: string }
|
||||
approver?: { name: string }
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export default function SectionTanggalTugasTask({ refreshing, isMemberDivision, isAdminDivision, status }: { refreshing: boolean, isMemberDivision: boolean, isAdminDivision: boolean, status?: number }) {
|
||||
const { colors } = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const entityUser = useSelector((state: any) => state.user);
|
||||
const update = useSelector((state: any) => state.taskUpdate)
|
||||
const [isModal, setModal] = useState(false)
|
||||
const [isSelect, setSelect] = useState(false)
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingAction, setLoadingAction] = useState(false)
|
||||
const [loadingRiwayat, setLoadingRiwayat] = useState(false)
|
||||
const arrSkeleton = Array.from({ length: 5 })
|
||||
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 [riwayatData, setRiwayatData] = useState<ApprovalRecord[]>([])
|
||||
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
||||
const [data, setData] = useState<Props[]>([])
|
||||
const [tugas, setTugas] = useState({
|
||||
id: '',
|
||||
status: 0,
|
||||
})
|
||||
const [tugas, setTugas] = useState({ id: '', status: 0 })
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
|
||||
const isApprover = entityUser.isApprover || ['supadmin', 'developer'].includes(entityUser.role)
|
||||
const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin'
|
||||
const canTakeAction = isMemberDivision || isAdmin
|
||||
|
||||
async function handleLoad(loading: boolean) {
|
||||
try {
|
||||
setLoading(loading)
|
||||
@@ -60,126 +77,136 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(status: number) {
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current));
|
||||
const response = await apiUpdateStatusTaskDivision({
|
||||
user: hasil,
|
||||
idProject: detail,
|
||||
status: status,
|
||||
}, tugas.id);
|
||||
if (response.success) {
|
||||
dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
|
||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: response.message, })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||
useEffect(() => { handleLoad(false) }, [update.task])
|
||||
useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing])
|
||||
useEffect(() => { handleLoad(true) }, [])
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
async function handleLoadRiwayat() {
|
||||
try {
|
||||
setLoadingRiwayat(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetTaskTugasApprovals({ user: hasil, id: tugas.id })
|
||||
setRiwayatData(response.data ?? [])
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setSelect(false)
|
||||
setLoadingRiwayat(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(false)
|
||||
}, [update.task])
|
||||
async function handleSubmitAjukan() {
|
||||
try {
|
||||
setLoadingAction(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiSubmitTaskTugas({ user: hasil, id: tugas.id })
|
||||
if (response.success) {
|
||||
dispatch(setUpdateTask({ ...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) {
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengajukan persetujuan" })
|
||||
} finally {
|
||||
setLoadingAction(false)
|
||||
setModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshing)
|
||||
handleLoad(false);
|
||||
}, [refreshing]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(true)
|
||||
}, [])
|
||||
async function handleSetujui() {
|
||||
try {
|
||||
setLoadingAction(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiApproveRejectTaskTugas({ user: hasil, id: tugas.id, action: 'approve' })
|
||||
if (response.success) {
|
||||
dispatch(setUpdateTask({ ...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) {
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menyetujui tugas" })
|
||||
} finally {
|
||||
setLoadingAction(false)
|
||||
setModalKonfirmasiSetujui(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTolak(note: string) {
|
||||
try {
|
||||
setLoadingAction(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiApproveRejectTaskTugas({ user: hasil, id: tugas.id, action: 'reject', note })
|
||||
if (response.success) {
|
||||
dispatch(setUpdateTask({ ...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) {
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menolak tugas" })
|
||||
} finally {
|
||||
setLoadingAction(false)
|
||||
setModalTolak(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current));
|
||||
const response = await apiDeleteTaskTugas({
|
||||
user: hasil,
|
||||
idProject: detail,
|
||||
}, tugas.id);
|
||||
const response = await apiDeleteTaskTugas({ user: hasil, idProject: detail }, tugas.id);
|
||||
if (response.success) {
|
||||
dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: response.message, })
|
||||
Toast.show({ type: 'small', text1: response.message })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
} finally {
|
||||
setModal(false);
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus data" })
|
||||
}
|
||||
}
|
||||
|
||||
const canApprove = isApprover || isAdminDivision
|
||||
const showAjukan = (isMemberDivision || canApprove) && tugas.status === 0 && status !== 3
|
||||
const showApproverActions = canApprove && tugas.status === 2
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||
<View>
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonTask key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
data.length > 0
|
||||
?
|
||||
data.map((item, index) => {
|
||||
return (
|
||||
<ItemSectionTanggalTugas
|
||||
key={index}
|
||||
done={item.status === 1}
|
||||
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>
|
||||
{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>
|
||||
|
||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
|
||||
title="Update Status"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setSelect(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* 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);
|
||||
setModal(false)
|
||||
router.push(`/division/${id}/task/${detail}/tugas-file/${tugas.id}?member=${isMemberDivision}`)
|
||||
}}
|
||||
/>
|
||||
@@ -187,53 +214,129 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
||||
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
||||
title="Detail Waktu"
|
||||
onPress={() => {
|
||||
setModal(false);
|
||||
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 */}
|
||||
{(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
|
||||
<View style={{ width: '100%', height: 15 }} />
|
||||
)}
|
||||
|
||||
{/* Baris 2 — aksi kondisional */}
|
||||
{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(`./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>
|
||||
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
router.push(`./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>
|
||||
|
||||
<ModalConfirmation
|
||||
visible={showDeleteModal}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menghapus data ini?"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false)
|
||||
handleDelete()
|
||||
}}
|
||||
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
confirmText="Hapus"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalSelect
|
||||
category="status-task"
|
||||
close={() => setSelect(false)}
|
||||
onSelect={(value) => { handleUpdate(Number(value.val)) }}
|
||||
title="Status"
|
||||
open={isSelect}
|
||||
valChoose={String(tugas.status)}
|
||||
<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}
|
||||
/>
|
||||
|
||||
<ModalListDetailTugasTask
|
||||
@@ -243,4 +346,4 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user