summaryrefslogtreecommitdiff
path: root/lib/rfqs/table/rfqs-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/table/rfqs-table.tsx')
-rw-r--r--lib/rfqs/table/rfqs-table.tsx264
1 files changed, 264 insertions, 0 deletions
diff --git a/lib/rfqs/table/rfqs-table.tsx b/lib/rfqs/table/rfqs-table.tsx
new file mode 100644
index 00000000..7d996816
--- /dev/null
+++ b/lib/rfqs/table/rfqs-table.tsx
@@ -0,0 +1,264 @@
+"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: '80vw' }}>
+ <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}
+ rfqType={rfqType}
+ />
+
+ <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