summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
commit675b4e3d8ffcb57a041db285417d81e61284d900 (patch)
tree254f3d6a6c0ce39ae8fba35618f3810e08945f19 /lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx
parent39f12cb19f29cbc5568057e154e6adf4789ae736 (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.tsx514
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