feat(FE-170,174,175): add approval history modal and integrate approval API in RecordingTable

This commit is contained in:
rstubryan
2025-11-05 13:41:55 +07:00
parent fac9d5fa42
commit b1457a5feb
2 changed files with 259 additions and 3 deletions
@@ -1,11 +1,13 @@
'use client'; 'use client';
import { useCallback, useState } from 'react'; import { useCallback, useState, useMemo, useEffect } 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 } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
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 { OptionType, useSelect } from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput';
@@ -21,6 +23,7 @@ import { RecordingApi } from '@/services/api/production';
import { AreaApi } from '@/services/api/master-data'; import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
import { KandangApi } from '@/services/api/master-data'; import { KandangApi } from '@/services/api/master-data';
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';
@@ -30,6 +33,7 @@ import TextArea from '@/components/input/TextArea';
import { Area } from '@/types/api/master-data/area'; import { Area } from '@/types/api/master-data/area';
import { Location } from '@/types/api/master-data/location'; import { Location } from '@/types/api/master-data/location';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { BaseApproval, BaseApiResponse } from '@/types/api/api-general';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', type = 'dropdown',
@@ -100,6 +104,209 @@ const RowOptionsMenu = ({
); );
}; };
const ApprovalHistoryModal = ({
ref,
currentApproval,
module_name = 'RECORDINGS',
}: {
ref: RefObject<HTMLDialogElement | null>;
currentApproval?: BaseApproval;
module_name?: string;
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const approvalHistoryUrl = useMemo(() => {
if (!isModalOpen) return null;
return `${ApprovalApi.basePath}?${new URLSearchParams({
module_name: module_name,
group_step_number: 'true',
}).toString()}`;
}, [module_name, 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}
className='btn btn-sm btn-circle btn-ghost'
>
<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'
: 'info'
}
>
{currentApproval.step_name}
</Badge>
<span className='text-sm text-gray-600'>
{currentApproval.action === 'APPROVED' && 'Disetujui'}
{currentApproval.action === 'REJECTED' && 'Ditolak'}
{currentApproval.action === 'CREATED' && 'Dibuat'}
</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'
: 'info'
}
size='sm'
>
{approval.action === 'APPROVED' && 'Disetujui'}
{approval.action === 'REJECTED' && 'Ditolak'}
{approval.action === 'CREATED' && 'Dibuat'}
</Badge>
</td>
<td className='max-w-xs'>
<div
className='truncate'
title={approval.notes || '-'}
>
{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 { const {
state: tableFilterState, state: tableFilterState,
@@ -142,6 +349,7 @@ const RecordingTable = () => {
const singleDeleteModal = useModal(); const singleDeleteModal = useModal();
const approveModal = useModal(); const approveModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
const approvalHistoryModal = useModal();
// State for selected values // State for selected values
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null); const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
@@ -472,11 +680,47 @@ const RecordingTable = () => {
}, },
{ {
header: 'Status Approval', header: 'Status Approval',
cell: (props) => props.row.original.approval?.step_name || '-', cell: (props) => {
const approval = props.row.original.approval;
if (!approval) return '-';
const statusColor =
approval.action === 'APPROVED'
? 'success'
: approval.action === 'REJECTED'
? 'error'
: 'info';
const openApprovalHistory = () => {
setSelectedRecording(props.row.original);
approvalHistoryModal.openModal();
};
return (
<button
onClick={openApprovalHistory}
className='btn btn-ghost btn-xs p-0 h-auto min-h-0 text-left hover:bg-transparent'
title='Klik untuk lihat riwayat approval'
>
<Badge variant='soft' color={statusColor} size='sm'>
{approval.step_name}
</Badge>
</button>
);
},
}, },
{ {
header: 'Catatan Approval', header: 'Catatan Approval',
cell: (props) => props.row.original.approval?.notes || '-', cell: (props) => {
const approval = props.row.original.approval;
if (!approval?.notes) return '-';
return (
<div className='max-w-xs' title={approval.notes}>
<p className='text-sm truncate'>{approval.notes}</p>
</div>
);
},
}, },
{ {
header: 'Status Grading Telur', header: 'Status Grading Telur',
@@ -652,6 +896,12 @@ const RecordingTable = () => {
rows={3} rows={3}
/> />
</ConfirmationModal> </ConfirmationModal>
<ApprovalHistoryModal
ref={approvalHistoryModal.ref}
currentApproval={selectedRecording?.approval}
module_name={'RECORDINGS'}
/>
</div> </div>
); );
}; };
+6
View File
@@ -0,0 +1,6 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApproval } from '@/types/api/api-general';
export const ApprovalApi = new BaseApiService<BaseApproval, unknown, unknown>(
'/approvals'
);