mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,174,175): add approval history modal and integrate approval API in RecordingTable
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user