// 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" interface GetColumnsProps { setRowAction: React.Dispatch | null>> gtcData: Record isLoadingGtcData: 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, 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 }) } return ( {hasSignedFile && ( <> 파일 미리보기 파일 다운로드 )} 재발송 { // 준법서약 템플릿인 경우 compliance 응답 페이지로 이동 if (contract.templateName?.includes('준법')) { try { const response = await getComplianceResponseByBasicContractId(contract.id); if (response) { router.push(`/evcp/compliance/${response.templateId}/responses/${response.id}`); } else { toast.error("준법서약 응답을 찾을 수 없습니다."); setRowAction({ type: "view", row }); } } catch (error) { console.error("Error fetching compliance response:", error); toast.error("응답 정보를 가져오는데 실패했습니다."); } } else { setRowAction({ type: "view", row }); } }}> 상세 정보 ) }, enableSorting: false, enableHiding: false, maxSize: 80, } return [ 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 contractGtcData = gtcData[contract.id] const handleOpenGTC = (e: React.MouseEvent) => { e.stopPropagation() if (contractGtcData?.gtcDocumentId) { const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contractGtcData.gtcDocumentId}?vendorId=${contract.vendorId}&vendorName=${encodeURIComponent(contract.vendorName || '')}&contractId=${contract.id}&templateId=${contract.templateId}` window.open(gtcUrl, '_blank') } } return (
{name || "-"}
{isGTCTemplate && (
{isLoadingGtcData ? ( ) : contractGtcData ? (
{contractGtcData.hasComments && ( 협의이력 )}
) : ( GTC )}
)}
) }, 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: "legalReviewRequestedAt", header: ({ column }) => ( ), cell: ({ row }) => { const date = row.getValue("legalReviewRequestedAt") as Date | null return date ? (
요청됨
{formatDateTime(date, "KR")}
) : (
-
) }, minSize: 140, }, // 법무검토 완료일 { accessorKey: "legalReviewCompletedAt", header: ({ column }) => ( ), cell: ({ row }) => { const date = row.getValue("legalReviewCompletedAt") as Date | null const requestedDate = row.getValue("legalReviewRequestedAt") as Date | null return date ? (
완료
{formatDateTime(date, "KR")}
) : requestedDate ? (
진행중
검토 대기
) : (
-
) }, 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, ] }