summaryrefslogtreecommitdiff
path: root/lib/b-rfq/summary-table/summary-rfq-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/summary-table/summary-rfq-columns.tsx')
-rw-r--r--lib/b-rfq/summary-table/summary-rfq-columns.tsx499
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