'use client'; // ADR-021: WorkflowLifecycle — แสดง Timeline การเดินเรื่องเอกสาร (US2, REQ-02, REQ-03) // แสดง Step ที่เสร็จแล้ว, Step ปัจจุบัน (active, มีสีพิเศษ), และ Step ที่ยังรอ import { useRef, useState } from 'react'; import { format } from 'date-fns'; import { th } from 'date-fns/locale'; import { CheckCircle2, XCircle, RotateCcw, MessageSquare, Clock, Loader2, Paperclip, Upload, X as XIcon } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; import apiClient from '@/lib/api/client'; import { useTranslations } from '@/hooks/use-translations'; import type { WorkflowAttachmentSummary, WorkflowHistoryItem } from '@/types/workflow'; // รูปแบบ response จาก POST /files/upload interface UploadedAttachment { publicId: string; originalFilename: string; } interface WorkflowLifecycleProps { history?: WorkflowHistoryItem[]; currentState?: string; isLoading?: boolean; error?: Error | null; // ADR-021 US4: callback เมื่อ User คลิก attachment chip เพื่อ preview onFileClick?: (attachment: WorkflowAttachmentSummary) => void; // ADR-021 T028: callback เมื่อ publicIds ของไฟล์แนบ Step ปัจจุบันเปลี่ยน onAttachmentsChange?: (publicIds: string[]) => void; // ADR-021 T041: บอก publicIds ที่ API คืน 404 (ไฟล์ถูกลบออกจาก Storage) unavailableAttachmentIds?: string[]; } // Icon ตาม Action ประเภท (labels resolve ผ่าน t() ใน component) const ACTION_ICON_MAP: Record = { APPROVE: , REJECT: , RETURN: , COMMENT: , ACKNOWLEDGE: , SUBMIT: , }; // สีของ Node ตาม Action (REQ-03: Active Step Color) function getNodeStyle(action: string, isLatest: boolean): { wrapper: string; icon: string } { const a = action.toUpperCase(); if (isLatest) { // Step ปัจจุบัน — เน้นด้วย primary ring return { wrapper: 'bg-primary/10 border-primary ring-2 ring-primary/30 ring-offset-2', icon: 'text-primary', }; } if (a === 'APPROVE' || a === 'ACKNOWLEDGE') { return { wrapper: 'bg-green-50 border-green-300', icon: 'text-green-600' }; } if (a === 'REJECT') { return { wrapper: 'bg-red-50 border-red-300', icon: 'text-red-600' }; } if (a === 'RETURN') { return { wrapper: 'bg-amber-50 border-amber-300', icon: 'text-amber-600' }; } return { wrapper: 'bg-blue-50 border-blue-200', icon: 'text-blue-600' }; } // แปลง Action key เป็น i18n key function getActionLabelKey(action: string): string { return `workflow.timeline.step.${action.toUpperCase()}`; } export function WorkflowLifecycle({ history, currentState, isLoading = false, error, onFileClick, onAttachmentsChange, unavailableAttachmentIds, }: WorkflowLifecycleProps) { const unavailableSet = new Set(unavailableAttachmentIds ?? []); const t = useTranslations(); // ADR-021 T028: สถานะการอัปโหลดไฟล์แนบประจำ Step ปัจจุบัน const [pendingFiles, setPendingFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); // อัปโหลดไฟล์ผ่าน Two-Phase upload (POST /files/upload → temp) async function handleFileUpload(files: FileList | null) { if (!files || files.length === 0) return; setIsUploading(true); const newUploaded: UploadedAttachment[] = []; for (const file of Array.from(files)) { const formData = new FormData(); formData.append('file', file); try { const res = await apiClient.post<{ data?: UploadedAttachment } & UploadedAttachment>( '/files/upload', formData ); const att: UploadedAttachment = (res.data as { data?: UploadedAttachment }).data ?? (res.data as UploadedAttachment); if (att?.publicId) newUploaded.push(att); } catch { toast.error(`${t('workflow.timeline.uploadError')} "${file.name}"`); } } const updated = [...pendingFiles, ...newUploaded]; setPendingFiles(updated); onAttachmentsChange?.(updated.map((f) => f.publicId)); setIsUploading(false); } function removeUploadedFile(publicId: string) { const updated = pendingFiles.filter((f) => f.publicId !== publicId); setPendingFiles(updated); onAttachmentsChange?.(updated.map((f) => f.publicId)); } if (isLoading) { return (
); } if (error) { return (
{t('workflow.timeline.loadError')}
); } if (!history || history.length === 0) { return (
{t('workflow.timeline.noHistory')}
); } return (
{/* เส้นแนวตั้งเชื่อม Node */} {/* ADR-021 T028: Upload Zone — แสดงเฉพาะ Step ปัจจุบัน (เมื่อ parent ต้องการเก็บไฟล์แนบ) */} {onAttachmentsChange && (
{/* Dropzone */}
fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { e.preventDefault(); setIsDragOver(false); void handleFileUpload(e.dataTransfer.files); }} > {isUploading ? ( ) : ( )}

{isUploading ? t('workflow.timeline.uploading') : t('workflow.timeline.uploadHint')}

{t('workflow.timeline.uploadTypes')}

void handleFileUpload(e.target.files)} />
{/* ไฟล์ที่อัปโหลดแล้ว (pending) */} {pendingFiles.length > 0 && (
{pendingFiles.map((f) => ( {f.originalFilename} ))}
)}
)}
); }