diff options
Diffstat (limited to 'lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx')
| -rw-r--r-- | lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx b/lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx new file mode 100644 index 00000000..b8f92fab --- /dev/null +++ b/lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx @@ -0,0 +1,409 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Edit, Trash2, Plus, Copy } from "lucide-react" +import { cn, compareItemNumber, formatDate } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { toast } from "sonner" +import { useSession } from "next-auth/react" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { type GtcClauseTreeView } from "@/db/schema/gtc" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<GtcClauseTreeView> | null>> + documentId: number + hasVendorInfo?: boolean + +} + +export function getColumns({ setRowAction, documentId, hasVendorInfo }: GetColumnsProps): ColumnDef<any>[] { + // 1) select + const selectColumn: ColumnDef<GtcClauseTreeView> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(v) => row.toggleSelected(!!v)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + } + + // 2) 조항 정보 + const clauseInfoColumns: ColumnDef<GtcClauseTreeView>[] = [ + { + accessorKey: "itemNumber", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="채번" />, + cell: ({ row }) => { + const itemNumber = row.getValue("itemNumber") as string + const depth = row.original.depth + const childrenCount = row.original.childrenCount + const isModified = row.original.vendorInfo?.isNumberModified + + return ( + <div className="flex items-center gap-2"> + <div style={{ marginLeft: `${depth * 20}px` }} className="flex items-center gap-1"> + <span + className={cn( + "font-mono text-sm font-medium", + isModified && "text-red-500 dark:text-red-400" + )} + > + {itemNumber} + </span> + {childrenCount > 0 && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger> + <Badge variant="outline" className="h-5 px-1 text-xs"> + {childrenCount} + </Badge> + </TooltipTrigger> + <TooltipContent> + <p>{childrenCount}개의 하위 조항</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + </div> + </div> + ) + }, + size: 100, + enableResizing: true, + sortingFn: (rowA, rowB, colId) => + compareItemNumber(rowA.getValue<string>(colId), rowB.getValue<string>(colId)), + meta: { excelHeader: "채번" }, + }, + { + accessorKey: "category", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="분류" />, + cell: ({ row }) => { + const category = row.getValue("category") as string + const isModified = row.original.vendorInfo?.isCategoryModified + return category ? ( + <Badge + variant="secondary" + className={cn("text-xs", isModified && "bg-red-100 text-red-600")} + > + {category} + </Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ) + }, + size: 100, + enableResizing: true, + meta: { excelHeader: "분류" }, + }, + { + accessorKey: "subtitle", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="소제목" />, + cell: ({ row }) => { + const subtitle = row.getValue("subtitle") as string + const depth = row.original.depth + const isModified = row.original.vendorInfo?.isSubtitleModified + + return ( + <div className="flex flex-col min-w-0"> + <span + className={cn( + "font-medium truncate", + depth === 0 && "text-base", + depth === 1 && "text-sm", + depth >= 2 && "text-sm text-muted-foreground", + isModified && "text-red-500 dark:text-red-400" + )} + title={subtitle} + > + {subtitle} + </span> + </div> + ) + }, + size: 150, + enableResizing: true, + meta: { excelHeader: "소제목" }, + }, + { + accessorKey: "content", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상세항목" />, + cell: ({ row }) => { + + const modifiedContent = row.original.vendorInfo?.modifiedContent + + const content = modifiedContent ? modifiedContent : row.getValue("content") as string | null + if (!content) { + return ( + <div className="flex items-center gap-2"> + <Badge variant="outline" className="text-xs">그룹핑 조항</Badge> + <span className="text-xs text-muted-foreground">상세내용 없음</span> + </div> + ) + } + const truncated = content.length > 100 ? `${content.substring(0, 100)}...` : content + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className=""> + {/* <p className="text-sm line-clamp-2 text-muted-foreground">{content}</p> */} + <p + className={cn( + "text-sm line-clamp-2", + modifiedContent ? "text-red-500 dark:text-red-400" : "text-muted-foreground" + )} + > + {content} + </p> + </div> + </TooltipTrigger> + <TooltipContent className="max-w-sm"> + <p className="whitespace-pre-wrap">{content}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + }, + size: 200, + maxSize: 500, + enableResizing: true, + meta: { excelHeader: "상세항목" }, + }, + ] + + // 3) 등록/수정 정보 + const auditColumns: ColumnDef<GtcClauseTreeView>[] = [ + { + accessorKey: "createdAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="작성일" />, + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date + return date ? formatDate(date, "KR") : "-" + }, + size: 120, + enableResizing: true, + meta: { excelHeader: "작성일" }, + }, + { + accessorKey: "createdByName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="작성자" />, + cell: ({ row }) => { + const v = row.getValue("createdByName") as string + return v ? <span className="text-sm">{v}</span> : <span className="text-muted-foreground">-</span> + }, + size: 80, + enableResizing: true, + meta: { excelHeader: "작성자" }, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="수정일" />, + cell: ({ row }) => { + const date = row.getValue("updatedAt") as Date + return <span className="text-sm">{date ? formatDate(date, "KR") : "-"}</span> + }, + size: 120, + enableResizing: true, + meta: { excelHeader: "수정일" }, + }, + { + accessorKey: "updatedByName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="수정자" />, + cell: ({ row }) => { + const v = row.getValue("updatedByName") as string + return v ? <span className="text-sm">{v}</span> : <span className="text-muted-foreground">-</span> + }, + size: 80, + enableResizing: true, + meta: { excelHeader: "수정자" }, + }, + ] + + // 벤더 관련 칼럼 추가 + const vendorColumns: ColumnDef<any>[] = hasVendorInfo ? [ + { + id: "vendorReviewStatus", + accessorFn: (row) => row.vendorInfo?.reviewStatus, + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="협의 상태" />, + cell: ({ row }) => { + const status = row.original.vendorInfo?.reviewStatus + if (!status) return <span className="text-muted-foreground">-</span> + + const statusMap = { + draft: { label: "초안", variant: "secondary" }, + pending: { label: "대기", variant: "outline" }, + reviewing: { label: "협의중", variant: "default" }, + approved: { label: "승인", variant: "success" }, + rejected: { label: "거부", variant: "destructive" }, + revised: { label: "수정", variant: "warning" }, + } + + const config = statusMap[status as keyof typeof statusMap] + return <Badge variant={config.variant as any}>{config.label}</Badge> + }, + size: 80, + }, + { + id: "vendorComment", + accessorFn: (row) => row.vendorInfo?.latestComment, + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="협의 코멘트" />, + cell: ({ row }) => { + const comment = row.original.vendorInfo?.latestComment + const history = row.original.vendorInfo?.negotiationHistory + + if (!comment) return <span className="text-muted-foreground">-</span> + + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className="flex items-center gap-1"> + <span className="text-sm truncate max-w-[200px]">{comment}</span> + {history && history.length > 1 && ( + <Badge variant="outline" className="h-5 px-1 text-xs"> + {history.length} + </Badge> + )} + </div> + </TooltipTrigger> + <TooltipContent className="max-w-md"> + <div className="space-y-2"> + {history?.slice(0, 3).map((h: any, idx: number) => ( + <div key={idx} className="border-b last:border-0 pb-2 last:pb-0"> + <p className="text-xs text-muted-foreground"> + {h.actorName} • {formatDate(h.createdAt, "KR")} + </p> + <p className="text-sm">{h.comment}</p> + </div> + ))} + </div> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + }, + size: 200, + }, + { + id: "vendorModifications", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="수정 항목" />, + cell: ({ row }) => { + const info = row.original.vendorInfo + if (!info) return <span className="text-muted-foreground">-</span> + + const mods = [] + if (info.isNumberModified) mods.push("채번") + if (info.isCategoryModified) mods.push("분류") + if (info.isSubtitleModified) mods.push("소제목") + if (info.isContentModified) mods.push("내용") + + if (mods.length === 0) return <span className="text-muted-foreground">없음</span> + + return ( + <div className="flex flex-wrap gap-1"> + {mods.map((mod) => ( + <Badge key={mod} variant="outline" className="text-xs"> + {mod} + </Badge> + ))} + </div> + ) + }, + size: 150, + }, + ] : [] + + // 4) actions + const actionsColumn: ColumnDef<GtcClauseTreeView> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const { data: session } = useSession() + const gtcClause = row.original + const currentUserId = React.useMemo( + () => (session?.user?.id ? Number(session.user.id) : null), + [session], + ) + + const handleEdit = () => setRowAction({ row, type: "update" }) + const handleDelete = () => setRowAction({ row, type: "delete" }) + const handleAddSubClause = () => setRowAction({ row, type: "addSubClause" }) + const handleDuplicate = () => setRowAction({ row, type: "duplicate" }) + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button aria-label="Open menu" variant="ghost" className="flex size-8 p-0 data-[state=open]:bg-muted"> + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-48"> + <DropdownMenuItem onSelect={handleEdit}> + <Edit className="mr-2 h-4 w-4" /> + 협의 확인 및 조항 수정 + </DropdownMenuItem> + {/* <DropdownMenuItem onSelect={handleAddSubClause}> + <Plus className="mr-2 h-4 w-4" /> + 하위 조항 추가 + </DropdownMenuItem> + <DropdownMenuItem onSelect={handleDuplicate}> + <Copy className="mr-2 h-4 w-4" /> + 복제 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem onSelect={handleDelete} className="text-destructive"> + <Trash2 className="mr-2 h-4 w-4" /> + 삭제 + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> */} + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, + maxSize: 40, + } + + // 🔹 그룹 헤더 제거: 평탄화된 컬럼 배열 반환 + return [ + selectColumn, + ...clauseInfoColumns, + ...vendorColumns, // 벤더 칼럼 추가 + ...auditColumns, + actionsColumn, + ] +}
\ No newline at end of file |
