Merge branch 'fix/adjustment-status-badge' into 'development'

[FIX/FE] Adjustment Usage of Status Badge (Recording, Purchase)

See merge request mbugroup/lti-web-client!297
This commit is contained in:
Rivaldi A N S
2026-02-02 04:37:37 +00:00
2 changed files with 120 additions and 282 deletions
@@ -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,51 @@ 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';
import { Color } from '@/types/theme';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
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, Color> = {
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): Color => {
return statusBadgeColorMap[status] || 'neutral';
};
const RowOptionsMenu = ({
type = 'dropdown',
@@ -135,221 +170,6 @@ const RowOptionsMenu = ({
);
};
const ApprovalHistoryModal = ({
ref,
currentApproval,
module_name = 'RECORDINGS',
module_id,
}: {
ref: RefObject<HTMLDialogElement | null>;
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<BaseApiResponse<GroupedApprovalResponse[]>> => {
return (await ApprovalApi.getAllFetcher(url)) as BaseApiResponse<
GroupedApprovalResponse[]
>;
};
const { data: approvalHistoryData, isLoading } = useSWR<
BaseApiResponse<GroupedApprovalResponse[]>
>(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 (
<Modal ref={ref} className={{ modalBox: 'max-w-2xl' }}>
<div className='w-full flex flex-col gap-6'>
{/* Header */}
<div className='flex items-center justify-between'>
<h2 className='text-xl font-bold'>Riwayat Approval</h2>
<Button
onClick={closeModalHandler}
variant='ghost'
className='btn-circle btn-sm p-0'
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
{isLoading ? (
<div className='flex justify-center py-8'>
<span className='loading loading-spinner loading-lg'></span>
</div>
) : (
<>
{/* Current Status */}
{currentApproval && (
<div className='bg-base-200 rounded-lg p-4'>
<h3 className='font-semibold mb-2'>Status Saat Ini</h3>
<div className='flex items-center gap-3'>
<Badge
variant='soft'
color={
currentApproval.action === 'APPROVED'
? 'success'
: currentApproval.action === 'REJECTED'
? 'error'
: currentApproval.action === 'UPDATED'
? 'warning'
: 'info'
}
>
{currentApproval.step_name}
</Badge>
<span className='text-sm text-gray-600'>
{currentApproval.action === 'APPROVED' && 'Disetujui'}
{currentApproval.action === 'REJECTED' && 'Ditolak'}
{currentApproval.action === 'CREATED' && 'Dibuat'}
{currentApproval.action === 'UPDATED' && 'Diperbarui'}
</span>
</div>
{currentApproval.notes && (
<p className='mt-2 text-sm text-gray-600'>
<span className='font-medium'>Catatan:</span>{' '}
{currentApproval.notes}
</p>
)}
<p className='mt-1 text-xs text-gray-500'>
Oleh: {currentApproval.action_by.name} {' '}
{formatDate(currentApproval.action_at, 'DD MMMM YYYY HH:mm')}
</p>
</div>
)}
{/* Full History */}
{approvalHistory.length > 0 && (
<div className='space-y-4'>
<h3 className='font-semibold'>Riwayat Lengkap</h3>
<div className='overflow-x-auto'>
<table className='table table-sm'>
<thead>
<tr>
<th>Tahap</th>
<th>Aksi</th>
<th>Catatan</th>
<th>Oleh</th>
<th>Waktu</th>
</tr>
</thead>
<tbody>
{approvalHistory
.sort(
(a: BaseApproval, b: BaseApproval) =>
new Date(b.action_at).getTime() -
new Date(a.action_at).getTime()
)
.map((approval: BaseApproval, index: number) => (
<tr key={index}>
<td>{approval.step_name}</td>
<td>
<Badge
variant='soft'
color={
approval.action === 'APPROVED'
? 'success'
: approval.action === 'REJECTED'
? 'error'
: approval.action === 'UPDATED'
? 'warning'
: 'info'
}
size='sm'
>
{approval.action === 'APPROVED' && 'Disetujui'}
{approval.action === 'REJECTED' && 'Ditolak'}
{approval.action === 'CREATED' && 'Dibuat'}
{approval.action === 'UPDATED' && 'Diperbarui'}
</Badge>
</td>
<td className='max-w-xs'>
<div className='truncate'>
{approval.notes || '-'}
</div>
</td>
<td>{approval.action_by.name}</td>
<td className='text-xs'>
{formatDate(
approval.action_at,
'DD MMMM YYYY HH:mm'
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</>
)}
</div>
</Modal>
);
};
const RecordingTable = () => {
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
const previousPathRef = useRef<string | null>(null);
@@ -395,7 +215,6 @@ const RecordingTable = () => {
const singleDeleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
const approvalHistoryModal = useModal();
const {
data: recordings,
@@ -1032,32 +851,19 @@ 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 = approval.step_name || getStatusText(status);
return (
<Badge
variant='soft'
<StatusBadge
color={statusColor}
text={statusText}
className={{
badge:
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
badge: 'whitespace-nowrap',
}}
onClick={openApprovalHistory}
>
{approval.step_name || approval.action}
</Badge>
/>
);
},
},
@@ -1208,7 +1014,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 +1035,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 +1049,6 @@ const RecordingTable = () => {
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
rows={3}
/>
<ApprovalHistoryModal
ref={approvalHistoryModal.ref}
currentApproval={selectedRecording?.approval}
module_name={'RECORDINGS'}
module_id={selectedRecording?.id}
/>
</div>
);
};
+66 -33
View File
@@ -16,7 +16,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import Badge from '@/components/Badge';
import StatusBadge from '@/components/helper/StatusBadge';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -25,6 +25,44 @@ 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<string, string> = {
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, Color> = {
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): Color => {
return statusBadgeColorMap[status] || 'neutral';
};
// ===== INTERFACES =====
interface RowOptionsMenuProps {
@@ -160,48 +198,42 @@ 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';
let statusColor: Color = '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 (
<Badge
variant='soft'
<StatusBadge
color={statusColor}
text={statusText}
className={{
badge: 'whitespace-nowrap',
}}
>
{isRejected ? 'Ditolak' : approval.step_name}
</Badge>
/>
);
},
},
@@ -369,6 +401,7 @@ const PurchaseTable = () => {
text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`}
secondaryButton={{
text: 'Tidak',
onClick: () => deleteModal.closeModal(),
}}
primaryButton={{
text: 'Ya',