mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
1036 lines
34 KiB
TypeScript
1036 lines
34 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import * as React from 'react';
|
|
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
|
import { Button } from '@/figma-make/components/base/button';
|
|
import { Badge } from '@/figma-make/components/base/badge';
|
|
import { Label } from '@/figma-make/components/base/label';
|
|
import { Textarea } from '@/figma-make/components/base/textarea';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from '@/figma-make/components/base/dialog';
|
|
import { toast } from 'sonner';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
|
import { isResponseError } from '@/lib/api-helper';
|
|
import Link from 'next/link';
|
|
import { Icon } from '@iconify/react';
|
|
import { Document } from '@/types/api/api-general';
|
|
|
|
interface ChecklistDetailRow {
|
|
checklist_id: string;
|
|
date: string;
|
|
kandang_name: string;
|
|
category: string;
|
|
status: string;
|
|
reject_reason: string | null;
|
|
phase_id: string;
|
|
phase_name: string;
|
|
activity_id: string;
|
|
activity_name: string;
|
|
activity_description: string | null;
|
|
time_type: string;
|
|
employee_id: string;
|
|
employee_name: string;
|
|
checked: boolean;
|
|
note: string | null;
|
|
}
|
|
|
|
interface ChecklistHeader {
|
|
date: string;
|
|
kandang_name: string;
|
|
category: string;
|
|
status: string;
|
|
reject_reason: string | null;
|
|
progress_percent: number;
|
|
total_phases: number;
|
|
total_activities: number;
|
|
}
|
|
|
|
interface PhaseGroup {
|
|
phase: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
timeGroups: {
|
|
[timeType: string]: {
|
|
activities: {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
employees: {
|
|
id: string;
|
|
name: string;
|
|
checked: boolean;
|
|
note: string | null;
|
|
}[];
|
|
}[];
|
|
};
|
|
};
|
|
}
|
|
|
|
interface ChecklistData {
|
|
id: number;
|
|
date: string;
|
|
kandang_id: string;
|
|
category: string;
|
|
status: string;
|
|
reject_reason: string | null;
|
|
kandang: {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
interface AssignmentQueryResult {
|
|
task_id: number;
|
|
employee_id: string;
|
|
checked: boolean;
|
|
note: string | null;
|
|
employees: {
|
|
id: number;
|
|
name: string;
|
|
} | null;
|
|
}
|
|
|
|
const CATEGORY_LABELS: { [key: string]: string } = {
|
|
pullet_open: 'Pullet Open',
|
|
pullet_close: 'Pullet Close',
|
|
produksi_open: 'Produksi Open',
|
|
produksi_close: 'Produksi Close',
|
|
};
|
|
|
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
|
const TIME_TYPE_LABELS: { [key: string]: string } = {
|
|
Umum: 'Umum',
|
|
Pagi: 'Pagi',
|
|
Siang: 'Siang',
|
|
Sore: 'Sore',
|
|
Malam: 'Malam',
|
|
};
|
|
|
|
export function DetailDailyChecklistContent() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const checklistId = searchParams.get('checklistId');
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [header, setHeader] = useState<ChecklistHeader | null>(null);
|
|
const [detailRows, setDetailRows] = useState<ChecklistDetailRow[]>([]);
|
|
const [phaseGroups, setPhaseGroups] = useState<PhaseGroup[]>([]);
|
|
const [employees, setEmployees] = useState<{ id: string; name: string }[]>(
|
|
[]
|
|
);
|
|
const [documents, setDocuments] = useState<Document[]>([]);
|
|
|
|
// Modals
|
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
|
const [rejectReason, setRejectReason] = useState('');
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (checklistId) {
|
|
fetchChecklistDetail();
|
|
}
|
|
}, [checklistId]);
|
|
|
|
const fetchChecklistDetail = async () => {
|
|
if (!checklistId) {
|
|
console.warn('checklistId missing');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const checklistDataRes =
|
|
await DailyChecklistApi.getOneDailyChecklist(checklistId);
|
|
|
|
if (isResponseError(checklistDataRes)) {
|
|
console.error('Error fetching checklist:', checklistDataRes.message);
|
|
toast.error('Data checklist tidak ditemukan');
|
|
router.push('/daily-checklist/list-daily-checklist');
|
|
return;
|
|
}
|
|
|
|
const rawDetailChecklist = checklistDataRes?.data;
|
|
|
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
|
|
|
const checklistData = {
|
|
id: rawDetailChecklist?.id,
|
|
date: rawDetailChecklist?.date,
|
|
kandang_id: rawDetailChecklist?.kandang.id,
|
|
category: rawDetailChecklist?.category,
|
|
status: rawDetailChecklist?.status,
|
|
reject_reason: rawDetailChecklist?.reject_reason,
|
|
kandang: rawDetailChecklist?.kandang,
|
|
};
|
|
|
|
const tasks = rawDetailChecklist?.tasks;
|
|
|
|
const castedChecklistData =
|
|
checklistData as unknown as ChecklistData | null;
|
|
|
|
if (!tasks || tasks.length === 0) {
|
|
toast.info('Checklist belum memiliki aktivitas');
|
|
setHeader({
|
|
date: castedChecklistData?.date || '-',
|
|
kandang_name: castedChecklistData?.kandang?.name || '-',
|
|
category: castedChecklistData?.category || '-',
|
|
status: castedChecklistData?.status || '-',
|
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
|
progress_percent: 0,
|
|
total_phases: 0,
|
|
total_activities: 0,
|
|
});
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const assignments: {
|
|
task_id: number;
|
|
checked: boolean;
|
|
note: string | null;
|
|
employee: {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
}[] = [];
|
|
|
|
tasks.forEach((task) => {
|
|
task.assignments.forEach((assignment) => {
|
|
assignments.push({
|
|
task_id: task.id,
|
|
checked: assignment.checked,
|
|
note: assignment.note,
|
|
employee: assignment.employee,
|
|
});
|
|
});
|
|
});
|
|
|
|
// ✅ Build detail rows from tasks and assignments
|
|
const detailRows: ChecklistDetailRow[] = [];
|
|
|
|
tasks.forEach((task) => {
|
|
const taskAssignments = assignments.filter(
|
|
(a) => a.task_id === task.id
|
|
);
|
|
|
|
taskAssignments.forEach((assignment) => {
|
|
detailRows.push({
|
|
checklist_id: checklistId,
|
|
date: castedChecklistData?.date || '-',
|
|
kandang_name: castedChecklistData?.kandang?.name || '-',
|
|
category: castedChecklistData?.category || '-',
|
|
status: castedChecklistData?.status || '-',
|
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
|
phase_id: String(task.phase_id),
|
|
phase_name: task.phase?.name || '-',
|
|
activity_id: String(task.phase_activity_id),
|
|
activity_name: task.phase_activity?.name || '-',
|
|
activity_description: task.phase_activity?.description || null,
|
|
time_type: task.time_type,
|
|
employee_id: String(assignment.employee.id),
|
|
employee_name: assignment.employee?.name || '-',
|
|
checked: assignment.checked,
|
|
note: assignment.note,
|
|
});
|
|
});
|
|
});
|
|
|
|
if (detailRows.length === 0) {
|
|
toast.info('Checklist belum memiliki assignment ABK');
|
|
setHeader({
|
|
date: castedChecklistData?.date || '-',
|
|
kandang_name: castedChecklistData?.kandang?.name || '-',
|
|
category: castedChecklistData?.category || '-',
|
|
status: castedChecklistData?.status || '-',
|
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
|
progress_percent: 0,
|
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
|
total_activities: tasks.length,
|
|
});
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setDetailRows(detailRows);
|
|
|
|
// Extract unique employees
|
|
const uniqueEmployees = Array.from(
|
|
new Map(
|
|
detailRows.map((row) => [
|
|
row.employee_id,
|
|
{ id: row.employee_id, name: row.employee_name },
|
|
])
|
|
).values()
|
|
);
|
|
|
|
uniqueEmployees.sort((a, b) =>
|
|
a.name.localeCompare(b.name, undefined, {
|
|
sensitivity: 'base',
|
|
})
|
|
);
|
|
|
|
setEmployees(uniqueEmployees);
|
|
|
|
// Group data by Phase → Time Type → Activity
|
|
groupDetailData(detailRows);
|
|
|
|
// Calculate progress
|
|
const totalCheckboxes = detailRows.length;
|
|
const checkedCount = detailRows.filter((row) => row.checked).length;
|
|
const progressPercent =
|
|
totalCheckboxes > 0
|
|
? Math.round((checkedCount / totalCheckboxes) * 100)
|
|
: 0;
|
|
|
|
const uniquePhases = new Set(detailRows.map((row) => row.phase_id));
|
|
const uniqueActivities = new Set(
|
|
detailRows.map((row) => row.activity_id)
|
|
);
|
|
|
|
setHeader({
|
|
date: castedChecklistData?.date || '-',
|
|
kandang_name: castedChecklistData?.kandang?.name || '-',
|
|
category: castedChecklistData?.category || '-',
|
|
status: castedChecklistData?.status || '-',
|
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
|
progress_percent: progressPercent,
|
|
total_phases: uniquePhases.size,
|
|
total_activities: uniqueActivities.size,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching checklist detail:', error);
|
|
toast.error('Terjadi kesalahan');
|
|
router.push('/daily-checklist/list-daily-checklist');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const groupDetailData = (rows: ChecklistDetailRow[]) => {
|
|
// Group by phase_id
|
|
const phaseMap = new Map<
|
|
string,
|
|
{
|
|
phase: { id: string; name: string };
|
|
activities: Map<
|
|
string,
|
|
{
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
time_type: string;
|
|
employees: Map<
|
|
string,
|
|
{
|
|
id: string;
|
|
name: string;
|
|
checked: boolean;
|
|
note: string | null;
|
|
}
|
|
>;
|
|
}
|
|
>;
|
|
}
|
|
>();
|
|
|
|
rows.forEach((row) => {
|
|
if (!phaseMap.has(row.phase_id)) {
|
|
phaseMap.set(row.phase_id, {
|
|
phase: { id: row.phase_id, name: row.phase_name },
|
|
activities: new Map(),
|
|
});
|
|
}
|
|
|
|
const phaseData = phaseMap.get(row.phase_id)!;
|
|
|
|
if (!phaseData.activities.has(row.activity_id)) {
|
|
phaseData.activities.set(row.activity_id, {
|
|
id: row.activity_id,
|
|
name: row.activity_name,
|
|
description: row.activity_description,
|
|
time_type: row.time_type,
|
|
employees: new Map(),
|
|
});
|
|
}
|
|
|
|
const activityData = phaseData.activities.get(row.activity_id)!;
|
|
activityData.employees.set(row.employee_id, {
|
|
id: row.employee_id,
|
|
name: row.employee_name,
|
|
checked: row.checked,
|
|
note: row.note,
|
|
});
|
|
});
|
|
|
|
// Convert to array and group by time_type
|
|
const grouped: PhaseGroup[] = [];
|
|
|
|
phaseMap.forEach((phaseData, phaseId) => {
|
|
const timeGroups: {
|
|
[timeType: string]: {
|
|
activities: {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
employees: {
|
|
id: string;
|
|
name: string;
|
|
checked: boolean;
|
|
note: string | null;
|
|
}[];
|
|
}[];
|
|
};
|
|
} = {};
|
|
|
|
phaseData.activities.forEach((activityData) => {
|
|
const timeType = activityData.time_type || 'Umum';
|
|
|
|
if (!timeGroups[timeType]) {
|
|
timeGroups[timeType] = { activities: [] };
|
|
}
|
|
|
|
timeGroups[timeType].activities.push({
|
|
id: activityData.id,
|
|
name: activityData.name,
|
|
description: activityData.description,
|
|
employees: Array.from(activityData.employees.values()),
|
|
});
|
|
});
|
|
|
|
grouped.push({
|
|
phase: phaseData.phase,
|
|
timeGroups,
|
|
});
|
|
});
|
|
|
|
setPhaseGroups(grouped);
|
|
};
|
|
|
|
const handleApprove = () => {
|
|
setShowApproveModal(true);
|
|
};
|
|
|
|
const handleReject = () => {
|
|
setRejectReason('');
|
|
setShowRejectModal(true);
|
|
};
|
|
|
|
const confirmApprove = async () => {
|
|
if (!checklistId) return;
|
|
|
|
try {
|
|
setActionLoading(true);
|
|
|
|
const approveRes = await DailyChecklistApi.approve(String(checklistId));
|
|
|
|
if (isResponseError(approveRes)) {
|
|
toast.error('Gagal approve checklist: ' + approveRes.message);
|
|
return;
|
|
}
|
|
|
|
toast.success('Checklist berhasil di-approve');
|
|
setShowApproveModal(false);
|
|
await fetchChecklistDetail();
|
|
} catch (error) {
|
|
console.error('Error approving checklist:', error);
|
|
toast.error('Terjadi kesalahan');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const confirmReject = async () => {
|
|
if (!checklistId) return;
|
|
|
|
if (!rejectReason.trim()) {
|
|
toast.error('Alasan reject harus diisi');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setActionLoading(true);
|
|
|
|
const rejectRes = await DailyChecklistApi.reject(
|
|
String(checklistId),
|
|
rejectReason
|
|
);
|
|
|
|
if (isResponseError(rejectRes)) {
|
|
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
|
return;
|
|
}
|
|
|
|
toast.success('Checklist berhasil di-reject');
|
|
setShowRejectModal(false);
|
|
setRejectReason('');
|
|
await fetchChecklistDetail();
|
|
} catch (error) {
|
|
console.error('Error rejecting checklist:', error);
|
|
toast.error('Terjadi kesalahan');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case 'DRAFT':
|
|
return (
|
|
<Badge
|
|
variant='outline'
|
|
className='border-gray-300 text-gray-700 bg-white'
|
|
>
|
|
Draft
|
|
</Badge>
|
|
);
|
|
case 'SUBMITTED':
|
|
return (
|
|
<Badge
|
|
variant='outline'
|
|
className='border-orange-300 text-orange-700 bg-white'
|
|
>
|
|
Submitted
|
|
</Badge>
|
|
);
|
|
case 'APPROVED':
|
|
return (
|
|
<Badge
|
|
variant='outline'
|
|
className='border-green-300 text-green-700 bg-white'
|
|
>
|
|
Approved
|
|
</Badge>
|
|
);
|
|
case 'REJECTED':
|
|
return (
|
|
<Badge
|
|
variant='outline'
|
|
className='border-red-300 text-red-700 bg-white'
|
|
>
|
|
Rejected
|
|
</Badge>
|
|
);
|
|
default:
|
|
return (
|
|
<Badge
|
|
variant='outline'
|
|
className='border-gray-300 text-gray-700 bg-white'
|
|
>
|
|
{status}
|
|
</Badge>
|
|
);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className='min-h-screen'>
|
|
<div className='p-6'>
|
|
<div className='mb-6'>
|
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
Detail Daily Checklist
|
|
</h1>
|
|
</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>
|
|
);
|
|
}
|
|
|
|
if (!header) {
|
|
return null;
|
|
}
|
|
|
|
const isReadOnly =
|
|
header.status === 'APPROVED' || header.status === 'REJECTED';
|
|
|
|
return (
|
|
<div className='min-h-screen'>
|
|
<div className='p-6'>
|
|
{/* Page Title with Back Button */}
|
|
<div className='mb-6 flex items-center gap-4'>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
onClick={() => router.push('/daily-checklist/list-daily-checklist')}
|
|
className='border-gray-200'
|
|
>
|
|
<ArrowLeft className='w-4 h-4 mr-1' />
|
|
Kembali
|
|
</Button>
|
|
<div className='flex-1'>
|
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
Detail Daily Checklist
|
|
</h1>
|
|
<p className='text-sm text-gray-600 mt-1'>
|
|
Lihat detail checklist harian
|
|
</p>
|
|
</div>
|
|
{header.status === 'SUBMITTED' && (
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
onClick={handleApprove}
|
|
disabled={actionLoading}
|
|
className='bg-green-600 hover:bg-green-700 text-white'
|
|
>
|
|
<CheckCircle className='w-4 h-4 mr-2' />
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
onClick={handleReject}
|
|
disabled={actionLoading}
|
|
variant='destructive'
|
|
className='bg-red-600 hover:bg-red-700 text-white'
|
|
>
|
|
<XCircle className='w-4 h-4 mr-2' />
|
|
Reject
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Header Info Card */}
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white mb-6'>
|
|
<CardContent className='p-6'>
|
|
<div className='grid grid-cols-2 md:grid-cols-4 gap-6'>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Tanggal</Label>
|
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
{formatDate(header.date)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Kandang</Label>
|
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
{header.kandang_name}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Kategori</Label>
|
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
{CATEGORY_LABELS[header.category] || header.category}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Status</Label>
|
|
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
|
</div>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Total Phase</Label>
|
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
{header.total_phases} fase
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className='text-xs text-gray-500'>Total Aktivitas</Label>
|
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
{header.total_activities} aktivitas
|
|
</p>
|
|
</div>
|
|
<div className='col-span-2'>
|
|
<Label className='text-xs text-gray-500'>Progress</Label>
|
|
<div className='flex items-center gap-3 mt-2'>
|
|
<div className='flex-1 bg-gray-200 rounded-full h-2.5'>
|
|
<div
|
|
className='bg-[#0069e0] h-2.5 rounded-full transition-all'
|
|
style={{
|
|
width: `${header.progress_percent}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<span className='text-sm font-medium text-gray-900'>
|
|
{header.progress_percent}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Reject Reason if rejected */}
|
|
{header.status === 'REJECTED' && header.reject_reason && (
|
|
<div className='mt-6 pt-6 border-t border-gray-200'>
|
|
<div className='flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg'>
|
|
<AlertCircle className='w-5 h-5 text-red-600 mt-0.5 flex-shrink-0' />
|
|
<div>
|
|
<Label className='text-sm font-medium text-red-900'>
|
|
Alasan Reject
|
|
</Label>
|
|
<p className='text-sm text-red-700 mt-1'>
|
|
{header.reject_reason}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Activity Checklist Table */}
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
<CardContent className='p-6'>
|
|
<h3 className='font-semibold text-gray-900 mb-4'>
|
|
Checklist Aktivitas
|
|
</h3>
|
|
|
|
{phaseGroups.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>
|
|
{employees.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>
|
|
{phaseGroups.flatMap((phaseGroup) => {
|
|
const timeTypes = Object.keys(phaseGroup.timeGroups).sort(
|
|
(a, b) =>
|
|
TIME_TYPE_ORDER.indexOf(a) -
|
|
TIME_TYPE_ORDER.indexOf(b)
|
|
);
|
|
|
|
const totalActivities = timeTypes.reduce(
|
|
(sum, timeType) =>
|
|
sum +
|
|
phaseGroup.timeGroups[timeType].activities.length,
|
|
0
|
|
);
|
|
|
|
const rows = [];
|
|
|
|
// PHASE Header - BLUE
|
|
rows.push(
|
|
<tr
|
|
key={`phase-${phaseGroup.phase.id}`}
|
|
className='bg-blue-50 border-b border-blue-200'
|
|
>
|
|
<td
|
|
colSpan={employees.length + 2}
|
|
className='py-2.5 px-4'
|
|
>
|
|
<div className='flex items-center gap-2'>
|
|
<span className='text-sm font-semibold text-blue-900'>
|
|
{phaseGroup.phase.name}
|
|
</span>
|
|
<Badge
|
|
variant='secondary'
|
|
className='text-xs bg-blue-100 text-blue-700 border-blue-200 rounded-lg'
|
|
>
|
|
{totalActivities} aktivitas
|
|
</Badge>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
|
|
// TIME_TYPE sub-headers and activities
|
|
timeTypes.forEach((timeType) => {
|
|
const timeGroup = phaseGroup.timeGroups[timeType];
|
|
const hasMultipleTimeTypes = timeTypes.length > 1;
|
|
|
|
// TIME Header (optional) - GRAY SOFT
|
|
if (hasMultipleTimeTypes) {
|
|
rows.push(
|
|
<tr
|
|
key={`time-${phaseGroup.phase.id}-${timeType}`}
|
|
className='bg-gray-50 border-b border-gray-200'
|
|
>
|
|
<td
|
|
colSpan={employees.length + 2}
|
|
className='py-2 px-4 pl-8'
|
|
>
|
|
<span className='text-xs font-medium text-gray-600'>
|
|
{TIME_TYPE_LABELS[timeType]} (
|
|
{timeGroup.activities.length} aktivitas)
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
// ACTIVITY rows
|
|
const activities = timeGroup.activities;
|
|
|
|
activities.sort((a, b) =>
|
|
a.name.localeCompare(b.name, undefined, {
|
|
sensitivity: 'base',
|
|
})
|
|
);
|
|
|
|
activities.forEach((activity, index) => {
|
|
const indentClass = hasMultipleTimeTypes
|
|
? 'pl-12'
|
|
: 'pl-8';
|
|
|
|
console.log({
|
|
activity,
|
|
});
|
|
|
|
rows.push(
|
|
<tr
|
|
key={`activity-${activity.id}-${index}`}
|
|
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>
|
|
{employees.map((emp) => {
|
|
const empData = activity.employees.find(
|
|
(e) => e.id === emp.id
|
|
);
|
|
return (
|
|
<td
|
|
key={emp.id}
|
|
className='text-center py-3 px-4 border-r border-gray-200'
|
|
>
|
|
<input
|
|
type='checkbox'
|
|
checked={empData?.checked || false}
|
|
disabled
|
|
className='checkbox-clean cursor-not-allowed'
|
|
/>
|
|
</td>
|
|
);
|
|
})}
|
|
<td className='py-3 px-4'>
|
|
{activity.employees.length > 0 &&
|
|
activity.employees[
|
|
activity.employees.length - 1
|
|
].note ? (
|
|
<p className='text-sm text-gray-600'>
|
|
{
|
|
activity.employees[
|
|
activity.employees.length - 1
|
|
].note
|
|
}
|
|
</p>
|
|
) : (
|
|
<p className='text-xs text-gray-400 italic'>
|
|
Tidak ada catatan
|
|
</p>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
});
|
|
});
|
|
|
|
return rows;
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<div className='text-center py-12 text-gray-500'>
|
|
Tidak ada data aktivitas
|
|
</div>
|
|
)}
|
|
|
|
{documents.length > 0 && (
|
|
<div className='mt-6'>
|
|
<h3 className='font-semibold text-gray-900 mb-2'>
|
|
Dokumen yang telah diupload
|
|
</h3>
|
|
|
|
<ul className='list-disc pl-4'>
|
|
{documents.map((existingDocument, existingDocumentIdx) => (
|
|
<li key={existingDocumentIdx}>
|
|
<div 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>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Approve Modal */}
|
|
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
|
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
|
<DialogHeader>
|
|
<DialogTitle>Approve Checklist</DialogTitle>
|
|
<DialogDescription>
|
|
Apakah Anda yakin ingin approve checklist ini?
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className='bg-gray-50 rounded-lg p-4 space-y-2'>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Tanggal:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{formatDate(header.date)}
|
|
</span>
|
|
</div>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Kandang:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{header.kandang_name}
|
|
</span>
|
|
</div>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Kategori:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{CATEGORY_LABELS[header.category] || header.category}
|
|
</span>
|
|
</div>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Progress:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{header.progress_percent}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className='flex gap-2'>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => setShowApproveModal(false)}
|
|
disabled={actionLoading}
|
|
className='border-gray-200'
|
|
>
|
|
Batal
|
|
</Button>
|
|
<Button
|
|
onClick={confirmApprove}
|
|
disabled={actionLoading}
|
|
className='bg-green-600 hover:bg-green-700 text-white'
|
|
>
|
|
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Reject Modal */}
|
|
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
|
<DialogHeader>
|
|
<DialogTitle>Reject Checklist</DialogTitle>
|
|
<DialogDescription>
|
|
Berikan alasan reject untuk checklist ini
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Tanggal:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{formatDate(header.date)}
|
|
</span>
|
|
</div>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Kandang:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{header.kandang_name}
|
|
</span>
|
|
</div>
|
|
<div className='flex justify-between text-sm'>
|
|
<span className='text-gray-600'>Kategori:</span>
|
|
<span className='font-medium text-gray-900'>
|
|
{CATEGORY_LABELS[header.category] || header.category}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor='reject-reason'>
|
|
Alasan Reject <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Textarea
|
|
id='reject-reason'
|
|
value={rejectReason}
|
|
onChange={(e) => setRejectReason(e.target.value)}
|
|
placeholder='Tuliskan alasan reject...'
|
|
className='mt-1.5 border-gray-200 min-h-[100px]'
|
|
disabled={actionLoading}
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter className='flex gap-2'>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => setShowRejectModal(false)}
|
|
disabled={actionLoading}
|
|
className='border-gray-200'
|
|
>
|
|
Batal
|
|
</Button>
|
|
<Button
|
|
onClick={confirmReject}
|
|
disabled={actionLoading}
|
|
variant='destructive'
|
|
className='bg-red-600 hover:bg-red-700 text-white'
|
|
>
|
|
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|