From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/vendor-quotations-table-columns.tsx | 1380 ++++++++++---------- .../table/vendor-quotations-table.tsx | 1028 ++++++++------- 2 files changed, 1233 insertions(+), 1175 deletions(-) (limited to 'lib/techsales-rfq/vendor-response/table') diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index 39de94ed..328def80 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -1,672 +1,710 @@ -"use client" - -import * as React from "react" -import { type ColumnDef } from "@tanstack/react-table" -import { Edit, Paperclip, Package } 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 { - TechSalesVendorQuotations, - TECH_SALES_QUOTATION_STATUS_CONFIG, - TECH_SALES_QUOTATION_STATUSES -} from "@/db/schema" -import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" -import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" - -interface QuotationWithRfqCode extends TechSalesVendorQuotations { - // RFQ 관련 정보 - rfqCode?: string; - materialCode?: string; - dueDate?: Date; - rfqStatus?: string; - - // 아이템 정보 - itemName?: string; - itemCount?: number; - - // 프로젝트 정보 - projNm?: string; - pspid?: string; - sector?: string; - - // RFQ 정보 - description?: string; - - // 벤더 정보 - vendorName?: string; - vendorCode?: string; - - // 사용자 정보 - createdByName?: string | null; - updatedByName?: string | null; - - // 첨부파일 개수 - attachmentCount?: number; -} - -interface GetColumnsProps { - router: AppRouterInstance; - openAttachmentsSheet: (rfqId: number) => void; - openItemsDialog: (rfq: { id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; }) => void; -} - -export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: GetColumnsProps): ColumnDef[] { - return [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="모두 선택" - className="translate-y-0.5" - /> - ), - cell: ({ row }) => { - const isRejected = row.original.status === TECH_SALES_QUOTATION_STATUSES.REJECTED; - const isAccepted = row.original.status === TECH_SALES_QUOTATION_STATUSES.ACCEPTED; - const isDisabled = isRejected || isAccepted; - - return ( - row.toggleSelected(!!value)} - aria-label="행 선택" - className="translate-y-0.5" - disabled={isDisabled} - /> - ); - }, - enableSorting: false, - enableHiding: false, - }, - // { - // accessorKey: "id", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => ( - //
- // {row.getValue("id")} - //
- // ), - // enableSorting: true, - // enableHiding: true, - // }, - { - accessorKey: "rfqCode", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const rfqCode = row.getValue("rfqCode") as string; - return ( -
- {rfqCode || "N/A"} -
- ); - }, - enableSorting: true, - enableHiding: false, - }, - // { - // accessorKey: "vendorName", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const vendorName = row.getValue("vendorName") as string; - // return ( - //
- // {vendorName || "N/A"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: false, - // }, - // { - // accessorKey: "vendorCode", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const vendorCode = row.getValue("vendorCode") as string; - // return ( - //
- // {vendorCode || "N/A"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - // { - // accessorKey: "materialCode", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const materialCode = row.getValue("materialCode") as string; - // return ( - //
- // {materialCode || "N/A"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - // { - // accessorKey: "itemName", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const itemName = row.getValue("itemName") as string; - // return ( - //
- // - // - // - // - // {itemName || "N/A"} - // - // - // - //

{itemName || "N/A"}

- //
- //
- //
- //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - { - accessorKey: "description", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const description = row.getValue("description") as string; - return ( -
- - - - - {description || "N/A"} - - - -

{description || "N/A"}

-
-
-
-
- ); - }, - enableSorting: true, - enableHiding: true, - }, - { - accessorKey: "projNm", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const projNm = row.getValue("projNm") as string; - return ( -
- - - - - {projNm || "N/A"} - - - -

{projNm || "N/A"}

-
-
-
-
- ); - }, - enableSorting: true, - enableHiding: true, - }, - // { - // accessorKey: "quotationCode", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const quotationCode = row.getValue("quotationCode") as string; - // return ( - //
- // {quotationCode || "미부여"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - // { - // accessorKey: "quotationVersion", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const quotationVersion = row.getValue("quotationVersion") as number; - // return ( - //
- // {quotationVersion || 1} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - { - id: "items", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const quotation = row.original - const itemCount = quotation.itemCount || 0 - - const handleClick = () => { - const rfq = { - id: quotation.rfqId, - rfqCode: quotation.rfqCode, - status: quotation.rfqStatus, - rfqType: "SHIP" as const, // 기본값 - } - openItemsDialog(rfq) - } - - return ( -
- - - - - - -

