From 96ba777cda69af8caf3a6e0e8bfc1aca5016fe58 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 23 Jun 2025 08:59:54 +0000 Subject: (최겸) 기술영업 해양 프로젝트 AVL 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/accepted-quotations-table-columns.tsx | 324 +++++++++++++++++++++ .../accepted-quotations-table-toolbar-actions.tsx | 51 ++++ .../table/accepted-quotations-table.tsx | 117 ++++++++ lib/tech-project-avl/validations.ts | 41 +++ 4 files changed, 533 insertions(+) create mode 100644 lib/tech-project-avl/table/accepted-quotations-table-columns.tsx create mode 100644 lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx create mode 100644 lib/tech-project-avl/table/accepted-quotations-table.tsx create mode 100644 lib/tech-project-avl/validations.ts (limited to 'lib/tech-project-avl') diff --git a/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx new file mode 100644 index 00000000..68a61f0a --- /dev/null +++ b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx @@ -0,0 +1,324 @@ +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { formatDate } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { Badge } from "@/components/ui/badge" +import { +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +// Accepted Quotation 타입 정의 +export interface AcceptedQuotationItem { + id: number + rfqId: number + vendorId: number + quotationCode: string | null + quotationVersion: number | null + totalPrice: string | null + currency: string | null + validUntil: Date | null + status: string + remark: string | null + submittedAt: Date | null + acceptedAt: Date | null + createdAt: Date + updatedAt: Date + + // RFQ 정보 + rfqCode: string | null + rfqType: string | null + description: string | null + dueDate: Date | null + rfqStatus: string | null + materialCode: string | null + + // Vendor 정보 + vendorName: string + vendorCode: string | null + vendorEmail: string | null + vendorCountry: string | null + + // Project 정보 + projNm: string | null + pspid: string | null + sector: string | null +} + +export function getColumns(): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + // const actionsColumn: ColumnDef = { + // id: "actions", + // cell: ({ row }) => ( + // + // + // + // + // + // setRowAction({ row, type: "open" })} + // > + // 견적서 보기 + // + // + // + // ), + // size: 40, + // enableSorting: false, + // enableHiding: false, + // } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 정의 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef[] = [ + { + accessorKey: "rfqCode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.rfqCode || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "RFQ 코드", + }, + }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.description || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "RFQ 설명", + }, + }, + { + accessorKey: "rfqType", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.rfqType || "-"} +
+ ), + enableSorting: true, + }, + { + accessorKey: "vendorName", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.vendorName} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "업체명", + }, + }, + { + accessorKey: "vendorCode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.vendorCode || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "업체 코드", + }, + }, + { + accessorKey: "quotationCode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.quotationCode || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "견적서 코드", + }, + }, + { + accessorKey: "totalPrice", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const price = row.original.totalPrice; + const currency = row.original.currency || "USD"; + return ( +
+ {price ? `${Number(price).toLocaleString()} ${currency}` : "-"} +
+ ); + }, + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "총 금액", + }, + }, + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.status} + + ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "상태", + }, + }, + { + accessorKey: "projNm", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.projNm || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "프로젝트명", + }, + }, + { + accessorKey: "materialCode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.materialCode || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "자재 코드", + }, + }, + { + accessorKey: "vendorCountry", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.vendorCountry || "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "국가", + }, + }, + { + accessorKey: "dueDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.dueDate ? formatDate(row.original.dueDate) : "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "마감일", + }, + }, + { + accessorKey: "acceptedAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.acceptedAt ? formatDate(row.original.acceptedAt) : "-"} +
+ ), + enableSorting: true, + enableHiding: true, + meta: { + excelHeader: "승인일", + }, + }, + ] + + return [selectColumn, ...dataColumns] +} \ No newline at end of file diff --git a/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx b/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx new file mode 100644 index 00000000..ae9aea60 --- /dev/null +++ b/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx @@ -0,0 +1,51 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, RefreshCcw } from "lucide-react" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { AcceptedQuotationItem } from "./accepted-quotations-table-columns" + +interface AcceptedQuotationsTableToolbarActionsProps { + table: Table + onRefresh?: () => void +} + +export function AcceptedQuotationsTableToolbarActions({ + table, + onRefresh +}: AcceptedQuotationsTableToolbarActionsProps) { + + return ( +
+ + + {onRefresh && ( + + )} +
+ ) +} \ No newline at end of file diff --git a/lib/tech-project-avl/table/accepted-quotations-table.tsx b/lib/tech-project-avl/table/accepted-quotations-table.tsx new file mode 100644 index 00000000..da33d0d5 --- /dev/null +++ b/lib/tech-project-avl/table/accepted-quotations-table.tsx @@ -0,0 +1,117 @@ +"use client" + +import * as React from "react" +import { type DataTableAdvancedFilterField } 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 { getColumns, type AcceptedQuotationItem } from "./accepted-quotations-table-columns" +import { AcceptedQuotationsTableToolbarActions } from "./accepted-quotations-table-toolbar-actions" + +interface AcceptedQuotationsTableProps { + data: AcceptedQuotationItem[] + pageCount: number + onRefresh?: () => void +} + +export function AcceptedQuotationsTable({ + data, + pageCount, + onRefresh, +}: AcceptedQuotationsTableProps) { + + // 필터 필드 정의 + const filterFields: DataTableAdvancedFilterField[] = [ + { + id: "rfqCode", + label: "RFQ 코드", + type: "text", + placeholder: "RFQ 코드로 필터...", + }, + { + id: "vendorName", + label: "업체명", + type: "text", + placeholder: "업체명으로 필터...", + }, + { + id: "vendorCode", + label: "업체 코드", + type: "text", + placeholder: "업체 코드로 필터...", + }, + { + id: "projNm", + label: "프로젝트명", + type: "text", + placeholder: "프로젝트명으로 필터...", + }, + { + id: "vendorCountry", + label: "국가", + type: "text", + placeholder: "국가로 필터...", + }, + { + id: "currency", + label: "통화", + type: "select", + options: [ + { label: "USD", value: "USD" }, + { label: "EUR", value: "EUR" }, + { label: "KRW", value: "KRW" }, + { label: "JPY", value: "JPY" }, + { label: "CNY", value: "CNY" }, + ], + }, + { + id: "rfqType", + label: "RFQ 타입", + type: "select", + options: [ + { label: "TOP", value: "TOP" }, + { label: "HULL", value: "HULL" }, + ], + }, + { + id: "dueDate", + label: "마감일", + type: "date", + }, + { + id: "acceptedAt", + label: "승인일", + type: "date", + }, + ] + + const columns = React.useMemo( + () => getColumns(), + [] + ) + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + initialState: { + sorting: [{ id: "acceptedAt", desc: true }], + columnPinning: { left: ["select"] }, + }, + getRowId: (originalRow) => `${originalRow.id}`, + }) + + return ( +
+ + + + +
+ ) +} \ No newline at end of file diff --git a/lib/tech-project-avl/validations.ts b/lib/tech-project-avl/validations.ts new file mode 100644 index 00000000..3e08b641 --- /dev/null +++ b/lib/tech-project-avl/validations.ts @@ -0,0 +1,41 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { AcceptedQuotationItem } from "./table/accepted-quotations-table-columns" + +export const searchParamsCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([ + { id: "acceptedAt", desc: true }, + ]), + + // 검색 필드 + rfqCode: parseAsString.withDefault(""), + vendorName: parseAsString.withDefault(""), + vendorCode: parseAsString.withDefault(""), + projNm: parseAsString.withDefault(""), + + // 필터 필드 + rfqType: parseAsStringEnum(["SHIP", "TOP", "HULL"]), + currency: parseAsStringEnum(["USD", "EUR", "KRW", "JPY", "CNY"]), + + // 날짜 범위 + from: parseAsString.withDefault(""), + to: parseAsString.withDefault(""), + + // advanced filter + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) \ No newline at end of file -- cgit v1.2.3