"use client"; import React, { useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Download, XCircle } from "lucide-react"; import { cancelVendorUploadedFile } from "@/lib/swp/vendor-actions"; import type { SwpFileApiResponse } from "@/lib/swp/api-client"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { formatSwpDate } from "@/lib/swp/utils"; import { SwpInboxHistoryDialog } from "./swp-inbox-history-dialog"; import { SwpNoteDialog } from "./swp-note-dialog"; // 업로드 필요 문서 타입 (DB stageDocuments에서 조회) interface RequiredDocument { vendorDocNumber: string; title: string; buyerSystemComment: string | null; } interface SwpInboxTableProps { files: SwpFileApiResponse[]; requiredDocs: RequiredDocument[]; projNo: string; vendorCode: string; userId: string; } // 테이블 행 데이터 (플랫하게 펼침) interface TableRowData { uploadId: string | null; docNo: string; revNo: string | null; stage: string | null; status: string | null; statusNm: string | null; actvNo: string | null; actvSeq: string | null; // 정렬용 crter: string | null; note1: string | null; pkgNo: string | null; file: SwpFileApiResponse | null; uploadDate: string | null; isRequiredDoc: boolean; // Row Span 정보 (0이면 렌더링 안함) spans: { uploadId: number; docNo: number; pkgNo: number; revNo: number; stage: number; status: number; actvNo: number; crter: number; note1: number; }; } // Status 집계 타입 interface StatusCount { status: string; statusNm: string; count: number; color: string; } export function SwpInboxTable({ files, requiredDocs, projNo, userId, }: SwpInboxTableProps) { const [selectedStatus, setSelectedStatus] = useState(null); const [selectedFiles, setSelectedFiles] = useState>(new Set()); // 선택된 파일 (fileKey) const [historyDialogOpen, setHistoryDialogOpen] = useState(false); const [selectedDocNo, setSelectedDocNo] = useState(null); const [noteDialogOpen, setNoteDialogOpen] = useState(false); const [noteDialogContent, setNoteDialogContent] = useState<{ title: string; content: string | null }>({ title: "", content: null }); // Status 집계 (API 응답 + 업로드 필요 문서) const statusCounts = useMemo(() => { const statusMap = new Map(); // API 응답 파일 집계 files.forEach((file) => { const status = file.STAT || "UNKNOWN"; const statusNm = file.STAT_NM || status; if (statusMap.has(status)) { statusMap.get(status)!.count++; } else { statusMap.set(status, { statusNm, count: 1 }); } }); // 업로드 필요 문서 집계 if (requiredDocs.length > 0) { const status = "UPLOAD_REQUIRED"; const statusNm = "Upload Required"; statusMap.set(status, { statusNm, count: requiredDocs.length }); } const counts: StatusCount[] = []; statusMap.forEach((value, status) => { const color = status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800 hover:bg-green-200" : status === "SCW02" ? "bg-blue-100 text-blue-800 hover:bg-blue-200" : status === "SCW01" ? "bg-yellow-100 text-yellow-800 hover:bg-yellow-200" : status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800 hover:bg-red-200" : status === "SCW07" ? "bg-purple-100 text-purple-800 hover:bg-purple-200" : status === "SCW09" ? "bg-gray-100 text-gray-800 hover:bg-gray-200" : status === "SCW00" ? "bg-orange-100 text-orange-800 hover:bg-orange-200" : status === "UPLOAD_REQUIRED" ? "bg-amber-100 text-amber-800 hover:bg-amber-200" : "bg-gray-100 text-gray-800 hover:bg-gray-200"; counts.push({ status, statusNm: value.statusNm, count: value.count, color, }); }); // 개수 순으로 정렬 (Upload Required를 맨 앞으로) return counts.sort((a, b) => { if (a.status === "UPLOAD_REQUIRED") return -1; if (b.status === "UPLOAD_REQUIRED") return 1; return b.count - a.count; }); }, [files, requiredDocs]); // 데이터 그룹화 및 플랫 변환 (Sorting logic applied) const tableRows = useMemo(() => { const rows: TableRowData[] = []; // 1. API 응답 파일 처리 files.forEach((file) => { // Status 필터링 if (selectedStatus && file.STAT !== selectedStatus) { return; } rows.push({ uploadId: file.BOX_SEQ || null, docNo: file.OWN_DOC_NO, revNo: file.REV_NO || null, stage: file.STAGE || null, status: file.STAT || null, statusNm: file.STAT_NM || null, actvNo: file.ACTV_NO || null, actvSeq: file.ACTV_SEQ || null, crter: file.CRTER || null, note1: file.NOTE1 || null, pkgNo: file.PKG_NO || null, file, uploadDate: file.CRTE_DTM || null, isRequiredDoc: false, spans: { uploadId: 1, docNo: 1, pkgNo: 1, revNo: 1, stage: 1, status: 1, actvNo: 1, crter: 1, note1: 1, } }); }); // 2. 업로드 필요 문서 처리 if (!selectedStatus || selectedStatus === "UPLOAD_REQUIRED") { requiredDocs.forEach((doc) => { rows.push({ uploadId: null, docNo: doc.vendorDocNumber, revNo: null, stage: null, status: "UPLOAD_REQUIRED", statusNm: "Upload Required", actvNo: null, actvSeq: null, crter: null, note1: doc.buyerSystemComment, pkgNo: null, file: null, uploadDate: null, isRequiredDoc: true, spans: { uploadId: 1, docNo: 1, pkgNo: 1, revNo: 1, stage: 1, status: 1, actvNo: 1, crter: 1, note1: 1, } }); }); } // 3. 정렬 적용 // 1) BOX_SEQ (DESC) -> 2) OWN_DOC_NO (DESC) -> 3) REV_NO (DESC) -> 4) ACTV_SEQ (DESC) -> 5) CRTE_DTM (DESC) rows.sort((a, b) => { // 1) BOX_SEQ const uploadIdA = a.uploadId || ""; const uploadIdB = b.uploadId || ""; if (uploadIdA !== uploadIdB) { return uploadIdB.localeCompare(uploadIdA); } // 2) OWN_DOC_NO const docNoA = a.docNo || ""; const docNoB = b.docNo || ""; if (docNoA !== docNoB) { return docNoB.localeCompare(docNoA); } // 3) REV_NO const revNoA = a.revNo || ""; const revNoB = b.revNo || ""; if (revNoA !== revNoB) { return revNoB.localeCompare(revNoA); } // 4) ACTV_SEQ const actvSeqA = a.actvSeq || ""; const actvSeqB = b.actvSeq || ""; if (actvSeqA !== actvSeqB) { return actvSeqB.localeCompare(actvSeqA); } // 5) CRTE_DTM const dateA = a.uploadDate || ""; const dateB = b.uploadDate || ""; return dateB.localeCompare(dateA); }); // 4. Row Span 계산 (간단 로직: 위와 같으면 합침) // 정렬된 상태에서 위에서 아래로 내려가며, 이전 행과 값이 같으면 현재 행의 span을 0으로 만들고 leader 행의 span을 증가시킴 // 각 컬럼별 리더 행의 인덱스를 추적 const leaders = { uploadId: 0, docNo: 0, pkgNo: 0, revNo: 0, stage: 0, status: 0, actvNo: 0, crter: 0, note1: 0, }; for (let i = 1; i < rows.length; i++) { const prev = rows[i - 1]; const curr = rows[i]; // 1) Upload ID (최상위) const mergeUploadId = curr.uploadId === prev.uploadId; if (mergeUploadId) { rows[leaders.uploadId].spans.uploadId++; curr.spans.uploadId = 0; } else { leaders.uploadId = i; } // 2) Document No (Upload ID에 종속) const mergeDocNo = mergeUploadId && (curr.docNo === prev.docNo); if (mergeDocNo) { rows[leaders.docNo].spans.docNo++; curr.spans.docNo = 0; } else { leaders.docNo = i; } // 3) PKG NO (Document에 종속) const mergePkgNo = mergeDocNo && (curr.pkgNo === prev.pkgNo); if (mergePkgNo) { rows[leaders.pkgNo].spans.pkgNo++; curr.spans.pkgNo = 0; } else { leaders.pkgNo = i; } // 4) Rev No (Document에 종속) const mergeRevNo = mergeDocNo && (curr.revNo === prev.revNo); if (mergeRevNo) { rows[leaders.revNo].spans.revNo++; curr.spans.revNo = 0; } else { leaders.revNo = i; } // 5) Stage (Rev No에 종속) const mergeStage = mergeRevNo && (curr.stage === prev.stage); if (mergeStage) { rows[leaders.stage].spans.stage++; curr.spans.stage = 0; } else { leaders.stage = i; } // 6) Activity (Rev No에 종속 - 보통 Activity는 Rev 안에 있음) const mergeActvNo = mergeRevNo && (curr.actvSeq === prev.actvSeq); if (mergeActvNo) { rows[leaders.actvNo].spans.actvNo++; curr.spans.actvNo = 0; } else { leaders.actvNo = i; } // 7) Status (Activity 내 파일들 - Activity에 종속) // Activity가 같고 Status 값도 같으면 합침 const mergeStatus = mergeActvNo && (curr.status === prev.status); if (mergeStatus) { rows[leaders.status].spans.status++; curr.spans.status = 0; } else { leaders.status = i; } // 8) CRTER (Activity에 종속) const mergeCrter = mergeActvNo && (curr.crter === prev.crter); if (mergeCrter) { rows[leaders.crter].spans.crter++; curr.spans.crter = 0; } else { leaders.crter = i; } // 9) DC Note (Activity에 종속) const mergeNote1 = mergeActvNo && (curr.note1 === prev.note1); if (mergeNote1) { rows[leaders.note1].spans.note1++; curr.spans.note1 = 0; } else { leaders.note1 = i; } } return rows; }, [files, requiredDocs, selectedStatus]); // 선택 가능한 파일들 (Standby 상태만) const selectableFiles = useMemo(() => { return tableRows .filter((row) => row.file && row.status === "SCW01") .map((row) => `${row.file!.BOX_SEQ}_${row.file!.FILE_SEQ}`); }, [tableRows]); // 전체 선택/해제 const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedFiles(new Set(selectableFiles)); } else { setSelectedFiles(new Set()); } }; // 개별 선택/해제 const handleSelectFile = (fileKey: string, checked: boolean | "indeterminate") => { const isChecked = checked === true; setSelectedFiles((prev) => { const newSet = new Set(prev); if (isChecked) { newSet.add(fileKey); } else { newSet.delete(fileKey); } return newSet; }); }; // 선택된 파일 일괄 취소 const handleBulkCancel = async () => { if (selectedFiles.size === 0) { toast.error("Please select files to cancel"); return; } const filesToCancel = tableRows.filter((row) => row.file && selectedFiles.has(`${row.file.BOX_SEQ}_${row.file.FILE_SEQ}`) ); if (filesToCancel.length === 0) { toast.error("No files to cancel"); return; } try { toast.info(`Canceling ${filesToCancel.length} files...`); // 병렬 취소 const cancelPromises = filesToCancel.map((row) => cancelVendorUploadedFile({ boxSeq: row.file!.BOX_SEQ!, actvSeq: row.file!.ACTV_SEQ!, userId, }) ); await Promise.all(cancelPromises); toast.success(`Canceled ${filesToCancel.length} files`); setSelectedFiles(new Set()); // 선택 초기화 // 페이지 리프레시 window.location.reload(); } catch (error) { console.error("Bulk cancel failed:", error); toast.error("Failed to cancel some files"); } }; const handleDownloadFile = async (file: SwpFileApiResponse) => { try { toast.info("Preparing file download..."); // API route를 통해 다운로드 const downloadUrl = `/api/swp/download/${encodeURIComponent(file.OWN_DOC_NO)}?projNo=${encodeURIComponent(projNo)}&fileName=${encodeURIComponent(file.FILE_NM)}`; // 새 탭에서 다운로드 window.open(downloadUrl, "_blank"); toast.success(`File download started: ${file.FILE_NM}`); } catch (error) { console.error("File download failed:", error); toast.error("Failed to download file"); } }; // 행 클릭 핸들러 (Document No 기준 전체 이력 보기) const handleRowClick = (docNo: string) => { setSelectedDocNo(docNo); setHistoryDialogOpen(true); }; const getStatusBadge = (status: string | null, statusNm: string | null) => { const displayStatus = statusNm || status || "-"; if (!status) return {displayStatus}; const color = status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800" : status === "SCW02" ? "bg-blue-100 text-blue-800" : status === "SCW01" ? "bg-yellow-100 text-yellow-800" : status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800" : status === "SCW07" ? "bg-purple-100 text-purple-800" : status === "SCW09" ? "bg-gray-100 text-gray-800" : status === "SCW00" ? "bg-orange-100 text-orange-800" : "bg-gray-100 text-gray-800"; return ( {displayStatus} ); }; if (files.length === 0 && requiredDocs.length === 0) { return (
No uploaded files.
); } return (
{/* Status 필터 UI */}
{statusCounts.map((statusCount) => ( ))}
{/* 선택된 파일 정보 및 일괄 취소 버튼 */} {selectedFiles.size > 0 && (
{selectedFiles.size} selected
)}
{/* 테이블 */} {tableRows.length === 0 ? (
No files with this status.
) : (
0 && selectedFiles.size === selectableFiles.length} onCheckedChange={handleSelectAll} disabled={selectableFiles.length === 0} /> Upload ID Document No PKG NO Rev No Stage Status Activity Upload ID (User) DC Note Attachment File Upload Date {tableRows.map((row, idx) => { const fileKey = row.file ? `${row.file.BOX_SEQ}_${row.file.FILE_SEQ}` : `required_${idx}`; const canSelect = row.status === "SCW01" && row.file; // Standby이고 file이 있는 경우만 선택 가능 const isSelected = canSelect && selectedFiles.has(fileKey); return ( handleRowClick(row.docNo)} > {/* Select Checkbox */} e.stopPropagation()}> {canSelect ? ( handleSelectFile(fileKey, checked)} /> ) : null} {/* Upload ID */} {row.spans.uploadId > 0 ? ( {row.uploadId || -} ) : null} {/* Document No */} {row.spans.docNo > 0 ? ( {row.docNo} ) : null} {/* PKG NO */} {row.spans.pkgNo > 0 ? ( {row.pkgNo || -} ) : null} {/* Rev No */} {row.spans.revNo > 0 ? ( {row.revNo || -} ) : null} {/* Stage */} {row.spans.stage > 0 ? ( {row.stage || -} ) : null} {/* Status */} {row.spans.status > 0 ? ( {getStatusBadge(row.status, row.statusNm)} ) : null} {/* Activity */} {row.spans.actvNo > 0 ? ( {row.actvNo || -} ) : null} {/* CRTER (Upload ID User) */} {row.spans.crter > 0 ? ( {row.crter || -} ) : null} {/* DC Note */} {row.spans.note1 > 0 ? ( { if (row.note1) { e.stopPropagation(); setNoteDialogContent({ title: "DC Note", content: row.note1 }); setNoteDialogOpen(true); } }} > {row.note1 ? (
{row.note1}
) : ( - )}
) : null} {/* Attachment File - 병합하지 않음 */} {row.file ? (
{row.file.FILE_NM}
) : ( - )}
{/* Upload Date - 병합하지 않음 */} {row.uploadDate ? formatSwpDate(row.uploadDate) : -}
); })}
)} {/* Document 전체 이력 Dialog */} {/* Note 전체 내용 Dialog */}
); }