feat: implement bulk approve & reject

This commit is contained in:
ValdiANS
2026-04-20 16:21:17 +07:00
parent c5a0cfe118
commit 5bf3d32636
@@ -40,11 +40,12 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import Table from '@/components/Table';
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table';
import { ColumnDef, Row } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import CheckboxInput from '@/components/input/CheckboxInput';
const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' },
@@ -122,12 +123,29 @@ export function ListDailyChecklistContent() {
// Modals
const [showApproveModal, setShowApproveModal] = useState(false);
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [showBulkRejectModal, setShowBulkRejectModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection);
const selectedRowItems = selectedRowIds.map((itemId) =>
checklistList.find((item) => item.id === parseInt(itemId))
);
const tableEnableRowSelectionHandler: (
row: Row<DailyChecklist>
) => boolean = (row) => {
return (
row.original.status !== 'APPROVED' && row.original.status !== 'REJECTED'
);
};
const handleDetail = (item: DailyChecklist) => {
router.push(
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
@@ -149,12 +167,21 @@ export function ListDailyChecklistContent() {
setShowApproveModal(true);
};
const handleBulkApprove = () => {
setShowBulkApproveModal(true);
};
const handleReject = (item: DailyChecklist) => {
setSelectedItem(item);
setRejectReason('');
setShowRejectModal(true);
};
const handleBulkReject = () => {
setRejectReason('');
setShowBulkRejectModal(true);
};
const handleDelete = (item: DailyChecklist) => {
// ✅ VALIDATION: Only DRAFT can be deleted
if (item.status !== 'DRAFT') {
@@ -195,6 +222,31 @@ export function ListDailyChecklistContent() {
}
};
const confirmBulkApprove = async () => {
if (!selectedRowIds.length) return;
try {
setActionLoading(true);
const approveRes = await DailyChecklistApi.bulkApprove(selectedRowIds);
if (isResponseError(approveRes)) {
toast.error('Gagal approve checklist: ' + approveRes.message);
return;
}
refreshChecklistList();
toast.success('Checklist berhasil di-approve');
setShowBulkApproveModal(false);
setRowSelection({});
} catch (error) {
console.error('Error approving checklist:', error);
toast.error('Terjadi kesalahan');
} finally {
setActionLoading(false);
}
};
const confirmReject = async () => {
if (!selectedItem) return;
@@ -229,6 +281,40 @@ export function ListDailyChecklistContent() {
}
};
const confirmBulkReject = async () => {
if (!selectedRowIds.length) return;
if (!rejectReason.trim()) {
toast.error('Alasan reject harus diisi');
return;
}
try {
setActionLoading(true);
const rejectRes = await DailyChecklistApi.bulkReject(
selectedRowIds,
rejectReason
);
if (isResponseError(rejectRes)) {
toast.error('Gagal reject checklist: ' + rejectRes.message);
return;
}
refreshChecklistList();
toast.success('Checklist berhasil di-reject');
setShowBulkRejectModal(false);
setRowSelection({});
setRejectReason('');
} catch (error) {
console.error('Error rejecting checklist:', error);
toast.error('Terjadi kesalahan');
} finally {
setActionLoading(false);
}
};
const confirmDelete = async () => {
if (!selectedItem) return;
@@ -325,6 +411,37 @@ export function ListDailyChecklistContent() {
};
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
{
id: 'select',
header: ({ table }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }) => {
const isCheckboxDisabled =
!row.getCanSelect() ||
row.original.status === 'APPROVED' ||
row.original.status === 'REJECTED';
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={isCheckboxDisabled}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
},
},
{
accessorKey: 'date',
header: 'Tanggal',
@@ -459,7 +576,8 @@ export function ListDailyChecklistContent() {
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<div className='mb-6 flex flex-row justify-between items-center gap-3'>
<div>
<h1 className='text-2xl font-semibold text-gray-900'>
List Daily Checklist
</h1>
@@ -468,6 +586,31 @@ export function ListDailyChecklistContent() {
</p>
</div>
<RequirePermission permissions='lti.daily_checklist.create'>
{selectedRowIds.length > 0 && (
<div className='flex flex-row items-center gap-3'>
<Button
size='sm'
onClick={handleBulkApprove}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-1' />
Bulk Approve {`(${selectedRowIds.length}) item`}
</Button>
<Button
size='sm'
variant='destructive'
onClick={handleBulkReject}
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-1' />
Bulk Reject {`(${selectedRowIds.length}) item`}
</Button>
</div>
)}
</RequirePermission>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-6'>
@@ -588,6 +731,10 @@ export function ListDailyChecklistContent() {
}
onPageChange={setPage}
isLoading={isLoadingChecklistList}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
withCheckbox
className={{
containerClassName: cn({
'w-full mb-20':
@@ -666,6 +813,76 @@ export function ListDailyChecklistContent() {
</DialogContent>
</Dialog>
{/* Bulk Approve Modal */}
<Dialog
open={showBulkApproveModal}
onOpenChange={setShowBulkApproveModal}
>
<DialogContent className='sm:max-w-md max-h-[80vh] overflow-y-auto bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>Approve Checklist</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin approve {selectedRowIds.length} checklist
ini?
</DialogDescription>
</DialogHeader>
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
{selectedRowItems.map((item) => (
<div
key={item?.id ?? 0}
className='bg-gray-50 rounded-lg p-4 space-y-2'
>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Tanggal:</span>
<span className='font-medium text-gray-900'>
{formatDate(item?.date ?? '')}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{item?.kandang?.name ?? '-'}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kategori:</span>
<span className='font-medium text-gray-900'>
{item?.category
? (CATEGORY_LABELS[item.category] ?? item?.category)
: item?.category}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Progress:</span>
<span className='font-medium text-gray-900'>
{item?.progress}%
</span>
</div>
</div>
))}
</div>
<DialogFooter className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowBulkApproveModal(false)}
disabled={actionLoading}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={confirmBulkApprove}
disabled={actionLoading}
className='bg-green-600 hover:bg-green-700 text-white'
>
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Reject Modal */}
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
@@ -735,6 +952,81 @@ export function ListDailyChecklistContent() {
</DialogContent>
</Dialog>
{/* Bulk Reject Modal */}
<Dialog open={showBulkRejectModal} onOpenChange={setShowBulkRejectModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>Reject Checklist</DialogTitle>
<DialogDescription>
Berikan alasan reject untuk checklist ini
</DialogDescription>
</DialogHeader>
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
{selectedRowItems.map((item) => (
<div
key={item?.id ?? 0}
className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'
>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Tanggal:</span>
<span className='font-medium text-gray-900'>
{formatDate(item?.date ?? '')}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{item?.kandang?.name ?? '-'}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kategori:</span>
<span className='font-medium text-gray-900'>
{item?.category
? CATEGORY_LABELS[item.category] || item?.category
: item?.category}
</span>
</div>
</div>
))}
</div>
<div>
<Label htmlFor='reject-reason'>
Alasan Reject <span className='text-red-500'>*</span>
</Label>
<Textarea
id='reject-reason'
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder='Tuliskan alasan reject...'
className='mt-1.5 border-gray-200 min-h-[100px]'
disabled={actionLoading}
/>
</div>
<DialogFooter className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowBulkRejectModal(false)}
disabled={actionLoading}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={confirmBulkReject}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Modal */}
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>