summaryrefslogtreecommitdiff
path: root/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
diff options
context:
space:
mode:
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.tsx418
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