mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE): Replace approval history modal with status badge
This commit is contained in:
@@ -7,14 +7,12 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { RefObject } from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import Modal from '@/components/Modal';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
@@ -28,14 +26,55 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { ApprovalApi } from '@/services/api/approval';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { BaseApproval, BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
|
// ===== 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,
|
||||||
|
'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 = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -135,221 +174,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 RecordingTable = () => {
|
||||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
||||||
const previousPathRef = useRef<string | null>(null);
|
const previousPathRef = useRef<string | null>(null);
|
||||||
@@ -395,7 +219,6 @@ const RecordingTable = () => {
|
|||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
const approvalHistoryModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: recordings,
|
data: recordings,
|
||||||
@@ -1032,32 +855,22 @@ const RecordingTable = () => {
|
|||||||
const approval = props.row.original.approval;
|
const approval = props.row.original.approval;
|
||||||
if (!approval) return '-';
|
if (!approval) return '-';
|
||||||
|
|
||||||
const statusColor =
|
const status = approval.action;
|
||||||
approval.action === 'APPROVED'
|
const statusColor = getStatusBadgeColor(status);
|
||||||
? 'success'
|
|
||||||
: approval.action === 'REJECTED'
|
|
||||||
? 'error'
|
|
||||||
: approval.action === 'UPDATED'
|
|
||||||
? 'warning'
|
|
||||||
: 'info';
|
|
||||||
|
|
||||||
const openApprovalHistory = () => {
|
const statusText =
|
||||||
setSelectedRecording(props.row.original);
|
status === 'REJECTED'
|
||||||
approvalHistoryModal.openModal();
|
? 'Ditolak'
|
||||||
};
|
: approval.step_name || getStatusText(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<StatusBadge
|
||||||
variant='soft'
|
|
||||||
color={statusColor}
|
color={statusColor}
|
||||||
|
text={statusText}
|
||||||
className={{
|
className={{
|
||||||
badge:
|
badge: 'whitespace-nowrap',
|
||||||
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
|
|
||||||
}}
|
}}
|
||||||
onClick={openApprovalHistory}
|
/>
|
||||||
>
|
|
||||||
{approval.step_name || approval.action}
|
|
||||||
</Badge>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1208,7 +1021,10 @@ const RecordingTable = () => {
|
|||||||
text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
approveModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
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)?`}
|
text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
@@ -1237,13 +1056,6 @@ const RecordingTable = () => {
|
|||||||
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
|
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ApprovalHistoryModal
|
|
||||||
ref={approvalHistoryModal.ref}
|
|
||||||
currentApproval={selectedRecording?.approval}
|
|
||||||
module_name={'RECORDINGS'}
|
|
||||||
module_id={selectedRecording?.id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user