feat: integrate ListDailyChecklistContent component to API

This commit is contained in:
ValdiANS
2026-01-09 14:12:36 +07:00
parent f765895988
commit ceb7cb6b2d
@@ -1,11 +1,10 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react'; import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { Input } from '@/figma-make/components/base/input';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
import { Textarea } from '@/figma-make/components/base/textarea'; import { Textarea } from '@/figma-make/components/base/textarea';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
@@ -25,40 +24,24 @@ import {
DialogFooter, DialogFooter,
} from '@/figma-make/components/base/dialog'; } from '@/figma-make/components/base/dialog';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import useSWR from 'swr';
interface ChecklistItem { import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
checklist_id: string; import { useTableFilter } from '@/services/hooks/useTableFilter';
date: string; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
kandang_name: string; import Table from '@/components/Table';
kandang_id: string; // ✅ Add kandang_id import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
category: string; import { cn } from '@/lib/helper';
status: string; import { ColumnDef } from '@tanstack/react-table';
progress_percent: number; import { useSelect } from '@/components/input/SelectInput';
total_phases: number; import { KandangApi } from '@/services/api/master-data';
total_activities: number; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
updated_at: string;
}
interface Kandang { interface Kandang {
id: string; id: string;
name: string; name: string;
} }
interface ChecklistQueryResult {
id: string;
date: string;
kandang_id: string;
category: string;
status: string;
updated_at: string;
kandang: {
id: string;
name: string;
} | null;
}
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' }, { value: 'ALL', label: 'Semua Status' },
{ value: 'DRAFT', label: 'Draft' }, { value: 'DRAFT', label: 'Draft' },
@@ -76,244 +59,80 @@ const CATEGORY_LABELS: { [key: string]: string } = {
export function ListDailyChecklistContent() { export function ListDailyChecklistContent() {
const router = useRouter(); const router = useRouter();
const [checklistList, setChecklistList] = useState<ChecklistItem[]>([]);
const [filteredList, setFilteredList] = useState<ChecklistItem[]>([]);
const [loading, setLoading] = useState(true);
// Master data const {
const [kandangList, setKandangList] = useState<Kandang[]>([]); 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',
},
});
// Filters const {
const [statusFilter, setStatusFilter] = useState('ALL'); data: checklistListRes,
const [kandangFilter, setKandangFilter] = useState('ALL'); isLoading: isLoadingChecklistList,
const [searchText, setSearchText] = useState(''); mutate: refreshChecklistList,
const [dateFrom, setDateFrom] = useState(''); } = useSWR(
const [dateTo, setDateTo] = useState(''); `${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 // Modals
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedItem, setSelectedItem] = useState<ChecklistItem | 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);
useEffect(() => { const handleDetail = (item: DailyChecklist) => {
fetchKandangList();
fetchChecklistList();
}, []);
useEffect(() => {
applyFilters();
}, [
checklistList,
statusFilter,
kandangFilter,
searchText,
dateFrom,
dateTo,
]);
const fetchKandangList = async () => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (error) {
console.error('Error fetching kandang:', error);
return;
}
setKandangList(data || []);
} catch (error) {
console.error('Error fetching kandang:', error);
}
};
const fetchChecklistList = async () => {
if (!isSupabaseConfigured()) {
console.warn('Supabase not configured');
setLoading(false);
return;
}
try {
setLoading(true);
// ✅ Fetch checklists with joins to get complete data
const { data: checklists, error } = await supabase
.from('daily_checklists')
.select(
`
id,
date,
kandang_id,
category,
status,
updated_at,
kandang:kandang_id (
id,
name
)
`
)
.order('date', { ascending: false })
.order('updated_at', { ascending: false });
if (error) {
console.error('Error fetching checklist list:', error);
toast.error('Gagal memuat data checklist');
return;
}
// ✅ For each checklist, fetch phases, activities, and assignments count
const enrichedData: ChecklistItem[] = await Promise.all(
((checklists as unknown as ChecklistQueryResult[]) || [])
.filter((checklist) => checklist.id) // ✅ Skip checklists with null ID
.map(async (checklist) => {
// Count phases
const { count: phaseCount } = await supabase
.from('daily_checklist_phases')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// Count activities (tasks)
const { count: activityCount } = await supabase
.from('daily_checklist_activity_tasks')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// ✅ NEW LOGIC: Calculate progress based on phase coverage
// Step 1: Get total phases in master data for this category
const { count: totalPhasesInMaster } = await supabase
.from('phases')
.select('*', { count: 'exact', head: true })
.eq('category_id', checklist.category);
// Step 2: Get phases that have at least 1 CHECKED assignment
// First, get all tasks for this checklist
const { data: tasks } = await supabase
.from('daily_checklist_activity_tasks')
.select('id, phase_id')
.eq('checklist_id', checklist.id);
const taskIds = (tasks || []).map((t) => t.id);
const uniquePhasesWithChecked = new Set<string>();
if (taskIds.length > 0) {
// Get assignments that are CHECKED
const { data: checkedAssignments } = await supabase
.from('daily_checklist_activity_task_assignments')
.select('task_id')
.in('task_id', taskIds)
.eq('checked', true); // ✅ Only get checked assignments
if (checkedAssignments && checkedAssignments.length > 0) {
// Map task_ids back to phase_ids
const checkedTaskIds = new Set(
checkedAssignments.map((a) => a.task_id)
);
tasks?.forEach((task) => {
if (checkedTaskIds.has(task.id)) {
uniquePhasesWithChecked.add(task.phase_id);
}
});
}
}
const phasesWithCheckedCount = uniquePhasesWithChecked.size;
// Step 3: Calculate progress
const progressPercent =
totalPhasesInMaster && totalPhasesInMaster > 0
? Math.round(
(phasesWithCheckedCount / totalPhasesInMaster) * 100
)
: 0;
return {
checklist_id: checklist.id,
date: checklist.date,
kandang_name: checklist.kandang?.name || '-',
kandang_id: checklist.kandang_id,
category: checklist.category,
status: checklist.status,
progress_percent: progressPercent,
total_phases: phaseCount || 0,
total_activities: activityCount || 0,
updated_at: checklist.updated_at,
};
})
);
setChecklistList(enrichedData);
} catch (error) {
console.error('Error fetching checklist list:', error);
toast.error('Terjadi kesalahan');
} finally {
setLoading(false);
}
};
const applyFilters = () => {
let filtered = [...checklistList];
// Filter by status
if (statusFilter && statusFilter !== 'ALL') {
filtered = filtered.filter((item) => item.status === statusFilter);
}
// ✅ Filter by kandang - use kandang_id directly from item
if (kandangFilter && kandangFilter !== 'ALL') {
filtered = filtered.filter((item) => item.kandang_id === kandangFilter);
}
// Filter by search text (kandang_name or category)
if (searchText) {
const searchLower = searchText.toLowerCase();
filtered = filtered.filter(
(item) =>
item.kandang_name.toLowerCase().includes(searchLower) ||
item.category.toLowerCase().includes(searchLower) ||
(CATEGORY_LABELS[item.category] || '')
.toLowerCase()
.includes(searchLower)
);
}
// Filter by date range
if (dateFrom) {
filtered = filtered.filter((item) => item.date >= dateFrom);
}
if (dateTo) {
filtered = filtered.filter((item) => item.date <= dateTo);
}
setFilteredList(filtered);
};
const handleDetail = (item: ChecklistItem) => {
router.push( router.push(
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.checklist_id}` `/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
); );
}; };
const handleApprove = (item: ChecklistItem) => { const handleApprove = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setShowApproveModal(true); setShowApproveModal(true);
}; };
const handleReject = (item: ChecklistItem) => { const handleReject = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setRejectReason(''); setRejectReason('');
setShowRejectModal(true); setShowRejectModal(true);
}; };
const handleDelete = (item: ChecklistItem) => { const handleDelete = (item: DailyChecklist) => {
// ✅ VALIDATION: Only DRAFT can be deleted // ✅ VALIDATION: Only DRAFT can be deleted
if (item.status !== 'DRAFT') { if (item.status !== 'DRAFT') {
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', { toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
@@ -327,29 +146,24 @@ export function ListDailyChecklistContent() {
}; };
const confirmApprove = async () => { const confirmApprove = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const approveRes = await DailyChecklistApi.approve(
.from('daily_checklists') String(selectedItem.id)
.update({ );
status: 'APPROVED',
updated_at: new Date().toISOString(),
})
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(approveRes)) {
console.error('Error approving checklist:', error); toast.error('Gagal approve checklist: ' + approveRes.message);
toast.error('Gagal approve checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil di-approve'); toast.success('Checklist berhasil di-approve');
setShowApproveModal(false); setShowApproveModal(false);
setSelectedItem(null); setSelectedItem(null);
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error approving checklist:', error); console.error('Error approving checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -359,7 +173,7 @@ export function ListDailyChecklistContent() {
}; };
const confirmReject = async () => { const confirmReject = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
if (!rejectReason.trim()) { if (!rejectReason.trim()) {
toast.error('Alasan reject harus diisi'); toast.error('Alasan reject harus diisi');
@@ -369,26 +183,21 @@ export function ListDailyChecklistContent() {
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const rejectRes = await DailyChecklistApi.reject(
.from('daily_checklists') String(selectedItem.id),
.update({ rejectReason
status: 'REJECTED', );
reject_reason: rejectReason,
updated_at: new Date().toISOString(),
})
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(rejectRes)) {
console.error('Error rejecting checklist:', error); toast.error('Gagal reject checklist: ' + rejectRes.message);
toast.error('Gagal reject checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil di-reject'); toast.success('Checklist berhasil di-reject');
setShowRejectModal(false); setShowRejectModal(false);
setSelectedItem(null); setSelectedItem(null);
setRejectReason(''); setRejectReason('');
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error rejecting checklist:', error); console.error('Error rejecting checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -398,26 +207,22 @@ export function ListDailyChecklistContent() {
}; };
const confirmDelete = async () => { const confirmDelete = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
.from('daily_checklists')
.delete()
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(deleteRes)) {
console.error('Error deleting checklist:', error); toast.error('Gagal hapus checklist: ' + deleteRes.message);
toast.error('Gagal hapus checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil dihapus'); toast.success('Checklist berhasil dihapus');
setShowDeleteModal(false); setShowDeleteModal(false);
setSelectedItem(null); setSelectedItem(null);
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error deleting checklist:', error); console.error('Error deleting checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -496,6 +301,117 @@ export function ListDailyChecklistContent() {
}); });
}; };
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: 'Update At',
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 === 'SUBMITTED' && (
<>
<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>
</>
)}
<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>
</div>
),
},
];
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
@@ -518,11 +434,11 @@ export function ListDailyChecklistContent() {
<Label>Periode Tanggal</Label> <Label>Periode Tanggal</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<DateRangePicker <DateRangePicker
dateFrom={dateFrom} dateFrom={tableFilterState.date_from}
dateTo={dateTo} dateTo={tableFilterState.date_to}
onDateChange={(from, to) => { onDateChange={(from, to) => {
setDateFrom(from); updateFilter('date_from', from);
setDateTo(to); updateFilter('date_to', to);
}} }}
/> />
</div> </div>
@@ -532,8 +448,10 @@ export function ListDailyChecklistContent() {
<Label htmlFor='kandang-filter'>Kandang</Label> <Label htmlFor='kandang-filter'>Kandang</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<Select <Select
value={kandangFilter} value={tableFilterState.kandang_id}
onValueChange={setKandangFilter} onValueChange={(value) => {
updateFilter('kandang_id', value === 'ALL' ? '' : value);
}}
> >
<SelectTrigger <SelectTrigger
id='kandang-filter' id='kandang-filter'
@@ -543,9 +461,12 @@ export function ListDailyChecklistContent() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}> <SelectItem
{kandang.name} key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -556,7 +477,12 @@ export function ListDailyChecklistContent() {
<div> <div>
<Label htmlFor='status-filter'>Status</Label> <Label htmlFor='status-filter'>Status</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select
value={tableFilterState.status}
onValueChange={(value) => {
updateFilter('status', value === 'ALL' ? '' : value);
}}
>
<SelectTrigger <SelectTrigger
id='status-filter' id='status-filter'
className='border-gray-200' className='border-gray-200'
@@ -577,159 +503,57 @@ export function ListDailyChecklistContent() {
<div> <div>
<Label htmlFor='search-text'>Cari</Label> <Label htmlFor='search-text'>Cari</Label>
<div className='relative mt-1.5'> <div className='relative mt-1.5'>
<Input <DebouncedTextInput
id='search-text' name='search'
type='text' placeholder='Kandang / Kategori'
placeholder='Kandang / Kategori...' value={tableFilterState.search}
value={searchText} onChange={(e) => updateFilter('search', e.target.value)}
onChange={(e) => setSearchText(e.target.value)} className={{
className='border-gray-200 pl-9' 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' />
}
/> />
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
</div> </div>
</div> </div>
</div> </div>
{/* Table Section */} {/* Table Section */}
{loading ? ( <Table<DailyChecklist>
<div className='text-center py-12 text-gray-500'> data={checklistList}
Memuat data... columns={checklistListColumns}
</div> pageSize={tableFilterState.pageSize}
) : filteredList.length > 0 ? ( onPageSizeChange={setPageSize}
<div className='overflow-x-auto'> rowOptions={[10, 20, 50, 100]}
<table className='w-full border border-gray-200 rounded-lg'> page={
<thead> isResponseSuccess(checklistListRes)
<tr className='bg-gray-50 border-b border-gray-200'> ? checklistListRes?.meta?.page
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'> : 0
Tanggal
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kategori
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Status
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Phase
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Progress
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Updated At
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aksi
</th>
</tr>
</thead>
<tbody>
{filteredList.map((item, index) => (
<tr
key={`${item.checklist_id}-${index}`}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
} }
> totalItems={
<td className='py-3 px-4 text-sm text-gray-900'> isResponseSuccess(checklistListRes)
{formatDate(item.date)} ? checklistListRes?.meta?.total_results
</td> : 0
<td className='py-3 px-4 text-sm text-gray-900'> }
{item.kandang_name} onPageChange={setPage}
</td> isLoading={isLoadingChecklistList}
<td className='py-3 px-4 text-sm text-gray-900'> className={{
{CATEGORY_LABELS[item.category] || item.category} containerClassName: cn({
</td> 'w-full mb-20':
<td className='py-3 px-4'> isResponseSuccess(checklistListRes) &&
{getStatusBadge(item.status)} checklistListRes?.data?.length === 0,
</td> }),
<td className='py-3 px-4 text-center text-sm text-gray-900'> tableWrapperClassName:
{item.total_phases} 'overflow-x-auto border border-solid border-base-content/10 rounded-none',
</td> headerRowClassName: 'bg-gray-50/50',
<td className='py-3 px-4 text-center text-sm text-gray-900'> headerColumnClassName:
{item.total_activities} 'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
</td> paginationClassName: 'px-4',
<td className='py-3 px-4 text-center'> }}
<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: `${item.progress_percent}%` }}
/> />
</div>
<span className='text-sm text-gray-700 font-medium'>
{item.progress_percent}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDateTime(item.updated_at)}
</td>
<td className='py-3 px-4'>
<div className='flex items-center justify-center gap-2'>
<Button
size='sm'
variant='outline'
onClick={() => handleDetail(item)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
{item.status === 'SUBMITTED' && (
<>
<Button
size='sm'
onClick={() => handleApprove(item)}
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(item)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-1' />
Reject
</Button>
</>
)}
<Button
size='sm'
variant='destructive'
onClick={() => handleDelete(item)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className='text-center py-12 text-gray-500'>
{searchText ||
dateFrom ||
dateTo ||
statusFilter !== 'ALL' ||
kandangFilter !== 'ALL'
? 'Tidak ada data yang sesuai dengan filter'
: 'Belum ada data checklist'}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -755,7 +579,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
@@ -768,7 +592,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Progress:</span> <span className='text-gray-600'>Progress:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.progress_percent}% {selectedItem.progress}%
</span> </span>
</div> </div>
</div> </div>
@@ -815,7 +639,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
@@ -888,7 +712,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>