Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu

This commit is contained in:
rstubryan
2026-01-09 15:37:49 +07:00
11 changed files with 890 additions and 923 deletions
+17 -17
View File
@@ -26,9 +26,9 @@
"next": "15.5.9", "next": "15.5.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.1.0", "react": "^19.1.2",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "^19.1.2",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.70.0", "react-hook-form": "^7.70.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
@@ -8174,12 +8174,12 @@
} }
}, },
"node_modules/jspdf-autotable": { "node_modules/jspdf-autotable": {
"version": "5.0.2", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"jspdf": "^2 || ^3" "jspdf": "^2 || ^3 || ^4"
} }
}, },
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
@@ -9376,9 +9376,9 @@
} }
}, },
"node_modules/react": { "node_modules/react": {
"version": "19.1.0", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"engines": { "engines": {
@@ -9407,16 +9407,16 @@
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.27.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.1.0" "react": "^19.2.3"
} }
}, },
"node_modules/react-dropzone": { "node_modules/react-dropzone": {
@@ -9916,9 +9916,9 @@
} }
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.26.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
+2 -2
View File
@@ -29,9 +29,9 @@
"next": "15.5.9", "next": "15.5.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.1.0", "react": "^19.1.2",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "^19.1.2",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.70.0", "react-hook-form": "^7.70.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
@@ -58,7 +58,7 @@ export function MultiSelect({
}; };
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant='outline' variant='outline'
@@ -115,8 +115,8 @@ export function MultiSelect({
onValueChange={onSearchChange} onValueChange={onSearchChange}
/> />
<CommandEmpty>No item found.</CommandEmpty> <CommandEmpty>No item found.</CommandEmpty>
<CommandList> <CommandList className='max-h-[300px] overflow-y-auto'>
<CommandGroup className='max-h-64 overflow-auto'> <CommandGroup className='overflow-visible'>
{options.map((option) => ( {options.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
+1 -1
View File
@@ -61,7 +61,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot='select-content' data-slot='select-content'
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[300px] min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' && position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className className
@@ -7,7 +7,6 @@ import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input'; import { Input } from '@/figma-make/components/base/input';
import { Textarea } from '@/figma-make/components/base/textarea';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { import {
Select, Select,
@@ -26,7 +25,20 @@ import {
} from '@/figma-make/components/base/dialog'; } from '@/figma-make/components/base/dialog';
import { DatePicker } from '@/figma-make/components/base/date-picker'; import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase'; 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 } 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';
// Static categories // Static categories
const CATEGORIES = [ const CATEGORIES = [
@@ -38,59 +50,68 @@ const CATEGORIES = [
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam']; const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = { const TIME_TYPE_LABELS: { [key: string]: string } = {
umum: 'Umum', Umum: 'Umum',
pagi: 'Pagi', Pagi: 'Pagi',
siang: 'Siang', Siang: 'Siang',
sore: 'Sore', Sore: 'Sore',
malam: 'Malam', Malam: 'Malam',
}; };
interface Kandang {
id: string;
name: string;
}
interface Phase { interface Phase {
id: string; id: string;
name: 'string'; name: 'string';
category: string; category: string;
} }
interface Activity {
id: string;
phase_id: string;
name: string;
description?: string;
time_type: string;
}
interface Employee {
id: string;
name: string;
kandang_id: string;
}
interface TaskAssignment {
task_id: string;
employee_id: string;
checked: boolean;
note: string | null;
}
export function DailyChecklistContent() { export function DailyChecklistContent() {
const [kandangId, setKandangId] = useState('');
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 [date, setDate] = useState(() => { const [date, setDate] = useState(() => {
const today = new Date(); const today = new Date();
return today.toISOString().split('T')[0]; return today.toISOString().split('T')[0];
}); });
const [kandangId, setKandangId] = useState('');
const [selectedCategory, setSelectedCategory] = useState(''); const [selectedCategory, setSelectedCategory] = useState('');
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]); const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
const [checklistName, setChecklistName] = useState('');
const [kandangList, setKandangList] = useState<Kandang[]>([]); const [selectedEmployees, setSelectedEmployees] = useState<
const [allPhases, setAllPhases] = useState<Phase[]>([]); { id: number; name: string }[]
const [employees, setEmployees] = useState<Employee[]>([]); >([]);
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null); const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT'); const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
@@ -98,7 +119,7 @@ export function DailyChecklistContent() {
// Activities grouped by phase // Activities grouped by phase
const [activitiesByPhase, setActivitiesByPhase] = useState<{ const [activitiesByPhase, setActivitiesByPhase] = useState<{
[phaseId: string]: Activity[]; [phaseId: string]: PhaseActivity[];
}>({}); }>({});
// Task IDs mapped by phase_activity_id for quick lookup // Task IDs mapped by phase_activity_id for quick lookup
@@ -116,7 +137,7 @@ export function DailyChecklistContent() {
const [showAbkModal, setShowAbkModal] = useState(false); const [showAbkModal, setShowAbkModal] = useState(false);
const [showPhaseModal, setShowPhaseModal] = useState(false); const [showPhaseModal, setShowPhaseModal] = useState(false);
const [tempSelectedEmployees, setTempSelectedEmployees] = useState< const [tempSelectedEmployees, setTempSelectedEmployees] = useState<
Employee[] { id: number; name: string }[]
>([]); >([]);
const [tempSelectedPhaseIds, setTempSelectedPhaseIds] = useState<string[]>( const [tempSelectedPhaseIds, setTempSelectedPhaseIds] = useState<string[]>(
[] []
@@ -126,7 +147,6 @@ export function DailyChecklistContent() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const [datePickerOpen, setDatePickerOpen] = useState(false);
// Format date for display // Format date for display
const formatDateForDisplay = (dateStr: string) => { const formatDateForDisplay = (dateStr: string) => {
@@ -143,23 +163,13 @@ export function DailyChecklistContent() {
// Fetch master data on mount // Fetch master data on mount
useEffect(() => { useEffect(() => {
if (!isSupabaseConfigured()) {
console.warn(
'Supabase not configured. Please add environment variables.'
);
setInitialLoading(false);
return;
}
fetchKandang();
fetchAllPhases();
setInitialLoading(false); setInitialLoading(false);
}, []); }, []);
// Check for existing checklist when unique key changes // Check for existing checklist when unique key changes
useEffect(() => { useEffect(() => {
const checkAndLoadChecklist = async () => { const checkAndLoadChecklist = async () => {
if (!date || !kandangId || !selectedCategory || !isSupabaseConfigured()) { if (!date || !kandangId || !selectedCategory) {
setDailyChecklistId(null); setDailyChecklistId(null);
setChecklistStatus('DRAFT'); setChecklistStatus('DRAFT');
setIsEditMode(false); setIsEditMode(false);
@@ -171,55 +181,47 @@ export function DailyChecklistContent() {
} }
try { try {
// UPSERT to get or create checklist (UNIQUE KEY: date, kandang_id, category) const checklist = await DailyChecklistApi.create({
const { data: checklist, error } = await supabase
.from('daily_checklists')
.upsert(
{
date, date,
kandang_id: kandangId, kandang_id: Number(kandangId),
category: selectedCategory, category: selectedCategory,
status: 'DRAFT', status: 'DRAFT',
updated_at: new Date().toISOString(), });
},
{
onConflict: 'date,kandang_id,category',
ignoreDuplicates: false,
}
)
.select()
.single();
if (error) { if (isResponseError(checklist)) {
console.error('Error upserting checklist:', error); console.error('Error upserting checklist:', checklist.message);
toast.error('Gagal memuat checklist'); toast.error('Gagal memuat checklist');
return; return;
} }
setDailyChecklistId(checklist.id); setDailyChecklistId(String(checklist?.data?.id));
setChecklistStatus(checklist.status); setChecklistStatus(String(checklist?.data?.status));
// Load existing phases for this checklist const existingPhases = await DailyChecklistApi.getOneDailyChecklist(
const { data: existingPhases, error: phaseError } = await supabase String(checklist?.data.id)
.from('daily_checklist_phases') );
.select('phase_id')
.eq('checklist_id', checklist.id); // ✅ Uses checklist_id
if (phaseError) { if (isResponseError(existingPhases)) {
console.error('Error loading phases:', phaseError); console.error('Error loading phases:', existingPhases.message);
} else if (existingPhases && existingPhases.length > 0) { } else if (
existingPhases &&
existingPhases.data &&
existingPhases.data.phases.length > 0
) {
// Existing checklist - EDIT MODE // Existing checklist - EDIT MODE
setIsEditMode(true); setIsEditMode(true);
const phaseIds = existingPhases.map((p) => p.phase_id); const phaseIds = existingPhases.data.phases.map((p) =>
String(p.phase_id)
);
setSelectedPhaseIds(phaseIds); setSelectedPhaseIds(phaseIds);
if (checklist.status === 'DRAFT') { if (checklist?.data?.status === 'DRAFT') {
toast.info('Checklist ditemukan - Mode Edit (Draft)', { toast.info('Checklist ditemukan - Mode Edit (Draft)', {
description: description:
'Anda dapat menambah atau mengubah data checklist ini', 'Anda dapat menambah atau mengubah data checklist ini',
}); });
} else { } else {
toast.warning(`Checklist sudah ${checklist.status}`, { toast.warning(`Checklist sudah ${checklist?.data?.status}`, {
description: 'Checklist tidak dapat diedit', description: 'Checklist tidak dapat diedit',
}); });
} }
@@ -239,32 +241,27 @@ export function DailyChecklistContent() {
// Load activities and tasks when phases change // Load activities and tasks when phases change
useEffect(() => { useEffect(() => {
const loadActivitiesAndTasks = async () => { const loadActivitiesAndTasks = async () => {
if ( if (!dailyChecklistId || selectedPhaseIds.length === 0) {
!dailyChecklistId ||
selectedPhaseIds.length === 0 ||
!isSupabaseConfigured()
) {
setActivitiesByPhase({}); setActivitiesByPhase({});
setTaskIdsByPhaseActivityId({}); setTaskIdsByPhaseActivityId({});
return; return;
} }
try { try {
// Fetch activities for selected phases const activitiesRes = await PhaseActivityApi.getAll({
const { data: activities, error: actError } = await supabase phase_ids: selectedPhaseIds.join(','),
.from('phase_activities') });
.select('id, phase_id, name, description, time_type')
.in('phase_id', selectedPhaseIds)
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
if (actError) { if (isResponseError(activitiesRes)) {
console.error('Error loading activities:', actError); console.error('Error loading activities:', activitiesRes.message);
toast.error('Gagal memuat aktivitas'); toast.error('Gagal memuat aktivitas');
return; return;
} }
const activities = activitiesRes?.data || [];
// Group activities by phase // Group activities by phase
const grouped: { [phaseId: string]: Activity[] } = {}; const grouped: { [phaseId: string]: PhaseActivity[] } = {};
(activities || []).forEach((act) => { (activities || []).forEach((act) => {
if (!grouped[act.phase_id]) { if (!grouped[act.phase_id]) {
grouped[act.phase_id] = []; grouped[act.phase_id] = [];
@@ -283,28 +280,30 @@ export function DailyChecklistContent() {
})); }));
if (taskUpserts.length > 0) { if (taskUpserts.length > 0) {
const { data: tasks, error: taskError } = await supabase const existingDailyChecklist =
.from('daily_checklist_activity_tasks') await DailyChecklistApi.getOneDailyChecklist(
.upsert(taskUpserts, { String(dailyChecklistId)
onConflict: 'checklist_id,phase_activity_id', );
ignoreDuplicates: false,
})
.select('id, phase_activity_id');
if (taskError) { if (isResponseError(existingDailyChecklist)) {
console.error('Error upserting tasks:', taskError); console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return; return;
} }
// Build task ID lookup // Build task ID lookup
const taskMap: { [phaseActivityId: string]: string } = {}; const taskMap: { [phaseActivityId: string]: string } = {};
(tasks || []).forEach((task) => { (existingDailyChecklist?.data?.tasks || []).forEach((task) => {
taskMap[task.phase_activity_id] = task.id; taskMap[String(task.phase_activity_id)] = String(task.id);
}); });
setTaskIdsByPhaseActivityId(taskMap); setTaskIdsByPhaseActivityId(taskMap);
// Load existing assignments for these tasks // Load existing assignments for these tasks
await loadAssignments(tasks.map((t) => t.id)); await loadAssignments(
existingDailyChecklist?.data?.tasks?.map((t) => String(t.id)) || []
);
} }
} catch (error) { } catch (error) {
console.error('Error loading activities and tasks:', error); console.error('Error loading activities and tasks:', error);
@@ -316,29 +315,28 @@ export function DailyChecklistContent() {
// Load employees when kandang changes // Load employees when kandang changes
useEffect(() => { useEffect(() => {
if (kandangId && isSupabaseConfigured()) { if (kandangId) {
fetchEmployees(kandangId);
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment) // ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
setSelectedEmployees([]); setSelectedEmployees([]);
setAssignments({}); setAssignments({});
} else { } else {
setEmployees([]);
setSelectedEmployees([]); setSelectedEmployees([]);
setAssignments({}); setAssignments({});
} }
}, [kandangId]); }, [kandangId]);
const loadAssignments = async (taskIds: string[]) => { const loadAssignments = async (taskIds: string[]) => {
if (taskIds.length === 0 || !isSupabaseConfigured()) return; if (taskIds.length === 0) return;
try { try {
const { data, error } = await supabase const existingDailyChecklist =
.from('daily_checklist_activity_task_assignments') await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
.select('task_id, employee_id, checked, note')
.in('task_id', taskIds);
if (error) { if (isResponseError(existingDailyChecklist)) {
console.error('Error loading assignments:', error); console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return; return;
} }
@@ -349,30 +347,43 @@ export function DailyChecklistContent() {
}; };
} = {}; } = {};
(data || []).forEach((assignment) => { (existingDailyChecklist?.data.tasks || []).forEach(
if (!assignmentMap[assignment.task_id]) { (dailyChecklistTask) => {
assignmentMap[assignment.task_id] = {}; if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
} }
assignmentMap[assignment.task_id][assignment.employee_id] = {
dailyChecklistTask.assignments.forEach((assignment) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
checked: assignment.checked, checked: assignment.checked,
note: assignment.note || '', note: assignment.note || '',
}; };
}); });
}
);
setAssignments(assignmentMap); setAssignments(assignmentMap);
// Load employees from assignments // Load employees from assignments
const employeeIds = Array.from( const employeeIds = Array.from(
new Set((data || []).map((a) => a.employee_id)) new Set(
(existingDailyChecklist?.data.assigned_employees || []).map(
(a) => a.id
)
)
); );
if (employeeIds.length > 0) {
const { data: empData, error: empError } = await supabase
.from('employees')
.select('id, name, kandang_id')
.in('id', employeeIds);
if (!empError && empData) { if (employeeIds.length > 0) {
setSelectedEmployees(empData); const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
} }
} }
} catch (error) { } catch (error) {
@@ -380,68 +391,6 @@ export function DailyChecklistContent() {
} }
}; };
const fetchKandang = async () => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (error) {
console.error('Error fetching kandang:', error);
return;
}
setKandangList(data || []);
} catch (error) {
console.error('Error fetching kandang:', error);
}
};
const fetchAllPhases = async () => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('phases')
.select('id, name, category')
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
if (error) {
console.error('Error fetching phases:', error);
return;
}
setAllPhases(data || []);
} catch (error) {
console.error('Error fetching phases:', error);
}
};
const fetchEmployees = async (kandangId: string) => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('employees')
.select('id, name, kandang_id')
.eq('kandang_id', kandangId)
.eq('is_active', true)
.order('name', { ascending: true });
if (error) {
console.error('Error fetching employees:', error);
return;
}
setEmployees(data || []);
} catch (error) {
console.error('Error fetching employees:', error);
}
};
// Phase selection modal // Phase selection modal
const handleAddPhase = () => { const handleAddPhase = () => {
if (!selectedCategory) { if (!selectedCategory) {
@@ -464,31 +413,25 @@ export function DailyChecklistContent() {
}; };
const applyPhaseSelection = async () => { const applyPhaseSelection = async () => {
if (!dailyChecklistId || !isSupabaseConfigured()) { if (!dailyChecklistId) {
toast.error('Checklist belum tersedia'); toast.error('Checklist belum tersedia');
return; return;
} }
try { try {
// Delete existing phase links
await supabase
.from('daily_checklist_phases')
.delete()
.eq('checklist_id', dailyChecklistId);
// Insert new phase links // Insert new phase links
if (tempSelectedPhaseIds.length > 0) { if (tempSelectedPhaseIds.length > 0) {
const phaseLinks = tempSelectedPhaseIds.map((phaseId) => ({ const setDailyChecklistPhaseRes =
checklist_id: dailyChecklistId, await DailyChecklistApi.setDailyChecklistPhase(
phase_id: phaseId, dailyChecklistId,
})); tempSelectedPhaseIds
);
const { error } = await supabase if (isResponseError(setDailyChecklistPhaseRes)) {
.from('daily_checklist_phases') console.error(
.insert(phaseLinks); 'Error saving phases:',
setDailyChecklistPhaseRes.message
if (error) { );
console.error('Error saving phases:', error);
toast.error('Gagal menyimpan fase'); toast.error('Gagal menyimpan fase');
return; return;
} }
@@ -545,18 +488,23 @@ export function DailyChecklistContent() {
(emp) => !tempSelectedEmployees.find((temp) => temp.id === emp.id) (emp) => !tempSelectedEmployees.find((temp) => temp.id === emp.id)
); );
if (removedEmployees.length > 0 && isSupabaseConfigured()) { if (removedEmployees.length > 0) {
const taskIds = Object.values(taskIdsByPhaseActivityId); removedEmployees.forEach(async (removedEmp) => {
if (taskIds.length > 0) { const removeEmployeeAssignmentRes =
await supabase await DailyChecklistApi.removeEmployeeAssignment(
.from('daily_checklist_activity_task_assignments') dailyChecklistId,
.delete() String(removedEmp.id)
.in('task_id', taskIds)
.in(
'employee_id',
removedEmployees.map((e) => e.id)
); );
if (isResponseError(removeEmployeeAssignmentRes)) {
console.error(
'Error removing employee assignment:',
removeEmployeeAssignmentRes.message
);
toast.error('Gagal menghapus tugas');
return;
} }
});
// Remove from state // Remove from state
const newAssignments = { ...assignments }; const newAssignments = { ...assignments };
@@ -573,7 +521,7 @@ export function DailyChecklistContent() {
(temp) => !selectedEmployees.find((emp) => emp.id === temp.id) (temp) => !selectedEmployees.find((emp) => emp.id === temp.id)
); );
if (addedEmployees.length > 0 && isSupabaseConfigured()) { if (addedEmployees.length > 0) {
const taskIds = Object.values(taskIdsByPhaseActivityId); const taskIds = Object.values(taskIdsByPhaseActivityId);
const newAssignments: { const newAssignments: {
task_id: string; task_id: string;
@@ -586,20 +534,23 @@ export function DailyChecklistContent() {
addedEmployees.forEach((emp) => { addedEmployees.forEach((emp) => {
newAssignments.push({ newAssignments.push({
task_id: taskId, task_id: taskId,
employee_id: emp.id, employee_id: String(emp.id),
checked: false, checked: false,
note: null, note: null,
}); });
}); });
}); });
if (newAssignments.length > 0) { const assignEmployeeRes =
await supabase await DailyChecklistApi.setDailyChecklistEmployees(
.from('daily_checklist_activity_task_assignments') dailyChecklistId,
.upsert(newAssignments, { addedEmployees.map((emp) => String(emp.id))
onConflict: 'task_id,employee_id', );
ignoreDuplicates: false,
}); if (isResponseError(assignEmployeeRes)) {
console.error('Error assigning employees:', assignEmployeeRes.message);
toast.error('Gagal mengassign ABK: ' + assignEmployeeRes.message);
return;
} }
} }
@@ -610,16 +561,15 @@ export function DailyChecklistContent() {
}; };
const handleRemoveAbk = async (employeeId: string) => { const handleRemoveAbk = async (employeeId: string) => {
if (!isSupabaseConfigured()) return; const deleteEmployeeRes = await DailyChecklistApi.removeEmployeeAssignment(
String(dailyChecklistId),
String(employeeId)
);
// Delete assignments for this employee if (isResponseError(deleteEmployeeRes)) {
const taskIds = Object.values(taskIdsByPhaseActivityId); console.error('Error deleting employee:', deleteEmployeeRes.message);
if (taskIds.length > 0) { toast.error('Gagal menghapus ABK: ' + deleteEmployeeRes.message);
await supabase return;
.from('daily_checklist_activity_task_assignments')
.delete()
.in('task_id', taskIds)
.eq('employee_id', employeeId);
} }
// Remove from state // Remove from state
@@ -629,7 +579,9 @@ export function DailyChecklistContent() {
}); });
setAssignments(newAssignments); setAssignments(newAssignments);
setSelectedEmployees(selectedEmployees.filter((e) => e.id !== employeeId)); setSelectedEmployees(
selectedEmployees.filter((e) => String(e.id) !== employeeId)
);
}; };
const handleCheckboxChange = async ( const handleCheckboxChange = async (
@@ -645,7 +597,6 @@ export function DailyChecklistContent() {
checked, checked,
taskId, taskId,
hasTaskId: !!taskId, hasTaskId: !!taskId,
isSupabaseConfigured: isSupabaseConfigured(),
checklistStatus, checklistStatus,
isEditable, isEditable,
}); });
@@ -657,12 +608,6 @@ export function DailyChecklistContent() {
return; return;
} }
if (!isSupabaseConfigured()) {
console.error('[CHECKBOX] Supabase not configured');
toast.error('Database tidak terkonfigurasi');
return;
}
if (!isEditable) { if (!isEditable) {
console.warn( console.warn(
'[CHECKBOX] Checklist is not editable, status:', '[CHECKBOX] Checklist is not editable, status:',
@@ -692,24 +637,23 @@ export function DailyChecklistContent() {
// Update database // Update database
const payload = { const payload = {
task_id: taskId, task_id: Number(taskId),
employee_id: employeeId, employee_id: Number(employeeId),
checked, checked,
note: assignments[taskId]?.[employeeId]?.note || null, note: assignments[taskId]?.[employeeId]?.note || null,
}; };
console.log('[CHECKBOX] Saving to database:', payload); console.log('[CHECKBOX] Saving to database:', payload);
const { error } = await supabase const checkOrUncheckAssignmentRes =
.from('daily_checklist_activity_task_assignments') await DailyChecklistApi.checkOrUncheckAssignment(payload);
.upsert(payload, {
onConflict: 'task_id,employee_id',
ignoreDuplicates: false,
});
if (error) { if (isResponseError(checkOrUncheckAssignmentRes)) {
console.error('[CHECKBOX] Database error:', error); console.error(
toast.error('Gagal menyimpan: ' + error.message); '[CHECKBOX] Database error:',
checkOrUncheckAssignmentRes.message
);
toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message);
// Revert state on error // Revert state on error
setAssignments((prev) => ({ setAssignments((prev) => ({
@@ -734,26 +678,25 @@ export function DailyChecklistContent() {
note: string note: string
) => { ) => {
const taskId = taskIdsByPhaseActivityId[activityId]; const taskId = taskIdsByPhaseActivityId[activityId];
if (!taskId || !isSupabaseConfigured()) return; if (!taskId) return;
// Update database // Update database
const { error } = await supabase const payload = {
.from('daily_checklist_activity_task_assignments') task_id: Number(taskId),
.upsert( employee_id: Number(employeeId),
{
task_id: taskId,
employee_id: employeeId,
checked: assignments[taskId]?.[employeeId]?.checked || false, checked: assignments[taskId]?.[employeeId]?.checked || false,
note: note || null, note: note || null,
}, };
{
onConflict: 'task_id,employee_id',
ignoreDuplicates: false,
}
);
if (error) { const checkOrUncheckAssignmentRes =
console.error('Error updating note:', error); await DailyChecklistApi.checkOrUncheckAssignment(payload);
if (isResponseError(checkOrUncheckAssignmentRes)) {
console.error(
'[CHECKBOX] Database error:',
checkOrUncheckAssignmentRes.message
);
toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message);
return; return;
} }
@@ -771,7 +714,7 @@ export function DailyChecklistContent() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!dailyChecklistId || !isSupabaseConfigured()) return; if (!dailyChecklistId) return;
if (selectedEmployees.length === 0) { if (selectedEmployees.length === 0) {
toast.error('Pilih minimal 1 ABK'); toast.error('Pilih minimal 1 ABK');
@@ -786,17 +729,10 @@ export function DailyChecklistContent() {
setLoading(true); setLoading(true);
try { try {
// Update status to SUBMITTED const submitRes = await DailyChecklistApi.submit(dailyChecklistId);
const { error } = await supabase
.from('daily_checklists')
.update({
status: 'SUBMITTED',
updated_at: new Date().toISOString(),
})
.eq('id', dailyChecklistId);
if (error) { if (isResponseError(submitRes)) {
console.error('Error submitting:', error); console.error('Error submitting:', submitRes.message);
toast.error('Gagal submit checklist'); toast.error('Gagal submit checklist');
return; return;
} }
@@ -841,13 +777,13 @@ export function DailyChecklistContent() {
[phaseId: string]: { [phaseId: string]: {
phase: Phase; phase: Phase;
timeGroups: { timeGroups: {
[timeType: string]: Activity[]; [timeType: string]: PhaseActivity[];
}; };
}; };
} = {}; } = {};
const selectedPhasesData = allPhases.filter((p) => const selectedPhasesData = allPhases.filter((p) =>
selectedPhaseIds.includes(p.id) selectedPhaseIds.includes(String(p.id))
); );
selectedPhasesData.forEach((phase) => { selectedPhasesData.forEach((phase) => {
@@ -900,7 +836,7 @@ export function DailyChecklistContent() {
} }
return ( return (
<div className='min-h-screen bg-[#F9FAFB]'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
{/* Page Title */} {/* Page Title */}
<div className='mb-6'> <div className='mb-6'>
@@ -967,9 +903,12 @@ export function DailyChecklistContent() {
<SelectValue placeholder='Pilih kandang' /> <SelectValue placeholder='Pilih kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{kandangList.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}> <SelectItem
{kandang.name} key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -1022,7 +961,7 @@ export function DailyChecklistContent() {
{selectedPhaseIds.length > 0 ? ( {selectedPhaseIds.length > 0 ? (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
{allPhases {allPhases
.filter((p) => selectedPhaseIds.includes(p.id)) .filter((p) => selectedPhaseIds.includes(String(p.id)))
.map((phase) => ( .map((phase) => (
<Badge <Badge
key={phase.id} key={phase.id}
@@ -1069,7 +1008,7 @@ export function DailyChecklistContent() {
{emp.name} {emp.name}
{isEditable && ( {isEditable && (
<button <button
onClick={() => handleRemoveAbk(emp.id)} onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900' className='ml-2 hover:text-gray-900'
> >
<X className='w-3 h-3' /> <X className='w-3 h-3' />
@@ -1226,8 +1165,8 @@ export function DailyChecklistContent() {
} }
onChange={(e) => onChange={(e) =>
handleCheckboxChange( handleCheckboxChange(
activity.id, String(activity.id),
emp.id, String(emp.id),
e.target.checked e.target.checked
) )
} }
@@ -1237,7 +1176,11 @@ export function DailyChecklistContent() {
</td> </td>
))} ))}
<td className='py-3 px-4'> <td className='py-3 px-4'>
<Textarea <DebouncedTextArea
delay={500}
name='notes'
rows={1}
placeholder='Catatan (opsional)'
value={ value={
taskId && selectedEmployees.length > 0 taskId && selectedEmployees.length > 0
? assignments[taskId]?.[ ? assignments[taskId]?.[
@@ -1248,16 +1191,13 @@ export function DailyChecklistContent() {
onChange={(e) => { onChange={(e) => {
if (selectedEmployees.length > 0) { if (selectedEmployees.length > 0) {
handleNoteChange( handleNoteChange(
activity.id, String(activity.id),
selectedEmployees[0].id, String(selectedEmployees[0].id),
e.target.value e.target.value
); );
} }
}} }}
placeholder='Catatan (opsional)'
disabled={!isEditable} disabled={!isEditable}
className='text-sm min-h-[36px] resize-none'
rows={1}
/> />
</td> </td>
</tr> </tr>
@@ -1401,7 +1341,9 @@ export function DailyChecklistContent() {
{filteredPhases.length > 0 ? ( {filteredPhases.length > 0 ? (
<div className='space-y-1.5'> <div className='space-y-1.5'>
{filteredPhases.map((phase) => { {filteredPhases.map((phase) => {
const isChecked = tempSelectedPhaseIds.includes(phase.id); const isChecked = tempSelectedPhaseIds.includes(
String(phase.id)
);
return ( return (
<label <label
@@ -1415,7 +1357,7 @@ export function DailyChecklistContent() {
<input <input
type='checkbox' type='checkbox'
checked={isChecked} checked={isChecked}
onChange={() => toggleTempPhase(phase.id)} onChange={() => toggleTempPhase(String(phase.id))}
className='checkbox-clean mt-0.5' className='checkbox-clean mt-0.5'
/> />
<div className='flex-1 min-w-0'> <div className='flex-1 min-w-0'>
@@ -1507,9 +1449,22 @@ export function DailyChecklistContent() {
const isChecked = tempSelectedEmployees.find( const isChecked = tempSelectedEmployees.find(
(e) => e.id === emp.id (e) => e.id === emp.id
); );
const kandang = kandangList.find( // const kandang = kandangOptions.find((k) => {
(k) => k.id === emp.kandang_id // 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 ( return (
<label <label
@@ -1532,7 +1487,9 @@ export function DailyChecklistContent() {
</p> </p>
{kandang && ( {kandang && (
<p className='text-xs text-gray-500 mt-0.5'> <p className='text-xs text-gray-500 mt-0.5'>
{kandang.name} <span
dangerouslySetInnerHTML={{ __html: kandang }}
/>
</p> </p>
)} )}
</div> </div>
@@ -1,11 +1,10 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react'; import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { Input } from '@/figma-make/components/base/input';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
import { Textarea } from '@/figma-make/components/base/textarea'; import { Textarea } from '@/figma-make/components/base/textarea';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
@@ -25,40 +24,24 @@ import {
DialogFooter, DialogFooter,
} from '@/figma-make/components/base/dialog'; } from '@/figma-make/components/base/dialog';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import useSWR from 'swr';
interface ChecklistItem { import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
checklist_id: string; import { useTableFilter } from '@/services/hooks/useTableFilter';
date: string; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
kandang_name: string; import Table from '@/components/Table';
kandang_id: string; // ✅ Add kandang_id import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
category: string; import { cn } from '@/lib/helper';
status: string; import { ColumnDef } from '@tanstack/react-table';
progress_percent: number; import { useSelect } from '@/components/input/SelectInput';
total_phases: number; import { KandangApi } from '@/services/api/master-data';
total_activities: number; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
updated_at: string;
}
interface Kandang { interface Kandang {
id: string; id: string;
name: string; name: string;
} }
interface ChecklistQueryResult {
id: string;
date: string;
kandang_id: string;
category: string;
status: string;
updated_at: string;
kandang: {
id: string;
name: string;
} | null;
}
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' }, { value: 'ALL', label: 'Semua Status' },
{ value: 'DRAFT', label: 'Draft' }, { value: 'DRAFT', label: 'Draft' },
@@ -76,244 +59,80 @@ const CATEGORY_LABELS: { [key: string]: string } = {
export function ListDailyChecklistContent() { export function ListDailyChecklistContent() {
const router = useRouter(); const router = useRouter();
const [checklistList, setChecklistList] = useState<ChecklistItem[]>([]);
const [filteredList, setFilteredList] = useState<ChecklistItem[]>([]);
const [loading, setLoading] = useState(true);
// Master data const {
const [kandangList, setKandangList] = useState<Kandang[]>([]); state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
date_from: '',
date_to: '',
search: '',
kandang_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
kandang_id: 'kandang_id',
status: 'status',
date_from: 'date_from',
date_to: 'date_to',
},
});
// Filters const {
const [statusFilter, setStatusFilter] = useState('ALL'); data: checklistListRes,
const [kandangFilter, setKandangFilter] = useState('ALL'); isLoading: isLoadingChecklistList,
const [searchText, setSearchText] = useState(''); mutate: refreshChecklistList,
const [dateFrom, setDateFrom] = useState(''); } = useSWR(
const [dateTo, setDateTo] = useState(''); `${DailyChecklistApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || []
: [];
// Modals // Modals
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedItem, setSelectedItem] = useState<ChecklistItem | null>(null); const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
useEffect(() => { const handleDetail = (item: DailyChecklist) => {
fetchKandangList();
fetchChecklistList();
}, []);
useEffect(() => {
applyFilters();
}, [
checklistList,
statusFilter,
kandangFilter,
searchText,
dateFrom,
dateTo,
]);
const fetchKandangList = async () => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (error) {
console.error('Error fetching kandang:', error);
return;
}
setKandangList(data || []);
} catch (error) {
console.error('Error fetching kandang:', error);
}
};
const fetchChecklistList = async () => {
if (!isSupabaseConfigured()) {
console.warn('Supabase not configured');
setLoading(false);
return;
}
try {
setLoading(true);
// ✅ Fetch checklists with joins to get complete data
const { data: checklists, error } = await supabase
.from('daily_checklists')
.select(
`
id,
date,
kandang_id,
category,
status,
updated_at,
kandang:kandang_id (
id,
name
)
`
)
.order('date', { ascending: false })
.order('updated_at', { ascending: false });
if (error) {
console.error('Error fetching checklist list:', error);
toast.error('Gagal memuat data checklist');
return;
}
// ✅ For each checklist, fetch phases, activities, and assignments count
const enrichedData: ChecklistItem[] = await Promise.all(
((checklists as unknown as ChecklistQueryResult[]) || [])
.filter((checklist) => checklist.id) // ✅ Skip checklists with null ID
.map(async (checklist) => {
// Count phases
const { count: phaseCount } = await supabase
.from('daily_checklist_phases')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// Count activities (tasks)
const { count: activityCount } = await supabase
.from('daily_checklist_activity_tasks')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// ✅ NEW LOGIC: Calculate progress based on phase coverage
// Step 1: Get total phases in master data for this category
const { count: totalPhasesInMaster } = await supabase
.from('phases')
.select('*', { count: 'exact', head: true })
.eq('category_id', checklist.category);
// Step 2: Get phases that have at least 1 CHECKED assignment
// First, get all tasks for this checklist
const { data: tasks } = await supabase
.from('daily_checklist_activity_tasks')
.select('id, phase_id')
.eq('checklist_id', checklist.id);
const taskIds = (tasks || []).map((t) => t.id);
const uniquePhasesWithChecked = new Set<string>();
if (taskIds.length > 0) {
// Get assignments that are CHECKED
const { data: checkedAssignments } = await supabase
.from('daily_checklist_activity_task_assignments')
.select('task_id')
.in('task_id', taskIds)
.eq('checked', true); // ✅ Only get checked assignments
if (checkedAssignments && checkedAssignments.length > 0) {
// Map task_ids back to phase_ids
const checkedTaskIds = new Set(
checkedAssignments.map((a) => a.task_id)
);
tasks?.forEach((task) => {
if (checkedTaskIds.has(task.id)) {
uniquePhasesWithChecked.add(task.phase_id);
}
});
}
}
const phasesWithCheckedCount = uniquePhasesWithChecked.size;
// Step 3: Calculate progress
const progressPercent =
totalPhasesInMaster && totalPhasesInMaster > 0
? Math.round(
(phasesWithCheckedCount / totalPhasesInMaster) * 100
)
: 0;
return {
checklist_id: checklist.id,
date: checklist.date,
kandang_name: checklist.kandang?.name || '-',
kandang_id: checklist.kandang_id,
category: checklist.category,
status: checklist.status,
progress_percent: progressPercent,
total_phases: phaseCount || 0,
total_activities: activityCount || 0,
updated_at: checklist.updated_at,
};
})
);
setChecklistList(enrichedData);
} catch (error) {
console.error('Error fetching checklist list:', error);
toast.error('Terjadi kesalahan');
} finally {
setLoading(false);
}
};
const applyFilters = () => {
let filtered = [...checklistList];
// Filter by status
if (statusFilter && statusFilter !== 'ALL') {
filtered = filtered.filter((item) => item.status === statusFilter);
}
// ✅ Filter by kandang - use kandang_id directly from item
if (kandangFilter && kandangFilter !== 'ALL') {
filtered = filtered.filter((item) => item.kandang_id === kandangFilter);
}
// Filter by search text (kandang_name or category)
if (searchText) {
const searchLower = searchText.toLowerCase();
filtered = filtered.filter(
(item) =>
item.kandang_name.toLowerCase().includes(searchLower) ||
item.category.toLowerCase().includes(searchLower) ||
(CATEGORY_LABELS[item.category] || '')
.toLowerCase()
.includes(searchLower)
);
}
// Filter by date range
if (dateFrom) {
filtered = filtered.filter((item) => item.date >= dateFrom);
}
if (dateTo) {
filtered = filtered.filter((item) => item.date <= dateTo);
}
setFilteredList(filtered);
};
const handleDetail = (item: ChecklistItem) => {
router.push( router.push(
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.checklist_id}` `/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
); );
}; };
const handleApprove = (item: ChecklistItem) => { const handleApprove = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setShowApproveModal(true); setShowApproveModal(true);
}; };
const handleReject = (item: ChecklistItem) => { const handleReject = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setRejectReason(''); setRejectReason('');
setShowRejectModal(true); setShowRejectModal(true);
}; };
const handleDelete = (item: ChecklistItem) => { const handleDelete = (item: DailyChecklist) => {
// ✅ VALIDATION: Only DRAFT can be deleted // ✅ VALIDATION: Only DRAFT can be deleted
if (item.status !== 'DRAFT') { if (item.status !== 'DRAFT') {
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', { toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
@@ -327,29 +146,24 @@ export function ListDailyChecklistContent() {
}; };
const confirmApprove = async () => { const confirmApprove = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const approveRes = await DailyChecklistApi.approve(
.from('daily_checklists') String(selectedItem.id)
.update({ );
status: 'APPROVED',
updated_at: new Date().toISOString(),
})
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(approveRes)) {
console.error('Error approving checklist:', error); toast.error('Gagal approve checklist: ' + approveRes.message);
toast.error('Gagal approve checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil di-approve'); toast.success('Checklist berhasil di-approve');
setShowApproveModal(false); setShowApproveModal(false);
setSelectedItem(null); setSelectedItem(null);
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error approving checklist:', error); console.error('Error approving checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -359,7 +173,7 @@ export function ListDailyChecklistContent() {
}; };
const confirmReject = async () => { const confirmReject = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
if (!rejectReason.trim()) { if (!rejectReason.trim()) {
toast.error('Alasan reject harus diisi'); toast.error('Alasan reject harus diisi');
@@ -369,26 +183,21 @@ export function ListDailyChecklistContent() {
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const rejectRes = await DailyChecklistApi.reject(
.from('daily_checklists') String(selectedItem.id),
.update({ rejectReason
status: 'REJECTED', );
reject_reason: rejectReason,
updated_at: new Date().toISOString(),
})
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(rejectRes)) {
console.error('Error rejecting checklist:', error); toast.error('Gagal reject checklist: ' + rejectRes.message);
toast.error('Gagal reject checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil di-reject'); toast.success('Checklist berhasil di-reject');
setShowRejectModal(false); setShowRejectModal(false);
setSelectedItem(null); setSelectedItem(null);
setRejectReason(''); setRejectReason('');
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error rejecting checklist:', error); console.error('Error rejecting checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -398,26 +207,22 @@ export function ListDailyChecklistContent() {
}; };
const confirmDelete = async () => { const confirmDelete = async () => {
if (!selectedItem || !isSupabaseConfigured()) return; if (!selectedItem) return;
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
.from('daily_checklists')
.delete()
.eq('id', selectedItem.checklist_id);
if (error) { if (isResponseError(deleteRes)) {
console.error('Error deleting checklist:', error); toast.error('Gagal hapus checklist: ' + deleteRes.message);
toast.error('Gagal hapus checklist');
return; return;
} }
refreshChecklistList();
toast.success('Checklist berhasil dihapus'); toast.success('Checklist berhasil dihapus');
setShowDeleteModal(false); setShowDeleteModal(false);
setSelectedItem(null); setSelectedItem(null);
await fetchChecklistList();
} catch (error) { } catch (error) {
console.error('Error deleting checklist:', error); console.error('Error deleting checklist:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -496,6 +301,117 @@ export function ListDailyChecklistContent() {
}); });
}; };
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
{
accessorKey: 'date',
header: 'Tanggal',
enableSorting: false,
cell: ({ row }) => formatDate(row.original.date),
},
{
accessorKey: 'kandang',
header: 'Kandang',
enableSorting: false,
cell: ({ row }) => row.original.kandang.name,
},
{
accessorKey: 'category',
header: 'Kategori',
enableSorting: false,
cell: ({ row }) =>
CATEGORY_LABELS[row.original.category] || row.original.category,
},
{
accessorKey: 'status',
header: 'Status',
enableSorting: false,
cell: ({ row }) => getStatusBadge(row.original.status),
},
{
accessorKey: 'total_phase',
header: 'Total Phase',
enableSorting: false,
},
{
accessorKey: 'total_activity',
header: 'Total Aktivitas',
enableSorting: false,
},
{
accessorKey: 'progress',
header: 'Progress',
enableSorting: false,
cell: ({ row }) => (
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='bg-[#0069e0] h-2 rounded-full transition-all'
style={{ width: `${row.original.progress}%` }}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{row.original.progress}%
</span>
</div>
),
},
{
accessorKey: 'updated_at',
header: 'Update At',
enableSorting: false,
cell: ({ row }) => formatDateTime(row.original.updated_at),
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<div className='flex items-center justify-center gap-2'>
<Button
size='sm'
variant='outline'
onClick={() => handleDetail(row.original)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
{row.original.status === 'SUBMITTED' && (
<>
<Button
size='sm'
onClick={() => handleApprove(row.original)}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-1' />
Approve
</Button>
<Button
size='sm'
variant='destructive'
onClick={() => handleReject(row.original)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-1' />
Reject
</Button>
</>
)}
<Button
size='sm'
variant='destructive'
onClick={() => handleDelete(row.original)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
</div>
),
},
];
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
@@ -518,11 +434,11 @@ export function ListDailyChecklistContent() {
<Label>Periode Tanggal</Label> <Label>Periode Tanggal</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<DateRangePicker <DateRangePicker
dateFrom={dateFrom} dateFrom={tableFilterState.date_from}
dateTo={dateTo} dateTo={tableFilterState.date_to}
onDateChange={(from, to) => { onDateChange={(from, to) => {
setDateFrom(from); updateFilter('date_from', from);
setDateTo(to); updateFilter('date_to', to);
}} }}
/> />
</div> </div>
@@ -532,8 +448,10 @@ export function ListDailyChecklistContent() {
<Label htmlFor='kandang-filter'>Kandang</Label> <Label htmlFor='kandang-filter'>Kandang</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<Select <Select
value={kandangFilter} value={tableFilterState.kandang_id}
onValueChange={setKandangFilter} onValueChange={(value) => {
updateFilter('kandang_id', value === 'ALL' ? '' : value);
}}
> >
<SelectTrigger <SelectTrigger
id='kandang-filter' id='kandang-filter'
@@ -543,9 +461,12 @@ export function ListDailyChecklistContent() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}> <SelectItem
{kandang.name} key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -556,7 +477,12 @@ export function ListDailyChecklistContent() {
<div> <div>
<Label htmlFor='status-filter'>Status</Label> <Label htmlFor='status-filter'>Status</Label>
<div className='mt-1.5'> <div className='mt-1.5'>
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select
value={tableFilterState.status}
onValueChange={(value) => {
updateFilter('status', value === 'ALL' ? '' : value);
}}
>
<SelectTrigger <SelectTrigger
id='status-filter' id='status-filter'
className='border-gray-200' className='border-gray-200'
@@ -577,159 +503,57 @@ export function ListDailyChecklistContent() {
<div> <div>
<Label htmlFor='search-text'>Cari</Label> <Label htmlFor='search-text'>Cari</Label>
<div className='relative mt-1.5'> <div className='relative mt-1.5'>
<Input <DebouncedTextInput
id='search-text' name='search'
type='text' placeholder='Kandang / Kategori'
placeholder='Kandang / Kategori...' value={tableFilterState.search}
value={searchText} onChange={(e) => updateFilter('search', e.target.value)}
onChange={(e) => setSearchText(e.target.value)} className={{
className='border-gray-200 pl-9' wrapper: 'w-full border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/> />
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
</div> </div>
</div> </div>
</div> </div>
{/* Table Section */} {/* Table Section */}
{loading ? ( <Table<DailyChecklist>
<div className='text-center py-12 text-gray-500'> data={checklistList}
Memuat data... columns={checklistListColumns}
</div> pageSize={tableFilterState.pageSize}
) : filteredList.length > 0 ? ( onPageSizeChange={setPageSize}
<div className='overflow-x-auto'> rowOptions={[10, 20, 50, 100]}
<table className='w-full border border-gray-200 rounded-lg'> page={
<thead> isResponseSuccess(checklistListRes)
<tr className='bg-gray-50 border-b border-gray-200'> ? checklistListRes?.meta?.page
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'> : 0
Tanggal
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kategori
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Status
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Phase
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Progress
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Updated At
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aksi
</th>
</tr>
</thead>
<tbody>
{filteredList.map((item, index) => (
<tr
key={`${item.checklist_id}-${index}`}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
} }
> totalItems={
<td className='py-3 px-4 text-sm text-gray-900'> isResponseSuccess(checklistListRes)
{formatDate(item.date)} ? checklistListRes?.meta?.total_results
</td> : 0
<td className='py-3 px-4 text-sm text-gray-900'> }
{item.kandang_name} onPageChange={setPage}
</td> isLoading={isLoadingChecklistList}
<td className='py-3 px-4 text-sm text-gray-900'> className={{
{CATEGORY_LABELS[item.category] || item.category} containerClassName: cn({
</td> 'w-full mb-20':
<td className='py-3 px-4'> isResponseSuccess(checklistListRes) &&
{getStatusBadge(item.status)} checklistListRes?.data?.length === 0,
</td> }),
<td className='py-3 px-4 text-center text-sm text-gray-900'> tableWrapperClassName:
{item.total_phases} 'overflow-x-auto border border-solid border-base-content/10 rounded-none',
</td> headerRowClassName: 'bg-gray-50/50',
<td className='py-3 px-4 text-center text-sm text-gray-900'> headerColumnClassName:
{item.total_activities} 'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
</td> paginationClassName: 'px-4',
<td className='py-3 px-4 text-center'> }}
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='bg-[#0069e0] h-2 rounded-full transition-all'
style={{ width: `${item.progress_percent}%` }}
/> />
</div>
<span className='text-sm text-gray-700 font-medium'>
{item.progress_percent}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDateTime(item.updated_at)}
</td>
<td className='py-3 px-4'>
<div className='flex items-center justify-center gap-2'>
<Button
size='sm'
variant='outline'
onClick={() => handleDetail(item)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
{item.status === 'SUBMITTED' && (
<>
<Button
size='sm'
onClick={() => handleApprove(item)}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-1' />
Approve
</Button>
<Button
size='sm'
variant='destructive'
onClick={() => handleReject(item)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-1' />
Reject
</Button>
</>
)}
<Button
size='sm'
variant='destructive'
onClick={() => handleDelete(item)}
className='bg-red-600 hover:bg-red-700 text-white'
>
<Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className='text-center py-12 text-gray-500'>
{searchText ||
dateFrom ||
dateTo ||
statusFilter !== 'ALL' ||
kandangFilter !== 'ALL'
? 'Tidak ada data yang sesuai dengan filter'
: 'Belum ada data checklist'}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -755,7 +579,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
@@ -768,7 +592,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Progress:</span> <span className='text-gray-600'>Progress:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.progress_percent}% {selectedItem.progress}%
</span> </span>
</div> </div>
</div> </div>
@@ -815,7 +639,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
@@ -888,7 +712,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span> <span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'> <span className='font-medium text-gray-900'>
{selectedItem.kandang_name} {selectedItem.kandang.name}
</span> </span>
</div> </div>
<div className='flex justify-between text-sm'> <div className='flex justify-between text-sm'>
@@ -17,8 +17,9 @@ import {
DialogFooter, DialogFooter,
} from '@/figma-make/components/base/dialog'; } from '@/figma-make/components/base/dialog';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError } from '@/lib/api-helper';
interface ChecklistDetailRow { interface ChecklistDetailRow {
checklist_id: string; checklist_id: string;
@@ -85,23 +86,6 @@ interface ChecklistData {
}; };
} }
interface TaskQueryResult {
id: number;
phase_id: string;
phase_activity_id: string;
time_type: string;
notes: string | null;
phases: {
id: number;
name: string;
} | null;
phase_activities: {
id: number;
name: string;
description: string | null;
} | null;
}
interface AssignmentQueryResult { interface AssignmentQueryResult {
task_id: number; task_id: number;
employee_id: string; employee_id: string;
@@ -120,13 +104,13 @@ const CATEGORY_LABELS: { [key: string]: string } = {
produksi_close: 'Produksi Close', produksi_close: 'Produksi Close',
}; };
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam']; const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = { const TIME_TYPE_LABELS: { [key: string]: string } = {
umum: 'Umum', Umum: 'Umum',
pagi: 'Pagi', Pagi: 'Pagi',
siang: 'Siang', Siang: 'Siang',
sore: 'Sore', Sore: 'Sore',
malam: 'Malam', Malam: 'Malam',
}; };
export function DetailDailyChecklistContent() { export function DetailDailyChecklistContent() {
@@ -155,8 +139,8 @@ export function DetailDailyChecklistContent() {
}, [checklistId]); }, [checklistId]);
const fetchChecklistDetail = async () => { const fetchChecklistDetail = async () => {
if (!isSupabaseConfigured() || !checklistId) { if (!checklistId) {
console.warn('Supabase not configured or checklistId missing'); console.warn('checklistId missing');
setLoading(false); setLoading(false);
return; return;
} }
@@ -164,69 +148,34 @@ export function DetailDailyChecklistContent() {
try { try {
setLoading(true); setLoading(true);
// ✅ Fetch checklist header with kandang name const checklistDataRes =
const { data: checklistData, error: checklistError } = await supabase await DailyChecklistApi.getOneDailyChecklist(checklistId);
.from('daily_checklists')
.select(
`
id,
date,
kandang_id,
category,
status,
reject_reason,
kandang:kandang_id (
id,
name
)
`
)
.eq('id', checklistId)
.single();
if (checklistError || !checklistData) { if (isResponseError(checklistDataRes)) {
console.error('Error fetching checklist:', checklistError); console.error('Error fetching checklist:', checklistDataRes.message);
toast.error('Data checklist tidak ditemukan'); toast.error('Data checklist tidak ditemukan');
router.push('/daily-checklist/list-daily-checklist'); router.push('/daily-checklist/list-daily-checklist');
return; return;
} }
// ✅ Fetch all tasks with their phase_activity details const rawDetailChecklist = checklistDataRes?.data;
const { data: tasks, error: tasksError } = await supabase
.from('daily_checklist_activity_tasks')
.select(
`
id,
phase_id,
phase_activity_id,
time_type,
notes,
phases:phase_id (
id,
name
),
phase_activities:phase_activity_id (
id,
name,
description
)
`
)
.eq('checklist_id', checklistId);
if (tasksError) { const checklistData = {
console.error('Error fetching tasks:', tasksError); id: rawDetailChecklist?.id,
toast.error('Gagal memuat detail checklist'); date: rawDetailChecklist?.date,
router.push('/daily-checklist/list-daily-checklist'); kandang_id: rawDetailChecklist?.kandang.id,
return; category: rawDetailChecklist?.category,
} status: rawDetailChecklist?.status,
reject_reason: rawDetailChecklist?.reject_reason,
kandang: rawDetailChecklist?.kandang,
};
const castedTasks = (tasks as unknown as TaskQueryResult[]) || []; const tasks = rawDetailChecklist?.tasks;
const castedChecklistData = const castedChecklistData =
checklistData as unknown as ChecklistData | null; checklistData as unknown as ChecklistData | null;
if (!castedTasks || castedTasks.length === 0) { if (!tasks || tasks.length === 0) {
toast.info('Checklist belum memiliki aktivitas'); toast.info('Checklist belum memiliki aktivitas');
setHeader({ setHeader({
date: castedChecklistData?.date || '-', date: castedChecklistData?.date || '-',
@@ -242,36 +191,32 @@ export function DetailDailyChecklistContent() {
return; return;
} }
// ✅ Fetch all assignments for these tasks const assignments: {
const taskIds = castedTasks.map((t) => t.id); task_id: number;
const { data: assignments, error: assignmentsError } = await supabase checked: boolean;
.from('daily_checklist_activity_task_assignments') note: string | null;
.select( employee: {
` id: number;
task_id, name: string;
employee_id, };
checked, }[] = [];
note,
employees:employee_id (
id,
name
)
`
)
.in('task_id', taskIds);
if (assignmentsError) { tasks.forEach((task) => {
console.error('Error fetching assignments:', assignmentsError); task.assignments.forEach((assignment) => {
} assignments.push({
task_id: task.id,
const castedAssignments = checked: assignment.checked,
(assignments as unknown as AssignmentQueryResult[]) || []; note: assignment.note,
employee: assignment.employee,
});
});
});
// ✅ Build detail rows from tasks and assignments // ✅ Build detail rows from tasks and assignments
const detailRows: ChecklistDetailRow[] = []; const detailRows: ChecklistDetailRow[] = [];
castedTasks.forEach((task) => { tasks.forEach((task) => {
const taskAssignments = (castedAssignments || []).filter( const taskAssignments = assignments.filter(
(a) => a.task_id === task.id (a) => a.task_id === task.id
); );
@@ -283,14 +228,14 @@ export function DetailDailyChecklistContent() {
category: castedChecklistData?.category || '-', category: castedChecklistData?.category || '-',
status: castedChecklistData?.status || '-', status: castedChecklistData?.status || '-',
reject_reason: castedChecklistData?.reject_reason || '-', reject_reason: castedChecklistData?.reject_reason || '-',
phase_id: task.phase_id, phase_id: String(task.phase_id),
phase_name: task.phases?.name || '-', phase_name: task.phase?.name || '-',
activity_id: task.phase_activity_id, activity_id: String(task.phase_activity_id),
activity_name: task.phase_activities?.name || '-', activity_name: task.phase_activity?.name || '-',
activity_description: task.phase_activities?.description || null, activity_description: task.phase_activity?.description || null,
time_type: task.time_type, time_type: task.time_type,
employee_id: assignment.employee_id, employee_id: String(assignment.employee.id),
employee_name: assignment.employees?.name || '-', employee_name: assignment.employee?.name || '-',
checked: assignment.checked, checked: assignment.checked,
note: assignment.note, note: assignment.note,
}); });
@@ -306,8 +251,8 @@ export function DetailDailyChecklistContent() {
status: castedChecklistData?.status || '-', status: castedChecklistData?.status || '-',
reject_reason: castedChecklistData?.reject_reason || '-', reject_reason: castedChecklistData?.reject_reason || '-',
progress_percent: 0, progress_percent: 0,
total_phases: new Set(castedTasks.map((t) => t.phase_id)).size, total_phases: new Set(tasks.map((t) => t.phase_id)).size,
total_activities: castedTasks.length, total_activities: tasks.length,
}); });
setLoading(false); setLoading(false);
return; return;
@@ -471,22 +416,15 @@ export function DetailDailyChecklistContent() {
}; };
const confirmApprove = async () => { const confirmApprove = async () => {
if (!checklistId || !isSupabaseConfigured()) return; if (!checklistId) return;
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const approveRes = await DailyChecklistApi.approve(String(checklistId));
.from('daily_checklists')
.update({
status: 'APPROVED',
updated_at: new Date().toISOString(),
})
.eq('id', checklistId);
if (error) { if (isResponseError(approveRes)) {
console.error('Error approving checklist:', error); toast.error('Gagal approve checklist: ' + approveRes.message);
toast.error('Gagal approve checklist');
return; return;
} }
@@ -502,7 +440,7 @@ export function DetailDailyChecklistContent() {
}; };
const confirmReject = async () => { const confirmReject = async () => {
if (!checklistId || !isSupabaseConfigured()) return; if (!checklistId) return;
if (!rejectReason.trim()) { if (!rejectReason.trim()) {
toast.error('Alasan reject harus diisi'); toast.error('Alasan reject harus diisi');
@@ -512,18 +450,13 @@ export function DetailDailyChecklistContent() {
try { try {
setActionLoading(true); setActionLoading(true);
const { error } = await supabase const rejectRes = await DailyChecklistApi.reject(
.from('daily_checklists') String(checklistId),
.update({ rejectReason
status: 'REJECTED', );
reject_reason: rejectReason,
updated_at: new Date().toISOString(),
})
.eq('id', checklistId);
if (error) { if (isResponseError(rejectRes)) {
console.error('Error rejecting checklist:', error); toast.error('Gagal reject checklist: ' + rejectRes.message);
toast.error('Gagal reject checklist');
return; return;
} }
@@ -47,6 +47,7 @@ import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity'; import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity';
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity'; import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import { Phase } from '@/types/api/daily-checklist/phase';
// Static categories - tidak bisa CRUD // Static categories - tidak bisa CRUD
const CATEGORIES = [ const CATEGORIES = [
@@ -64,13 +65,6 @@ const TIME_TYPES = [
{ value: 'Malam', label: 'Malam' }, { value: 'Malam', label: 'Malam' },
]; ];
interface Phase {
id: string;
name: string;
category: string;
activityCount?: number;
}
export function MasterAktivitasContent() { export function MasterAktivitasContent() {
const [selectedCategory, setSelectedCategory] = useState<string>(''); const [selectedCategory, setSelectedCategory] = useState<string>('');
const [selectedPhase, setSelectedPhase] = useState<Phase | null>(null); const [selectedPhase, setSelectedPhase] = useState<Phase | null>(null);
@@ -103,7 +97,7 @@ export function MasterAktivitasContent() {
SWRHttpKey SWRHttpKey
>( >(
selectedPhase?.id selectedPhase?.id
? `${PhaseActivityApi.basePath}?page=1&limit=100&phase_id=${selectedPhase.id}` ? `${PhaseActivityApi.basePath}?page=1&limit=100&phase_ids=${selectedPhase.id}`
: '', : '',
httpClientFetcher, httpClientFetcher,
{ {
@@ -171,7 +165,7 @@ export function MasterAktivitasContent() {
const handleEditPhase = (phase: Phase) => { const handleEditPhase = (phase: Phase) => {
setPhaseModalMode('edit'); setPhaseModalMode('edit');
setPhaseForm({ setPhaseForm({
id: phase.id, id: String(phase.id),
name: phase.name, name: phase.name,
}); });
setShowPhaseModal(true); setShowPhaseModal(true);
@@ -265,7 +259,7 @@ export function MasterAktivitasContent() {
setPhaseToDelete(null); setPhaseToDelete(null);
// Clear selection if deleted phase was selected // Clear selection if deleted phase was selected
if (selectedPhase?.id === phaseToDelete) { if (selectedPhase?.id === Number(phaseToDelete)) {
setSelectedPhase(null); setSelectedPhase(null);
} }
} catch (error) { } catch (error) {
@@ -543,7 +537,7 @@ export function MasterAktivitasContent() {
{phase.name} {phase.name}
</p> </p>
<p className='text-xs text-gray-500 mt-1'> <p className='text-xs text-gray-500 mt-1'>
{phase.activityCount || 0} aktivitas {phase.activity_count || 0} aktivitas
</p> </p>
</div> </div>
<DropdownMenu> <DropdownMenu>
@@ -567,7 +561,9 @@ export function MasterAktivitasContent() {
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeletePhaseClick(phase.id)} onClick={() =>
handleDeletePhaseClick(String(phase.id))
}
className='text-red-600' className='text-red-600'
> >
<Trash2 className='mr-2 h-4 w-4' /> <Trash2 className='mr-2 h-4 w-4' />
@@ -0,0 +1,200 @@
import axios from 'axios';
import { BaseApiService } from '@/services/api/base';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import {
CreateDailyChecklistPayload,
DailyChecklist,
DetailDailyChecklist,
} from '@/types/api/daily-checklist/daily-checklist';
export class DailyChecklistApiService extends BaseApiService<
DailyChecklist,
CreateDailyChecklistPayload,
unknown
> {
constructor(basePath: string = '/daily-checklists') {
super(basePath);
}
async getOneDailyChecklist(id: string) {
try {
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
const getOneDailyChecklistRes = await httpClient<
BaseApiResponse<DetailDailyChecklist>
>(getOneDailyChecklistPath);
return getOneDailyChecklistRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse<DetailDailyChecklist>>(error)) {
return error.response?.data;
}
return undefined;
}
}
async setDailyChecklistPhase(id: string, phaseIds: string[]) {
try {
const setDailyChecklistPhasePath = `${this.basePath}/phase/${id}`;
const setDailyChecklistPhaseRes = await httpClient<BaseApiResponse>(
setDailyChecklistPhasePath,
{
method: 'POST',
body: { phase_ids: phaseIds.join(',') },
}
);
return setDailyChecklistPhaseRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async removeEmployeeAssignment(id: string, employeeId: string) {
try {
const removeEmployeeAssignmentPath = `${this.basePath}/${id}/assignments/${employeeId}`;
const removeEmployeeAssignmentRes = await httpClient<BaseApiResponse>(
removeEmployeeAssignmentPath,
{
method: 'DELETE',
}
);
return removeEmployeeAssignmentRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async setDailyChecklistEmployees(checklistId: string, employeeIds: string[]) {
try {
const setDailyChecklistPhasePath = `${this.basePath}/assignment/${checklistId}`;
const setDailyChecklistPhaseRes = await httpClient<BaseApiResponse>(
setDailyChecklistPhasePath,
{
method: 'POST',
body: { employee_ids: employeeIds.join(',') },
}
);
return setDailyChecklistPhaseRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async getTasksByChecklistId(id: string) {
try {
const getTasksByChecklistIdPath = `${this.basePath}/tasks?checklist_id=${id}&page=1&limit=100`;
const getTasksByChecklistIdRes = await httpClient<BaseApiResponse>(
getTasksByChecklistIdPath
);
return getTasksByChecklistIdRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async checkOrUncheckAssignment(payload: {
task_id: number;
employee_id: number;
checked: boolean;
note: string | null;
}) {
try {
const checkOrUncheckAssignmentPath = `${this.basePath}/assignment`;
const checkOrUncheckAssignmentRes = await httpClient<BaseApiResponse>(
checkOrUncheckAssignmentPath,
{
method: 'POST',
body: payload,
}
);
return checkOrUncheckAssignmentRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async submit(id: string) {
try {
const submitPath = `${this.basePath}/${id}`;
const submitRes = await httpClient<BaseApiResponse>(submitPath, {
method: 'PATCH',
body: {
status: 'SUBMITTED',
reject_reason: '',
},
});
return submitRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async approve(id: string) {
try {
const approvePath = `${this.basePath}/${id}`;
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
method: 'PATCH',
body: {
status: 'APPROVED',
reject_reason: '',
},
});
return approveRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async reject(id: string, rejectReason: string) {
try {
const rejectPath = `${this.basePath}/${id}`;
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
method: 'PATCH',
body: {
status: 'REJECTED',
reject_reason: rejectReason,
},
});
return rejectRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
}
export const DailyChecklistApi = new DailyChecklistApiService(
'/daily-checklists'
);
+56
View File
@@ -0,0 +1,56 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseKandang } from '@/types/api/master-data/kandang';
import { Phase } from '@/types/api/daily-checklist/phase';
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
export type BaseDailyChecklist = {
id: number;
name: string;
status: string;
category: string;
date: string;
kandang: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
total_phase: number;
total_activity: number;
progress: number;
};
export type DailyChecklist = BaseMetadata & BaseDailyChecklist;
export type DetailDailyChecklist = BaseDailyChecklist & {
reject_reason: string | null;
phases: {
id: number;
phase_id: number;
phase: Phase;
}[];
tasks: {
id: number;
checklist_id: number;
phase_id: number;
phase_activity_id: number;
time_type: string;
notes: string | null;
phase: Phase;
phase_activity: PhaseActivity;
assignments: {
employee: {
id: number;
name: string;
};
checked: boolean;
note: string | null;
}[];
}[];
assigned_employees: {
id: number;
name: string;
}[];
};
export type CreateDailyChecklistPayload = {
date: string;
kandang_id: number;
category: string;
status: string;
};
+1
View File
@@ -5,6 +5,7 @@ export type BasePhase = {
name: string; name: string;
is_active: boolean; is_active: boolean;
category: string; category: string;
activity_count: number;
}; };
export type Phase = BaseMetadata & BasePhase; export type Phase = BaseMetadata & BasePhase;