diff --git a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx index b0fb87e3..9936a31f 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -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([]); - const [filteredList, setFilteredList] = useState([]); - const [loading, setLoading] = useState(true); - // Master data - const [kandangList, setKandangList] = useState([]); + 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(null); + const [selectedItem, setSelectedItem] = useState(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(); - - 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[] = [ + { + 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 }) => ( +
+
+
+
+ + {row.original.progress}% + +
+ ), + }, + { + 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 }) => ( +
+ + {row.original.status === 'SUBMITTED' && ( + <> + + + + )} + +
+ ), + }, + ]; + return (
@@ -518,11 +434,11 @@ export function ListDailyChecklistContent() {
{ - setDateFrom(from); - setDateTo(to); + updateFilter('date_from', from); + updateFilter('date_to', to); }} />
@@ -532,8 +448,10 @@ export function ListDailyChecklistContent() {
+ setSearchText(e.target.value)} - className='border-gray-200 pl-9' + 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={ + + } /> -
{/* Table Section */} - {loading ? ( -
- Memuat data... -
- ) : filteredList.length > 0 ? ( -
- - - - - - - - - - - - - - - - {filteredList.map((item, index) => ( - - - - - - - - - - - - ))} - -
- Tanggal - - Kandang - - Kategori - - Status - - Total Phase - - Total Aktivitas - - Progress - - Updated At - - Aksi -
- {formatDate(item.date)} - - {item.kandang_name} - - {CATEGORY_LABELS[item.category] || item.category} - - {getStatusBadge(item.status)} - - {item.total_phases} - - {item.total_activities} - -
-
-
-
- - {item.progress_percent}% - -
-
- {formatDateTime(item.updated_at)} - -
- - {item.status === 'SUBMITTED' && ( - <> - - - - )} - -
-
-
- ) : ( -
- {searchText || - dateFrom || - dateTo || - statusFilter !== 'ALL' || - kandangFilter !== 'ALL' - ? 'Tidak ada data yang sesuai dengan filter' - : 'Belum ada data checklist'} -
- )} + + 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', + }} + />
@@ -755,7 +579,7 @@ export function ListDailyChecklistContent() {
Kandang: - {selectedItem.kandang_name} + {selectedItem.kandang.name}
@@ -768,7 +592,7 @@ export function ListDailyChecklistContent() {
Progress: - {selectedItem.progress_percent}% + {selectedItem.progress}%
@@ -815,7 +639,7 @@ export function ListDailyChecklistContent() {
Kandang: - {selectedItem.kandang_name} + {selectedItem.kandang.name}
@@ -888,7 +712,7 @@ export function ListDailyChecklistContent() {
Kandang: - {selectedItem.kandang_name} + {selectedItem.kandang.name}