"use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Plus, Send, Eye, Edit, Trash2, Building2, Calendar, DollarSign, FileText, RefreshCw, Mail, CheckCircle, Clock, XCircle, AlertCircle, Settings2, ClipboardList, Globe, Package, MapPin, Info, Loader2 } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; import { ClientDataTable } from "@/components/client-data-table/data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import type { DataTableAdvancedFilterField } from "@/types/table"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { AddVendorDialog } from "./add-vendor-dialog"; import { BatchUpdateConditionsDialog } from "./batch-update-conditions-dialog"; import { SendRfqDialog } from "./send-rfq-dialog"; // import { VendorDetailDialog } from "./vendor-detail-dialog"; // import { sendRfqToVendors } from "@/app/actions/rfq/send-rfq.action"; import { getRfqSendData, getSelectedVendorsWithEmails, type RfqSendData, type VendorEmailInfo } from "../service" // 타입 정의 interface RfqDetail { detailId: number; vendorId: number | null; vendorName: string | null; vendorCode: string | null; vendorCountry: string | null; vendorEmail?: string | null; vendorCategory?: string | null; vendorGrade?: string | null; basicContract?: string | null; shortList: boolean; currency: string | null; paymentTermsCode: string | null; paymentTermsDescription: string | null; incotermsCode: string | null; incotermsDescription: string | null; incotermsDetail?: string | null; deliveryDate: Date | null; contractDuration: string | null; taxCode: string | null; placeOfShipping?: string | null; placeOfDestination?: string | null; materialPriceRelatedYn?: boolean | null; sparepartYn?: boolean | null; firstYn?: boolean | null; firstDescription?: string | null; sparepartDescription?: string | null; updatedAt?: Date | null; updatedByUserName?: string | null; } interface VendorResponse { id: number; vendorId: number; status: "초대됨" | "작성중" | "제출완료" | "수정요청" | "최종확정" | "취소"; responseVersion: number; isLatest: boolean; submittedAt: Date | null; totalAmount: number | null; currency: string | null; vendorDeliveryDate: Date | null; quotedItemCount?: number; attachmentCount?: number; } // Props 타입 정의 interface RfqVendorTableProps { rfqId: number; rfqCode?: string; rfqDetails: RfqDetail[]; vendorResponses: VendorResponse[]; rfqInfo?: { rfqTitle: string; rfqType: string; projectCode?: string; projectName?: string; picName?: string; picCode?: string; picTeam?: string; packageNo?: string; packageName?: string; designPicName?: string; designTeam?: string; materialGroup?: string; materialGroupDesc?: string; dueDate: Date; quotationType?: string; evaluationApply?: boolean; contractType?: string; }; attachments?: Array<{ id: number; attachmentType: string; serialNo: string; currentRevision: string; description?: string; fileName?: string; fileSize?: number; uploadedAt?: Date; }>; } // 상태별 아이콘 반환 const getStatusIcon = (status: string) => { switch (status) { case "초대됨": return ; case "작성중": return ; case "제출완료": return ; case "수정요청": return ; case "최종확정": return ; case "취소": return ; default: return ; } }; // 상태별 색상 const getStatusVariant = (status: string) => { switch (status) { case "초대됨": return "secondary"; case "작성중": return "outline"; case "제출완료": return "default"; case "수정요청": return "warning"; case "최종확정": return "success"; case "취소": return "destructive"; default: return "outline"; } }; // 데이터 병합 (rfqDetails + vendorResponses) const mergeVendorData = ( rfqDetails: RfqDetail[], vendorResponses: VendorResponse[], rfqCode?: string ): (RfqDetail & { response?: VendorResponse; rfqCode?: string })[] => { return rfqDetails.map(detail => { const response = vendorResponses.find( r => r.vendorId === detail.vendorId && r.isLatest ); return { ...detail, response, rfqCode }; }); }; // 추가 조건 포맷팅 const formatAdditionalConditions = (data: any) => { const conditions = []; if (data.firstYn) conditions.push("초도품"); if (data.materialPriceRelatedYn) conditions.push("연동제"); if (data.sparepartYn) conditions.push("스페어"); return conditions.length > 0 ? conditions.join(", ") : "-"; }; export function RfqVendorTable({ rfqId, rfqCode, rfqDetails, vendorResponses, rfqInfo, attachments, }: RfqVendorTableProps) { const [isRefreshing, setIsRefreshing] = React.useState(false); const [selectedRows, setSelectedRows] = React.useState([]); const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false); const [isBatchUpdateOpen, setIsBatchUpdateOpen] = React.useState(false); const [selectedVendor, setSelectedVendor] = React.useState(null); const [isSendDialogOpen, setIsSendDialogOpen] = React.useState(false); const [isLoadingSendData, setIsLoadingSendData] = React.useState(false); const [sendDialogData, setSendDialogData] = React.useState<{ rfqInfo: RfqSendData['rfqInfo'] | null; attachments: RfqSendData['attachments']; selectedVendors: VendorEmailInfo[]; }>({ rfqInfo: null, attachments: [], selectedVendors: [], }); // 데이터 병합 const mergedData = React.useMemo( () => mergeVendorData(rfqDetails, vendorResponses, rfqCode), [rfqDetails, vendorResponses, rfqCode] ); // 일괄 발송 핸들러 const handleBulkSend = React.useCallback(async () => { if (selectedRows.length === 0) { toast.warning("발송할 벤더를 선택해주세요."); return; } try { setIsLoadingSendData(true); // 선택된 벤더 ID들 추출 const selectedVendorIds = selectedRows .map(row => row.vendorId) .filter(id => id != null); if (selectedVendorIds.length === 0) { toast.error("유효한 벤더가 선택되지 않았습니다."); return; } // 병렬로 데이터 가져오기 (에러 처리 포함) const [rfqSendData, vendorEmailInfos] = await Promise.all([ getRfqSendData(rfqId), getSelectedVendorsWithEmails(rfqId, selectedVendorIds) ]); // 데이터 검증 if (!rfqSendData?.rfqInfo) { toast.error("RFQ 정보를 불러올 수 없습니다."); return; } if (!vendorEmailInfos || vendorEmailInfos.length === 0) { toast.error("선택된 벤더의 이메일 정보를 찾을 수 없습니다."); return; } // 다이얼로그 데이터 설정 setSendDialogData({ rfqInfo: rfqSendData.rfqInfo, attachments: rfqSendData.attachments || [], selectedVendors: vendorEmailInfos.map(v => ({ vendorId: v.vendorId, vendorName: v.vendorName, vendorCode: v.vendorCode, vendorCountry: v.vendorCountry, vendorEmail: v.vendorEmail, representativeEmail: v.representativeEmail, contacts: v.contacts || [], contactsByPosition: v.contactsByPosition || {}, primaryEmail: v.primaryEmail, currency: v.currency, })), }); // 다이얼로그 열기 setIsSendDialogOpen(true); } catch (error) { console.error("RFQ 발송 데이터 로드 실패:", error); toast.error("데이터를 불러오는데 실패했습니다. 다시 시도해주세요."); } finally { setIsLoadingSendData(false); } }, [selectedRows, rfqId]); // RFQ 발송 핸들러 const handleSendRfq = React.useCallback(async (data: { vendors: Array<{ vendorId: number; vendorName: string; vendorCode?: string | null; vendorCountry?: string | null; vendorEmail?: string | null; currency?: string | null; additionalRecipients: string[]; }>; attachments: number[]; message?: string; }) => { try { // 서버 액션 호출 // const result = await sendRfqToVendors({ // rfqId, // rfqCode, // vendors: data.vendors, // attachmentIds: data.attachments, // message: data.message, // }); // 임시 성공 처리 console.log("RFQ 발송 데이터:", data); // 성공 후 처리 setSelectedRows([]); setSendDialogData({ rfqInfo: null, attachments: [], selectedVendors: [], }); toast.success(`${data.vendors.length}개 업체에 RFQ를 발송했습니다.`); } catch (error) { console.error("RFQ 발송 실패:", error); toast.error("RFQ 발송에 실패했습니다."); throw error; } }, [rfqId, rfqCode]); // 액션 처리 const handleAction = React.useCallback(async (action: string, vendor: any) => { switch (action) { case "view": setSelectedVendor(vendor); break; case "send": // 개별 RFQ 발송 try { setIsLoadingSendData(true); const [rfqSendData, vendorEmailInfos] = await Promise.all([ getRfqSendData(rfqId), getSelectedVendorsWithEmails(rfqId, [vendor.vendorId]) ]); if (!rfqSendData?.rfqInfo || !vendorEmailInfos || vendorEmailInfos.length === 0) { toast.error("벤더 정보를 불러올 수 없습니다."); return; } setSendDialogData({ rfqInfo: rfqSendData.rfqInfo, attachments: rfqSendData.attachments || [], selectedVendors: vendorEmailInfos.map(v => ({ vendorId: v.vendorId, vendorName: v.vendorName, vendorCode: v.vendorCode, vendorCountry: v.vendorCountry, vendorEmail: v.vendorEmail, representativeEmail: v.representativeEmail, contacts: v.contacts || [], contactsByPosition: v.contactsByPosition || {}, primaryEmail: v.primaryEmail, currency: v.currency, })), }); setIsSendDialogOpen(true); } catch (error) { console.error("개별 발송 데이터 로드 실패:", error); toast.error("데이터를 불러오는데 실패했습니다."); } finally { setIsLoadingSendData(false); } break; case "edit": toast.info("수정 기능은 준비중입니다."); break; case "delete": if (confirm(`${vendor.vendorName}을(를) 삭제하시겠습니까?`)) { toast.success(`${vendor.vendorName}이(가) 삭제되었습니다.`); } break; case "response-detail": toast.info(`${vendor.vendorName}의 회신 상세를 확인합니다.`); break; } }, [rfqId]); // 컬럼 정의 const columns: ColumnDef[] = React.useMemo(() => [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!v)} aria-label="select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!v)} aria-label="select row" className="translate-y-0.5" /> ), size: 40, enableSorting: false, enableHiding: false, }, { accessorKey: "rfqCode", header: ({ column }) => , cell: ({ row }) => { return ( {row.original.rfqCode || "-"} ); }, size: 120, }, { accessorKey: "vendorName", header: ({ column }) => , cell: ({ row }) => { const vendor = row.original; return (
{vendor.vendorName || "-"} {vendor.vendorCode}
); }, size: 180, }, { accessorKey: "vendorCategory", header: ({ column }) => , cell: ({ row }) => row.original.vendorCategory || "-", size: 100, }, { accessorKey: "vendorCountry", header: ({ column }) => , cell: ({ row }) => { const country = row.original.vendorCountry; const isLocal = country === "KR" || country === "한국"; return ( {country || "-"} ); }, size: 100, }, { accessorKey: "vendorGrade", header: ({ column }) => , cell: ({ row }) => { const grade = row.original.vendorGrade; if (!grade) return -; const gradeColor = { "A": "text-green-600", "B": "text-blue-600", "C": "text-yellow-600", "D": "text-red-600", }[grade] || "text-gray-600"; return {grade}; }, size: 100, }, { accessorKey: "basicContract", header: ({ column }) => , cell: ({ row }) => row.original.basicContract || "-", size: 100, }, { accessorKey: "currency", header: ({ column }) => , cell: ({ row }) => { const currency = row.original.currency; return currency ? ( {currency} ) : ( - ); }, size: 80, }, { accessorKey: "paymentTermsCode", header: ({ column }) => , cell: ({ row }) => { const code = row.original.paymentTermsCode; const desc = row.original.paymentTermsDescription; return ( {code || "-"} {desc && (

{desc}

)}
); }, size: 100, }, { accessorKey: "taxCode", header: ({ column }) => , cell: ({ row }) => row.original.taxCode || "-", size: 60, }, { accessorKey: "deliveryDate", header: ({ column }) => , cell: ({ row }) => { const deliveryDate = row.original.deliveryDate; const contractDuration = row.original.contractDuration; return (
{deliveryDate && !rfqCode?.startsWith("F") && ( {format(new Date(deliveryDate), "yyyy-MM-dd")} )} {contractDuration && rfqCode?.startsWith("F") && ( {contractDuration} )} {!deliveryDate && !contractDuration && ( - )}
); }, size: 120, }, { accessorKey: "incotermsCode", header: ({ column }) => , cell: ({ row }) => { const code = row.original.incotermsCode; const detail = row.original.incotermsDetail; return (
{code || "-"}
{detail && (

{detail}

)}
); }, size: 100, }, { accessorKey: "placeOfShipping", header: ({ column }) => , cell: ({ row }) => { const place = row.original.placeOfShipping; return place ? (
{place}
) : ( - ); }, size: 100, }, { accessorKey: "placeOfDestination", header: ({ column }) => , cell: ({ row }) => { const place = row.original.placeOfDestination; return place ? (
{place}
) : ( - ); }, size: 100, }, { id: "additionalConditions", header: "추가조건", cell: ({ row }) => { const conditions = formatAdditionalConditions(row.original); if (conditions === "-") { return -; } const items = conditions.split(", "); return (
{items.map((item, idx) => ( {item} ))}
); }, size: 120, }, { accessorKey: "response.submittedAt", header: ({ column }) => , cell: ({ row }) => { const submittedAt = row.original.response?.submittedAt; if (!submittedAt) { return 미참여; } return (
참여 {format(new Date(submittedAt), "MM-dd")}
); }, size: 100, }, { id: "responseDetail", header: "회신상세", cell: ({ row }) => { const hasResponse = !!row.original.response?.submittedAt; if (!hasResponse) { return -; } return ( ); }, size: 80, }, { accessorKey: "shortList", header: ({ column }) => , cell: ({ row }) => ( row.original.shortList ? ( 선정 ) : ( 대기 ) ), size: 80, }, { accessorKey: "updatedAt", header: ({ column }) => , cell: ({ row }) => { const date = row.original.updatedAt; return date ? ( {format(new Date(date), "MM-dd HH:mm")} ) : ( - ); }, size: 100, }, { accessorKey: "updatedByUserName", header: ({ column }) => , cell: ({ row }) => { const name = row.original.updatedByUserName; return name ? ( {name} ) : ( - ); }, size: 100, }, { id: "actions", header: "작업", cell: ({ row }) => { const vendor = row.original; const hasResponse = !!vendor.response; return ( handleAction("view", vendor)}> 상세보기 {!hasResponse && ( handleAction("send", vendor)} disabled={isLoadingSendData} > RFQ 발송 )} handleAction("edit", vendor)}> 조건 수정 handleAction("delete", vendor)} className="text-red-600" > 삭제 ); }, size: 60, }, ], [handleAction, rfqCode, isLoadingSendData]); const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "vendorName", label: "벤더명", type: "text" }, { id: "vendorCode", label: "벤더코드", type: "text" }, { id: "vendorCountry", label: "국가", type: "text" }, { id: "response.status", label: "응답 상태", type: "select", options: [ { label: "초대됨", value: "초대됨" }, { label: "작성중", value: "작성중" }, { label: "제출완료", value: "제출완료" }, { label: "수정요청", value: "수정요청" }, { label: "최종확정", value: "최종확정" }, { label: "취소", value: "취소" }, ] }, { id: "shortList", label: "Short List", type: "select", options: [ { label: "선정", value: "true" }, { label: "대기", value: "false" }, ] }, ]; // 선택된 벤더 정보 (BatchUpdate용) const selectedVendorsForBatch = React.useMemo(() => { return selectedRows.map(row => ({ id: row.vendorId, vendorName: row.vendorName, vendorCode: row.vendorCode, })); }, [selectedRows]); // 추가 액션 버튼들 const additionalActions = React.useMemo(() => (
{selectedRows.length > 0 && ( <> )}
), [selectedRows, isRefreshing, isLoadingSendData, handleBulkSend]); return ( <> {additionalActions} {/* 벤더 추가 다이얼로그 */} { toast.success("벤더가 추가되었습니다."); setIsAddDialogOpen(false); }} /> {/* 조건 일괄 설정 다이얼로그 */} { toast.success("조건이 업데이트되었습니다."); setIsBatchUpdateOpen(false); setSelectedRows([]); }} /> {/* RFQ 발송 다이얼로그 */} {/* 벤더 상세 다이얼로그 */} {/* {selectedVendor && ( !open && setSelectedVendor(null)} vendor={selectedVendor} rfqId={rfqId} /> )} */} ); }