Files
lti-web-client/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx
T

794 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge';
import { Label } from '@/figma-make/components/base/label';
import { Textarea } from '@/figma-make/components/base/textarea';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
import useSWR from 'swr';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { useTableFilter } from '@/services/hooks/useTableFilter';
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 { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission';
interface Kandang {
id: string;
name: string;
}
const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' },
{ value: 'DRAFT', label: 'Draft' },
{ value: 'SUBMITTED', label: 'Submitted' },
{ value: 'APPROVED', label: 'Approved' },
{ value: 'REJECTED', label: 'Rejected' },
];
const CATEGORY_LABELS: { [key: string]: string } = {
pullet_open: 'Pullet Open',
pullet_close: 'Pullet Close',
produksi_open: 'Produksi Open',
produksi_close: 'Produksi Close',
};
export function ListDailyChecklistContent() {
const router = useRouter();
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
date_from: '',
date_to: '',
search: '',
kandang_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
kandang_id: 'kandang_id',
status: 'status',
date_from: 'date_from',
date_to: 'date_to',
},
});
const {
data: checklistListRes,
isLoading: isLoadingChecklistList,
mutate: refreshChecklistList,
} = useSWR(
`${DailyChecklistApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || []
: [];
// Modals
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const handleDetail = (item: DailyChecklist) => {
router.push(
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
);
};
const handleEdit = (item: DailyChecklist) => {
const formattedDate = new Date(item.date).toISOString().split('T')[0];
const kandangId = item.kandang.id;
const category = item.category;
router.push(
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
);
};
const handleApprove = (item: DailyChecklist) => {
setSelectedItem(item);
setShowApproveModal(true);
};
const handleReject = (item: DailyChecklist) => {
setSelectedItem(item);
setRejectReason('');
setShowRejectModal(true);
};
const handleDelete = (item: DailyChecklist) => {
// ✅ VALIDATION: Only DRAFT can be deleted
if (item.status !== 'DRAFT') {
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
description: `Status saat ini: ${item.status}`,
});
return;
}
setSelectedItem(item);
setShowDeleteModal(true);
};
const confirmApprove = async () => {
if (!selectedItem) return;
try {
setActionLoading(true);
const approveRes = await DailyChecklistApi.approve(
String(selectedItem.id)
);
if (isResponseError(approveRes)) {
toast.error('Gagal approve checklist: ' + approveRes.message);
return;
}
refreshChecklistList();
toast.success('Checklist berhasil di-approve');
setShowApproveModal(false);
setSelectedItem(null);
} catch (error) {
console.error('Error approving checklist:', error);
toast.error('Terjadi kesalahan');
} finally {
setActionLoading(false);
}
};
const confirmReject = async () => {
if (!selectedItem) return;
if (!rejectReason.trim()) {
toast.error('Alasan reject harus diisi');
return;
}
try {
setActionLoading(true);
const rejectRes = await DailyChecklistApi.reject(
String(selectedItem.id),
rejectReason
);
if (isResponseError(rejectRes)) {
toast.error('Gagal reject checklist: ' + rejectRes.message);
return;
}
refreshChecklistList();
toast.success('Checklist berhasil di-reject');
setShowRejectModal(false);
setSelectedItem(null);
setRejectReason('');
} catch (error) {
console.error('Error rejecting checklist:', error);
toast.error('Terjadi kesalahan');
} finally {
setActionLoading(false);
}
};
const confirmDelete = async () => {
if (!selectedItem) return;
try {
setActionLoading(true);
const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
if (isResponseError(deleteRes)) {
toast.error('Gagal hapus checklist: ' + deleteRes.message);
return;
}
refreshChecklistList();
toast.success('Checklist berhasil dihapus');
setShowDeleteModal(false);
setSelectedItem(null);
} catch (error) {
console.error('Error deleting checklist:', error);
toast.error('Terjadi kesalahan');
} finally {
setActionLoading(false);
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'DRAFT':
return (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
Draft
</Badge>
);
case 'SUBMITTED':
return (
<Badge
variant='outline'
className='border-orange-300 text-orange-700 bg-white'
>
Submitted
</Badge>
);
case 'APPROVED':
return (
<Badge
variant='outline'
className='border-green-300 text-green-700 bg-white'
>
Approved
</Badge>
);
case 'REJECTED':
return (
<Badge
variant='outline'
className='border-red-300 text-red-700 bg-white'
>
Rejected
</Badge>
);
default:
return (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
{status}
</Badge>
);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
{
accessorKey: 'date',
header: 'Tanggal',
enableSorting: false,
cell: ({ row }) => formatDate(row.original.date),
},
{
accessorKey: 'kandang',
header: 'Kandang',
enableSorting: false,
cell: ({ row }) => row.original.kandang.name,
},
{
accessorKey: 'category',
header: 'Kategori',
enableSorting: false,
cell: ({ row }) =>
CATEGORY_LABELS[row.original.category] || row.original.category,
},
{
accessorKey: 'status',
header: 'Status',
enableSorting: false,
cell: ({ row }) => getStatusBadge(row.original.status),
},
{
accessorKey: 'total_phase',
header: 'Total Phase',
enableSorting: false,
},
{
accessorKey: 'total_activity',
header: 'Total Aktivitas',
enableSorting: false,
},
{
accessorKey: 'progress',
header: 'Progress',
enableSorting: false,
cell: ({ row }) => (
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='bg-[#0069e0] h-2 rounded-full transition-all'
style={{ width: `${row.original.progress}%` }}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{row.original.progress}%
</span>
</div>
),
},
{
accessorKey: 'updated_at',
header: 'Diperbarui',
enableSorting: false,
cell: ({ row }) => formatDateTime(row.original.updated_at),
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<div className='flex items-center justify-center gap-2'>
<Button
size='sm'
variant='outline'
onClick={() => handleDetail(row.original)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
{row.original.status === 'DRAFT' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(row.original)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Edit className='w-4 h-4 mr-1' />
Edit
</Button>
</RequirePermission>
)}
{row.original.status === 'SUBMITTED' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<Button
size='sm'
onClick={() => handleApprove(row.original)}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-1' />
Approve
</Button>
<Button
size='sm'
variant='destructive'
onClick={() => handleReject(row.original)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-1' />
Reject
</Button>
</RequirePermission>
)}
{row.original.status === 'DRAFT' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<Button
size='sm'
variant='destructive'
onClick={() => handleDelete(row.original)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
</RequirePermission>
)}
</div>
),
},
];
return (
<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>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-6'>
{/* Filters Section */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 pb-6 border-b border-gray-200'>
<div>
<Label>Periode Tanggal</Label>
<div className='mt-1.5'>
<DateRangePicker
dateFrom={tableFilterState.date_from}
dateTo={tableFilterState.date_to}
onDateChange={(from, to) => {
updateFilter('date_from', from);
updateFilter('date_to', to);
}}
/>
</div>
</div>
<div>
<Label htmlFor='kandang-filter'>Kandang</Label>
<div className='mt-1.5'>
<Select
value={tableFilterState.kandang_id}
onValueChange={(value) => {
updateFilter('kandang_id', value === 'ALL' ? '' : value);
}}
>
<SelectTrigger
id='kandang-filter'
className='border-gray-200'
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor='status-filter'>Status</Label>
<div className='mt-1.5'>
<Select
value={tableFilterState.status}
onValueChange={(value) => {
updateFilter('status', value === 'ALL' ? '' : value);
}}
>
<SelectTrigger
id='status-filter'
className='border-gray-200'
>
<SelectValue placeholder='Semua Status' />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor='search-text'>Cari</Label>
<div className='relative mt-1.5'>
<DebouncedTextInput
name='search'
placeholder='Kandang / Kategori'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
</div>
</div>
{/* Table Section */}
<Table<DailyChecklist>
data={checklistList}
columns={checklistListColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(checklistListRes)
? checklistListRes?.meta?.page
: 0
}
totalItems={
isResponseSuccess(checklistListRes)
? checklistListRes?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingChecklistList}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(checklistListRes) &&
checklistListRes?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Approve Modal */}
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>Approve Checklist</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin approve checklist ini?
</DialogDescription>
</DialogHeader>
{selectedItem && (
<div 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(selectedItem.date)}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.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'>
{CATEGORY_LABELS[selectedItem.category] ||
selectedItem.category}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Progress:</span>
<span className='font-medium text-gray-900'>
{selectedItem.progress}%
</span>
</div>
</div>
)}
<DialogFooter className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowApproveModal(false)}
disabled={actionLoading}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={confirmApprove}
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'>
<DialogHeader>
<DialogTitle>Reject Checklist</DialogTitle>
<DialogDescription>
Berikan alasan reject untuk checklist ini
</DialogDescription>
</DialogHeader>
{selectedItem && (
<div 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(selectedItem.date)}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.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'>
{CATEGORY_LABELS[selectedItem.category] ||
selectedItem.category}
</span>
</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={() => setShowRejectModal(false)}
disabled={actionLoading}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={confirmReject}
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'>
<DialogHeader>
<DialogTitle className='text-red-600'>
Hapus Checklist
</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menghapus checklist ini? Data yang dihapus
tidak dapat dikembalikan.
</DialogDescription>
</DialogHeader>
{selectedItem && (
<>
<div className='bg-red-50 border border-red-200 rounded-lg p-4 space-y-2 mb-2'>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Tanggal:</span>
<span className='font-medium text-gray-900'>
{formatDate(selectedItem.date)}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.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'>
{CATEGORY_LABELS[selectedItem.category] ||
selectedItem.category}
</span>
</div>
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Status:</span>
{getStatusBadge(selectedItem.status)}
</div>
</div>
<div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
<p className='text-xs text-yellow-800'>
<strong>Peringatan:</strong> Semua data terkait (phases,
activities, assignments) akan ikut terhapus secara permanen.
</p>
</div>
</>
)}
<DialogFooter className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowDeleteModal(false)}
disabled={actionLoading}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={confirmDelete}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
{actionLoading ? 'Memproses...' : 'Ya, Hapus'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}