From 4a1464185b0cb52d39817bc4550b7715369401b2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 10:30:34 +0700 Subject: [PATCH 1/3] refactor(FE): Replace approval history modal with status badge --- .../production/recording/RecordingTable.tsx | 310 ++++-------------- 1 file changed, 61 insertions(+), 249 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index cd98b597..6beb0954 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -7,14 +7,12 @@ import React, { useEffect, useRef, } from 'react'; -import { RefObject } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext } from '@tanstack/react-table'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import RequirePermission from '@/components/helper/RequirePermission'; import { useModal } from '@/components/Modal'; -import Modal from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -28,14 +26,55 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { type Recording } from '@/types/api/production/recording'; import { RecordingApi } from '@/services/api/production'; -import { ApprovalApi } from '@/services/api/approval'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import toast from 'react-hot-toast'; import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; -import { BaseApproval, BaseApiResponse } from '@/types/api/api-general'; + +// ===== STATUS BADGE UTILITIES ===== +const statusTextMap: Record = { + APPROVED: 'Disetujui', + Disetujui: 'Disetujui', + REJECTED: 'Ditolak', + Ditolak: 'Ditolak', + CREATED: 'Dibuat', + UPDATED: 'Diperbarui', +}; + +const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; + +const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' | 'warning' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Dibuat: 'neutral', + created: 'neutral', + dibuat: 'neutral', + UPDATED: 'warning', + Diperbarui: 'warning', + updated: 'warning', + diperbarui: 'warning', +}; + +const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' | 'warning' => { + return statusBadgeColorMap[status] || 'neutral'; +}; const RowOptionsMenu = ({ type = 'dropdown', @@ -135,221 +174,6 @@ const RowOptionsMenu = ({ ); }; -const ApprovalHistoryModal = ({ - ref, - currentApproval, - module_name = 'RECORDINGS', - module_id, -}: { - ref: RefObject; - currentApproval?: BaseApproval; - module_name?: string; - module_id?: number | undefined; -}) => { - const [isModalOpen, setIsModalOpen] = useState(false); - - const approvalHistoryUrl = useMemo(() => { - if (!isModalOpen) return null; - const params = new URLSearchParams({ - module_name: module_name, - group_step_number: 'true', - }); - - if (module_id) { - params.append('module_id', module_id.toString()); - } - - return `${ApprovalApi.basePath}?${params.toString()}`; - }, [module_name, module_id, isModalOpen]); - - type GroupedApprovalResponse = { - step_number: number; - step_name: string; - approvals: BaseApproval[]; - }; - - const fetchGroupedApprovals = async ( - url: string - ): Promise> => { - return (await ApprovalApi.getAllFetcher(url)) as BaseApiResponse< - GroupedApprovalResponse[] - >; - }; - - const { data: approvalHistoryData, isLoading } = useSWR< - BaseApiResponse - >(approvalHistoryUrl, fetchGroupedApprovals); - - useEffect(() => { - const checkModalOpen = () => { - const isOpen = ref.current?.open || false; - setIsModalOpen(isOpen); - }; - - checkModalOpen(); - - const observer = new MutationObserver(checkModalOpen); - if (ref.current) { - observer.observe(ref.current, { - attributes: true, - attributeFilter: ['open'], - }); - } - - return () => observer.disconnect(); - }, [ref]); - - const approvalHistory = useMemo(() => { - if (!approvalHistoryData || approvalHistoryData.status !== 'success') - return []; - - const groupedData = approvalHistoryData.data || []; - const flattenedApprovals: BaseApproval[] = []; - - groupedData.forEach((group) => { - group.approvals.forEach((approval) => { - flattenedApprovals.push(approval); - }); - }); - - return flattenedApprovals; - }, [approvalHistoryData]); - - const closeModalHandler = () => { - ref.current?.close(); - }; - - return ( - -
- {/* Header */} -
-

Riwayat Approval

