diff options
Diffstat (limited to 'lib/b-rfq/summary-table/summary-rfq-columns.tsx')
| -rw-r--r-- | lib/b-rfq/summary-table/summary-rfq-columns.tsx | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/lib/b-rfq/summary-table/summary-rfq-columns.tsx b/lib/b-rfq/summary-table/summary-rfq-columns.tsx new file mode 100644 index 00000000..f620858a --- /dev/null +++ b/lib/b-rfq/summary-table/summary-rfq-columns.tsx @@ -0,0 +1,499 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Eye, Calendar, AlertTriangle, CheckCircle2, Clock, FileText } from "lucide-react" + +import { formatDate, cn } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Progress } from "@/components/ui/progress" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { useRouter } from "next/navigation" +import { RfqDashboardView } from "@/db/schema" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +type NextRouter = ReturnType<typeof useRouter>; + +interface GetRFQColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<RfqDashboardView> | null>>; + router: NextRouter; +} + +// 상태에 따른 Badge 변형 결정 함수 +function getStatusBadge(status: string) { + switch (status) { + case "DRAFT": + return { variant: "outline" as const, label: "초안" }; + case "Doc. Received": + return { variant: "secondary" as const, label: "문서접수" }; + case "PIC Assigned": + return { variant: "secondary" as const, label: "담당자배정" }; + case "Doc. Confirmed": + return { variant: "default" as const, label: "문서확인" }; + case "Init. RFQ Sent": + return { variant: "default" as const, label: "초기RFQ발송" }; + case "Init. RFQ Answered": + return { variant: "default" as const, label: "초기RFQ회신" }; + case "TBE started": + return { variant: "secondary" as const, label: "TBE시작" }; + case "TBE finished": + return { variant: "secondary" as const, label: "TBE완료" }; + case "Final RFQ Sent": + return { variant: "default" as const, label: "최종RFQ발송" }; + case "Quotation Received": + return { variant: "default" as const, label: "견적접수" }; + case "Vendor Selected": + return { variant: "success" as const, label: "업체선정" }; + default: + return { variant: "outline" as const, label: status }; + } +} + +function getProgressBadge(progress: number) { + if (progress >= 100) { + return { variant: "success" as const, label: "완료" }; + } else if (progress >= 70) { + return { variant: "default" as const, label: "진행중" }; + } else if (progress >= 30) { + return { variant: "secondary" as const, label: "초기진행" }; + } else { + return { variant: "outline" as const, label: "시작" }; + } +} + +function getUrgencyLevel(daysToDeadline: number): "high" | "medium" | "low" { + if (daysToDeadline <= 3) return "high"; + if (daysToDeadline <= 7) return "medium"; + return "low"; +} + +export function getRFQColumns({ setRowAction, router }: GetRFQColumnsProps): ColumnDef<RfqDashboardView>[] { + + // Select 컬럼 + const selectColumn: ColumnDef<RfqDashboardView> = { + 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" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }; + + // RFQ 코드 컬럼 + const rfqCodeColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "rfqCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="RFQ 코드" /> + ), + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-medium">{row.getValue("rfqCode")}</span> + {row.original.description && ( + <span className="text-xs text-muted-foreground truncate max-w-[200px]"> + {row.original.description} + </span> + )} + </div> + ), + }; + + // 프로젝트 정보 컬럼 + const projectColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => { + const projectName = row.original.projectName; + const projectCode = row.original.projectCode; + + if (!projectName) { + return <span className="text-muted-foreground">-</span>; + } + + return ( + <div className="flex flex-col"> + <span className="font-medium">{projectName}</span> + <div className="flex items-center gap-2 text-xs text-muted-foreground"> + {projectCode && <span>{projectCode}</span>} + </div> + </div> + ); + }, + }; + + // 패키지 정보 컬럼 + const packageColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "packageNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="패키지" /> + ), + cell: ({ row }) => { + const packageNo = row.original.packageNo; + const packageName = row.original.packageName; + + if (!packageNo) { + return <span className="text-muted-foreground">-</span>; + } + + return ( + <div className="flex flex-col"> + <span className="font-medium">{packageNo}</span> + {packageName && ( + <span className="text-xs text-muted-foreground truncate max-w-[150px]"> + {packageName} + </span> + )} + </div> + ); + }, + }; + + const updatedColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "updatedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Updated By" /> + ), + cell: ({ row }) => { + const updatedByName = row.original.updatedByName; + const updatedByEmail = row.original.updatedByEmail; + + if (!updatedByName) { + return <span className="text-muted-foreground">-</span>; + } + + return ( + <div className="flex flex-col"> + <span className="font-medium">{updatedByName}</span> + {updatedByEmail && ( + <span className="text-xs text-muted-foreground truncate max-w-[150px]"> + {updatedByEmail} + </span> + )} + </div> + ); + }, + }; + + + // 상태 컬럼 + const statusColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "status", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + cell: ({ row }) => { + const statusBadge = getStatusBadge(row.original.status); + return <Badge variant={statusBadge.variant}>{statusBadge.label}</Badge>; + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + }; + + // 진행률 컬럼 + const progressColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "overallProgress", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="진행률" /> + ), + cell: ({ row }) => { + const progress = row.original.overallProgress; + const progressBadge = getProgressBadge(progress); + + return ( + <div className="flex flex-col gap-1 min-w-[120px]"> + <div className="flex items-center justify-between"> + <span className="text-sm font-medium">{progress}%</span> + <Badge variant={progressBadge.variant} className="text-xs"> + {progressBadge.label} + </Badge> + </div> + <Progress value={progress} className="h-2" /> + </div> + ); + }, + }; + + // 마감일 컬럼 + const dueDateColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "dueDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="마감일" /> + ), + cell: ({ row }) => { + const dueDate = row.original.dueDate; + const daysToDeadline = row.original.daysToDeadline; + const urgencyLevel = getUrgencyLevel(daysToDeadline); + + if (!dueDate) { + return <span className="text-muted-foreground">-</span>; + } + + return ( + <div className="flex flex-col"> + <div className="flex items-center gap-2"> + <Calendar className="h-4 w-4 text-muted-foreground" /> + <span>{formatDate(dueDate, 'KR')}</span> + </div> + <div className="flex items-center gap-1 text-xs"> + {urgencyLevel === "high" && ( + <AlertTriangle className="h-3 w-3 text-red-500" /> + )} + {urgencyLevel === "medium" && ( + <Clock className="h-3 w-3 text-yellow-500" /> + )} + {urgencyLevel === "low" && ( + <CheckCircle2 className="h-3 w-3 text-green-500" /> + )} + <span className={cn( + urgencyLevel === "high" && "text-red-500", + urgencyLevel === "medium" && "text-yellow-600", + urgencyLevel === "low" && "text-green-600" + )}> + {daysToDeadline > 0 ? `${daysToDeadline}일 남음` : + daysToDeadline === 0 ? "오늘 마감" : + `${Math.abs(daysToDeadline)}일 지남`} + </span> + </div> + </div> + ); + }, + }; + + // 담당자 컬럼 + const picColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "picName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="구매 담당자" /> + ), + cell: ({ row }) => { + const picName = row.original.picName; + return picName ? ( + <span>{picName}</span> + ) : ( + <span className="text-muted-foreground">미배정</span> + ); + }, + }; + + const engPicColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "engPicName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="설계 담당자" /> + ), + cell: ({ row }) => { + const picName = row.original.engPicName; + return picName ? ( + <span>{picName}</span> + ) : ( + <span className="text-muted-foreground">미배정</span> + ); + }, + }; + + + const pjtCompanyColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "projectCompany", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 Company" /> + ), + cell: ({ row }) => { + const projectCompany = row.original.projectCompany; + return projectCompany ? ( + <span>{projectCompany}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + }; + + const pjtFlagColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "projectFlag", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 Flag" /> + ), + cell: ({ row }) => { + const projectFlag = row.original.projectFlag; + return projectFlag ? ( + <span>{projectFlag}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + }; + + + const pjtSiteColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "projectSite", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 Site" /> + ), + cell: ({ row }) => { + const projectSite = row.original.projectSite; + return projectSite ? ( + <span>{projectSite}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + }; + const remarkColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "remark", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="비고" /> + ), + cell: ({ row }) => { + const remark = row.original.remark; + return remark ? ( + <span>{remark}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + }; + + // 첨부파일 수 컬럼 + const attachmentColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "totalAttachments", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="첨부파일" /> + ), + cell: ({ row }) => { + const count = row.original.totalAttachments; + return ( + <div className="flex items-center gap-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <span>{count}</span> + </div> + ); + }, + }; + + // 벤더 현황 컬럼 + const vendorStatusColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "initialVendorCount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="벤더 현황" /> + ), + cell: ({ row }) => { + const initial = row.original.initialVendorCount; + const final = row.original.finalVendorCount; + const initialRate = row.original.initialResponseRate; + const finalRate = row.original.finalResponseRate; + + return ( + <div className="flex flex-col gap-1 text-xs"> + <div className="flex items-center justify-between"> + <span className="text-muted-foreground">초기:</span> + <span>{initial}개사 ({initialRate}%)</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-muted-foreground">최종:</span> + <span>{final}개사 ({finalRate}%)</span> + </div> + </div> + ); + }, + }; + + // 생성일 컬럼 + const createdAtColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + cell: ({ row }) => { + const dateVal = row.original.createdAt as Date; + return formatDate(dateVal, 'KR'); + }, + }; + + const updatedAtColumn: ColumnDef<RfqDashboardView> = { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="수정일" /> + ), + cell: ({ row }) => { + const dateVal = row.original.updatedAt as Date; + return formatDate(dateVal, 'KR'); + }, + }; + + // Actions 컬럼 + const actionsColumn: ColumnDef<RfqDashboardView> = { + id: "detail", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상세내용" /> + ), + // enableHiding: false, + cell: function Cell({ row }) { + const rfq = row.original; + const detailUrl = `/b-rfq/${rfq.rfqId}/initial`; + + return ( + + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + onClick={() => router.push(detailUrl)} + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + ); + }, + size: 40, + }; + + return [ + selectColumn, + rfqCodeColumn, + projectColumn, + packageColumn, + statusColumn, + picColumn, + progressColumn, + dueDateColumn, + actionsColumn, + + engPicColumn, + + pjtCompanyColumn, + pjtFlagColumn, + pjtSiteColumn, + + attachmentColumn, + vendorStatusColumn, + createdAtColumn, + + updatedAtColumn, + updatedColumn, + remarkColumn + ]; +}
\ No newline at end of file |
