diff options
Diffstat (limited to 'lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx')
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx | 418 |
1 files changed, 418 insertions, 0 deletions
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<React.SetStateAction<DataTableRowAction<BasicContractView> | 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<BasicContractView>[] { + + const selectColumn: ColumnDef<BasicContractView> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + const actionsColumn: ColumnDef<BasicContractView> = { + 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 ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">Open menu</span> + <MoreHorizontal className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {hasSignedFile && ( + <> + <DropdownMenuItem onClick={handlePreview}> + <Eye className="mr-2 h-4 w-4" /> + 파일 미리보기 + </DropdownMenuItem> + <DropdownMenuItem onClick={handleDownload}> + <Download className="mr-2 h-4 w-4" /> + 파일 다운로드 + </DropdownMenuItem> + </> + )} + <DropdownMenuItem onClick={handleResend}> + <Mail className="mr-2 h-4 w-4" /> + 재발송 + </DropdownMenuItem> + <DropdownMenuItem onClick={() => setRowAction({ type: "view", row })}> + <FileText className="mr-2 h-4 w-4" /> + 상세 정보 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + enableSorting: false, + enableHiding: false, + maxSize: 80, + } + + return [ + selectColumn, + + // 업체 코드 + { + accessorKey: "vendorCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="업체코드" /> + ), + cell: ({ row }) => { + const code = row.getValue("vendorCode") as string | null + return code ? ( + <span className="font-mono text-sm bg-gray-100 px-2 py-1 rounded"> + {code} + </span> + ) : "-" + }, + minSize: 120, + }, + + // 업체명 + { + accessorKey: "vendorName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="업체명" /> + ), + cell: ({ row }) => { + const name = row.getValue("vendorName") as string | null + return ( + <div className="font-medium">{name || "-"}</div> + ) + }, + minSize: 180, + }, + + // 진행상태 + { + accessorKey: "status", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="진행상태" /> + ), + 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 ( + <Badge variant={variantMap[config.color as keyof typeof variantMap]}> + {config.label} + </Badge> + ) + }, + minSize: 140, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + + // 요청일 + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="요청일" /> + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date + return ( + <div className="text-sm"> + <div>{formatDateTime(date, "KR")}</div> + </div> + ) + }, + minSize: 130, + }, + + // 마감일 + { + accessorKey: "deadline", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="마감일" /> + ), + 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 ( + <div className={`text-sm flex items-center gap-1 ${ + isOverdue ? 'text-red-600 font-medium' : + isNearDeadline ? 'text-orange-600' : + 'text-gray-900' + }`}> + <Clock className="h-3 w-3" /> + <div> + <div>{deadlineDate.toLocaleDateString('ko-KR')}</div> + {isOverdue && <div className="text-xs">(지연)</div>} + {isNearDeadline && !isOverdue && <div className="text-xs">(임박)</div>} + </div> + </div> + ) + }, + minSize: 120, + }, + + // 협력업체 서명일 + { + accessorKey: "vendorSignedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체 서명" /> + ), + cell: ({ row }) => { + const date = row.getValue("vendorSignedAt") as Date | null + return date ? ( + <div className="text-sm text-blue-600"> + <div className="font-medium">완료</div> + <div className="text-xs">{formatDateTime(date, "KR")}</div> + </div> + ) : ( + <div className="text-sm text-gray-400">미완료</div> + ) + }, + minSize: 130, + }, + + // 법무검토 요청일 + { + accessorKey: "legalReviewRequestedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="법무검토 요청" /> + ), + cell: ({ row }) => { + const date = row.getValue("legalReviewRequestedAt") as Date | null + return date ? ( + <div className="text-sm text-purple-600"> + <div className="font-medium">요청됨</div> + <div className="text-xs">{formatDateTime(date, "KR")}</div> + </div> + ) : ( + <div className="text-sm text-gray-400">-</div> + ) + }, + minSize: 140, + }, + + // 법무검토 완료일 + { + accessorKey: "legalReviewCompletedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="법무검토 완료" /> + ), + cell: ({ row }) => { + const date = row.getValue("legalReviewCompletedAt") as Date | null + const requestedDate = row.getValue("legalReviewRequestedAt") as Date | null + + return date ? ( + <div className="text-sm text-indigo-600"> + <div className="font-medium">완료</div> + <div className="text-xs">{formatDateTime(date, "KR")}</div> + </div> + ) : requestedDate ? ( + <div className="text-sm text-orange-500"> + <div className="font-medium">진행중</div> + <div className="text-xs">검토 대기</div> + </div> + ) : ( + <div className="text-sm text-gray-400">-</div> + ) + }, + minSize: 140, + }, + + // 계약완료일 + { + accessorKey: "completedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약완료" /> + ), + cell: ({ row }) => { + const date = row.getValue("completedAt") as Date | null + return date ? ( + <div className="text-sm text-emerald-600"> + <div className="font-medium">완료</div> + <div className="text-xs">{formatDateTime(date, "KR")}</div> + </div> + ) : ( + <div className="text-sm text-gray-400">미완료</div> + ) + }, + minSize: 120, + }, + + + // 서명된 파일 + { + accessorKey: "signedFileName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="서명파일" /> + ), + 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 <div className="text-sm text-gray-400">파일 없음</div> + } + + 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 ( + <div className="flex items-center gap-2"> + <div className="text-sm"> + <div className="font-medium text-blue-600 truncate max-w-[150px]" title={fileName}> + 서명파일 + </div> + <div className="text-xs text-gray-500">클릭하여 다운로드</div> + </div> + <div className="flex gap-1"> + <Button + variant="ghost" + size="sm" + onClick={handleQuickPreview} + className="h-6 w-6 p-0" + title="미리보기" + > + <Eye className="h-3 w-3" /> + </Button> + <Button + variant="ghost" + size="sm" + onClick={handleQuickDownload} + className="h-6 w-6 p-0" + title="다운로드" + > + <Download className="h-3 w-3" /> + </Button> + </div> + </div> + ) + }, + minSize: 200, + enableSorting: false, + }, + + actionsColumn, + ] +}
\ No newline at end of file |