{itemCount > 0 ? `${itemCount}개 아이템 보기` : "아이템 없음"}

-
-
-
-
- ) - }, - enableSorting: false, - enableHiding: true, - }, - { - id: "attachments", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const quotation = row.original - const attachmentCount = quotation.attachmentCount || 0 - const handleClick = () => { - openAttachmentsSheet(quotation.rfqId) - } - - return ( -
- - - - - - -

{attachmentCount > 0 ? `${attachmentCount}개 첨부파일 보기` : "첨부파일 없음"}

-
-
-
-
- ) - }, - enableSorting: false, - enableHiding: true, - }, - { - accessorKey: "status", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const status = row.getValue("status") as string; - - const statusConfig = TECH_SALES_QUOTATION_STATUS_CONFIG[status as keyof typeof TECH_SALES_QUOTATION_STATUS_CONFIG] || { - label: status, - variant: "secondary" as const - }; - - return ( -
- - {statusConfig.label} - -
- ); - }, - enableSorting: true, - enableHiding: false, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)); - }, - }, - { - accessorKey: "currency", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const currency = row.getValue("currency") as string; - return ( -
- {currency || "N/A"} -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - { - accessorKey: "totalPrice", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const totalPrice = row.getValue("totalPrice") as string; - const currency = row.getValue("currency") as string; - - if (!totalPrice || totalPrice === "0") { - return ( -
- 미입력 -
- ); - } - - return ( -
- - {formatCurrency(parseFloat(totalPrice), currency || "USD")} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - { - accessorKey: "validUntil", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const validUntil = row.getValue("validUntil") as Date; - return ( -
- - {validUntil ? formatDate(validUntil) : "N/A"} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - { - accessorKey: "submittedAt", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const submittedAt = row.getValue("submittedAt") as Date; - return ( -
- - {submittedAt ? formatDateTime(submittedAt) : "미제출"} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - // { - // accessorKey: "acceptedAt", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const acceptedAt = row.getValue("acceptedAt") as Date; - // return ( - //
- // - // {acceptedAt ? formatDateTime(acceptedAt) : "미승인"} - // - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - { - accessorKey: "dueDate", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const dueDate = row.getValue("dueDate") as Date; - const isOverdue = dueDate && new Date() > new Date(dueDate); - - return ( -
- - {dueDate ? formatDate(dueDate) : "N/A"} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - // { - // accessorKey: "rejectionReason", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const rejectionReason = row.getValue("rejectionReason") as string; - // return ( - //
- // {rejectionReason ? ( - // - // - // - // - // {rejectionReason} - // - // - // - //

{rejectionReason}

- //
- //
- //
- // ) : ( - // N/A - // )} - //
- // ); - // }, - // enableSorting: false, - // enableHiding: true, - // }, - { - accessorKey: "createdAt", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const createdAt = row.getValue("createdAt") as Date; - return ( -
- - {createdAt ? formatDateTime(createdAt) : "N/A"} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - { - accessorKey: "updatedAt", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const updatedAt = row.getValue("updatedAt") as Date; - return ( -
- - {updatedAt ? formatDateTime(updatedAt) : "N/A"} - -
- ); - }, - enableSorting: true, - enableHiding: true, - }, - // { - // accessorKey: "createdByName", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const createdByName = row.getValue("createdByName") as string; - // return ( - //
- // {createdByName || "N/A"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - // { - // accessorKey: "updatedByName", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const updatedByName = row.getValue("updatedByName") as string; - // return ( - //
- // {updatedByName || "N/A"} - //
- // ); - // }, - // enableSorting: true, - // enableHiding: true, - // }, - { - id: "actions", - header: "작업", - cell: ({ row }) => { - const quotation = row.original; - const rfqCode = quotation.rfqCode || "N/A"; - const tooltipText = `${rfqCode} 견적서 작성`; - const isRejected = quotation.status === "Rejected"; - const isAccepted = quotation.status === "Accepted"; - const isDisabled = isRejected || isAccepted; - - return ( -
- - - - - - -

{isRejected ? "거절된 견적서는 편집할 수 없습니다" : isAccepted ? "승인된 견적서는 편집할 수 없습니다" : tooltipText}

-
-
-
-
- ); - }, - enableSorting: false, - enableHiding: false, - }, - ]; +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { Edit, Paperclip, Package, Users } 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 { + TechSalesVendorQuotations, + TECH_SALES_QUOTATION_STATUS_CONFIG, + TECH_SALES_QUOTATION_STATUSES +} from "@/db/schema" +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +interface QuotationWithRfqCode extends TechSalesVendorQuotations { + // RFQ 관련 정보 + rfqCode?: string; + materialCode?: string; + dueDate?: Date; + rfqStatus?: string; + + // 아이템 정보 + itemName?: string; + itemCount?: number; + + // 프로젝트 정보 + projNm?: string; + pspid?: string; + sector?: string; + + // RFQ 정보 + description?: string; + + // 벤더 정보 + vendorName?: string; + vendorCode?: string; + + // 사용자 정보 + createdByName?: string | null; + updatedByName?: string | null; + + // 첨부파일 개수 + attachmentCount?: number; +} + +interface GetColumnsProps { + router: AppRouterInstance; + openAttachmentsSheet: (rfqId: number) => void; + openItemsDialog: (rfq: { id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; }) => void; + openContactsDialog: (quotationId: number, vendorName?: string) => void; +} + +export function getColumns({ router, openAttachmentsSheet, openItemsDialog, openContactsDialog }: GetColumnsProps): ColumnDef[] { + return [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="모두 선택" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => { + const isRejected = row.original.status === TECH_SALES_QUOTATION_STATUSES.REJECTED; + const isAccepted = row.original.status === TECH_SALES_QUOTATION_STATUSES.ACCEPTED; + const isDisabled = isRejected || isAccepted; + + return ( + row.toggleSelected(!!value)} + aria-label="행 선택" + className="translate-y-0.5" + disabled={isDisabled} + /> + ); + }, + enableSorting: false, + enableHiding: false, + }, + // { + // accessorKey: "id", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => ( + //
+ // {row.getValue("id")} + //
+ // ), + // enableSorting: true, + // enableHiding: true, + // }, + { + accessorKey: "rfqCode", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const rfqCode = row.getValue("rfqCode") as string; + return ( +
+ {rfqCode || "N/A"} +
+ ); + }, + enableSorting: true, + enableHiding: false, + }, + // { + // accessorKey: "vendorName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const vendorName = row.getValue("vendorName") as string; + // return ( + //
+ // {vendorName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: false, + // }, + // { + // accessorKey: "vendorCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const vendorCode = row.getValue("vendorCode") as string; + // return ( + //
+ // {vendorCode || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "materialCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const materialCode = row.getValue("materialCode") as string; + // return ( + //
+ // {materialCode || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "itemName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const itemName = row.getValue("itemName") as string; + // return ( + //
+ // + // + // + // + // {itemName || "N/A"} + // + // + // + //

{itemName || "N/A"}

+ //
+ //
+ //
+ //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const description = row.getValue("description") as string; + return ( +
+ + + + + {description || "N/A"} + + + +

{description || "N/A"}

+
+
+
+
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: "projNm", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const projNm = row.getValue("projNm") as string; + return ( +
+ + + + + {projNm || "N/A"} + + + +

{projNm || "N/A"}

+
+
+
+
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + // { + // accessorKey: "quotationCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const quotationCode = row.getValue("quotationCode") as string; + // return ( + //
+ // {quotationCode || "미부여"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "quotationVersion", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const quotationVersion = row.getValue("quotationVersion") as number; + // return ( + //
+ // {quotationVersion || 1} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + { + id: "items", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const quotation = row.original + const itemCount = quotation.itemCount || 0 + + const handleClick = () => { + const rfq = { + id: quotation.rfqId, + rfqCode: quotation.rfqCode, + status: quotation.rfqStatus, + rfqType: "SHIP" as const, // 기본값 + } + openItemsDialog(rfq) + } + + return ( +
+ + + + + + +

{itemCount > 0 ? `${itemCount}개 아이템 보기` : "아이템 없음"}

+
+
+
+
+ ) + }, + enableSorting: false, + enableHiding: true, + }, + { + id: "attachments", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const quotation = row.original + const attachmentCount = quotation.attachmentCount || 0 + const handleClick = () => { + openAttachmentsSheet(quotation.rfqId) + } + + return ( +
+ + + + + + +

{attachmentCount > 0 ? `${attachmentCount}개 첨부파일 보기` : "첨부파일 없음"}

+
+
+
+
+ ) + }, + enableSorting: false, + enableHiding: true, + }, + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const status = row.getValue("status") as string; + + const statusConfig = TECH_SALES_QUOTATION_STATUS_CONFIG[status as keyof typeof TECH_SALES_QUOTATION_STATUS_CONFIG] || { + label: status, + variant: "secondary" as const + }; + + return ( +
+ + {statusConfig.label} + +
+ ); + }, + enableSorting: true, + enableHiding: false, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + }, + { + accessorKey: "currency", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const currency = row.getValue("currency") as string; + return ( +
+ {currency || "N/A"} +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: "totalPrice", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const totalPrice = row.getValue("totalPrice") as string; + const currency = row.getValue("currency") as string; + + if (!totalPrice || totalPrice === "0") { + return ( +
+ 미입력 +
+ ); + } + + return ( +
+ + {formatCurrency(parseFloat(totalPrice), currency || "USD")} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: "validUntil", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const validUntil = row.getValue("validUntil") as Date; + return ( +
+ + {validUntil ? formatDate(validUntil) : "N/A"} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: "submittedAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const submittedAt = row.getValue("submittedAt") as Date; + return ( +
+ + {submittedAt ? formatDateTime(submittedAt) : "미제출"} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + // { + // accessorKey: "acceptedAt", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const acceptedAt = row.getValue("acceptedAt") as Date; + // return ( + //
+ // + // {acceptedAt ? formatDateTime(acceptedAt) : "미승인"} + // + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + { + accessorKey: "dueDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const dueDate = row.getValue("dueDate") as Date; + const isOverdue = dueDate && new Date() > new Date(dueDate); + + return ( +
+ + {dueDate ? formatDate(dueDate) : "N/A"} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + // { + // accessorKey: "rejectionReason", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const rejectionReason = row.getValue("rejectionReason") as string; + // return ( + //
+ // {rejectionReason ? ( + // + // + // + // + // {rejectionReason} + // + // + // + //

{rejectionReason}

+ //
+ //
+ //
+ // ) : ( + // N/A + // )} + //
+ // ); + // }, + // enableSorting: false, + // enableHiding: true, + // }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as Date; + return ( +
+ + {createdAt ? formatDateTime(createdAt) : "N/A"} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const updatedAt = row.getValue("updatedAt") as Date; + return ( +
+ + {updatedAt ? formatDateTime(updatedAt) : "N/A"} + +
+ ); + }, + enableSorting: true, + enableHiding: true, + }, + // { + // accessorKey: "createdByName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const createdByName = row.getValue("createdByName") as string; + // return ( + //
+ // {createdByName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "updatedByName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const updatedByName = row.getValue("updatedByName") as string; + // return ( + //
+ // {updatedByName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + { + id: "actions", + header: "작업", + cell: ({ row }) => { + const quotation = row.original; + const rfqCode = quotation.rfqCode || "N/A"; + const tooltipText = `${rfqCode} 견적서 작성`; + const isRejected = quotation.status === "Rejected"; + const isAccepted = quotation.status === "Accepted"; + const isDisabled = isRejected || isAccepted; + + return ( +
+ + + + + + +

{isRejected ? "거절된 견적서는 편집할 수 없습니다" : isAccepted ? "승인된 견적서는 편집할 수 없습니다" : tooltipText}

+
+
+
+
+ ); + }, + enableSorting: false, + enableHiding: false, + }, + { + id: "contacts", + header: "담당자", + cell: ({ row }) => { + const quotation = row.original; + + const handleClick = () => { + openContactsDialog(quotation.id, quotation.vendorName); + }; + + return ( +
+ + + + + + +

RFQ 발송 담당자 보기

+
+
+
+
+ ); + }, + enableSorting: false, + enableHiding: true, + }, + ]; } \ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index e79d7c4d..5bb219bf 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -1,505 +1,525 @@ -// lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx -"use client" - -import * as React from "react" -import { useSearchParams } from "next/navigation" -import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table" -import { useDataTable } from "@/hooks/use-data-table" -import { DataTable } from "@/components/data-table/data-table" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QUOTATION_STATUS_CONFIG } from "@/db/schema" -import { useRouter } from "next/navigation" -import { getColumns } from "./vendor-quotations-table-columns" -import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" -import { RfqItemsViewDialog } from "../../table/rfq-items-view-dialog" -import { getTechSalesRfqAttachments, getVendorQuotations, rejectTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" -import { toast } from "sonner" -import { Skeleton } from "@/components/ui/skeleton" -import { Button } from "@/components/ui/button" -import { X } from "lucide-react" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog" -import { Textarea } from "@/components/ui/textarea" -import { Label } from "@/components/ui/label" - -interface QuotationWithRfqCode extends TechSalesVendorQuotations { - rfqCode?: string | null; - materialCode?: string | null; - dueDate?: Date; - rfqStatus?: string; - itemName?: string | null; - projNm?: string | null; - description?: string | null; - attachmentCount?: number; - itemCount?: number; - pspid?: string | null; - sector?: string | null; - vendorName?: string | null; - vendorCode?: string | null; - createdByName?: string | null; - updatedByName?: string | null; -} - -interface VendorQuotationsTableProps { - vendorId: string; - rfqType?: "SHIP" | "TOP" | "HULL"; -} - -// 로딩 스켈레톤 컴포넌트 -function TableLoadingSkeleton() { - return ( -
- {/* 툴바 스켈레톤 */} -
-
- - -
-
- - -
-
- - {/* 테이블 헤더 스켈레톤 */} -
-
-
- - - - - - - -
-
- - {/* 테이블 행 스켈레톤 */} - {Array.from({ length: 5 }).map((_, index) => ( -
-
- - - - - - - -
-
- ))} -
- - {/* 페이지네이션 스켈레톤 */} -
- -
- - - -
-
-
- ) -} - -export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTableProps) { - const searchParams = useSearchParams() - const router = useRouter() - - // 첨부파일 시트 상태 - const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) - const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null) - const [attachmentsDefault, setAttachmentsDefault] = React.useState([]) - - // 아이템 다이얼로그 상태 - const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) - const [selectedRfqForItems, setSelectedRfqForItems] = React.useState<{ id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; } | null>(null) - - // 거절 다이얼로그 상태 - const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false) - const [rejectionReason, setRejectionReason] = React.useState("") - const [isRejecting, setIsRejecting] = React.useState(false) - - // 데이터 로딩 상태 - const [data, setData] = React.useState([]) - const [pageCount, setPageCount] = React.useState(0) - const [total, setTotal] = React.useState(0) - const [isLoading, setIsLoading] = React.useState(true) - const [isInitialLoad, setIsInitialLoad] = React.useState(true) // 최초 로딩 구분 - - // URL 파라미터에서 설정 읽기 - const initialSettings = React.useMemo(() => ({ - page: parseInt(searchParams?.get('page') || '1'), - perPage: parseInt(searchParams?.get('perPage') || '10'), - sort: searchParams?.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "updatedAt", desc: true }], - filters: searchParams?.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], - joinOperator: (searchParams?.get('joinOperator') as "and" | "or") || "and", - basicFilters: searchParams?.get('basicFilters') ? JSON.parse(searchParams.get('basicFilters')!) : [], - basicJoinOperator: (searchParams?.get('basicJoinOperator') as "and" | "or") || "and", - search: searchParams?.get('search') || '', - from: searchParams?.get('from') || '', - to: searchParams?.get('to') || '', - }), [searchParams]) - - // 데이터 로드 함수 - const loadData = React.useCallback(async () => { - try { - setIsLoading(true) - - console.log('🔍 [VendorQuotationsTable] 데이터 로드 요청:', { - vendorId, - settings: initialSettings - }) - - const result = await getVendorQuotations({ - page: initialSettings.page, - perPage: initialSettings.perPage, - sort: initialSettings.sort, - filters: initialSettings.filters, - joinOperator: initialSettings.joinOperator, - basicFilters: initialSettings.basicFilters, - basicJoinOperator: initialSettings.basicJoinOperator, - search: initialSettings.search, - from: initialSettings.from, - to: initialSettings.to, - rfqType: rfqType, - }, vendorId) - - console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', { - dataLength: result.data.length, - pageCount: result.pageCount, - total: result.total - }) - - setData(result.data as QuotationWithRfqCode[]) - setPageCount(result.pageCount) - setTotal(result.total) - } catch (error) { - console.error('데이터 로드 오류:', error) - toast.error('데이터를 불러오는 중 오류가 발생했습니다.') - } finally { - setIsLoading(false) - setIsInitialLoad(false) - } - }, [vendorId, initialSettings, rfqType]) - - // URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함) - React.useEffect(() => { - loadData() - }, [ - searchParams?.get('page'), - searchParams?.get('perPage'), - searchParams?.get('sort'), - searchParams?.get('filters'), - searchParams?.get('joinOperator'), - searchParams?.get('basicFilters'), - searchParams?.get('basicJoinOperator'), - searchParams?.get('search'), - searchParams?.get('from'), - searchParams?.get('to'), - // vendorId와 rfqType 변경도 감지 - vendorId, - rfqType - ]) - - // 데이터 안정성을 위한 메모이제이션 - const stableData = React.useMemo(() => { - return data; - }, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]); - - // 첨부파일 시트 열기 함수 (벤더는 RFQ_COMMON 타입만 조회) - const openAttachmentsSheet = React.useCallback(async (rfqId: number) => { - try { - // RFQ 정보 조회 (data에서 rfqId에 해당하는 데이터 찾기) - const quotationWithRfq = data.find(item => item.rfqId === rfqId) - if (!quotationWithRfq) { - toast.error("RFQ 정보를 찾을 수 없습니다.") - return - } - - // 실제 첨부파일 목록 조회 API 호출 - const result = await getTechSalesRfqAttachments(rfqId) - - if (result.error) { - toast.error(result.error) - return - } - - // API 응답을 ExistingTechSalesAttachment 형식으로 변환하고 RFQ_COMMON 타입만 필터링 - const attachments: ExistingTechSalesAttachment[] = result.data - .filter(att => att.attachmentType === "RFQ_COMMON") // 벤더는 RFQ_COMMON 타입만 조회 - .map(att => ({ - id: att.id, - techSalesRfqId: att.techSalesRfqId || rfqId, - fileName: att.fileName, - originalFileName: att.originalFileName, - filePath: att.filePath, - fileSize: att.fileSize || undefined, - fileType: att.fileType || undefined, - attachmentType: att.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT", - description: att.description || undefined, - createdBy: att.createdBy, - createdAt: att.createdAt, - })) - - setAttachmentsDefault(attachments) - setSelectedRfqForAttachments({ - id: rfqId, - rfqCode: quotationWithRfq.rfqCode || null, - status: quotationWithRfq.rfqStatus || "Unknown" - }) - setAttachmentsOpen(true) - } catch (error) { - console.error("첨부파일 조회 오류:", error) - toast.error("첨부파일 조회 중 오류가 발생했습니다.") - } - }, [data]) - - // 아이템 다이얼로그 열기 함수 - const openItemsDialog = React.useCallback((rfq: { id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; }) => { - setSelectedRfqForItems(rfq) - setItemsDialogOpen(true) - }, []) - - // 거절 처리 함수 - const handleRejectQuotations = React.useCallback(async () => { - if (!table) return; - - const selectedRows = table.getFilteredSelectedRowModel().rows; - const quotationIds = selectedRows.map(row => row.original.id); - - if (quotationIds.length === 0) { - toast.error("거절할 견적서를 선택해주세요."); - return; - } - - // 거절할 수 없는 상태의 견적서가 있는지 확인 - const invalidStatuses = selectedRows.filter(row => - row.original.status === "Accepted" || row.original.status === "Rejected" - ); - - if (invalidStatuses.length > 0) { - toast.error("이미 승인되었거나 거절된 견적서는 거절할 수 없습니다."); - return; - } - - setIsRejecting(true); - - try { - const result = await rejectTechSalesVendorQuotations({ - quotationIds, - rejectionReason: rejectionReason.trim() || undefined, - }); - - if (result.success) { - toast.success(result.message); - setRejectDialogOpen(false); - setRejectionReason(""); - table.resetRowSelection(); - // 데이터 다시 로드 - await loadData(); - } else { - toast.error(result.error || "견적서 거절 중 오류가 발생했습니다."); - } - } catch (error) { - console.error("견적서 거절 오류:", error); - toast.error("견적서 거절 중 오류가 발생했습니다."); - } finally { - setIsRejecting(false); - } - }, [rejectionReason, loadData]); - - // 테이블 컬럼 정의 - const columns = React.useMemo(() => getColumns({ - router, - openAttachmentsSheet, - openItemsDialog, - }), [router, openAttachmentsSheet, openItemsDialog]) - - // 필터 필드 - const filterFields = React.useMemo[]>(() => [ - { - id: "status", - label: "상태", - options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({ - label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label, - value: statusValue, - })) - }, - { - id: "rfqCode", - label: "RFQ 번호", - placeholder: "RFQ 번호 검색...", - }, - { - id: "materialCode", - label: "자재 그룹", - placeholder: "자재 그룹 검색...", - } - ], []) - - // 고급 필터 필드 - const advancedFilterFields = React.useMemo[]>(() => [ - { - id: "rfqCode", - label: "RFQ 번호", - type: "text", - }, - { - id: "materialCode", - label: "자재 그룹", - type: "text", - }, - { - id: "status", - label: "상태", - type: "multi-select", - options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({ - label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label, - value: statusValue, - })), - }, - { - id: "validUntil", - label: "유효기간", - type: "date", - }, - { - id: "submittedAt", - label: "제출일", - type: "date", - }, - ], []) - - // useDataTable 훅 사용 - const { table } = useDataTable({ - data: stableData, - columns: columns as any, // 타입 오류 임시 해결 - pageCount, - rowCount: total, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - enableColumnResizing: true, - columnResizeMode: 'onChange', - enableRowSelection: true, // 행 선택 활성화 - initialState: { - sorting: initialSettings.sort, - columnPinning: { right: ["actions", "items", "attachments"] }, - }, - getRowId: (originalRow) => String(originalRow.id), - shallow: false, - clearOnDefault: true, - defaultColumn: { - minSize: 50, - maxSize: 500, - }, - }) - - // 최초 로딩 시 전체 스켈레톤 표시 - if (isInitialLoad && isLoading) { - return ( -
-
- -
-
- ) - } - - return ( -
-
-
- - - {/* 선택된 행이 있을 때 거절 버튼 표시 */} - {table && table.getFilteredSelectedRowModel().rows.length > 0 && ( - - - - - - - 견적서 거절 - - 선택한 {table.getFilteredSelectedRowModel().rows.length}개의 견적서를 거절하시겠습니까? - 거절된 견적서는 다시 되돌릴 수 없습니다. - - -
-
- -