mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
feat: integrate ListDailyChecklistContent component to API
This commit is contained in:
+263
-439
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Eye, CheckCircle, XCircle, Search, Trash2 } 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 { Input } from '@/figma-make/components/base/input';
|
||||
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';
|
||||
@@ -25,40 +24,24 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/figma-make/components/base/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface ChecklistItem {
|
||||
checklist_id: string;
|
||||
date: string;
|
||||
kandang_name: string;
|
||||
kandang_id: string; // ✅ Add kandang_id
|
||||
category: string;
|
||||
status: string;
|
||||
progress_percent: number;
|
||||
total_phases: number;
|
||||
total_activities: number;
|
||||
updated_at: string;
|
||||
}
|
||||
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';
|
||||
|
||||
interface Kandang {
|
||||
id: 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 = [
|
||||
{ value: 'ALL', label: 'Semua Status' },
|
||||
{ value: 'DRAFT', label: 'Draft' },
|
||||
@@ -76,244 +59,80 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
||||
|
||||
export function ListDailyChecklistContent() {
|
||||
const router = useRouter();
|
||||
const [checklistList, setChecklistList] = useState<ChecklistItem[]>([]);
|
||||
const [filteredList, setFilteredList] = useState<ChecklistItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Master data
|
||||
const [kandangList, setKandangList] = useState<Kandang[]>([]);
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
// Filters
|
||||
const [statusFilter, setStatusFilter] = useState('ALL');
|
||||
const [kandangFilter, setKandangFilter] = useState('ALL');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
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<ChecklistItem | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
const handleDetail = (item: DailyChecklist) => {
|
||||
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);
|
||||
setShowApproveModal(true);
|
||||
};
|
||||
|
||||
const handleReject = (item: ChecklistItem) => {
|
||||
const handleReject = (item: DailyChecklist) => {
|
||||
setSelectedItem(item);
|
||||
setRejectReason('');
|
||||
setShowRejectModal(true);
|
||||
};
|
||||
|
||||
const handleDelete = (item: ChecklistItem) => {
|
||||
const handleDelete = (item: DailyChecklist) => {
|
||||
// ✅ VALIDATION: Only DRAFT can be deleted
|
||||
if (item.status !== 'DRAFT') {
|
||||
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
||||
@@ -327,29 +146,24 @@ export function ListDailyChecklistContent() {
|
||||
};
|
||||
|
||||
const confirmApprove = async () => {
|
||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
||||
if (!selectedItem) return;
|
||||
|
||||
try {
|
||||
setActionLoading(true);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('daily_checklists')
|
||||
.update({
|
||||
status: 'APPROVED',
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', selectedItem.checklist_id);
|
||||
const approveRes = await DailyChecklistApi.approve(
|
||||
String(selectedItem.id)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('Error approving checklist:', error);
|
||||
toast.error('Gagal approve checklist');
|
||||
if (isResponseError(approveRes)) {
|
||||
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshChecklistList();
|
||||
toast.success('Checklist berhasil di-approve');
|
||||
setShowApproveModal(false);
|
||||
setSelectedItem(null);
|
||||
await fetchChecklistList();
|
||||
} catch (error) {
|
||||
console.error('Error approving checklist:', error);
|
||||
toast.error('Terjadi kesalahan');
|
||||
@@ -359,7 +173,7 @@ export function ListDailyChecklistContent() {
|
||||
};
|
||||
|
||||
const confirmReject = async () => {
|
||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
||||
if (!selectedItem) return;
|
||||
|
||||
if (!rejectReason.trim()) {
|
||||
toast.error('Alasan reject harus diisi');
|
||||
@@ -369,26 +183,21 @@ export function ListDailyChecklistContent() {
|
||||
try {
|
||||
setActionLoading(true);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('daily_checklists')
|
||||
.update({
|
||||
status: 'REJECTED',
|
||||
reject_reason: rejectReason,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', selectedItem.checklist_id);
|
||||
const rejectRes = await DailyChecklistApi.reject(
|
||||
String(selectedItem.id),
|
||||
rejectReason
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('Error rejecting checklist:', error);
|
||||
toast.error('Gagal reject checklist');
|
||||
if (isResponseError(rejectRes)) {
|
||||
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshChecklistList();
|
||||
toast.success('Checklist berhasil di-reject');
|
||||
setShowRejectModal(false);
|
||||
setSelectedItem(null);
|
||||
setRejectReason('');
|
||||
await fetchChecklistList();
|
||||
} catch (error) {
|
||||
console.error('Error rejecting checklist:', error);
|
||||
toast.error('Terjadi kesalahan');
|
||||
@@ -398,26 +207,22 @@ export function ListDailyChecklistContent() {
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
||||
if (!selectedItem) return;
|
||||
|
||||
try {
|
||||
setActionLoading(true);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('daily_checklists')
|
||||
.delete()
|
||||
.eq('id', selectedItem.checklist_id);
|
||||
const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting checklist:', error);
|
||||
toast.error('Gagal hapus checklist');
|
||||
if (isResponseError(deleteRes)) {
|
||||
toast.error('Gagal hapus checklist: ' + deleteRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshChecklistList();
|
||||
toast.success('Checklist berhasil dihapus');
|
||||
setShowDeleteModal(false);
|
||||
setSelectedItem(null);
|
||||
await fetchChecklistList();
|
||||
} catch (error) {
|
||||
console.error('Error deleting checklist:', error);
|
||||
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 (
|
||||
<div className='min-h-screen'>
|
||||
<div className='p-6'>
|
||||
@@ -518,11 +434,11 @@ export function ListDailyChecklistContent() {
|
||||
<Label>Periode Tanggal</Label>
|
||||
<div className='mt-1.5'>
|
||||
<DateRangePicker
|
||||
dateFrom={dateFrom}
|
||||
dateTo={dateTo}
|
||||
dateFrom={tableFilterState.date_from}
|
||||
dateTo={tableFilterState.date_to}
|
||||
onDateChange={(from, to) => {
|
||||
setDateFrom(from);
|
||||
setDateTo(to);
|
||||
updateFilter('date_from', from);
|
||||
updateFilter('date_to', to);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -532,8 +448,10 @@ export function ListDailyChecklistContent() {
|
||||
<Label htmlFor='kandang-filter'>Kandang</Label>
|
||||
<div className='mt-1.5'>
|
||||
<Select
|
||||
value={kandangFilter}
|
||||
onValueChange={setKandangFilter}
|
||||
value={tableFilterState.kandang_id}
|
||||
onValueChange={(value) => {
|
||||
updateFilter('kandang_id', value === 'ALL' ? '' : value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
id='kandang-filter'
|
||||
@@ -543,9 +461,12 @@ export function ListDailyChecklistContent() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||
{kandangList.map((kandang) => (
|
||||
<SelectItem key={kandang.id} value={kandang.id}>
|
||||
{kandang.name}
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
key={kandang.value}
|
||||
value={String(kandang.value)}
|
||||
>
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -556,7 +477,12 @@ export function ListDailyChecklistContent() {
|
||||
<div>
|
||||
<Label htmlFor='status-filter'>Status</Label>
|
||||
<div className='mt-1.5'>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<Select
|
||||
value={tableFilterState.status}
|
||||
onValueChange={(value) => {
|
||||
updateFilter('status', value === 'ALL' ? '' : value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
id='status-filter'
|
||||
className='border-gray-200'
|
||||
@@ -577,159 +503,57 @@ export function ListDailyChecklistContent() {
|
||||
<div>
|
||||
<Label htmlFor='search-text'>Cari</Label>
|
||||
<div className='relative mt-1.5'>
|
||||
<Input
|
||||
id='search-text'
|
||||
type='text'
|
||||
placeholder='Kandang / Kategori...'
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className='border-gray-200 pl-9'
|
||||
<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' />
|
||||
}
|
||||
/>
|
||||
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
{loading ? (
|
||||
<div className='text-center py-12 text-gray-500'>
|
||||
Memuat data...
|
||||
</div>
|
||||
) : filteredList.length > 0 ? (
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full border border-gray-200 rounded-lg'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||
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'
|
||||
}
|
||||
>
|
||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
||||
{formatDate(item.date)}
|
||||
</td>
|
||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
||||
{item.kandang_name}
|
||||
</td>
|
||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
||||
{CATEGORY_LABELS[item.category] || item.category}
|
||||
</td>
|
||||
<td className='py-3 px-4'>
|
||||
{getStatusBadge(item.status)}
|
||||
</td>
|
||||
<td className='py-3 px-4 text-center text-sm text-gray-900'>
|
||||
{item.total_phases}
|
||||
</td>
|
||||
<td className='py-3 px-4 text-center text-sm text-gray-900'>
|
||||
{item.total_activities}
|
||||
</td>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
@@ -755,7 +579,7 @@ export function ListDailyChecklistContent() {
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Kandang:</span>
|
||||
<span className='font-medium text-gray-900'>
|
||||
{selectedItem.kandang_name}
|
||||
{selectedItem.kandang.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-between text-sm'>
|
||||
@@ -768,7 +592,7 @@ export function ListDailyChecklistContent() {
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Progress:</span>
|
||||
<span className='font-medium text-gray-900'>
|
||||
{selectedItem.progress_percent}%
|
||||
{selectedItem.progress}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -815,7 +639,7 @@ export function ListDailyChecklistContent() {
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Kandang:</span>
|
||||
<span className='font-medium text-gray-900'>
|
||||
{selectedItem.kandang_name}
|
||||
{selectedItem.kandang.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-between text-sm'>
|
||||
@@ -888,7 +712,7 @@ export function ListDailyChecklistContent() {
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Kandang:</span>
|
||||
<span className='font-medium text-gray-900'>
|
||||
{selectedItem.kandang_name}
|
||||
{selectedItem.kandang.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-between text-sm'>
|
||||
|
||||
Reference in New Issue
Block a user