Files
lti-web-client/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx
T
2026-05-12 14:38:50 +07:00

1172 lines
38 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import * as React from 'react';
import {
ArrowLeft,
CheckCircle,
XCircle,
AlertCircle,
Share2,
} from 'lucide-react';
import * as htmlToImage from 'html-to-image';
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';
import RequirePermission from '@/components/helper/RequirePermission';
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',
empty_kandang: 'Kandang Kosong',
};
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum',
Pagi: 'Pagi',
Siang: 'Siang',
Sore: 'Sore',
Malam: 'Malam',
};
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 [, 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);
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
useEffect(() => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [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) => {
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',
});
};
const isMobileDevice = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
const getStatusMessage = () => {
switch (header?.status) {
case 'DRAFT':
return 'Checklist harian perlu disubmit';
case 'SUBMITTED':
return 'Checklist harian menunggu persetujuan';
case 'APPROVED':
return 'Checklist harian telah disetujui';
case 'REJECTED':
return 'Checklist harian telah ditolak';
default:
return '';
}
};
const shareHandler = async () => {
const isMobile = isMobileDevice();
if (isMobile) {
setIsGeneratingImage(true);
}
const baseTitle = `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`;
const statusMsg = getStatusMessage();
const statusInfo = `\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}`;
const urlMessage = `\n\nView full checklist: ${window.location.href}`;
const fullMessage = baseTitle + statusInfo + urlMessage;
let shareData: ShareData;
if (isMobile) {
const htmlBlob = await htmlToImage.toBlob(document.body, {
backgroundColor: '#ffffff',
});
const imgFile = new File(
[htmlBlob!],
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
{
type: 'image/png',
}
);
shareData = {
files: [imgFile],
title: baseTitle,
text: fullMessage,
};
} else {
shareData = {
title: baseTitle,
text: fullMessage,
url: window.location.href,
};
}
setIsGeneratingImage(false);
try {
if (!navigator.canShare(shareData)) {
toast.error(
'Gagal membagikan checklist, coba dengan perangkat yang berbeda'
);
return;
}
await navigator.share(shareData);
toast.success('Checklist berhasil dibagikan');
} catch (error) {
toast.error('Gagal membagikan checklist');
}
};
const shareToWhatsAppHandler = async () => {
const isMobile = isMobileDevice();
setIsGeneratingImage(true);
const statusMsg = getStatusMessage();
const category = header?.category || '';
const message = encodeURIComponent(
`Daily Checklist\n\nTanggal: ${formatDate(header?.date || '')}\nKandang: ${header?.kandang_name}\nKategori: ${CATEGORY_LABELS[category] || category}\nProgress: ${header?.progress_percent}%\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
);
setIsGeneratingImage(false);
const whatsappUrl = isMobile
? `https://wa.me/?text=${message}`
: `https://web.whatsapp.com/send?text=${message}`;
window.open(whatsappUrl, '_blank');
};
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;
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Action Buttons */}
<div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
<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 items-center gap-2 flex-wrap'>
{header.status === 'SUBMITTED' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<div className='flex gap-2 flex-wrap'>
<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>
</RequirePermission>
)}
<Button
variant='outline'
size='sm'
onClick={shareHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Share2 className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan'}
{isGeneratingImage && 'Memuat...'}
</Button>
<Button
variant='outline'
size='sm'
onClick={shareToWhatsAppHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Icon icon='mdi:whatsapp' className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan via WhatsApp'}
{isGeneratingImage && 'Memuat...'}
</Button>
</div>
</div>
{/* Page Title */}
<div className='mb-6'>
<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 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 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';
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>
);
}