// simple-basic-contracts-detail-columns.tsx "use client" import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" import { formatDateTime } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { Button } from "@/components/ui/button" import { MoreHorizontal, Download, Eye, Mail, FileText, Clock, MessageCircle, Loader2 } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { BasicContractView } from "@/db/schema" import { downloadFile, quickPreview } from "@/lib/file-download" import { toast } from "sonner" import { useRouter } from "next/navigation" import { getComplianceResponseByBasicContractId } from "@/lib/compliance/services" type RedFlagResolutionState = { resolved: boolean resolvedAt: Date | null pendingApprovalId: string | null } export interface GetColumnsProps { setRowAction: React.Dispatch | null>> gtcData: Record isLoadingGtcData: boolean agreementCommentData: Record isLoadingAgreementCommentData: boolean redFlagData: Record isLoadingRedFlagData: boolean redFlagResolutionData: Record isLoadingRedFlagResolutionData: boolean isComplianceTemplate: boolean router: NextRouter; } type NextRouter = ReturnType; const CONTRACT_STATUS_CONFIG = { PENDING: { label: "발송완료", color: "gray" }, VENDOR_SIGNED: { label: "협력업체 서명완료", color: "blue" }, BUYER_SIGNED: { label: "구매팀 서명완료", color: "green" }, LEGAL_REVIEW_REQUESTED: { label: "법무검토 요청", color: "purple" }, LEGAL_REVIEW_COMPLETED: { label: "법무검토 완료", color: "indigo" }, COMPLETED: { label: "계약완료", color: "emerald" }, REJECTED: { label: "거절됨", color: "red" }, } as const export function getDetailColumns({ setRowAction, gtcData, isLoadingGtcData, agreementCommentData, isLoadingAgreementCommentData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, isLoadingRedFlagResolutionData, isComplianceTemplate, router }: GetColumnsProps): ColumnDef[] { const selectColumn: ColumnDef = { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Select row" className="translate-y-0.5" /> ), maxSize: 30, enableSorting: false, enableHiding: false, } const actionsColumn: ColumnDef = { id: "actions", header: "작업", cell: ({ row }) => { const contract = row.original const hasSignedFile = contract.signedFilePath && contract.signedFileName const handleDownload = async () => { if (!hasSignedFile) { toast.error("다운로드할 파일이 없습니다") return } await downloadFile( contract.signedFilePath!, contract.signedFileName!, { action: 'download', showToast: true, onError: (error) => { console.error("Download failed:", error) }, onSuccess: (fileName, fileSize) => { console.log(`Downloaded: ${fileName} (${fileSize} bytes)`) } } ) } const handlePreview = async () => { if (!hasSignedFile) { toast.error("미리볼 파일이 없습니다") return } await quickPreview(contract.signedFilePath!, contract.signedFileName!) } const handleResend = () => { setRowAction({ type: "resend", row } as DataTableRowAction) } return ( {hasSignedFile && ( <> 파일 미리보기 파일 다운로드 )} 재발송 ) }, enableSorting: false, enableHiding: false, maxSize: 80, } // Red Flag 발생여부 컬럼 (준법서약 템플릿만) const redFlagColumn: ColumnDef = { id: "redFlag", header: ({ column }) => ( ), cell: ({ row }) => { const contract = row.original; const contractId = contract.id; // 로딩 중이면 로딩 표시 if (isLoadingRedFlagData) { return ; } const hasRedFlag = redFlagData[contractId] || false; if (hasRedFlag) { return ( Red Flag ); } return (
-
); }, minSize: 120, enableHiding: false, } // Red Flag 해제 컬럼 (준법서약 템플릿만) const redFlagResolutionColumn: ColumnDef = { id: "redFlagResolution", header: ({ column }) => ( ), cell: ({ row }) => { const contract = row.original; const contractId = contract.id; // 로딩 중이면 로딩 표시 if (isLoadingRedFlagResolutionData) { return ; } const resolution = redFlagResolutionData[contractId]; if (resolution?.resolved && resolution.resolvedAt) { return (
해제됨
{formatDateTime(resolution.resolvedAt, "KR")}
); } if (resolution?.pendingApprovalId) { return (
해소요청 진행중
결재 ID: {resolution.pendingApprovalId.slice(-6)}
); } return (
-
); }, minSize: 140, enableHiding: false, } // 기본 컬럼 배열 const baseColumns: ColumnDef[] = [ selectColumn, // 업체 코드 { accessorKey: "vendorCode", header: ({ column }) => ( ), cell: ({ row }) => { const code = row.getValue("vendorCode") as string | null return code ? ( {code} ) : "-" }, minSize: 120, }, // 업체명 (GTC 정보 포함) { accessorKey: "vendorName", header: ({ column }) => ( ), cell: ({ row }) => { const name = row.getValue("vendorName") as string | null const contract = row.original const isGTCTemplate = contract.templateName?.includes('GTC') const isComplianceContract = contract.templateName?.includes('준법') const contractGtcData = gtcData[contract.id] const complianceNegotiation = agreementCommentData[contract.id] const hasComplianceRedFlag = !!redFlagData[contract.id] const isNegotiationCompleted = !!contract.negotiationCompletedAt const handleOpenGTC = (e: React.MouseEvent) => { e.stopPropagation() // 상세보기와 동일하게 contract.id를 경로 파라미터로 사용 const params = new URLSearchParams(); params.set("contractId", contract.id.toString()); if (contract.templateId) { params.set("templateId", contract.templateId.toString()); } if (contract.vendorId) { params.set("vendorId", contract.vendorId.toString()); } if (contract.vendorName) { params.set("vendorName", contract.vendorName); } const query = params.toString(); const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contract.id}${query ? `?${query}` : ""}`; window.open(gtcUrl, '_blank'); } return (
{name || "-"}
{isGTCTemplate && (
{isLoadingGtcData ? ( ) : contractGtcData ? (
{contractGtcData.hasComments && ( 협의이력 )}
) : ( GTC )}
)} {isComplianceContract && (
{isLoadingAgreementCommentData ? ( ) : isNegotiationCompleted ? ( 협의 완료 ) : complianceNegotiation?.hasComments ? ( { event.stopPropagation(); if (typeof window === "undefined") return; const params = new URLSearchParams(); if (contract.templateId) { params.set("templateId", contract.templateId.toString()); } if (contract.vendorId) { params.set("vendorId", contract.vendorId.toString()); } if (contract.vendorName) { params.set("vendorName", contract.vendorName); } const query = params.toString(); const complianceUrl = `/evcp/basic-contract/compliance-comments/${contract.id}${query ? `?${query}` : ""}`; window.open(complianceUrl, "_blank", "noopener,noreferrer"); }} style={{ cursor: "pointer" }} > 협의 진행중 ({complianceNegotiation.commentCount}) ) : ( hasComplianceRedFlag && !isNegotiationCompleted && ( { event.stopPropagation() if (typeof window === "undefined") return const params = new URLSearchParams() if (contract.templateId) { params.set("templateId", contract.templateId.toString()) } if (contract.vendorId) { params.set("vendorId", contract.vendorId.toString()) } if (contract.vendorName) { params.set("vendorName", contract.vendorName) } const query = params.toString() const complianceUrl = `/evcp/basic-contract/compliance-comments/${contract.id}${query ? `?${query}` : ""}` window.open(complianceUrl, "_blank", "noopener,noreferrer") }} style={{ cursor: "pointer" }} > 협의 코멘트 작성 ) )}
)}
) }, minSize: 250, }, // 진행상태 { accessorKey: "status", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.getValue("status") as keyof typeof CONTRACT_STATUS_CONFIG const config = CONTRACT_STATUS_CONFIG[status] || { label: status, color: "gray" } const variantMap = { gray: "secondary", blue: "default", green: "default", purple: "secondary", indigo: "secondary", emerald: "default", red: "destructive", } as const return ( {config.label} ) }, minSize: 140, filterFn: (row, id, value) => { return value.includes(row.getValue(id)) }, }, // 요청일 { accessorKey: "createdAt", header: ({ column }) => ( ), cell: ({ row }) => { const date = row.getValue("createdAt") as Date return (
{formatDateTime(date, "KR")}
) }, minSize: 130, }, // 마감일 { accessorKey: "deadline", header: ({ column }) => ( ), cell: ({ row }) => { const deadline = row.getValue("deadline") as string | null const status = row.getValue("status") as string if (!deadline) return "-" const deadlineDate = new Date(deadline) const today = new Date() const isOverdue = deadlineDate < today && !["COMPLETED", "REJECTED"].includes(status) const isNearDeadline = !isOverdue && deadlineDate.getTime() - today.getTime() < 2 * 24 * 60 * 60 * 1000 // 2일 이내 return (
{deadlineDate.toLocaleDateString('ko-KR')}
{isOverdue &&
(지연)
} {isNearDeadline && !isOverdue &&
(임박)
}
) }, minSize: 120, }, // 협력업체 서명일 { accessorKey: "vendorSignedAt", header: ({ column }) => ( ), cell: ({ row }) => { const date = row.getValue("vendorSignedAt") as Date | null return date ? (
완료
{formatDateTime(date, "KR")}
) : (
미완료
) }, minSize: 130, }, // 법무검토 상태 { accessorKey: "legalReviewStatus", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.getValue("legalReviewStatus") as string | null // PRGS_STAT_DSC 연동값 우선 표시 if (status) { return
{status}
} // 동기화된 값이 없으면 빈 값 처리 return
-
}, minSize: 140, }, // 계약완료일 { accessorKey: "completedAt", header: ({ column }) => ( ), cell: ({ row }) => { const date = row.getValue("completedAt") as Date | null return date ? (
완료
{formatDateTime(date, "KR")}
) : (
미완료
) }, minSize: 120, }, // 서명된 파일 { accessorKey: "signedFileName", header: ({ column }) => ( ), cell: ({ row }) => { const fileName = row.getValue("signedFileName") as string | null const filePath = row.original.signedFilePath const vendorSignedAt = row.original.vendorSignedAt if (!fileName || !filePath|| !vendorSignedAt) { return
파일 없음
} const handleQuickDownload = async (e: React.MouseEvent) => { e.stopPropagation() await downloadFile(filePath, fileName, { action: 'download', showToast: true }) } const handleQuickPreview = async (e: React.MouseEvent) => { e.stopPropagation() await quickPreview(filePath, fileName) } return (
서명파일
클릭하여 다운로드
) }, minSize: 200, enableSorting: false, }, actionsColumn, ] // 준법서약 템플릿인 경우 Red Flag 컬럼과 해제 컬럼을 법무검토 상태 뒤에 추가 if (isComplianceTemplate) { const legalReviewStatusIndex = baseColumns.findIndex((col) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (col as any).accessorKey === 'legalReviewStatus' }) if (legalReviewStatusIndex !== -1) { baseColumns.splice(legalReviewStatusIndex + 1, 0, redFlagColumn, redFlagResolutionColumn) } } return baseColumns }