From c50c1100059df0764fb8a5581d67ef2c8777b1f0 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 11 May 2026 16:47:33 +0700 Subject: [PATCH 01/16] fix: show excess day --- src/components/pages/production/recording/RecordingTable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index f5050844..75090109 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -866,7 +866,8 @@ const RecordingTable = () => { <> {props.row.original.day} (Minggu ke- - {props.row.original.project_flock.production_standart.week}) + {props.row.original.week} hari ke- + {props.row.original.excess_days}) ); From 69b998a61a13f01d10818adbaf3bc0e2b76b5d27 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 11 May 2026 16:47:47 +0700 Subject: [PATCH 02/16] fix: update footer styling --- .../report/marketing/tab/DailyMarketingTab.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index f1b63684..817fe921 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -554,7 +554,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'qty', cell: (props) => formatNumber(props.row.original.qty), footer: () => ( -
+
{summaryTotal?.total_qty ? formatNumber(summaryTotal.total_qty) : '-'} @@ -567,7 +567,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'average_weight_kg', cell: (props) => formatNumber(props.row.original.average_weight_kg), footer: () => ( -
+
{summaryTotal?.average_weight_kg ? formatNumber(summaryTotal.average_weight_kg) : '-'} @@ -580,7 +580,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'total_weight_kg', cell: (props) => formatNumber(props.row.original.total_weight_kg), footer: () => ( -
+
{summaryTotal?.total_weight_kg ? formatNumber(summaryTotal.total_weight_kg) : '-'} @@ -593,9 +593,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'sales_price_per_kg', cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), footer: () => ( -
+
{summaryTotal?.average_sales_price - ? formatNumber(summaryTotal.average_sales_price) + ? formatCurrency(summaryTotal.average_sales_price) : '-'}
), @@ -606,7 +606,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'hpp_price_per_kg', cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), footer: () => ( -
+
{summaryTotal?.total_hpp_price_per_kg ? formatCurrency(summaryTotal.total_hpp_price_per_kg) : '-'} @@ -619,7 +619,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { accessorKey: 'sales_amount', cell: (props) => formatCurrency(props.row.original.sales_amount), footer: () => ( -
+
{summaryTotal?.total_sales_amount ? formatCurrency(summaryTotal.total_sales_amount) : '-'} From e7569b7448f12109285629efd576c1953b548ddc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 11 May 2026 16:50:44 +0700 Subject: [PATCH 03/16] fix: hit API when user click Simpan Draft/Submit and and empty kandang end date --- .../daily-checklist/DailyChecklistContent.tsx | 1854 +++++++++-------- .../api/daily-checklist/daily-checklist.ts | 30 +- .../api/daily-checklist/daily-checklist.d.ts | 4 + 3 files changed, 1018 insertions(+), 870 deletions(-) diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 40a2c130..2fbb79be 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Plus, X, @@ -89,20 +89,25 @@ export function DailyChecklistContent() { const pathname = usePathname(); const searchParams = useSearchParams(); + // If checklistId is in URL, we are in edit mode + const checklistIdFromUrl = searchParams.get('checklistId'); + const [kandangId, setKandangId] = useState( searchParams.get('kandang_id') || '' ); const [date, setDate] = useState(() => { const paramDate = searchParams.get('date'); if (paramDate) return paramDate; - const today = new Date(); return today.toISOString().split('T')[0]; }); const [selectedCategory, setSelectedCategory] = useState( searchParams.get('category') || '' ); - const [emptyKandang, setEmptyKandang] = useState(false); + // Initialize emptyKandang from URL so it stays consistent with selectedCategory + const [emptyKandang, setEmptyKandang] = useState( + searchParams.get('category') === 'empty_kandang' + ); const isKandangEmpty = selectedCategory === 'empty_kandang'; @@ -123,9 +128,7 @@ export function DailyChecklistContent() { const { data: employeesRes } = useSWR( `${EmployeeApi.basePath}?page=1&limit=500&kandang_id=${kandangId}&is_active=true`, EmployeeApi.getAllFetcher, - { - keepPreviousData: true, - } + { keepPreviousData: true } ); const allPhases = isResponseSuccess(phases) ? phases.data || [] : []; @@ -134,7 +137,6 @@ export function DailyChecklistContent() { : []; const [selectedPhaseIds, setSelectedPhaseIds] = useState([]); - const [selectedEmployees, setSelectedEmployees] = useState< { id: number; name: string }[] >([]); @@ -145,21 +147,20 @@ export function DailyChecklistContent() { const [dailyChecklistId, setDailyChecklistId] = useState(null); const [checklistStatus, setChecklistStatus] = useState('DRAFT'); - // const [isEditMode, setIsEditMode] = useState(false); - // Activities grouped by phase + // Activities grouped by phase_id const [activitiesByPhase, setActivitiesByPhase] = useState<{ [phaseId: string]: PhaseActivity[]; }>({}); - // Task IDs mapped by phase_activity_id for quick lookup + // Task IDs mapped by phase_activity_id const [taskIdsByPhaseActivityId, setTaskIdsByPhaseActivityId] = useState<{ [phaseActivityId: string]: string; }>({}); - // Assignments: { task_id: { employee_id: { checked, note } } } + // Assignments keyed by phaseActivityId (works for both create and edit modes) const [assignments, setAssignments] = useState<{ - [taskId: string]: { + [phaseActivityId: string]: { [employeeId: string]: { checked: boolean; note: string }; }; }>({}); @@ -177,85 +178,215 @@ export function DailyChecklistContent() { const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); const [isLoadingDraft, setIsLoadingDraft] = useState(false); - const [initialLoading, setInitialLoading] = useState(true); + const [initialLoading, setInitialLoading] = useState(!!checklistIdFromUrl); + + const [emptyKandangEndDate, setEmptyKandangEndDate] = useState(''); const [existingDocuments, setExistingDocuments] = useState([]); const [documents, setDocuments] = useState([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState([]); - const handleKandangScroll = (e: React.UIEvent) => { - const target = e.target as HTMLDivElement; + // Tracks the last checklistId we loaded to prevent re-loading after URL update + const loadedChecklistIdRef = useRef(null); + // Prevents the kandang-change effect from clearing employees/assignments during initial edit load + const skipKandangClearRef = useRef(false); + // Mirror of server-side phases/employees; used to diff on save in edit mode + const serverPhaseIdsRef = useRef([]); + const serverEmployeeIdsRef = useRef([]); - if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) { - if (!isLoadingMoreKandang) { - loadMoreKandang(); - } - } - }; + const isChecklistStatusDraft = checklistStatus === 'DRAFT'; + const isEditMode = dailyChecklistId !== null; - // Sync state to URL query params + // Load checklist data when checklistId is in URL (edit mode) useEffect(() => { - const params = new URLSearchParams(searchParams.toString()); - let pendingUpdate = false; - - // Sync date - if (date) { - if (params.get('date') !== date) { - params.set('date', date); - pendingUpdate = true; - } - } else if (params.has('date')) { - params.delete('date'); - pendingUpdate = true; - } - - // Sync kandang_id - if (kandangId) { - if (params.get('kandang_id') !== kandangId) { - params.set('kandang_id', kandangId); - pendingUpdate = true; - } - } else if (params.has('kandang_id')) { - params.delete('kandang_id'); - pendingUpdate = true; - } - - // Sync category - if (selectedCategory) { - if (params.get('category') !== selectedCategory) { - params.set('category', selectedCategory); - pendingUpdate = true; - } - } else if (params.has('category')) { - params.delete('category'); - pendingUpdate = true; - } - - if (pendingUpdate) { - router.replace(`${pathname}?${params.toString()}`); - } - }, [date, kandangId, selectedCategory, pathname, router, searchParams]); - - useEffect(() => { - if (!emptyKandang) { - setSelectedCategory(''); + if (!checklistIdFromUrl) { + setInitialLoading(false); return; } - setSelectedCategory('empty_kandang'); - }, [emptyKandang]); + // Skip if we already loaded this checklist (prevents reload after URL update on save) + if (loadedChecklistIdRef.current === checklistIdFromUrl) return; + loadedChecklistIdRef.current = checklistIdFromUrl; + const loadChecklist = async () => { + setInitialLoading(true); + try { + const checklistData = + await DailyChecklistApi.getOneDailyChecklist(checklistIdFromUrl); + + if (isResponseError(checklistData) || !checklistData?.data) { + toast.error('Gagal memuat checklist'); + router.push('/daily-checklist'); + return; + } + + const data = checklistData.data; + + // Pre-fill form fields + const rawDate = data.date || ''; + setDate(rawDate.length > 10 ? rawDate.slice(0, 10) : rawDate); + skipKandangClearRef.current = true; + setKandangId(String(data.kandang?.id || '')); + + const isEmptyKandang = + !!data.empty_kandang || data.category === 'empty_kandang'; + setEmptyKandang(isEmptyKandang); + setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category); + + if (isEmptyKandang && data.empty_kandang_end_date) { + const rawEnd = data.empty_kandang_end_date; + setEmptyKandangEndDate( + rawEnd.length > 10 ? rawEnd.slice(0, 10) : rawEnd + ); + } + + setDailyChecklistId(String(data.id)); + setChecklistStatus(data.status); + + // Pre-fill phases + const phaseIds = (data.phases || []).map((p) => String(p.phase_id)); + setSelectedPhaseIds(phaseIds); + serverPhaseIdsRef.current = phaseIds; + + // Pre-fill employees + const loadedEmployees = data.assigned_employees || []; + setSelectedEmployees(loadedEmployees); + serverEmployeeIdsRef.current = loadedEmployees.map((e) => String(e.id)); + + // Pre-fill documents + setExistingDocuments(data.document_urls || []); + + // Build task map and assignments (keyed by phaseActivityId) + const taskMap: { [phaseActivityId: string]: string } = {}; + const assignmentMap: { + [phaseActivityId: string]: { + [employeeId: string]: { checked: boolean; note: string }; + }; + } = {}; + + (data.tasks || []).forEach((task) => { + const paId = String(task.phase_activity_id); + taskMap[paId] = String(task.id); + + if (!assignmentMap[paId]) assignmentMap[paId] = {}; + + task.assignments.forEach((assignment) => { + assignmentMap[paId][String(assignment.employee.id)] = { + checked: assignment.checked, + note: assignment.note || '', + }; + }); + }); + + setTaskIdsByPhaseActivityId(taskMap); + setAssignments(assignmentMap); + } catch (error) { + console.error('Error loading checklist:', error); + toast.error('Terjadi kesalahan saat memuat checklist'); + } finally { + setInitialLoading(false); + } + }; + + loadChecklist(); + }, [checklistIdFromUrl, router]); + + // Load activities whenever selected phases change (read-only, safe to call anytime) useEffect(() => { - if (selectedCategory === 'empty_kandang') { - setEmptyKandang(true); + const loadActivities = async () => { + if (selectedPhaseIds.length === 0) { + setActivitiesByPhase({}); + return; + } + + try { + const activitiesRes = await PhaseActivityApi.getAll({ + phase_ids: selectedPhaseIds.join(','), + limit: '100', + }); + + if (isResponseError(activitiesRes)) { + toast.error('Gagal memuat aktivitas'); + return; + } + + const activities = activitiesRes?.data || []; + const grouped: { [phaseId: string]: PhaseActivity[] } = {}; + activities.forEach((act) => { + if (!grouped[act.phase_id]) grouped[act.phase_id] = []; + grouped[act.phase_id].push(act); + }); + setActivitiesByPhase(grouped); + } catch (error) { + console.error('Error loading activities:', error); + } + }; + + loadActivities(); + }, [selectedPhaseIds]); + + // // Sync form state to URL query params + // useEffect(() => { + // const params = new URLSearchParams(searchParams.toString()); + // let pendingUpdate = false; + + // if (date) { + // if (params.get('date') !== date) { + // params.set('date', date); + // pendingUpdate = true; + // } + // } else if (params.has('date')) { + // params.delete('date'); + // pendingUpdate = true; + // } + + // if (kandangId) { + // if (params.get('kandang_id') !== kandangId) { + // params.set('kandang_id', kandangId); + // pendingUpdate = true; + // } + // } else if (params.has('kandang_id')) { + // params.delete('kandang_id'); + // pendingUpdate = true; + // } + + // if (selectedCategory) { + // if (params.get('category') !== selectedCategory) { + // params.set('category', selectedCategory); + // pendingUpdate = true; + // } + // } else if (params.has('category')) { + // params.delete('category'); + // pendingUpdate = true; + // } + + // if (pendingUpdate) { + // router.replace(`${pathname}?${params.toString()}`); + // } + // }, [date, kandangId, selectedCategory, pathname, router, searchParams]); + + // Clear employees and assignments when kandang changes (skip during initial edit load) + useEffect(() => { + if (skipKandangClearRef.current) { + skipKandangClearRef.current = false; + return; } - }, [selectedCategory]); + setSelectedEmployees([]); + setAssignments({}); + }, [kandangId]); + + const handleKandangScroll = (e: React.UIEvent) => { + const target = e.target as HTMLDivElement; + if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) { + if (!isLoadingMoreKandang) loadMoreKandang(); + } + }; const formatDateForDisplay = (dateStr: string) => { if (!dateStr) return 'Pilih tanggal'; const [year, month, day] = dateStr.split('-'); - const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); - return date.toLocaleDateString('id-ID', { + const d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); + return d.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', @@ -264,8 +395,8 @@ export function DailyChecklistContent() { }; const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { + const d = new Date(dateString); + return d.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric', @@ -293,245 +424,111 @@ export function DailyChecklistContent() { } }; - // Fetch master data on mount - useEffect(() => { - setInitialLoading(false); - }, []); - - // Check for existing checklist when unique key changes - useEffect(() => { - const checkAndLoadChecklist = async () => { - if (!date || !kandangId || (!emptyKandang && !selectedCategory)) { - setDailyChecklistId(null); - setChecklistStatus('DRAFT'); - // setIsEditMode(false); - setSelectedPhaseIds([]); - setActivitiesByPhase({}); - setTaskIdsByPhaseActivityId({}); - setAssignments({}); - return; - } - - try { - const checklist = await DailyChecklistApi.create({ - date, - kandang_id: Number(kandangId), - category: emptyKandang ? 'empty_kandang' : selectedCategory, - status: 'DRAFT', - empty_kandang: emptyKandang, - }); - - if (isResponseError(checklist)) { - console.error('Error upserting checklist:', checklist.message); - toast.error('Gagal memuat checklist: ' + checklist.message); - return; - } - - setDailyChecklistId(String(checklist?.data?.id)); - setChecklistStatus(String(checklist?.data?.status)); - - const existingPhases = await DailyChecklistApi.getOneDailyChecklist( - String(checklist?.data.id) - ); - - if (isResponseError(existingPhases)) { - console.error('Error loading phases:', existingPhases.message); - } else if ( - existingPhases && - existingPhases.data && - existingPhases.data.phases.length === 0 - ) { - toast.success('Berhasil membuat daily checklist!'); - } else if ( - existingPhases && - existingPhases.data && - existingPhases.data.phases.length > 0 - ) { - // Existing checklist - EDIT MODE - // setIsEditMode(true); - const phaseIds = existingPhases.data.phases.map((p) => - String(p.phase_id) - ); - setSelectedPhaseIds(phaseIds); - - if (checklist?.data?.status === 'DRAFT') { - toast.info('Checklist ditemukan - Mode Edit (Draft)', { - description: - 'Anda dapat menambah atau mengubah data checklist ini', - }); - } else { - toast.warning(`Checklist sudah ${checklist?.data?.status}`, { - description: 'Checklist tidak dapat diedit', - }); - } - } else { - // New checklist - CREATE MODE - // setIsEditMode(false); - setSelectedPhaseIds([]); - } - } catch (error) { - console.error('Error checking checklist:', error); - } - }; - - checkAndLoadChecklist(); - }, [date, kandangId, selectedCategory, emptyKandang]); - - // Load activities and tasks when phases change - useEffect(() => { - const loadActivitiesAndTasks = async () => { - if (!dailyChecklistId || selectedPhaseIds.length === 0) { - setActivitiesByPhase({}); - setTaskIdsByPhaseActivityId({}); - return; - } - - try { - const activitiesRes = await PhaseActivityApi.getAll({ - phase_ids: selectedPhaseIds.join(','), - limit: '100', - }); - - if (isResponseError(activitiesRes)) { - console.error('Error loading activities:', activitiesRes.message); - toast.error('Gagal memuat aktivitas'); - return; - } - - const activities = activitiesRes?.data || []; - - // Group activities by phase - const grouped: { [phaseId: string]: PhaseActivity[] } = {}; - (activities || []).forEach((act) => { - if (!grouped[act.phase_id]) { - grouped[act.phase_id] = []; - } - grouped[act.phase_id].push(act); - }); - setActivitiesByPhase(grouped); - - // Ensure tasks exist for all activities (UPSERT) - const taskUpserts = (activities || []).map((act) => ({ - checklist_id: dailyChecklistId, - phase_activity_id: act.id, - phase_id: act.phase_id, - time_type: act.time_type, - notes: null, - })); - - if (taskUpserts.length > 0) { - const existingDailyChecklist = - await DailyChecklistApi.getOneDailyChecklist( - String(dailyChecklistId) - ); - - if (isResponseError(existingDailyChecklist)) { - console.error( - 'Error loading assignments:', - existingDailyChecklist.message - ); - return; - } - - // Build task ID lookup - const taskMap: { [phaseActivityId: string]: string } = {}; - (existingDailyChecklist?.data?.tasks || []).forEach((task) => { - taskMap[String(task.phase_activity_id)] = String(task.id); - }); - setTaskIdsByPhaseActivityId(taskMap); - - // Load existing assignments for these tasks - await loadAssignments( - existingDailyChecklist?.data?.tasks?.map((t) => String(t.id)) || [] - ); - } - } catch (error) { - console.error('Error loading activities and tasks:', error); - } - }; - - loadActivitiesAndTasks(); - }, [dailyChecklistId, selectedPhaseIds]); - - // Load employees when kandang changes - useEffect(() => { - if (kandangId) { - // ✅ Clear selected employees ketika kandang berubah (reset ABK assignment) - setSelectedEmployees([]); - setAssignments({}); - } else { - setSelectedEmployees([]); - setAssignments({}); + const getStatusBadgeClass = () => { + switch (checklistStatus) { + case 'SUBMITTED': + return 'border-blue-300 text-blue-700 bg-white'; + case 'APPROVED': + return 'border-green-300 text-green-700 bg-white'; + case 'REJECTED': + return 'border-red-300 text-red-700 bg-white'; + default: + return 'border-blue-300 text-blue-700 bg-white'; } - }, [kandangId]); + }; - const loadAssignments = async (taskIds: string[]) => { - if (taskIds.length === 0) return; - - try { - const existingDailyChecklist = - await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId)); - - if (isResponseError(existingDailyChecklist)) { - console.error( - 'Error loading assignments:', - existingDailyChecklist.message + // Persist all local state to the server (shared by save draft and submit flows) + const persistChecklistData = async (currentChecklistId: string) => { + if (!isKandangEmpty) { + // Set phases + if (selectedPhaseIds.length > 0) { + const setPhaseRes = await DailyChecklistApi.setDailyChecklistPhase( + currentChecklistId, + selectedPhaseIds ); - return; + if (isResponseError(setPhaseRes)) { + toast.error('Gagal menyimpan fase'); + return false; + } } - // set existing document - setExistingDocuments(existingDailyChecklist?.data.document_urls || []); - - // Build assignments map - const assignmentMap: { - [taskId: string]: { - [employeeId: string]: { checked: boolean; note: string }; - }; - } = {}; - - (existingDailyChecklist?.data.tasks || []).forEach( - (dailyChecklistTask) => { - if (!assignmentMap[dailyChecklistTask.id]) { - assignmentMap[dailyChecklistTask.id] = {}; - } - - dailyChecklistTask.assignments.forEach((assignment) => { - if (!assignmentMap[dailyChecklistTask.id]) { - assignmentMap[dailyChecklistTask.id] = {}; - } - assignmentMap[dailyChecklistTask.id][assignment.employee.id] = { - checked: assignment.checked, - note: assignment.note || '', - }; - }); - } - ); - - setAssignments(assignmentMap); - - // Load employees from assignments - const employeeIds = Array.from( - new Set( - (existingDailyChecklist?.data.assigned_employees || []).map( - (a) => a.id - ) - ) - ); - - if (employeeIds.length > 0) { - const existingDailyChecklist = - await DailyChecklistApi.getOneDailyChecklist( - String(dailyChecklistId) + // Set employees + if (selectedEmployees.length > 0) { + const setEmployeesRes = + await DailyChecklistApi.setDailyChecklistEmployees( + currentChecklistId, + selectedEmployees.map((e) => String(e.id)) ); - - if (isResponseSuccess(existingDailyChecklist)) { - setSelectedEmployees(existingDailyChecklist.data.assigned_employees); + if (isResponseError(setEmployeesRes)) { + toast.error('Gagal menyimpan ABK'); + return false; + } + } + + // Fetch tasks to get IDs, then save non-default assignments + if (selectedPhaseIds.length > 0 && selectedEmployees.length > 0) { + const checklistData = + await DailyChecklistApi.getOneDailyChecklist(currentChecklistId); + + if (!isResponseError(checklistData) && checklistData?.data?.tasks) { + const newTaskMap: { [phaseActivityId: string]: string } = {}; + checklistData.data.tasks.forEach((task) => { + newTaskMap[String(task.phase_activity_id)] = String(task.id); + }); + setTaskIdsByPhaseActivityId(newTaskMap); + + const assignmentPromises: Promise[] = []; + Object.keys(assignments).forEach((phaseActivityId) => { + const taskId = newTaskMap[phaseActivityId]; + if (!taskId) return; + Object.keys(assignments[phaseActivityId]).forEach((employeeId) => { + const { checked, note } = + assignments[phaseActivityId][employeeId]; + if (checked || (note && note !== '')) { + assignmentPromises.push( + DailyChecklistApi.checkOrUncheckAssignment({ + task_id: Number(taskId), + employee_id: Number(employeeId), + checked, + note: note || null, + }) + ); + } + }); + }); + + await Promise.all(assignmentPromises); } } - } catch (error) { - console.error('Error loading assignments:', error); } + + return true; + }; + + // Ensure a checklist record exists; returns the ID or null on failure + const ensureChecklist = async (): Promise => { + if (dailyChecklistId) return dailyChecklistId; + + const createRes = await DailyChecklistApi.create({ + date, + kandang_id: Number(kandangId), + category: emptyKandang ? 'empty_kandang' : selectedCategory, + status: 'DRAFT', + empty_kandang: emptyKandang, + empty_kandang_end_date: emptyKandang ? emptyKandangEndDate || null : null, + }); + + if (isResponseError(createRes) || !createRes?.data?.id) { + toast.error( + 'Gagal membuat checklist: ' + + (isResponseError(createRes) ? createRes.message : 'unknown error') + ); + return null; + } + + const newId = String(createRes.data.id); + setDailyChecklistId(newId); + return newId; }; // Phase selection modal @@ -543,8 +540,6 @@ export function DailyChecklistContent() { setTempSelectedPhaseIds([...selectedPhaseIds]); setSearchPhase(''); setShowPhaseModal(true); - setTempSelectedEmployees([]); - setSelectedEmployees([]); }; const toggleTempPhase = (phaseId: string) => { @@ -557,42 +552,16 @@ export function DailyChecklistContent() { } }; - const applyPhaseSelection = async () => { - if (!dailyChecklistId) { - toast.error('Checklist belum tersedia'); - return; - } - + // Phase selection — local state only; persisted on Simpan Draft / Submit + const applyPhaseSelection = () => { if (!tempSelectedPhaseIds.length) { toast.error('Pilih minimal satu fase'); return; } - try { - // Insert new phase links - const setDailyChecklistPhaseRes = - await DailyChecklistApi.setDailyChecklistPhase( - dailyChecklistId, - tempSelectedPhaseIds - ); - - if (isResponseError(setDailyChecklistPhaseRes)) { - console.error( - 'Error saving phases:', - setDailyChecklistPhaseRes.message - ); - toast.error('Gagal menyimpan fase'); - return; - } - - setSelectedPhaseIds([...tempSelectedPhaseIds]); - setShowPhaseModal(false); - setSearchPhase(''); - toast.success('Fase berhasil disimpan'); - } catch (error) { - console.error('Error applying phase selection:', error); - toast.error('Terjadi kesalahan'); - } + setSelectedPhaseIds([...tempSelectedPhaseIds]); + setShowPhaseModal(false); + setSearchPhase(''); }; // ABK selection modal @@ -617,255 +586,346 @@ export function DailyChecklistContent() { } }; - const applyAbkSelection = async () => { - if (!dailyChecklistId) { - toast.error('Checklist belum tersedia'); - return; - } - - // Remove assignments for employees that were deselected + // ABK selection — local state only; persisted on Simpan Draft / Submit + const applyAbkSelection = () => { const removedEmployees = selectedEmployees.filter( (emp) => !tempSelectedEmployees.find((temp) => temp.id === emp.id) ); if (removedEmployees.length > 0) { - removedEmployees.forEach(async (removedEmp) => { - const removeEmployeeAssignmentRes = - await DailyChecklistApi.removeEmployeeAssignment( - dailyChecklistId, - String(removedEmp.id) - ); - - if (isResponseError(removeEmployeeAssignmentRes)) { - console.error( - 'Error removing employee assignment:', - removeEmployeeAssignmentRes.message - ); - toast.error('Gagal menghapus tugas'); - return; - } - }); - - // Remove from state const newAssignments = { ...assignments }; - Object.keys(newAssignments).forEach((taskId) => { + Object.keys(newAssignments).forEach((paId) => { removedEmployees.forEach((emp) => { - delete newAssignments[taskId][emp.id]; + delete newAssignments[paId][String(emp.id)]; }); }); setAssignments(newAssignments); } - // Add assignments for newly selected employees (upsert with checked=false) - const addedEmployees = tempSelectedEmployees.filter( - (temp) => !selectedEmployees.find((emp) => emp.id === temp.id) - ); - - if (addedEmployees.length > 0) { - const taskIds = Object.values(taskIdsByPhaseActivityId); - const newAssignments: { - task_id: string; - employee_id: string; - checked: boolean; - note: string | null; - }[] = []; - - taskIds.forEach((taskId) => { - addedEmployees.forEach((emp) => { - newAssignments.push({ - task_id: taskId, - employee_id: String(emp.id), - checked: false, - note: null, - }); - }); - }); - - const assignEmployeeRes = - await DailyChecklistApi.setDailyChecklistEmployees( - dailyChecklistId, - addedEmployees.map((emp) => String(emp.id)) - ); - - if (isResponseError(assignEmployeeRes)) { - console.error('Error assigning employees:', assignEmployeeRes.message); - toast.error('Gagal mengassign ABK: ' + assignEmployeeRes.message); - return; - } - } - setSelectedEmployees([...tempSelectedEmployees]); setShowAbkModal(false); setSearchAbk(''); - toast.success('ABK berhasil disimpan'); }; - const handleRemoveAbk = async (employeeId: string) => { - const deleteEmployeeRes = await DailyChecklistApi.removeEmployeeAssignment( - String(dailyChecklistId), - String(employeeId) - ); - - if (isResponseError(deleteEmployeeRes)) { - console.error('Error deleting employee:', deleteEmployeeRes.message); - toast.error('Gagal menghapus ABK: ' + deleteEmployeeRes.message); - return; - } - - // Remove from state + // Remove ABK — local state only; persisted on Simpan Draft / Submit + const handleRemoveAbk = (employeeId: string) => { const newAssignments = { ...assignments }; - Object.keys(newAssignments).forEach((taskId) => { - delete newAssignments[taskId][employeeId]; + Object.keys(newAssignments).forEach((paId) => { + delete newAssignments[paId][employeeId]; }); setAssignments(newAssignments); - setSelectedEmployees( selectedEmployees.filter((e) => String(e.id) !== employeeId) ); }; - const handleCheckboxChange = async ( + // Checkbox change — local state only; persisted on Simpan Draft / Submit + const handleCheckboxChange = ( activityId: string, employeeId: string, checked: boolean ) => { - const taskId = taskIdsByPhaseActivityId[activityId]; + if (!isChecklistStatusDraft) return; - if (!taskId) { - console.error('[CHECKBOX] No taskId found for activityId:', activityId); - console.error('[CHECKBOX] Available taskIds:', taskIdsByPhaseActivityId); - toast.error('Task ID tidak ditemukan. Coba refresh halaman.'); - return; - } - - if (!isChecklistStatusDraft) { - console.warn( - '[CHECKBOX] Checklist is not editable, status:', - checklistStatus - ); - return; - } - - // Optimistically update UI first - setAssignments((prev) => { - const updated = { - ...prev, - [taskId]: { - ...prev[taskId], - [employeeId]: { - checked, - note: prev[taskId]?.[employeeId]?.note || '', - }, + setAssignments((prev) => ({ + ...prev, + [activityId]: { + ...prev[activityId], + [employeeId]: { + checked, + note: prev[activityId]?.[employeeId]?.note || '', }, - }; - - return updated; - }); - - // Update database - const payload = { - task_id: Number(taskId), - employee_id: Number(employeeId), - checked, - note: assignments[taskId]?.[employeeId]?.note || null, - }; - - const checkOrUncheckAssignmentRes = - await DailyChecklistApi.checkOrUncheckAssignment(payload); - - if (isResponseError(checkOrUncheckAssignmentRes)) { - console.error( - '[CHECKBOX] Database error:', - checkOrUncheckAssignmentRes.message - ); - toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message); - - // Revert state on error - setAssignments((prev) => ({ - ...prev, - [taskId]: { - ...prev[taskId], - [employeeId]: { - checked: !checked, - note: prev[taskId]?.[employeeId]?.note || '', - }, - }, - })); - return; - } + }, + })); }; - const handleNoteChange = async ( + // Note change — local state only; persisted on Simpan Draft / Submit + const handleNoteChange = ( activityId: string, employeeId: string, note: string ) => { - const taskId = taskIdsByPhaseActivityId[activityId]; - if (!taskId) return; + if (!isChecklistStatusDraft) return; - // Update database - const payload = { - task_id: Number(taskId), - employee_id: Number(employeeId), - checked: assignments[taskId]?.[employeeId]?.checked || false, - note: note || null, - }; - - const checkOrUncheckAssignmentRes = - await DailyChecklistApi.checkOrUncheckAssignment(payload); - - if (isResponseError(checkOrUncheckAssignmentRes)) { - console.error( - '[CHECKBOX] Database error:', - checkOrUncheckAssignmentRes.message - ); - toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message); - return; - } - - // Update state setAssignments((prev) => ({ ...prev, - [taskId]: { - ...prev[taskId], + [activityId]: { + ...prev[activityId], [employeeId]: { - checked: prev[taskId]?.[employeeId]?.checked || false, + checked: prev[activityId]?.[employeeId]?.checked || false, note, }, }, })); }; - const handleSubmit = async () => { - if (!dailyChecklistId) return; + // Persist phases, employees, and assignments to the server (edit mode) + const persistEditModeData = async (currentChecklistId: string) => { + const localPhaseIds = selectedPhaseIds; + const localEmployeeIds = selectedEmployees.map((e) => String(e.id)); - if (selectedEmployees.length === 0) { - toast.error('Pilih minimal 1 ABK'); + // Phases — sync if changed + const prevPhaseIds = serverPhaseIdsRef.current; + const phasesChanged = + localPhaseIds.length !== prevPhaseIds.length || + localPhaseIds.some((id) => !prevPhaseIds.includes(id)); + + if (phasesChanged && localPhaseIds.length > 0) { + const res = await DailyChecklistApi.setDailyChecklistPhase( + currentChecklistId, + localPhaseIds + ); + if (isResponseError(res)) { + toast.error('Gagal menyimpan fase'); + return false; + } + } + + // Employees — diff vs last known server state + const prevEmployeeIds = serverEmployeeIdsRef.current; + const removedIds = prevEmployeeIds.filter( + (id) => !localEmployeeIds.includes(id) + ); + const addedIds = localEmployeeIds.filter( + (id) => !prevEmployeeIds.includes(id) + ); + + for (const empId of removedIds) { + const res = await DailyChecklistApi.removeEmployeeAssignment( + currentChecklistId, + empId + ); + if (isResponseError(res)) { + toast.error('Gagal menghapus ABK'); + return false; + } + } + + if (addedIds.length > 0) { + const res = await DailyChecklistApi.setDailyChecklistEmployees( + currentChecklistId, + addedIds + ); + if (isResponseError(res)) { + toast.error('Gagal menyimpan ABK'); + return false; + } + } + + // Re-fetch to get up-to-date task IDs whenever phases or employees changed + // (server regenerates tasks when phases/assignments change) + let currentTaskMap = taskIdsByPhaseActivityId; + if (phasesChanged || removedIds.length > 0 || addedIds.length > 0) { + const refreshed = + await DailyChecklistApi.getOneDailyChecklist(currentChecklistId); + if (!isResponseError(refreshed) && refreshed?.data?.tasks) { + const newTaskMap: { [phaseActivityId: string]: string } = {}; + refreshed.data.tasks.forEach((task) => { + newTaskMap[String(task.phase_activity_id)] = String(task.id); + }); + setTaskIdsByPhaseActivityId(newTaskMap); + currentTaskMap = newTaskMap; + } + } + + // Assignments — use the freshest task map + const assignmentPromises: Promise[] = []; + Object.keys(assignments).forEach((phaseActivityId) => { + const taskId = currentTaskMap[phaseActivityId]; + if (!taskId) return; + Object.keys(assignments[phaseActivityId]).forEach((employeeId) => { + const { checked, note } = assignments[phaseActivityId][employeeId]; + assignmentPromises.push( + DailyChecklistApi.checkOrUncheckAssignment({ + task_id: Number(taskId), + employee_id: Number(employeeId), + checked, + note: note || null, + }) + ); + }); + }); + await Promise.all(assignmentPromises); + + // Update server-state mirrors + serverPhaseIdsRef.current = localPhaseIds; + serverEmployeeIdsRef.current = localEmployeeIds; + + return true; + }; + + const handleNewChecklist = () => { + const today = new Date().toISOString().split('T')[0]; + setDate(today); + setKandangId(''); + setSelectedCategory(''); + setEmptyKandang(false); + setEmptyKandangEndDate(''); + setSelectedPhaseIds([]); + setSelectedEmployees([]); + setDailyChecklistId(null); + setChecklistStatus('DRAFT'); + setActivitiesByPhase({}); + setTaskIdsByPhaseActivityId({}); + setAssignments({}); + setExistingDocuments([]); + setDocuments([]); + setDeletedDocumentIds([]); + loadedChecklistIdRef.current = null; + + const params = new URLSearchParams(searchParams.toString()); + params.delete('checklistId'); + router.replace(`${pathname}?${params.toString()}`); + }; + + const handleSaveDraft = async () => { + if (!date || !kandangId || (!emptyKandang && !selectedCategory)) { + toast.error('Lengkapi tanggal, kandang, dan kategori terlebih dahulu'); return; } - if (selectedPhaseIds.length === 0) { - toast.error('Pilih minimal 1 fase'); + setIsLoadingDraft(true); + + try { + const currentChecklistId = await ensureChecklist(); + if (!currentChecklistId) return; + + if (isEditMode) { + // Update base fields in edit mode + const updateRes = await DailyChecklistApi.update( + Number(currentChecklistId), + { + date, + kandang_id: Number(kandangId), + category: emptyKandang ? 'empty_kandang' : selectedCategory, + status: 'DRAFT', + empty_kandang: emptyKandang, + empty_kandang_end_date: emptyKandang + ? emptyKandangEndDate || null + : null, + } + ); + if (isResponseError(updateRes)) { + toast.error('Gagal memperbarui checklist'); + return; + } + const ok = await persistEditModeData(currentChecklistId); + if (!ok) return; + } else { + // In create mode, persist all local state to the server + const ok = await persistChecklistData(currentChecklistId); + if (!ok) return; + } + + const uploadRes = await DailyChecklistApi.uploadImage( + Number(currentChecklistId), + 'DRAFT', + documents, + deletedDocumentIds + ); + + if (isResponseError(uploadRes)) { + toast.error('Gagal menyimpan dokumen'); + return; + } + + // After first save, push checklistId into URL so refresh keeps edit mode + if (!isEditMode) { + loadedChecklistIdRef.current = currentChecklistId; + const params = new URLSearchParams(searchParams.toString()); + params.set('checklistId', currentChecklistId); + router.replace(`${pathname}?${params.toString()}`); + } + + // Refresh existing documents list + const refreshed = + await DailyChecklistApi.getOneDailyChecklist(currentChecklistId); + if (!isResponseError(refreshed) && refreshed?.data) { + setExistingDocuments(refreshed.data.document_urls || []); + } + + setDocuments([]); + setDeletedDocumentIds([]); + toast.success('Draft tersimpan!'); + } catch (error) { + console.error('Error saving draft:', error); + toast.error('Terjadi kesalahan'); + } finally { + setIsLoadingDraft(false); + } + }; + + const handleSubmit = async () => { + if (!date || !kandangId || (!emptyKandang && !selectedCategory)) { + toast.error('Lengkapi tanggal, kandang, dan kategori terlebih dahulu'); return; } + if (!isKandangEmpty) { + if (selectedEmployees.length === 0) { + toast.error('Pilih minimal 1 ABK'); + return; + } + if (selectedPhaseIds.length === 0) { + toast.error('Pilih minimal 1 fase'); + return; + } + } + setIsLoadingSubmit(true); try { + const currentChecklistId = await ensureChecklist(); + if (!currentChecklistId) return; + + if (isEditMode) { + // Update base fields in edit mode before submitting + const updateRes = await DailyChecklistApi.update( + Number(currentChecklistId), + { + date, + kandang_id: Number(kandangId), + category: emptyKandang ? 'empty_kandang' : selectedCategory, + status: 'DRAFT', + empty_kandang: emptyKandang, + empty_kandang_end_date: emptyKandang + ? emptyKandangEndDate || null + : null, + } + ); + if (isResponseError(updateRes)) { + toast.error('Gagal memperbarui checklist'); + return; + } + const ok = await persistEditModeData(currentChecklistId); + if (!ok) return; + } else { + // In create mode, persist all local state first + const ok = await persistChecklistData(currentChecklistId); + if (!ok) return; + } + const submitRes = await DailyChecklistApi.submit( - dailyChecklistId, + currentChecklistId, documents, deletedDocumentIds ); if (isResponseError(submitRes)) { - console.error('Error submitting:', submitRes.message); toast.error('Gagal submit checklist'); return; } setChecklistStatus('SUBMITTED'); + // Push checklistId into URL + if (!isEditMode) { + loadedChecklistIdRef.current = currentChecklistId; + const params = new URLSearchParams(searchParams.toString()); + params.set('checklistId', currentChecklistId); + router.replace(`${pathname}?${params.toString()}`); + } + const shareToWhatsApp = () => { const kandangName = kandangOptions.find((k) => String(k.value) === kandangId)?.label || @@ -875,12 +935,10 @@ export function DailyChecklistContent() { const message = encodeURIComponent( `Daily Checklist\n\nTanggal: ${formatDate(date)}\nKandang: ${kandangName}\nKategori: ${CATEGORY_LABELS[category] || category}\nStatus: SUBMITTED${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}` ); - const isMobile = isMobileDevice(); const whatsappUrl = isMobile ? `https://wa.me/?text=${message}` : `https://web.whatsapp.com/send?text=${message}`; - window.open(whatsappUrl, '_blank'); }; @@ -893,7 +951,7 @@ export function DailyChecklistContent() { )}

@@ -1115,7 +1154,6 @@ export function DailyChecklistContent() { {kandang.label} ))} - {isLoadingMoreKandang && (

@@ -1131,7 +1169,10 @@ export function DailyChecklistContent() { - handleCheckboxChange( - String(activity.id), - String(emp.id), - e.target.checked - ) +

+ {activity.name} +

+ {activity.description && ( +

+ {activity.description} +

+ )} + + {sortedSelectedEmployees.map( + (emp) => ( + + + handleCheckboxChange( + activityId, + String(emp.id), + e.target.checked + ) + } + disabled={ + !isChecklistStatusDraft + } + className='checkbox-clean' + /> + + ) + )} + + 0 + ? assignments[activityId]?.[ + String( + selectedEmployees[0].id + ) + ]?.note || '' + : '' } + onChange={(e) => { + if ( + selectedEmployees.length > 0 + ) { + handleNoteChange( + activityId, + String( + selectedEmployees[0].id + ), + e.target.value + ); + } + }} disabled={!isChecklistStatusDraft} - className='checkbox-clean' /> - ))} - - 0 - ? assignments[taskId]?.[ - selectedEmployees[0].id - ]?.note || '' - : '' - } - onChange={(e) => { - if ( - selectedEmployees.length > 0 - ) { - handleNoteChange( - String(activity.id), - String( - selectedEmployees[0].id - ), - e.target.value - ); - } - }} - disabled={!isChecklistStatusDraft} - /> - - - ); - }); + + ); + }); }); return rows; @@ -1475,7 +1537,7 @@ export function DailyChecklistContent() {
) : (
- {!dailyChecklistId ? ( + {!hasFormBase || !selectedCategory ? (

@@ -1514,136 +1576,198 @@ export function DailyChecklistContent() { )} - {!isKandangEmpty && - dailyChecklistId && - selectedPhaseIds.length > 0 && - selectedEmployees.length > 0 && ( - <> - {existingDocuments.length > 0 && ( -
-

- Dokumen yang telah diupload -

- {existingDocuments.map( - (existingDocument, existingDocumentIdx) => ( -
+ {existingDocuments.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ {existingDocuments.map( + (existingDocument, existingDocumentIdx) => ( +
+ - + + + {isChecklistStatusDraft && ( + + )} +
+ ) + )} +
+ )} - {isChecklistStatusDraft && ( - - )} -
- ) - )} -
- )} + {/* Documents for empty kandang */} + {isKandangEmpty && isChecklistStatusDraft && ( + <> + {existingDocuments.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ {existingDocuments.map( + (existingDocument, existingDocumentIdx) => ( +
+ + {existingDocument.name}{' '} + + - {isChecklistStatusDraft && ( - { - setDocuments(files); - }} - onDelete={(deletedFileIdx: number) => { - const newRequestDocuments = [...documents]; + +
+ ) + )} +
+ )} - newRequestDocuments?.splice(deletedFileIdx, 1); - - setDocuments(newRequestDocuments); - }} - disabled={!isChecklistStatusDraft} - className={{ - wrapper: 'mt-6', - inputWrapper: 'flex items-center', - label: 'font-semibold text-gray-900', - }} - maxSize={5242880} // 5 MB - bottomLabel='Ukuran file maksimal 5MB' - /> - )} - - )} + setDocuments(files)} + onDelete={(deletedFileIdx: number) => { + const next = [...documents]; + next.splice(deletedFileIdx, 1); + setDocuments(next); + }} + disabled={false} + className={{ + wrapper: 'mt-6', + inputWrapper: 'flex items-center', + label: 'font-semibold text-gray-900', + }} + maxSize={5242880} + bottomLabel='Ukuran file maksimal 5MB' + /> + + )} {/* Action Buttons */} - {!isKandangEmpty && - dailyChecklistId && - selectedPhaseIds.length > 0 && - selectedEmployees.length > 0 && - isChecklistStatusDraft && ( -
- - -
- )} + {canShowActions && ( +
+ + +
+ )}

@@ -1702,7 +1826,6 @@ export function DailyChecklistContent() { const isChecked = tempSelectedPhaseIds.includes( String(phase.id) ); - return (