diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
| commit | 675b4e3d8ffcb57a041db285417d81e61284d900 (patch) | |
| tree | 254f3d6a6c0ce39ae8fba35618f3810e08945f19 /lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx | |
| parent | 39f12cb19f29cbc5568057e154e6adf4789ae736 (diff) | |
(대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정
Diffstat (limited to 'lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx')
| -rw-r--r-- | lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx b/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx new file mode 100644 index 00000000..144c6c43 --- /dev/null +++ b/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx @@ -0,0 +1,514 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { + FileText, + Edit, + Send, + Eye, + Clock, + CheckCircle, + AlertCircle, + XCircle, + Mail, + UserX +} from "lucide-react" +import { formatCurrency, formatDate, formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { useRouter } from "next/navigation" +import type { VendorQuotationView } from "./service" +import { ParticipationDialog } from "./participation-dialog" + +// 통합 상태 배지 컴포넌트 (displayStatus 사용) +function DisplayStatusBadge({ status }: { status: string | null }) { + if (!status) return null + + const config = { + "미응답": { variant: "secondary" as const, icon: Mail, label: "응답 대기" }, + "불참": { variant: "destructive" as const, icon: UserX, label: "불참" }, + "작성중": { variant: "outline" as const, icon: Edit, label: "작성중" }, + "제출완료": { variant: "default" as const, icon: CheckCircle, label: "제출완료" }, + "수정요청": { variant: "warning" as const, icon: AlertCircle, label: "수정요청" }, + "최종확정": { variant: "success" as const, icon: CheckCircle, label: "최종확정" }, + "취소": { variant: "destructive" as const, icon: XCircle, label: "취소" }, + } + + const { variant, icon: Icon, label } = config[status as keyof typeof config] || { + variant: "outline" as const, + icon: Clock, + label: status + } + + return ( + <Badge variant={variant} className="gap-1"> + <Icon className="h-3 w-3" /> + {label} + </Badge> + ) +} + +// RFQ 상태 배지 (기존 유지) +function RfqStatusBadge({ status }: { status: string }) { + const config: Record<string, { variant: "default" | "secondary" | "outline" | "destructive" | "warning" | "success" }> = { + "RFQ 생성": { variant: "outline" }, + "구매담당지정": { variant: "secondary" }, + "견적요청문서 확정": { variant: "secondary" }, + "TBE 완료": { variant: "warning" }, + "RFQ 발송": { variant: "default" }, + "견적접수": { variant: "success" }, + "최종업체선정": { variant: "success" }, + } + + const { variant } = config[status] || { variant: "outline" as const } + return <Badge variant={variant}>{status}</Badge> +} + +type NextRouter = ReturnType<typeof useRouter> + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorQuotationView> | null>>; + router: NextRouter; + vendorId: number; // 추가: 벤더 ID 전달 +} + +export function getColumns({ + setRowAction, + router, + vendorId, // 추가 +}: GetColumnsProps): ColumnDef<VendorQuotationView>[] { + + // 체크박스 컬럼 (기존 유지) + const selectColumn: ColumnDef<VendorQuotationView> = { + 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, + } + + // 액션 컬럼 + const actionsColumn: ColumnDef<VendorQuotationView> = { + id: "actions", + header: "작업", + enableHiding: false, + cell: ({ row }) => { + const rfqId = row.original.id + const rfqCode = row.original.rfqCode + const displayStatus = row.original.displayStatus + const rfqLastDetailsId = row.original.rfqLastDetailsId + const [showParticipationDialog, setShowParticipationDialog] = React.useState(false) + + // displayStatus 기반으로 액션 결정 + switch (displayStatus) { + case "미응답": + return ( + <> + <Button + variant="default" + size="sm" + onClick={() => setShowParticipationDialog(true)} + className="h-8" + > + <Mail className="h-4 w-4 mr-1" /> + 참여 여부 결정 + </Button> + {showParticipationDialog && ( + <ParticipationDialog + rfqId={rfqId} + rfqCode={rfqCode} + rfqLastDetailsId={rfqLastDetailsId} + currentStatus={displayStatus} + onClose={() => setShowParticipationDialog(false)} + /> + )} + </> + ) + + case "불참": + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <span className="text-sm text-muted-foreground">불참</span> + </TooltipTrigger> + {row.original.nonParticipationReason && ( + <TooltipContent> + <p className="max-w-xs"> + 불참 사유: {row.original.nonParticipationReason} + </p> + </TooltipContent> + )} + </Tooltip> + </TooltipProvider> + ) + + case "작성중": + case "대기중": + return ( + <Button + variant="default" + size="sm" + onClick={() => router.push(`/partners/rfq-last/${rfqId}`)} + className="h-8" + > + <Edit className="h-4 w-4 mr-1" /> + 견적서 작성 + </Button> + ) + + case "수정요청": + return ( + <Button + variant="warning" + size="sm" + onClick={() => router.push(`/partners/rfq-last/${rfqId}`)} + className="h-8" + > + <AlertCircle className="h-4 w-4 mr-1" /> + 견적서 수정 + </Button> + ) + + case "제출완료": + case "최종확정": + return ( + <Button + variant="outline" + size="sm" + onClick={() => router.push(`/partners/rfq-last/${rfqId}`)} + className="h-8" + > + <Eye className="h-4 w-4 mr-1" /> + 견적서 보기 + </Button> + ) + + case "취소": + return ( + <span className="text-sm text-muted-foreground">취소됨</span> + ) + + default: + return null + } + }, + size: 150, + } + + // 기본 컬럼들 + const columns: ColumnDef<VendorQuotationView>[] = [ + selectColumn, + { + accessorKey: "rfqCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="RFQ 번호" /> + ), + cell: ({ row }) => { + const value = row.getValue("rfqCode") + return ( + <span className="font-mono text-sm font-medium"> + {value || "-"} + </span> + ) + }, + size: 140, + minSize: 120, + maxSize: 180, + enableResizing: true, + }, + { + accessorKey: "rfqType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="RFQ 유형" /> + ), + cell: ({ row }) => { + const rfqCode = row.original.rfqCode + const value = row.getValue("rfqType") + + // F로 시작하지 않으면 빈 값 반환 + if (!rfqCode?.startsWith('F')) { + return null + } + + const typeMap: Record<string, string> = { + "ITB": "ITB", + "RFQ": "RFQ", + "일반견적": "일반견적" + } + return typeMap[value as string] || value || "-" + }, + size: 100, + minSize: 80, + maxSize: 120, + enableResizing: true, + // F로 시작하지 않을 때 컬럼 숨기기 + enableHiding: true, + }, + { + accessorKey: "rfqTitle", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="RFQ 제목" /> + ), + cell: ({ row }) => { + const rfqCode = row.original.rfqCode + const value = row.getValue("rfqTitle") + + // F로 시작하지 않으면 빈 값 반환 + if (!rfqCode?.startsWith('F')) { + return null + } + + return value || "-" + }, + minSize: 200, + maxSize: 400, + enableResizing: true, + enableHiding: true, + }, + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-mono text-xs text-muted-foreground"> + {row.original.projectCode} + </span> + <span className="max-w-[200px] truncate" title={row.original.projectName || ""}> + {row.original.projectName || "-"} + </span> + </div> + ), + minSize: 150, + maxSize: 300, + enableResizing: true, + }, + { + accessorKey: "itemName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="품목명" /> + ), + cell: ({ row }) => row.getValue("itemName") || "-", + minSize: 150, + maxSize: 300, + enableResizing: true, + }, + { + accessorKey: "packageName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="패키지" /> + ), + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-mono text-xs text-muted-foreground"> + {row.original.packageNo} + </span> + <span className="max-w-[200px] truncate" title={row.original.packageName || ""}> + {row.original.packageName || "-"} + </span> + </div> + ), + minSize: 120, + maxSize: 250, + enableResizing: true, + }, + { + accessorKey: "MaterialGroup", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹" /> + ), + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-mono text-xs text-muted-foreground"> + {row.original.majorItemMaterialCategory} + </span> + <span className="max-w-[200px] truncate" title={row.original.majorItemMaterialDescription || ""}> + {row.original.majorItemMaterialDescription || "-"} + </span> + </div> + ), + minSize: 120, + maxSize: 250, + enableResizing: true, + }, + + { + id: "rfqDocument", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 자료" />, + cell: ({ row }) => ( + <Button + variant="ghost" + size="sm" + onClick={() => setRowAction({ row, type: "attachment" })} + > + <FileText className="h-4 w-4" /> + </Button> + ), + size: 80, + }, + // 견적품목수 - 수정됨 + { + accessorKey: "prItemsCount", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적품목수" />, + cell: ({ row }) => ( + <Button + variant="ghost" + size="sm" + className="font-mono text-sm p-1 h-auto" + onClick={() => setRowAction({ row, type: "items" })} + > + {row.original.prItemsCount || 0} + </Button> + ), + size: 90, + }, + + { + accessorKey: "engPicName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="설계담당자" />, + cell: ({ row }) => row.original.engPicName || "-", + size: 100, + }, + + { + accessorKey: "picUserName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구매담당자" />, + cell: ({ row }) => row.original.picUserName || row.original.picName || "-", + size: 100, + }, + + { + accessorKey: "submittedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="제출일" /> + ), + cell: ({ row }) => { + return row.original.submittedAt + ? formatDateTime(new Date(row.original.submittedAt)) + : "-" + }, + size: 150, + minSize: 120, + maxSize: 180, + enableResizing: true, + }, + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="견적 금액" /> + ), + cell: ({ row }) => { + if (!row.original.totalAmount) return "-" + return formatCurrency( + row.original.totalAmount, + row.original.vendorCurrency || "USD" + ) + }, + size: 140, + minSize: 120, + maxSize: 180, + enableResizing: true, + }, + { + accessorKey: "displayStatus", // 변경: responseStatus → displayStatus + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + cell: ({ row }) => <DisplayStatusBadge status={row.original.displayStatus} />, + size: 120, + minSize: 100, + maxSize: 150, + enableResizing: true, + }, + { + accessorKey: "rfqSendDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="발송일" /> + ), + cell: ({ row }) => { + const value = row.getValue("rfqSendDate") + return value ? formatDateTime(new Date(value as string)) : "-" + }, + size: 150, + minSize: 120, + maxSize: 180, + enableResizing: true, + }, + { + accessorKey: "participationRepliedAt", // 추가: 참여 응답일 + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="참여 응답일" /> + ), + cell: ({ row }) => { + const value = row.getValue("participationRepliedAt") + return value ? formatDateTime(new Date(value as string)) : "-" + }, + size: 150, + minSize: 120, + maxSize: 180, + enableResizing: true, + enableHiding: true, // 선택적 표시 + }, + { + accessorKey: "dueDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="마감일" /> + ), + cell: ({ row }) => { + const value = row.getValue("dueDate") + const now = new Date() + const dueDate = value ? new Date(value as string) : null + const isOverdue = dueDate && dueDate < now + const isNearDeadline = dueDate && + (dueDate.getTime() - now.getTime()) < (24 * 60 * 60 * 1000) // 24시간 이내 + + return ( + <span className={ + isOverdue ? "text-red-600 font-semibold" : + isNearDeadline ? "text-orange-600 font-semibold" : + "" + }> + {dueDate ? formatDateTime(dueDate) : "-"} + </span> + ) + }, + size: 150, + minSize: 120, + maxSize: 180, + enableResizing: true, + }, + actionsColumn, + ] + + return columns +}
\ No newline at end of file |