- -
- - {isLoading ? ( -
- -
- ) : ( - <> - {/* Current Status */} - {currentApproval && ( -
-

Status Saat Ini

-
- - {currentApproval.step_name} - - - {currentApproval.action === 'APPROVED' && 'Disetujui'} - {currentApproval.action === 'REJECTED' && 'Ditolak'} - {currentApproval.action === 'CREATED' && 'Dibuat'} - {currentApproval.action === 'UPDATED' && 'Diperbarui'} - -
- {currentApproval.notes && ( -

- Catatan:{' '} - {currentApproval.notes} -

- )} -

- Oleh: {currentApproval.action_by.name} •{' '} - {formatDate(currentApproval.action_at, 'DD MMMM YYYY HH:mm')} -

-
- )} - - {/* Full History */} - {approvalHistory.length > 0 && ( -
-

Riwayat Lengkap

-
- - - - - - - - - - - - {approvalHistory - .sort( - (a: BaseApproval, b: BaseApproval) => - new Date(b.action_at).getTime() - - new Date(a.action_at).getTime() - ) - .map((approval: BaseApproval, index: number) => ( - - - - - - - - ))} - -
TahapAksiCatatanOlehWaktu
{approval.step_name} - - {approval.action === 'APPROVED' && 'Disetujui'} - {approval.action === 'REJECTED' && 'Ditolak'} - {approval.action === 'CREATED' && 'Dibuat'} - {approval.action === 'UPDATED' && 'Diperbarui'} - - -
- {approval.notes || '-'} -
-
{approval.action_by.name} - {formatDate( - approval.action_at, - 'DD MMMM YYYY HH:mm' - )} -
-
-
- )} - - )} -
-
- ); -}; - const RecordingTable = () => { const { searchValue, setSearchValue, resetSearchValue } = useUiStore(); const previousPathRef = useRef(null); @@ -395,7 +219,6 @@ const RecordingTable = () => { const singleDeleteModal = useModal(); const approveModal = useModal(); const rejectModal = useModal(); - const approvalHistoryModal = useModal(); const { data: recordings, @@ -1032,32 +855,22 @@ const RecordingTable = () => { const approval = props.row.original.approval; if (!approval) return '-'; - const statusColor = - approval.action === 'APPROVED' - ? 'success' - : approval.action === 'REJECTED' - ? 'error' - : approval.action === 'UPDATED' - ? 'warning' - : 'info'; + const status = approval.action; + const statusColor = getStatusBadgeColor(status); - const openApprovalHistory = () => { - setSelectedRecording(props.row.original); - approvalHistoryModal.openModal(); - }; + const statusText = + status === 'REJECTED' + ? 'Ditolak' + : approval.step_name || getStatusText(status); return ( - - {approval.step_name || approval.action} - + /> ); }, }, @@ -1208,7 +1021,10 @@ const RecordingTable = () => { text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`} secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + approveModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -1226,7 +1042,10 @@ const RecordingTable = () => { text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`} secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + rejectModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -1237,13 +1056,6 @@ const RecordingTable = () => { placeholder='(Opsional) Tambahkan catatan untuk reject ini...' rows={3} /> - - ); }; From 3ca4b324d316f470708803e5da79f1dc77977585 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 10:46:20 +0700 Subject: [PATCH 2/3] refactor(FE): Unify status badge logic and use StatusBadge --- .../production/recording/RecordingTable.tsx | 5 +- .../pages/purchase/PurchaseTable.tsx | 99 +++++++++++++------ 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6beb0954..a461e2d8 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -858,10 +858,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - const statusText = - status === 'REJECTED' - ? 'Ditolak' - : approval.step_name || getStatusText(status); + const statusText = approval.step_name || getStatusText(status); return ( = { + APPROVED: 'Disetujui', + Disetujui: 'Disetujui', + REJECTED: 'Ditolak', + Ditolak: 'Ditolak', + CREATED: 'Dibuat', + UPDATED: 'Diperbarui', +}; + +const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; + +const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Dibuat: 'neutral', + created: 'neutral', + dibuat: 'neutral', + UPDATED: 'warning', + Diperbarui: 'warning', + updated: 'warning', + diperbarui: 'warning', +}; + +const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' => { + return statusBadgeColorMap[status] || 'neutral'; +}; + // ===== INTERFACES ===== interface RowOptionsMenuProps { type: 'dropdown' | 'collapse'; @@ -160,48 +202,48 @@ const PurchaseTable = () => { const approval = props.row.original.latest_approval; if (!approval) return '-'; - const isRejected = approval.action === 'REJECTED'; + const status = approval.action; let statusColor: | 'warning' | 'success' | 'neutral' | 'error' - | 'primary' - | 'info' = 'neutral'; + | 'info' + | 'primary' = 'neutral'; - switch (approval.step_number) { - case 1: - statusColor = 'neutral'; - break; - case 2: - statusColor = 'primary'; - break; - case 3: - statusColor = 'info'; - break; - case 4: - statusColor = 'warning'; - break; - case 5: - statusColor = 'success'; - break; + if (status === 'REJECTED') { + statusColor = getStatusBadgeColor(status); + } else { + switch (approval.step_number) { + case 1: + statusColor = 'neutral'; + break; + case 2: + statusColor = 'primary'; + break; + case 3: + statusColor = 'info'; + break; + case 4: + statusColor = 'warning'; + break; + case 5: + statusColor = 'success'; + break; + } } - if (isRejected) { - statusColor = 'error'; - } + const statusText = approval.step_name || getStatusText(status); return ( - - {isRejected ? 'Ditolak' : approval.step_name} - + /> ); }, }, @@ -369,6 +411,7 @@ const PurchaseTable = () => { text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`} secondaryButton={{ text: 'Tidak', + onClick: () => deleteModal.closeModal(), }} primaryButton={{ text: 'Ya', From 85556c0db0eae30b21a431747ebaff91d31fb6e0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 11:10:58 +0700 Subject: [PATCH 3/3] refactor(FE): Use Color type for badge color typings --- .../production/recording/RecordingTable.tsx | 10 +++------- .../pages/purchase/PurchaseTable.tsx | 18 ++++-------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a461e2d8..36a4e69a 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -33,6 +33,7 @@ import Badge from '@/components/Badge'; import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; +import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -48,10 +49,7 @@ const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; -const statusBadgeColorMap: Record< - string, - 'success' | 'error' | 'neutral' | 'info' | 'warning' -> = { +const statusBadgeColorMap: Record = { APPROVED: 'success', Disetujui: 'success', approved: 'success', @@ -70,9 +68,7 @@ const statusBadgeColorMap: Record< diperbarui: 'warning', }; -const getStatusBadgeColor = ( - status: string -): 'success' | 'error' | 'neutral' | 'info' | 'warning' => { +const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 6773ba99..d0c72dac 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -25,6 +25,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; import { Purchase } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; +import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -40,10 +41,7 @@ const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; -const statusBadgeColorMap: Record< - string, - 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' -> = { +const statusBadgeColorMap: Record = { APPROVED: 'success', Disetujui: 'success', approved: 'success', @@ -62,9 +60,7 @@ const statusBadgeColorMap: Record< diperbarui: 'warning', }; -const getStatusBadgeColor = ( - status: string -): 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' => { +const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; @@ -204,13 +200,7 @@ const PurchaseTable = () => { const status = approval.action; - let statusColor: - | 'warning' - | 'success' - | 'neutral' - | 'error' - | 'info' - | 'primary' = 'neutral'; + let statusColor: Color = 'neutral'; if (status === 'REJECTED') { statusColor = getStatusBadgeColor(status);