mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +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 Table from '@/components/Table';
|
||||||
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { cn } from '@/lib/helper';
|
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 { useSelect } from '@/components/input/SelectInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -122,12 +123,29 @@ export function ListDailyChecklistContent() {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
|
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
|
const [showBulkRejectModal, setShowBulkRejectModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
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) => {
|
const handleDetail = (item: DailyChecklist) => {
|
||||||
router.push(
|
router.push(
|
||||||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||||||
@@ -149,12 +167,21 @@ export function ListDailyChecklistContent() {
|
|||||||
setShowApproveModal(true);
|
setShowApproveModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkApprove = () => {
|
||||||
|
setShowBulkApproveModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReject = (item: DailyChecklist) => {
|
const handleReject = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
setShowRejectModal(true);
|
setShowRejectModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkReject = () => {
|
||||||
|
setRejectReason('');
|
||||||
|
setShowBulkRejectModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = (item: DailyChecklist) => {
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
// ✅ VALIDATION: Only DRAFT can be deleted
|
// ✅ VALIDATION: Only DRAFT can be deleted
|
||||||
if (item.status !== 'DRAFT') {
|
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 () => {
|
const confirmReject = async () => {
|
||||||
if (!selectedItem) return;
|
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 () => {
|
const confirmDelete = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -325,6 +411,37 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
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',
|
accessorKey: 'date',
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
@@ -459,13 +576,39 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className='mb-6'>
|
<div className='mb-6 flex flex-row justify-between items-center gap-3'>
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
<div>
|
||||||
List Daily Checklist
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
</h1>
|
List Daily Checklist
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
</h1>
|
||||||
Daftar semua checklist harian
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Main Card */}
|
{/* Main Card */}
|
||||||
@@ -588,6 +731,10 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
isLoading={isLoadingChecklistList}
|
isLoading={isLoadingChecklistList}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
withCheckbox
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn({
|
||||||
'w-full mb-20':
|
'w-full mb-20':
|
||||||
@@ -666,6 +813,76 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 */}
|
{/* Reject Modal */}
|
||||||
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
@@ -735,6 +952,81 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 */}
|
{/* Delete Modal */}
|
||||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
|||||||
Reference in New Issue
Block a user