From 7548e2ad6948f1c6aa102fcac408bc6c9c0f9796 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 27 Aug 2025 12:06:26 +0000 Subject: (대표님, 최겸) 기본계약, 입찰, 파일라우트, 계약서명라우트, 인포메이션, 메뉴설정, PQ(메일템플릿 관련) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../basic-contracts-detail-columns.tsx | 418 +++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx (limited to 'lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx') diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx new file mode 100644 index 00000000..80c39d1e --- /dev/null +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -0,0 +1,418 @@ +// 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 } 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" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +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 }: 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 && ( + <> + + + 파일 미리보기 + + + + 파일 다운로드 + + + )} + + + 재발송 + + 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, + }, + + // 업체명 + { + accessorKey: "vendorName", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const name = row.getValue("vendorName") as string | null + return ( +
{name || "-"}
+ ) + }, + minSize: 180, + }, + + // 진행상태 + { + 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, + ] +} \ No newline at end of file -- cgit v1.2.3