diff options
Diffstat (limited to 'lib/rfqs-ship/table/rfqs-table.tsx')
| -rw-r--r-- | lib/rfqs-ship/table/rfqs-table.tsx | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/lib/rfqs-ship/table/rfqs-table.tsx b/lib/rfqs-ship/table/rfqs-table.tsx new file mode 100644 index 00000000..287f1d53 --- /dev/null +++ b/lib/rfqs-ship/table/rfqs-table.tsx @@ -0,0 +1,263 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { toSentenceCase } from "@/lib/utils" +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 { getRFQStatusIcon } from "@/lib/tasks/utils" +import { useFeatureFlags } from "./feature-flags-provider" +import { getColumns } from "./rfqs-table-columns" +import { fetchRfqAttachments, fetchRfqItems, getRfqs, getRfqStatusCounts } from "../service" +import { RfqItem, RfqWithItemCount, rfqs } from "@/db/schema/rfq" +import { RfqsTableFloatingBar } from "./rfqs-table-floating-bar" +import { UpdateRfqSheet } from "./update-rfq-sheet" +import { DeleteRfqsDialog } from "./delete-rfqs-dialog" +import { RfqsTableToolbarActions } from "./rfqs-table-toolbar-actions" +import { RfqsItemsDialog } from "./ItemsDialog" +import { getAllItems } from "@/lib/items/service" +import { RfqAttachmentsSheet } from "./attachment-rfq-sheet" +import { useRouter } from "next/navigation" +import { RfqType } from "../validations" + +interface RfqsTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getRfqs>>, + Awaited<ReturnType<typeof getRfqStatusCounts>>, + Awaited<ReturnType<typeof getAllItems>>, + ] + >; + rfqType?: RfqType; // rfqType props 추가 +} + +export interface ExistingAttachment { + id: number; + fileName: string; + filePath: string; + createdAt?: Date; + vendorId?: number | null; + size?: number; +} + +export interface ExistingItem { + id?: number; + itemCode: string; + description: string | null; + quantity: number | null; + uom: string | null; +} + +export function RfqsTable({ promises, rfqType = RfqType.PURCHASE }: RfqsTableProps) { + const { featureFlags } = useFeatureFlags() + + const [{ data, pageCount }, statusCounts, items] = React.use(promises) + const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) + const [selectedRfqIdForAttachments, setSelectedRfqIdForAttachments] = React.useState<number | null>(null) + const [attachDefault, setAttachDefault] = React.useState<ExistingAttachment[]>([]) + const [itemsDefault, setItemsDefault] = React.useState<ExistingItem[]>([]) + + const router = useRouter() + + const itemsList = items?.map((v) => ({ + code: v.itemCode ?? "", + name: v.itemName ?? "", + })); + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<RfqWithItemCount> | null>(null) + + const [rowData, setRowData] = React.useState<RfqWithItemCount[]>(() => data) + + const [itemsModalOpen, setItemsModalOpen] = React.useState(false); + const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null); + + + const selectedRfq = React.useMemo(() => { + return rowData.find(row => row.rfqId === selectedRfqId) || null; + }, [rowData, selectedRfqId]); + + // rfqType에 따른 제목 계산 + const getRfqTypeTitle = () => { + return rfqType === RfqType.PURCHASE ? "Purchase RFQ" : "Budgetary RFQ"; + }; + + async function openItemsModal(rfqId: number) { + const itemList = await fetchRfqItems(rfqId) + setItemsDefault(itemList) + setSelectedRfqId(rfqId); + setItemsModalOpen(true); + } + + async function openAttachmentsSheet(rfqId: number) { + // 4.1) Fetch current attachments from server (server action) + const list = await fetchRfqAttachments(rfqId) // returns ExistingAttachment[] + setAttachDefault(list) + setSelectedRfqIdForAttachments(rfqId) + setAttachmentsOpen(true) + setSelectedRfqId(rfqId); + } + + function handleAttachmentsUpdated(rfqId: number, newCount: number, newList?: ExistingAttachment[]) { + // 5.1) update rowData itemCount + setRowData(prev => + prev.map(r => + r.rfqId === rfqId + ? { ...r, itemCount: newCount } + : r + ) + ) + // 5.2) if newList is provided, store it + if (newList) { + setAttachDefault(newList) + } + } + + const columns = React.useMemo(() => getColumns({ + setRowAction, router, + // we pass openItemsModal as a prop so the itemsColumn can call it + openItemsModal, + openAttachmentsSheet, + rfqType + }), [setRowAction, router, rfqType]); + + /** + * This component can render either a faceted filter or a search filter based on the `options` prop. + */ + const filterFields: DataTableFilterField<RfqWithItemCount>[] = [ + { + id: "rfqCode", + label: "RFQ Code", + placeholder: "Filter RFQ Code...", + }, + { + id: "status", + label: "Status", + options: rfqs.status.enumValues?.map((status) => { + // 명시적으로 status를 허용된 리터럴 타입으로 변환 + const s = status as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; + return { + label: toSentenceCase(s), + value: s, + icon: getRFQStatusIcon(s), + count: statusCounts[s], + }; + }), + + } + ] + + /** + * Advanced filter fields for the data table. + */ + const advancedFilterFields: DataTableAdvancedFilterField<RfqWithItemCount>[] = [ + { + id: "rfqCode", + label: "RFQ Code", + type: "text", + }, + { + id: "description", + label: "Description", + type: "text", + }, + { + id: "projectCode", + label: "Project Code", + type: "text", + }, + { + id: "dueDate", + label: "Due Date", + type: "date", + }, + { + id: "status", + label: "Status", + type: "multi-select", + options: rfqs.status.enumValues?.map((status) => { + // 명시적으로 status를 허용된 리터럴 타입으로 변환 + const s = status as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; + return { + label: toSentenceCase(s), + value: s, + icon: getRFQStatusIcon(s), + count: statusCounts[s], + }; + }), + + }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.rfqId), + shallow: false, + clearOnDefault: true, + }) + + return ( + <div style={{ maxWidth: '100vw' }}> + <DataTable + table={table} + // floatingBar={<RfqsTableFloatingBar table={table} />} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <RfqsTableToolbarActions table={table} rfqType={rfqType} /> + </DataTableAdvancedToolbar> + </DataTable> + + <UpdateRfqSheet + open={rowAction?.type === "update"} + onOpenChange={() => setRowAction(null)} + rfq={rowAction?.row.original ?? null} + /> + + <DeleteRfqsDialog + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + rfqs={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + /> + + <RfqsItemsDialog + open={itemsModalOpen} + onOpenChange={setItemsModalOpen} + rfq={selectedRfq ?? null} + itemsList={itemsList} + defaultItems={itemsDefault} + rfqType={rfqType} + /> + + <RfqAttachmentsSheet + open={attachmentsOpen} + onOpenChange={setAttachmentsOpen} + defaultAttachments={attachDefault} + rfqType={rfqType} + rfq={selectedRfq ?? null} + onAttachmentsUpdated={handleAttachmentsUpdated} + /> + </div> + ) +}
\ No newline at end of file |
