mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
2055 lines
75 KiB
TypeScript
2055 lines
75 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
Plus,
|
|
X,
|
|
Save,
|
|
Send,
|
|
Info,
|
|
FilePlus,
|
|
ListChecks,
|
|
Loader2,
|
|
} 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 { 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';
|
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
|
|
|
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' },
|
|
{ value: 'empty_kandang', label: 'Kandang Kosong' },
|
|
];
|
|
|
|
const CATEGORY_LABELS: { [key: string]: string } = {
|
|
pullet_open: 'Pullet Open',
|
|
pullet_close: 'Pullet Close',
|
|
produksi_open: 'Produksi Open',
|
|
produksi_close: 'Produksi Close',
|
|
empty_kandang: 'Kandang Kosong',
|
|
};
|
|
|
|
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();
|
|
|
|
// 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') || ''
|
|
);
|
|
// Initialize emptyKandang from URL so it stays consistent with selectedCategory
|
|
const [emptyKandang, setEmptyKandang] = useState(
|
|
searchParams.get('category') === 'empty_kandang'
|
|
);
|
|
|
|
const isKandangEmpty = selectedCategory === 'empty_kandang';
|
|
|
|
const {
|
|
options: kandangOptions,
|
|
isLoadingMore: isLoadingMoreKandang,
|
|
loadMore: loadMoreKandang,
|
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
|
|
|
const { data: phases } = useSWR<
|
|
BaseApiResponse<Phase[] | undefined>,
|
|
AxiosError<BaseApiResponse>,
|
|
SWRHttpKey
|
|
>(`${PhaseApi.basePath}?page=1&limit=100`, httpClientFetcher, {
|
|
keepPreviousData: true,
|
|
});
|
|
|
|
const { data: employeesRes } = 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 sortedSelectedEmployees = selectedEmployees.toSorted((a, b) =>
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
|
|
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
|
|
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
|
|
|
|
// Activities grouped by phase_id
|
|
const [activitiesByPhase, setActivitiesByPhase] = useState<{
|
|
[phaseId: string]: PhaseActivity[];
|
|
}>({});
|
|
|
|
// Task IDs mapped by phase_activity_id
|
|
const [taskIdsByPhaseActivityId, setTaskIdsByPhaseActivityId] = useState<{
|
|
[phaseActivityId: string]: string;
|
|
}>({});
|
|
|
|
// Assignments keyed by phaseActivityId (works for both create and edit modes)
|
|
const [assignments, setAssignments] = useState<{
|
|
[phaseActivityId: 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(!!checklistIdFromUrl);
|
|
|
|
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState<string>('');
|
|
const [emptyKandangEndDateError, setEmptyKandangEndDateError] =
|
|
useState<string>('');
|
|
|
|
const [preloadedKandang, setPreloadedKandang] = useState<{
|
|
id: string;
|
|
name: string;
|
|
} | null>(null);
|
|
|
|
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
|
const [documents, setDocuments] = useState<File[]>([]);
|
|
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
|
|
|
// Tracks the last checklistId we loaded to prevent re-loading after URL update
|
|
const loadedChecklistIdRef = useRef<string | null>(null);
|
|
// Prevents the kandang-change effect from clearing employees/assignments during initial edit load
|
|
const skipKandangClearRef = useRef<boolean>(false);
|
|
// Mirror of server-side phases/employees; used to diff on save in edit mode
|
|
const serverPhaseIdsRef = useRef<string[]>([]);
|
|
const serverEmployeeIdsRef = useRef<string[]>([]);
|
|
|
|
const isChecklistStatusDraft = checklistStatus === 'DRAFT';
|
|
const isEditMode = dailyChecklistId !== null;
|
|
|
|
// Load checklist data when checklistId is in URL (edit mode)
|
|
useEffect(() => {
|
|
if (!checklistIdFromUrl) {
|
|
setInitialLoading(false);
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
const loadedKandangId = String(data.kandang?.id || '');
|
|
setKandangId(loadedKandangId);
|
|
if (data.kandang?.name) {
|
|
setPreloadedKandang({ id: loadedKandangId, name: data.kandang.name });
|
|
}
|
|
|
|
const isEmptyKandang =
|
|
!!data.empty_kandang || data.category === 'empty_kandang';
|
|
setEmptyKandang(isEmptyKandang);
|
|
setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category);
|
|
|
|
if (
|
|
isEmptyKandang &&
|
|
data.empty_kandang &&
|
|
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(() => {
|
|
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;
|
|
}
|
|
setSelectedEmployees([]);
|
|
setAssignments({});
|
|
}, [kandangId]);
|
|
|
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
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 d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
return d.toLocaleDateString('id-ID', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const d = new Date(dateString);
|
|
return d.toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
};
|
|
|
|
const isMobileDevice = () => {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
navigator.userAgent
|
|
);
|
|
};
|
|
|
|
const getStatusMessage = () => {
|
|
switch (checklistStatus) {
|
|
case 'DRAFT':
|
|
return 'Checklist harian perlu disubmit';
|
|
case 'SUBMITTED':
|
|
return 'Checklist harian menunggu persetujuan';
|
|
case 'APPROVED':
|
|
return 'Checklist harian telah disetujui';
|
|
case 'REJECTED':
|
|
return 'Checklist harian telah ditolak';
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
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';
|
|
}
|
|
};
|
|
|
|
// 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
|
|
);
|
|
if (isResponseError(setPhaseRes)) {
|
|
toast.error('Gagal menyimpan fase');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Set employees
|
|
if (selectedEmployees.length > 0) {
|
|
const setEmployeesRes =
|
|
await DailyChecklistApi.setDailyChecklistEmployees(
|
|
currentChecklistId,
|
|
selectedEmployees.map((e) => String(e.id))
|
|
);
|
|
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<unknown>[] = [];
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Ensure a checklist record exists; returns the ID or null on failure
|
|
const ensureChecklist = async (): Promise<string | null> => {
|
|
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
|
|
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]);
|
|
}
|
|
};
|
|
|
|
// Phase selection — local state only; persisted on Simpan Draft / Submit
|
|
const applyPhaseSelection = () => {
|
|
if (!tempSelectedPhaseIds.length) {
|
|
toast.error('Pilih minimal satu fase');
|
|
return;
|
|
}
|
|
|
|
setSelectedPhaseIds([...tempSelectedPhaseIds]);
|
|
setShowPhaseModal(false);
|
|
setSearchPhase('');
|
|
};
|
|
|
|
// 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]);
|
|
}
|
|
};
|
|
|
|
// 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) {
|
|
const newAssignments = { ...assignments };
|
|
Object.keys(newAssignments).forEach((paId) => {
|
|
removedEmployees.forEach((emp) => {
|
|
delete newAssignments[paId][String(emp.id)];
|
|
});
|
|
});
|
|
setAssignments(newAssignments);
|
|
}
|
|
|
|
setSelectedEmployees([...tempSelectedEmployees]);
|
|
setShowAbkModal(false);
|
|
setSearchAbk('');
|
|
};
|
|
|
|
// Remove ABK — local state only; persisted on Simpan Draft / Submit
|
|
const handleRemoveAbk = (employeeId: string) => {
|
|
const newAssignments = { ...assignments };
|
|
Object.keys(newAssignments).forEach((paId) => {
|
|
delete newAssignments[paId][employeeId];
|
|
});
|
|
setAssignments(newAssignments);
|
|
setSelectedEmployees(
|
|
selectedEmployees.filter((e) => String(e.id) !== employeeId)
|
|
);
|
|
};
|
|
|
|
// Checkbox change — local state only; persisted on Simpan Draft / Submit
|
|
const handleCheckboxChange = (
|
|
activityId: string,
|
|
employeeId: string,
|
|
checked: boolean
|
|
) => {
|
|
if (!isChecklistStatusDraft) return;
|
|
|
|
setAssignments((prev) => ({
|
|
...prev,
|
|
[activityId]: {
|
|
...prev[activityId],
|
|
[employeeId]: {
|
|
checked,
|
|
note: prev[activityId]?.[employeeId]?.note || '',
|
|
},
|
|
},
|
|
}));
|
|
};
|
|
|
|
// Note change — local state only; persisted on Simpan Draft / Submit
|
|
const handleNoteChange = (
|
|
activityId: string,
|
|
employeeId: string,
|
|
note: string
|
|
) => {
|
|
if (!isChecklistStatusDraft) return;
|
|
|
|
setAssignments((prev) => ({
|
|
...prev,
|
|
[activityId]: {
|
|
...prev[activityId],
|
|
[employeeId]: {
|
|
checked: prev[activityId]?.[employeeId]?.checked || false,
|
|
note,
|
|
},
|
|
},
|
|
}));
|
|
};
|
|
|
|
// 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));
|
|
|
|
// 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<unknown>[] = [];
|
|
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 (emptyKandang && !emptyKandangEndDate) {
|
|
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
|
|
return;
|
|
}
|
|
|
|
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 (emptyKandang && !emptyKandangEndDate) {
|
|
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
|
|
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(
|
|
currentChecklistId,
|
|
documents,
|
|
deletedDocumentIds
|
|
);
|
|
|
|
if (isResponseError(submitRes)) {
|
|
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 ||
|
|
kandangId;
|
|
const statusMsg = getStatusMessage();
|
|
const category = selectedCategory || '';
|
|
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');
|
|
};
|
|
|
|
toast.success('Checklist berhasil disubmit untuk approval', {
|
|
action: {
|
|
label: 'Bagikan ke WhatsApp',
|
|
onClick: shareToWhatsApp,
|
|
},
|
|
description: (
|
|
<button
|
|
onClick={() =>
|
|
router.push(
|
|
`/daily-checklist/list-daily-checklist/detail/?checklistId=${currentChecklistId}`
|
|
)
|
|
}
|
|
className='text-blue-600 hover:text-blue-800 underline font-medium'
|
|
>
|
|
Lihat Detail
|
|
</button>
|
|
),
|
|
});
|
|
} catch (error) {
|
|
console.error('Error submitting:', error);
|
|
toast.error('Terjadi kesalahan');
|
|
} finally {
|
|
setIsLoadingSubmit(false);
|
|
}
|
|
};
|
|
|
|
// Filter helpers
|
|
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 === filteredEmployees.length &&
|
|
filteredEmployees.length > 0 &&
|
|
tempSelectedEmployees.every((t) =>
|
|
filteredEmployees.some((f) => f.id === t.id)
|
|
);
|
|
|
|
const isAllPhasesSelected =
|
|
tempSelectedPhaseIds.length === filteredPhases.length &&
|
|
filteredPhases.length > 0 &&
|
|
tempSelectedPhaseIds.every((id) =>
|
|
filteredPhases.some((p) => String(p.id) === String(id))
|
|
);
|
|
|
|
const toggleSelectAllAbk = () => {
|
|
if (isAllAbkSelected) {
|
|
setTempSelectedEmployees([]);
|
|
} else {
|
|
setTempSelectedEmployees([...filteredEmployees]);
|
|
}
|
|
};
|
|
|
|
// Group activities by Phase → TimeType
|
|
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: {} };
|
|
}
|
|
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;
|
|
};
|
|
|
|
// Visibility helpers
|
|
const hasFormBase = !!(date && kandangId);
|
|
const hasCategory = !!(isKandangEmpty || selectedCategory);
|
|
const canShowPhaseSection =
|
|
!isKandangEmpty && hasFormBase && !!selectedCategory;
|
|
const canShowAbkSection = canShowPhaseSection && selectedPhaseIds.length > 0;
|
|
const canShowTable = canShowAbkSection && selectedEmployees.length > 0;
|
|
const canShowActions =
|
|
isChecklistStatusDraft &&
|
|
hasFormBase &&
|
|
hasCategory &&
|
|
(isKandangEmpty ||
|
|
(selectedPhaseIds.length > 0 && selectedEmployees.length > 0));
|
|
|
|
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 justify-between'>
|
|
<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={getStatusBadgeClass()}>
|
|
{checklistStatus}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{isEditMode && (
|
|
<Button
|
|
onClick={handleNewChecklist}
|
|
variant='outline'
|
|
size='sm'
|
|
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
|
|
>
|
|
<Plus className='w-4 h-4 mr-1' />
|
|
Tambah Baru
|
|
</Button>
|
|
)}
|
|
</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 onScroll={handleKandangScroll}>
|
|
{preloadedKandang &&
|
|
!kandangOptions.some(
|
|
(k) => String(k.value) === preloadedKandang.id
|
|
) && (
|
|
<SelectItem value={preloadedKandang.id}>
|
|
{preloadedKandang.name}
|
|
</SelectItem>
|
|
)}
|
|
{kandangOptions.map((kandang, kandangIdx) => (
|
|
<SelectItem
|
|
key={`${kandang.value}-${kandangIdx}`}
|
|
value={String(kandang.value)}
|
|
>
|
|
{kandang.label}
|
|
</SelectItem>
|
|
))}
|
|
{isLoadingMoreKandang && (
|
|
<div className='flex justify-center p-2'>
|
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
|
</div>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor='category'>
|
|
Kategori <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Select
|
|
value={selectedCategory}
|
|
onValueChange={(val) => {
|
|
setSelectedCategory(val);
|
|
setEmptyKandang(val === 'empty_kandang');
|
|
}}
|
|
disabled={!isChecklistStatusDraft || emptyKandang}
|
|
>
|
|
<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>
|
|
|
|
<div className='mb-6 pb-6 border-b border-gray-200'>
|
|
<div className='flex flex-col gap-4 md:flex-row md:items-end md:gap-6'>
|
|
<label className='flex items-center gap-2 text-sm font-medium text-gray-900'>
|
|
<input
|
|
type='checkbox'
|
|
checked={emptyKandang}
|
|
onChange={(e) => {
|
|
const checked = e.target.checked;
|
|
setEmptyKandang(checked);
|
|
setSelectedCategory(checked ? 'empty_kandang' : '');
|
|
}}
|
|
disabled={!isChecklistStatusDraft}
|
|
className='checkbox-clean'
|
|
/>
|
|
<span>Kandang Kosong</span>
|
|
</label>
|
|
</div>
|
|
|
|
{emptyKandang && (
|
|
<div className='grid grid-cols-3 gap-4'>
|
|
<div className='mt-4'>
|
|
<Label htmlFor='empty_kandang_end_date'>
|
|
Tanggal Akhir Kandang Kosong{' '}
|
|
<span className='text-red-500'>*</span>
|
|
</Label>
|
|
<div className='mt-1.5'>
|
|
<DatePicker
|
|
date={emptyKandangEndDate}
|
|
onDateChange={(val) => {
|
|
setEmptyKandangEndDate(val);
|
|
if (val) setEmptyKandangEndDateError('');
|
|
}}
|
|
disabled={!isChecklistStatusDraft}
|
|
placeholder='Pilih tanggal'
|
|
formatDisplay={formatDateForDisplay}
|
|
hasError={!!emptyKandangEndDateError}
|
|
/>
|
|
{emptyKandangEndDateError && (
|
|
<p className='text-xs text-red-500 mt-1'>
|
|
{emptyKandangEndDateError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Phase Selection Section */}
|
|
{canShowPhaseSection && (
|
|
<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 */}
|
|
{canShowAbkSection && (
|
|
<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 */}
|
|
{!isKandangEmpty && (
|
|
<>
|
|
{canShowTable ? (
|
|
<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>
|
|
{sortedSelectedEmployees.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)
|
|
);
|
|
|
|
const totalActivities = timeTypes.reduce(
|
|
(sum, tt) => sum + timeGroups[tt].length,
|
|
0
|
|
);
|
|
|
|
const rows = [];
|
|
|
|
// Phase header row
|
|
rows.push(
|
|
<tr
|
|
key={`phase-${phaseId}`}
|
|
className='bg-blue-50 border-b border-blue-200'
|
|
>
|
|
<td
|
|
colSpan={
|
|
sortedSelectedEmployees.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>
|
|
);
|
|
|
|
timeTypes.forEach((timeType) => {
|
|
const activities = timeGroups[timeType];
|
|
const hasMultiple = timeTypes.length > 1;
|
|
|
|
if (hasMultiple) {
|
|
rows.push(
|
|
<tr
|
|
key={`time-${phaseId}-${timeType}`}
|
|
className='bg-gray-50 border-b border-gray-200'
|
|
>
|
|
<td
|
|
colSpan={
|
|
sortedSelectedEmployees.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>
|
|
);
|
|
}
|
|
|
|
activities
|
|
.slice()
|
|
.sort((a, b) =>
|
|
a.name.localeCompare(b.name, undefined, {
|
|
sensitivity: 'base',
|
|
})
|
|
)
|
|
.forEach((activity, index) => {
|
|
const activityId = String(activity.id);
|
|
const indentClass = hasMultiple
|
|
? '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>
|
|
{sortedSelectedEmployees.map(
|
|
(emp) => (
|
|
<td
|
|
key={emp.id}
|
|
className='text-center py-3 px-4 border-r border-gray-200'
|
|
>
|
|
<input
|
|
type='checkbox'
|
|
checked={
|
|
assignments[activityId]?.[
|
|
emp.id
|
|
]?.checked || false
|
|
}
|
|
onChange={(e) =>
|
|
handleCheckboxChange(
|
|
activityId,
|
|
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={
|
|
selectedEmployees.length > 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}
|
|
/>
|
|
</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'>
|
|
{!hasFormBase || !selectedCategory ? (
|
|
<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>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Documents */}
|
|
{!isKandangEmpty && canShowTable && (
|
|
<>
|
|
{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((prev) => [
|
|
...prev,
|
|
existingDocument.id,
|
|
]);
|
|
setExistingDocuments((prev) => {
|
|
const next = [...prev];
|
|
next.splice(existingDocumentIdx, 1);
|
|
return next;
|
|
});
|
|
}}
|
|
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 next = [...documents];
|
|
next.splice(deletedFileIdx, 1);
|
|
setDocuments(next);
|
|
}}
|
|
disabled={!isChecklistStatusDraft}
|
|
className={{
|
|
wrapper: 'mt-6',
|
|
inputWrapper: 'flex items-center',
|
|
label: 'font-semibold text-gray-900',
|
|
}}
|
|
maxSize={5242880}
|
|
bottomLabel='Ukuran file maksimal 5MB'
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Documents for empty kandang */}
|
|
{isKandangEmpty && isChecklistStatusDraft && (
|
|
<>
|
|
{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>
|
|
|
|
<Button
|
|
type='button'
|
|
variant='ghost'
|
|
color='error'
|
|
onClick={() => {
|
|
setDeletedDocumentIds((prev) => [
|
|
...prev,
|
|
existingDocument.id,
|
|
]);
|
|
setExistingDocuments((prev) => {
|
|
const next = [...prev];
|
|
next.splice(existingDocumentIdx, 1);
|
|
return next;
|
|
});
|
|
}}
|
|
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>
|
|
)}
|
|
|
|
<DropFileInput
|
|
name='Dokumen'
|
|
label='Dokumen'
|
|
values={documents}
|
|
onChange={(files) => 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 */}
|
|
{canShowActions && (
|
|
<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(
|
|
filteredPhases.map((p) => String(p.id))
|
|
);
|
|
}
|
|
}}
|
|
className='checkbox-clean'
|
|
/>
|
|
<span className='text-sm font-medium text-gray-700 group-hover:text-gray-900'>
|
|
Pilih Semua ({filteredPhases.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>
|
|
|
|
{filteredEmployees.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 ({filteredEmployees.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 = 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>
|
|
);
|
|
}
|