summaryrefslogtreecommitdiff
path: root/lib/basic-contract/gtc-vendor/gtc-clauses-table-columns.tsx
diff options
context:
space:
mode:
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.tsx409
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