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

1733 lines
60 KiB
TypeScript

'use client';
import * as React from 'react';
import { useState, useEffect } from 'react';
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr';
import { BaseApiResponse, Document } from '@/types/api/api-general';
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Employee } from '@/types/api/daily-checklist/employee';
import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity';
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react';
// Static categories
const CATEGORIES = [
{ value: 'pullet_open', label: 'Pullet Open' },
{ value: 'pullet_close', label: 'Pullet Close' },
{ value: 'produksi_open', label: 'Produksi Open' },
{ value: 'produksi_close', label: 'Produksi Close' },
];
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum',
Pagi: 'Pagi',
Siang: 'Siang',
Sore: 'Sore',
Malam: 'Malam',
};
interface Phase {
id: string;
name: 'string';
category: string;
}
export function DailyChecklistContent() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
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 { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
const {
data: phases,
isLoading: isLoadingPhases,
mutate: refreshPhases,
} = useSWR<
BaseApiResponse<Phase[] | undefined>,
AxiosError<BaseApiResponse>,
SWRHttpKey
>(`${PhaseApi.basePath}?page=1&limit=100`, httpClientFetcher, {
keepPreviousData: true,
});
const {
data: employeesRes,
isLoading: isLoadingEmployees,
mutate: refreshEmployees,
} = useSWR(
`${EmployeeApi.basePath}?page=1&limit=500&kandang_id=${kandangId}&is_active=true`,
EmployeeApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const allPhases = isResponseSuccess(phases) ? phases.data || [] : [];
const employees = isResponseSuccess(employeesRes)
? employeesRes.data || []
: [];
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
const [selectedEmployees, setSelectedEmployees] = useState<
{ id: number; name: string }[]
>([]);
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
// const [isEditMode, setIsEditMode] = useState(false);
// Activities grouped by phase
const [activitiesByPhase, setActivitiesByPhase] = useState<{
[phaseId: string]: PhaseActivity[];
}>({});
// Task IDs mapped by phase_activity_id for quick lookup
const [taskIdsByPhaseActivityId, setTaskIdsByPhaseActivityId] = useState<{
[phaseActivityId: string]: string;
}>({});
// Assignments: { task_id: { employee_id: { checked, note } } }
const [assignments, setAssignments] = useState<{
[taskId: string]: {
[employeeId: string]: { checked: boolean; note: string };
};
}>({});
const [showAbkModal, setShowAbkModal] = useState(false);
const [showPhaseModal, setShowPhaseModal] = useState(false);
const [tempSelectedEmployees, setTempSelectedEmployees] = useState<
{ id: number; name: string }[]
>([]);
const [tempSelectedPhaseIds, setTempSelectedPhaseIds] = useState<string[]>(
[]
);
const [searchAbk, setSearchAbk] = useState('');
const [searchPhase, setSearchPhase] = useState('');
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const [isLoadingDraft, setIsLoadingDraft] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
// Sync state to URL query params
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]);
// Format date for display
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', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
// Fetch master data on mount
useEffect(() => {
setInitialLoading(false);
}, []);
// Check for existing checklist when unique key changes
useEffect(() => {
const checkAndLoadChecklist = async () => {
if (!date || !kandangId || !selectedCategory) {
setDailyChecklistId(null);
setChecklistStatus('DRAFT');
// setIsEditMode(false);
setSelectedPhaseIds([]);
setActivitiesByPhase({});
setTaskIdsByPhaseActivityId({});
setAssignments({});
return;
}
try {
const checklist = await DailyChecklistApi.create({
date,
kandang_id: Number(kandangId),
category: selectedCategory,
status: 'DRAFT',
});
if (isResponseError(checklist)) {
console.error('Error upserting checklist:', checklist.message);
toast.error('Gagal memuat checklist');
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
) {
// 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]);
// 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(','),
});
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({});
}
}, [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
);
return;
}
// 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)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
}
}
} catch (error) {
console.error('Error loading assignments:', error);
}
};
// Phase selection modal
const handleAddPhase = () => {
if (!selectedCategory) {
toast.error('Pilih kategori terlebih dahulu');
return;
}
setTempSelectedPhaseIds([...selectedPhaseIds]);
setSearchPhase('');
setShowPhaseModal(true);
};
const toggleTempPhase = (phaseId: string) => {
if (tempSelectedPhaseIds.includes(phaseId)) {
setTempSelectedPhaseIds(
tempSelectedPhaseIds.filter((id) => id !== phaseId)
);
} else {
setTempSelectedPhaseIds([...tempSelectedPhaseIds, phaseId]);
}
};
const applyPhaseSelection = async () => {
if (!dailyChecklistId) {
toast.error('Checklist belum tersedia');
return;
}
try {
// Insert new phase links
if (tempSelectedPhaseIds.length > 0) {
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');
}
};
// ABK selection modal
const handleAddAbk = () => {
if (!kandangId) {
toast.error('Pilih kandang terlebih dahulu');
return;
}
setTempSelectedEmployees([...selectedEmployees]);
setSearchAbk('');
setShowAbkModal(true);
};
const toggleTempEmployee = (employee: Employee) => {
const isSelected = tempSelectedEmployees.find((e) => e.id === employee.id);
if (isSelected) {
setTempSelectedEmployees(
tempSelectedEmployees.filter((e) => e.id !== employee.id)
);
} else {
setTempSelectedEmployees([...tempSelectedEmployees, employee]);
}
};
const toggleSelectAllAbk = () => {
if (tempSelectedEmployees.length === employees.length) {
setTempSelectedEmployees([]);
} else {
setTempSelectedEmployees([...employees]);
}
};
const applyAbkSelection = async () => {
if (!dailyChecklistId) {
toast.error('Checklist belum tersedia');
return;
}
// Remove assignments for employees that were deselected
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) => {
removedEmployees.forEach((emp) => {
delete newAssignments[taskId][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
const newAssignments = { ...assignments };
Object.keys(newAssignments).forEach((taskId) => {
delete newAssignments[taskId][employeeId];
});
setAssignments(newAssignments);
setSelectedEmployees(
selectedEmployees.filter((e) => String(e.id) !== employeeId)
);
};
const handleCheckboxChange = async (
activityId: string,
employeeId: string,
checked: boolean
) => {
const taskId = taskIdsByPhaseActivityId[activityId];
// console.log('[CHECKBOX] Click detected:', {
// activityId,
// employeeId,
// checked,
// taskId,
// hasTaskId: !!taskId,
// checklistStatus,
// isChecklistStatusDraft,
// });
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 || '',
},
},
};
// console.log(
// '[CHECKBOX] State updated optimistically:',
// updated[taskId]?.[employeeId]
// );
return updated;
});
// Update database
const payload = {
task_id: Number(taskId),
employee_id: Number(employeeId),
checked,
note: assignments[taskId]?.[employeeId]?.note || null,
};
// console.log('[CHECKBOX] Saving to database:', payload);
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;
}
// console.log('[CHECKBOX] Saved successfully');
};
const handleNoteChange = async (
activityId: string,
employeeId: string,
note: string
) => {
const taskId = taskIdsByPhaseActivityId[activityId];
if (!taskId) 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],
[employeeId]: {
checked: prev[taskId]?.[employeeId]?.checked || false,
note,
},
},
}));
};
const handleSubmit = async () => {
if (!dailyChecklistId) return;
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 submitRes = await DailyChecklistApi.submit(
dailyChecklistId,
documents,
deletedDocumentIds
);
if (isResponseError(submitRes)) {
console.error('Error submitting:', submitRes.message);
toast.error('Gagal submit checklist');
return;
}
setChecklistStatus('SUBMITTED');
toast.success('Checklist berhasil disubmit untuk approval');
} catch (error) {
console.error('Error submitting:', error);
toast.error('Terjadi kesalahan');
} finally {
setIsLoadingSubmit(false);
}
};
const handleSaveDraft = async () => {
if (!dailyChecklistId) return;
setIsLoadingDraft(true);
const uploadImageRes = await DailyChecklistApi.uploadImage(
Number(dailyChecklistId),
'DRAFT',
documents,
deletedDocumentIds
);
if (isResponseError(uploadImageRes)) {
console.error('Error saving draft:', uploadImageRes.message);
toast.error('Gagal menyimpan draft');
setIsLoadingDraft(false);
return;
}
setIsLoadingDraft(false);
toast.success('Draft tersimpan!');
};
// Filter functions
const filteredEmployees = employees.filter((emp) =>
emp.name.toLowerCase().includes(searchAbk.toLowerCase())
);
const availablePhases = allPhases.filter(
(p) => p.category === selectedCategory
);
const filteredPhases = availablePhases.filter((phase) =>
phase.name.toLowerCase().includes(searchPhase.toLowerCase())
);
const isAllAbkSelected =
tempSelectedEmployees.length === employees.length && employees.length > 0;
const isAllPhasesSelected =
tempSelectedPhaseIds.length === availablePhases.length &&
availablePhases.length > 0;
// Group activities by PHASE → TIME_TYPE → ACTIVITIES
const groupActivitiesByPhase = () => {
const grouped: {
[phaseId: string]: {
phase: Phase;
timeGroups: {
[timeType: string]: PhaseActivity[];
};
};
} = {};
const selectedPhasesData = allPhases.filter((p) =>
selectedPhaseIds.includes(String(p.id))
);
selectedPhasesData.forEach((phase) => {
const phaseActivities = activitiesByPhase[phase.id] || [];
if (!grouped[phase.id]) {
grouped[phase.id] = {
phase,
timeGroups: {},
};
}
// Group activities by time_type within this phase
phaseActivities.forEach((activity) => {
const timeType = activity.time_type || 'Umum';
if (!grouped[phase.id].timeGroups[timeType]) {
grouped[phase.id].timeGroups[timeType] = [];
}
grouped[phase.id].timeGroups[timeType].push(activity);
});
});
return grouped;
};
const isChecklistStatusDraft = checklistStatus === 'DRAFT';
if (initialLoading) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Daily Checklist
</h1>
<p className='text-sm text-gray-600 mt-1'>
Checklist Harian Aktivitas Kandang
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<div className='flex items-center gap-3'>
<h1 className='text-2xl font-semibold text-gray-900'>
Daily Checklist
</h1>
{isChecklistStatusDraft && (
<Badge
variant='outline'
className='border-amber-300 text-amber-700 bg-white'
>
<Info className='w-3 h-3 mr-1' />
Mode Edit
</Badge>
)}
{checklistStatus !== 'DRAFT' && (
<Badge
variant='outline'
className='border-blue-300 text-blue-700 bg-white'
>
{checklistStatus}
</Badge>
)}
</div>
<p className='text-sm text-gray-600 mt-1'>
Checklist Harian Aktivitas Kandang
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-6'>
{/* Form Section */}
<div className='grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 pb-6 border-b border-gray-200'>
<div>
<Label htmlFor='date'>
Tanggal <span className='text-red-500'>*</span>
</Label>
<div className='mt-1.5'>
<DatePicker
date={date}
onDateChange={setDate}
disabled={!isChecklistStatusDraft}
placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay}
/>
</div>
</div>
<div>
<Label htmlFor='kandang'>
Kandang <span className='text-red-500'>*</span>
</Label>
<Select
value={kandangId}
onValueChange={setKandangId}
disabled={!isChecklistStatusDraft}
>
<SelectTrigger
id='kandang'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Pilih kandang' />
</SelectTrigger>
<SelectContent>
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='category'>
Kategori <span className='text-red-500'>*</span>
</Label>
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
disabled={!isChecklistStatusDraft}
>
<SelectTrigger
id='category'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Pilih kategori' />
</SelectTrigger>
<SelectContent>
{CATEGORIES.map((category) => (
<SelectItem key={category.value} value={category.value}>
{category.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Phase Selection Section */}
{dailyChecklistId && (
<div className='mb-6 pb-6 border-b border-gray-200'>
{isChecklistStatusDraft && (
<div className='flex items-center justify-between mb-3'>
<Label>Fase / Tahap</Label>
<Button
onClick={handleAddPhase}
size='sm'
variant='outline'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
disabled={!selectedCategory || !isChecklistStatusDraft}
>
<Plus className='w-4 h-4 mr-1' />
Pilih Fase
</Button>
</div>
)}
{selectedPhaseIds.length > 0 ? (
<div className='flex flex-wrap gap-2'>
{allPhases
.filter((p) => selectedPhaseIds.includes(String(p.id)))
.map((phase) => (
<Badge
key={phase.id}
variant='secondary'
className='px-3 py-1.5 bg-blue-50 text-blue-700 border border-blue-200 rounded-lg'
>
{phase.name}
</Badge>
))}
</div>
) : (
<p className='text-sm text-gray-500'>
Belum ada fase dipilih
</p>
)}
</div>
)}
{/* ABK Assignment Section */}
{dailyChecklistId && selectedPhaseIds.length > 0 && (
<div className='mb-6 pb-6 border-b border-gray-200'>
{isChecklistStatusDraft && (
<div className='flex items-center justify-between mb-3'>
<Label>ABK Assignment</Label>
<Button
onClick={handleAddAbk}
size='sm'
variant='outline'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
disabled={!kandangId || !isChecklistStatusDraft}
>
<Plus className='w-4 h-4 mr-1' />
Tambah ABK
</Button>
</div>
)}
{selectedEmployees.length > 0 ? (
<div className='flex flex-wrap gap-2'>
{selectedEmployees.map((emp) => (
<Badge
key={emp.id}
variant='secondary'
className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg'
>
{emp.name}
{isChecklistStatusDraft && (
<button
onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900'
>
<X className='w-3 h-3' />
</button>
)}
</Badge>
))}
</div>
) : (
<p className='text-sm text-gray-500'>Belum ada ABK dipilih</p>
)}
</div>
)}
{/* Activity Checklist Table */}
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 ? (
<div>
<h3 className='font-semibold text-gray-900 mb-4'>
Checklist Aktivitas
</h3>
{Object.keys(activitiesByPhase).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 border-r border-gray-200 min-w-[200px]'>
Aktivitas
</th>
{selectedEmployees.map((emp) => (
<th
key={emp.id}
className='text-center py-3 px-4 text-sm font-semibold text-gray-700 border-r border-gray-200 min-w-[100px]'
>
{emp.name}
</th>
))}
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700 min-w-[200px]'>
Catatan
</th>
</tr>
</thead>
<tbody>
{Object.keys(groupActivitiesByPhase()).flatMap(
(phaseId) => {
const phaseData = groupActivitiesByPhase()[phaseId];
const { phase, timeGroups } = phaseData;
const timeTypes = Object.keys(timeGroups).sort(
(a, b) =>
TIME_TYPE_ORDER.indexOf(a) -
TIME_TYPE_ORDER.indexOf(b)
);
// Count total activities in this phase
const totalActivities = timeTypes.reduce(
(sum, timeType) =>
sum + timeGroups[timeType].length,
0
);
// Build all rows for this phase
const rows = [];
// PHASE Header (Main parent) - BLUE
rows.push(
<tr
key={`phase-${phaseId}`}
className='bg-blue-50 border-b border-blue-200'
>
<td
colSpan={selectedEmployees.length + 2}
className='py-2.5 px-4'
>
<div className='flex items-center gap-2'>
<span className='text-sm font-semibold text-blue-900'>
{phase.name}
</span>
<Badge
variant='secondary'
className='text-xs bg-blue-100 text-blue-700 border-blue-200 rounded-lg'
>
{totalActivities} aktivitas
</Badge>
</div>
</td>
</tr>
);
// TIME_TYPE sub-headers and activities
timeTypes.forEach((timeType) => {
const activities = timeGroups[timeType];
const hasMultipleTimeTypes = timeTypes.length > 1;
// TIME Header (optional, only if phase has multiple time types) - GRAY SOFT
if (hasMultipleTimeTypes) {
rows.push(
<tr
key={`time-${phaseId}-${timeType}`}
className='bg-gray-50 border-b border-gray-200'
>
<td
colSpan={selectedEmployees.length + 2}
className='py-2 px-4 pl-8'
>
<span className='text-xs font-medium text-gray-600'>
{TIME_TYPE_LABELS[timeType]} (
{activities.length} aktivitas)
</span>
</td>
</tr>
);
}
// ACTIVITY rows (Child rows with checkboxes)
activities.forEach((activity, index) => {
const taskId =
taskIdsByPhaseActivityId[activity.id];
const indentClass = hasMultipleTimeTypes
? 'pl-12'
: 'pl-8';
rows.push(
<tr
key={`activity-${activity.id}`}
className={
index % 2 === 0
? 'bg-white'
: 'bg-gray-50/50'
}
>
<td
className={`py-3 px-4 ${indentClass} border-r border-gray-200`}
>
<p className='text-sm text-gray-900'>
{activity.name}
</p>
{activity.description && (
<p className='text-xs text-gray-500 mt-0.5'>
{activity.description}
</p>
)}
</td>
{selectedEmployees.map((emp) => (
<td
key={emp.id}
className='text-center py-3 px-4 border-r border-gray-200'
>
<input
type='checkbox'
checked={
assignments[taskId]?.[emp.id]
?.checked || false
}
onChange={(e) =>
handleCheckboxChange(
String(activity.id),
String(emp.id),
e.target.checked
)
}
disabled={!isChecklistStatusDraft}
className='checkbox-clean'
/>
</td>
))}
<td className='py-3 px-4'>
<DebouncedTextArea
delay={500}
name='notes'
rows={1}
placeholder='Catatan (opsional)'
value={
taskId && selectedEmployees.length > 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}
/>
</td>
</tr>
);
});
});
return rows;
}
)}
</tbody>
</table>
</div>
) : (
<div className='flex flex-col items-center justify-center py-16 text-center'>
<ListChecks className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Tidak Ada Aktivitas
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Tidak ada aktivitas untuk fase yang dipilih. Silakan
tambahkan aktivitas di Master Aktivitas.
</p>
</div>
)}
</div>
) : (
<div className='flex flex-col items-center justify-center py-16 text-center'>
{!dailyChecklistId ? (
<div>
<FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Mulai Checklist Baru
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Pilih tanggal, kandang, dan kategori untuk memulai
checklist harian Anda.
</p>
</div>
) : selectedPhaseIds.length === 0 ? (
<div className='flex flex-col items-center text-center'>
<FilePlus className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih Fase / Tahap
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap
aktivitas yang akan dikerjakan.
</p>
</div>
) : (
<div>
<FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih ABK
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja
yang akan ditugaskan.
</p>
</div>
)}
</div>
)}
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && (
<>
{existingDocuments.length > 0 && (
<div className='mt-6'>
<h3 className='font-semibold text-gray-900 mb-2'>
Dokumen yang telah diupload
</h3>
{existingDocuments.map(
(existingDocument, existingDocumentIdx) => (
<div
key={existingDocumentIdx}
className='w-full flex flex-wrap justify-between'
>
<Link
href={existingDocument.url}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{existingDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
{isChecklistStatusDraft && (
<Button
type='button'
variant='ghost'
color='error'
onClick={() => {
setDeletedDocumentIds((prevIds) => [
...prevIds,
existingDocument.id,
]);
setExistingDocuments(
(prevExistingDocument) => {
const newExistingDocuments = [
...prevExistingDocument,
];
newExistingDocuments.splice(
existingDocumentIdx,
1
);
return newExistingDocuments;
}
);
}}
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='fluent:delete-12-regular'
width={20}
height={20}
/>
</Button>
)}
</div>
)
)}
</div>
)}
{isChecklistStatusDraft && (
<DropFileInput
name='Dokumen'
label='Dokumen'
values={documents}
onChange={(files) => {
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',
}}
/>
)}
</>
)}
{/* Action Buttons */}
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 &&
isChecklistStatusDraft && (
<div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'>
<Button
onClick={handleSaveDraft}
variant='outline'
disabled={isLoadingDraft}
className='border-gray-200'
>
<Save className='w-4 h-4 mr-2' />
{isLoadingDraft ? (
<span className='loading loading-spinner loading-sm' />
) : (
'Simpan Draft'
)}
</Button>
<Button
onClick={handleSubmit}
disabled={isLoadingSubmit}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Send className='w-4 h-4 mr-2' />
{isLoadingSubmit
? 'Mengirim Checklist...'
: 'Submit Checklist'}
</Button>
</div>
)}
</CardContent>
</Card>
</div>
{/* Phase Selection Modal */}
<Dialog
open={showPhaseModal}
onOpenChange={() => setShowPhaseModal(false)}
>
<DialogContent className='sm:max-w-lg bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>Pilih Fase / Tahap</DialogTitle>
<DialogDescription>
Pilih satu atau lebih fase yang akan dikerjakan
</DialogDescription>
</DialogHeader>
<div className='pb-3 border-b border-gray-200'>
<Input
type='text'
placeholder='Cari nama fase...'
value={searchPhase}
onChange={(e) => setSearchPhase(e.target.value)}
className='border-gray-200'
/>
</div>
{availablePhases.length > 0 && (
<div className='flex items-center gap-3 px-1 py-2'>
<label className='flex items-center gap-2 cursor-pointer group'>
<input
type='checkbox'
checked={isAllPhasesSelected}
onChange={() => {
if (isAllPhasesSelected) {
setTempSelectedPhaseIds([]);
} else {
setTempSelectedPhaseIds(
availablePhases.map((p) => String(p.id))
);
}
}}
className='checkbox-clean'
/>
<span className='text-sm font-medium text-gray-700 group-hover:text-gray-900'>
Pilih Semua ({availablePhases.length} Fase)
</span>
</label>
</div>
)}
<div className='max-h-[320px] overflow-y-auto -mx-1 px-1'>
{filteredPhases.length > 0 ? (
<div className='space-y-1.5'>
{filteredPhases.map((phase) => {
const isChecked = tempSelectedPhaseIds.includes(
String(phase.id)
);
return (
<label
key={phase.id}
className={`flex items-start gap-3 px-4 py-3 rounded-lg border cursor-pointer transition-all ${
isChecked
? 'bg-blue-50 border-[#0069e0]'
: 'bg-white border-gray-200 hover:bg-gray-50 hover:border-gray-300'
}`}
>
<input
type='checkbox'
checked={isChecked}
onChange={() => toggleTempPhase(String(phase.id))}
className='checkbox-clean mt-0.5'
/>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium text-gray-900'>
{phase.name}
</p>
</div>
</label>
);
})}
</div>
) : (
<p className='text-center text-gray-500 py-8 text-sm'>
{searchPhase
? 'Tidak ada fase ditemukan'
: 'Tidak ada fase tersedia'}
</p>
)}
</div>
<DialogFooter className='flex items-center justify-between border-t border-gray-200 pt-4'>
<div className='text-sm text-gray-600'>
<Badge
variant='secondary'
className='bg-gray-100 text-gray-700 border-gray-200'
>
Terpilih: {tempSelectedPhaseIds.length} Fase
</Badge>
</div>
<div className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowPhaseModal(false)}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={applyPhaseSelection}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
Terapkan
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
{/* ABK Selection Modal */}
<Dialog open={showAbkModal} onOpenChange={() => setShowAbkModal(false)}>
<DialogContent className='sm:max-w-lg bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>Pilih ABK</DialogTitle>
<DialogDescription>
Pilih satu atau lebih ABK yang ditugaskan
</DialogDescription>
</DialogHeader>
<div className='pb-3 border-b border-gray-200'>
<Input
type='text'
placeholder='Cari nama ABK...'
value={searchAbk}
onChange={(e) => setSearchAbk(e.target.value)}
className='border-gray-200'
/>
</div>
{employees.length > 0 && (
<div className='flex items-center gap-3 px-1 py-2'>
<label className='flex items-center gap-2 cursor-pointer group'>
<input
type='checkbox'
checked={isAllAbkSelected}
onChange={toggleSelectAllAbk}
className='checkbox-clean'
/>
<span className='text-sm font-medium text-gray-700 group-hover:text-gray-900'>
Pilih Semua ({employees.length} ABK)
</span>
</label>
</div>
)}
<div className='max-h-[320px] overflow-y-auto -mx-1 px-1'>
{filteredEmployees.length > 0 ? (
<div className='space-y-1.5'>
{filteredEmployees.map((emp) => {
const isChecked = tempSelectedEmployees.find(
(e) => e.id === emp.id
);
// const kandang = kandangOptions.find((k) => {
// const formattedKandangIds = emp.kandangs.map((empKandang) =>
// String(empKandang.id)
// );
// return formattedKandangIds.includes(String(k.value));
// });
const kandang = emp.kandangs
.map((empKandang) => {
if (String(empKandang.id) === kandangId) {
return `<b>${empKandang.name}</b>`;
}
return empKandang.name;
})
.join(', ');
return (
<label
key={emp.id}
className={`flex items-start gap-3 px-4 py-3 rounded-lg border cursor-pointer transition-all ${
isChecked
? 'bg-blue-50 border-[#0069e0]'
: 'bg-white border-gray-200 hover:bg-gray-50 hover:border-gray-300'
}`}
>
<input
type='checkbox'
checked={!!isChecked}
onChange={() => toggleTempEmployee(emp)}
className='checkbox-clean mt-0.5'
/>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium text-gray-900'>
{emp.name}
</p>
{kandang && (
<p className='text-xs text-gray-500 mt-0.5'>
<span
dangerouslySetInnerHTML={{ __html: kandang }}
/>
</p>
)}
</div>
</label>
);
})}
</div>
) : (
<p className='text-center text-gray-500 py-8 text-sm'>
{searchAbk
? 'Tidak ada ABK ditemukan'
: 'Tidak ada ABK tersedia'}
</p>
)}
</div>
<DialogFooter className='flex items-center justify-between border-t border-gray-200 pt-4'>
<div className='text-sm text-gray-600'>
<Badge
variant='secondary'
className='bg-gray-100 text-gray-700 border-gray-200'
>
Terpilih: {tempSelectedEmployees.length} ABK
</Badge>
</div>
<div className='flex gap-2'>
<Button
variant='outline'
onClick={() => setShowAbkModal(false)}
className='border-gray-200'
>
Batal
</Button>
<Button
onClick={applyAbkSelection}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
Terapkan
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}