mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat: implement bulk approve & reject
This commit is contained in:
+300
-8
@@ -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,13 +576,39 @@ export function ListDailyChecklistContent() {
|
||||
<div className='min-h-screen'>
|
||||
<div className='p-6'>
|
||||
{/* Page Title */}
|
||||
<div className='mb-6'>
|
||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||
List Daily Checklist
|
||||
</h1>
|
||||
<p className='text-sm text-gray-600 mt-1'>
|
||||
Daftar semua checklist harian
|
||||
</p>
|
||||
<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>
|
||||
<p className='text-sm text-gray-600 mt-1'>
|
||||
Daftar semua checklist harian
|
||||
</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 */}
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user