diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
| commit | 675b4e3d8ffcb57a041db285417d81e61284d900 (patch) | |
| tree | 254f3d6a6c0ce39ae8fba35618f3810e08945f19 /lib/tbe-last/table/tbe-last-table.tsx | |
| parent | 39f12cb19f29cbc5568057e154e6adf4789ae736 (diff) | |
(대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정
Diffstat (limited to 'lib/tbe-last/table/tbe-last-table.tsx')
| -rw-r--r-- | lib/tbe-last/table/tbe-last-table.tsx | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/lib/tbe-last/table/tbe-last-table.tsx b/lib/tbe-last/table/tbe-last-table.tsx new file mode 100644 index 00000000..64707e4e --- /dev/null +++ b/lib/tbe-last/table/tbe-last-table.tsx @@ -0,0 +1,419 @@ +// lib/tbe-last/table/tbe-last-table.tsx + +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { 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 { getColumns } from "./tbe-last-table-columns" +import { TbeLastView } from "@/db/schema" +import { getAllTBELast, getTBESessionDetail } from "@/lib/tbe-last/service" +import { Button } from "@/components/ui/button" +import { Download, RefreshCw } from "lucide-react" +import { exportTableToExcel } from "@/lib/export" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from "@/components/ui/dialog" +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription +} from "@/components/ui/sheet" +import { Badge } from "@/components/ui/badge" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ScrollArea } from "@/components/ui/scroll-area" +import { formatDate } from "@/lib/utils" + +interface TbeLastTableProps { + promises: Promise<[ + Awaited<ReturnType<typeof getAllTBELast>>, + ]> +} + +export function TbeLastTable({ promises }: TbeLastTableProps) { + const router = useRouter() + const [{ data, pageCount }] = React.use(promises) + + // Dialog states + const [sessionDetailOpen, setSessionDetailOpen] = React.useState(false) + const [documentsOpen, setDocumentsOpen] = React.useState(false) + const [prItemsOpen, setPrItemsOpen] = React.useState(false) + const [evaluationOpen, setEvaluationOpen] = React.useState(false) + + const [selectedSessionId, setSelectedSessionId] = React.useState<number | null>(null) + const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null) + const [selectedSession, setSelectedSession] = React.useState<TbeLastView | null>(null) + const [sessionDetail, setSessionDetail] = React.useState<any>(null) + const [isLoadingDetail, setIsLoadingDetail] = React.useState(false) + + // Load session detail when needed + const loadSessionDetail = React.useCallback(async (sessionId: number) => { + setIsLoadingDetail(true) + try { + const detail = await getTBESessionDetail(sessionId) + setSessionDetail(detail) + } catch (error) { + console.error("Failed to load session detail:", error) + } finally { + setIsLoadingDetail(false) + } + }, []) + + // Handlers + const handleOpenSessionDetail = React.useCallback((sessionId: number) => { + setSelectedSessionId(sessionId) + setSessionDetailOpen(true) + loadSessionDetail(sessionId) + }, [loadSessionDetail]) + + const handleOpenDocuments = React.useCallback((sessionId: number) => { + setSelectedSessionId(sessionId) + setDocumentsOpen(true) + loadSessionDetail(sessionId) + }, [loadSessionDetail]) + + const handleOpenPrItems = React.useCallback((rfqId: number) => { + setSelectedRfqId(rfqId) + setPrItemsOpen(true) + loadSessionDetail(rfqId) + }, [loadSessionDetail]) + + const handleOpenEvaluation = React.useCallback((session: TbeLastView) => { + setSelectedSession(session) + setEvaluationOpen(true) + }, []) + + const handleRefresh = React.useCallback(() => { + router.refresh() + }, [router]) + + // Table columns + const columns = React.useMemo( + () => + getColumns({ + onOpenSessionDetail: handleOpenSessionDetail, + onOpenDocuments: handleOpenDocuments, + onOpenPrItems: handleOpenPrItems, + onOpenEvaluation: handleOpenEvaluation, + }), + [handleOpenSessionDetail, handleOpenDocuments, handleOpenPrItems, handleOpenEvaluation] + ) + + // Filter fields + const filterFields: DataTableFilterField<TbeLastView>[] = [ + { + id: "sessionStatus", + label: "Status", + type: "select", + options: [ + { label: "준비중", value: "준비중" }, + { label: "진행중", value: "진행중" }, + { label: "검토중", value: "검토중" }, + { label: "보류", value: "보류" }, + { label: "완료", value: "완료" }, + ], + }, + { + id: "evaluationResult", + label: "Result", + type: "select", + options: [ + { label: "Pass", value: "pass" }, + { label: "Conditional Pass", value: "conditional_pass" }, + { label: "Non-Pass", value: "non_pass" }, + ], + }, + ] + + // Data table + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["documents", "comments"] }, + }, + getRowId: (originalRow) => String(originalRow.tbeSessionId), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={filterFields} + shallow={false} + > + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + className="gap-2" + > + <RefreshCw className="size-4" /> + <span>Refresh</span> + </Button> + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "tbe-sessions", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" /> + <span>Export</span> + </Button> + </div> + </DataTableAdvancedToolbar> + </DataTable> + + {/* Session Detail Dialog */} + <Dialog open={sessionDetailOpen} onOpenChange={setSessionDetailOpen}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>TBE Session Detail</DialogTitle> + <DialogDescription> + {sessionDetail?.session?.sessionCode} - {sessionDetail?.session?.vendorName} + </DialogDescription> + </DialogHeader> + {isLoadingDetail ? ( + <div className="p-8 text-center">Loading...</div> + ) : sessionDetail ? ( + <div className="space-y-4"> + {/* Session info */} + <div className="grid grid-cols-2 gap-4"> + <div> + <p className="text-sm font-medium">RFQ Code</p> + <p className="text-sm text-muted-foreground">{sessionDetail.session.rfqCode}</p> + </div> + <div> + <p className="text-sm font-medium">Status</p> + <Badge>{sessionDetail.session.sessionStatus}</Badge> + </div> + <div> + <p className="text-sm font-medium">Project</p> + <p className="text-sm text-muted-foreground"> + {sessionDetail.session.projectCode} - {sessionDetail.session.projectName} + </p> + </div> + <div> + <p className="text-sm font-medium">Package</p> + <p className="text-sm text-muted-foreground"> + {sessionDetail.session.packageNo} - {sessionDetail.session.packageName} + </p> + </div> + </div> + + {/* PR Items */} + {sessionDetail.prItems?.length > 0 && ( + <div> + <h3 className="font-medium mb-2">PR Items</h3> + <div className="border rounded-lg"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b"> + <th className="text-left p-2">PR No</th> + <th className="text-left p-2">Material Code</th> + <th className="text-left p-2">Description</th> + <th className="text-left p-2">Qty</th> + <th className="text-left p-2">Delivery</th> + </tr> + </thead> + <tbody> + {sessionDetail.prItems.map((item: any) => ( + <tr key={item.id} className="border-b"> + <td className="p-2">{item.prNo}</td> + <td className="p-2">{item.materialCode}</td> + <td className="p-2">{item.materialDescription}</td> + <td className="p-2">{item.quantity} {item.uom}</td> + <td className="p-2"> + {item.deliveryDate ? formatDate(item.deliveryDate, "KR") : "-"} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + )} + </div> + ) : null} + </DialogContent> + </Dialog> + + {/* Documents Sheet */} + <Sheet open={documentsOpen} onOpenChange={setDocumentsOpen}> + <SheetContent className="w-[600px] sm:w-[800px]"> + <SheetHeader> + <SheetTitle>Documents & Comments</SheetTitle> + <SheetDescription> + Review documents and PDFTron comments + </SheetDescription> + </SheetHeader> + + {isLoadingDetail ? ( + <div className="p-8 text-center">Loading...</div> + ) : sessionDetail?.documents ? ( + <Tabs defaultValue="buyer" className="mt-4"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="buyer">Buyer Documents</TabsTrigger> + <TabsTrigger value="vendor">Vendor Documents</TabsTrigger> + </TabsList> + + <TabsContent value="buyer"> + <ScrollArea className="h-[calc(100vh-200px)]"> + <div className="space-y-2"> + {sessionDetail.documents + .filter((doc: any) => doc.documentSource === "buyer") + .map((doc: any) => ( + <div key={doc.documentId} className="border rounded-lg p-3"> + <div className="flex items-start justify-between"> + <div className="flex-1"> + <p className="font-medium text-sm">{doc.documentName}</p> + <p className="text-xs text-muted-foreground"> + Type: {doc.documentType} | Status: {doc.reviewStatus} + </p> + </div> + <div className="flex items-center gap-2"> + {doc.comments.totalCount > 0 && ( + <Badge variant={doc.comments.openCount > 0 ? "destructive" : "secondary"}> + {doc.comments.openCount}/{doc.comments.totalCount} comments + </Badge> + )} + <Button size="sm" variant="outline"> + View in PDFTron + </Button> + </div> + </div> + </div> + ))} + </div> + </ScrollArea> + </TabsContent> + + <TabsContent value="vendor"> + <ScrollArea className="h-[calc(100vh-200px)]"> + <div className="space-y-2"> + {sessionDetail.documents + .filter((doc: any) => doc.documentSource === "vendor") + .map((doc: any) => ( + <div key={doc.documentId} className="border rounded-lg p-3"> + <div className="flex items-start justify-between"> + <div className="flex-1"> + <p className="font-medium text-sm">{doc.documentName}</p> + <p className="text-xs text-muted-foreground"> + Type: {doc.documentType} | Status: {doc.reviewStatus} + </p> + {doc.submittedAt && ( + <p className="text-xs text-muted-foreground"> + Submitted: {formatDate(doc.submittedAt, "KR")} + </p> + )} + </div> + <div className="flex items-center gap-2"> + <Button size="sm" variant="outline"> + Download + </Button> + <Button size="sm" variant="outline"> + Review + </Button> + </div> + </div> + </div> + ))} + </div> + </ScrollArea> + </TabsContent> + </Tabs> + ) : null} + </SheetContent> + </Sheet> + + {/* PR Items Dialog */} + <Dialog open={prItemsOpen} onOpenChange={setPrItemsOpen}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>PR Items</DialogTitle> + <DialogDescription> + Purchase Request items for this RFQ + </DialogDescription> + </DialogHeader> + {sessionDetail?.prItems && ( + <div className="border rounded-lg"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b bg-muted/50"> + <th className="text-left p-2">PR No</th> + <th className="text-left p-2">Material Code</th> + <th className="text-left p-2">Description</th> + <th className="text-left p-2">Size</th> + <th className="text-left p-2">Qty</th> + <th className="text-left p-2">Unit</th> + <th className="text-left p-2">Delivery</th> + <th className="text-left p-2">Major</th> + </tr> + </thead> + <tbody> + {sessionDetail.prItems.map((item: any) => ( + <tr key={item.id} className="border-b hover:bg-muted/20"> + <td className="p-2">{item.prNo}</td> + <td className="p-2">{item.materialCode}</td> + <td className="p-2">{item.materialDescription}</td> + <td className="p-2">{item.size || "-"}</td> + <td className="p-2 text-right">{item.quantity}</td> + <td className="p-2">{item.uom}</td> + <td className="p-2"> + {item.deliveryDate ? formatDate(item.deliveryDate, "KR") : "-"} + </td> + <td className="p-2 text-center"> + {item.majorYn && <Badge variant="default">Major</Badge>} + </td> + </tr> + ))} + </tbody> + </table> + </div> + )} + </DialogContent> + </Dialog> + + {/* Evaluation Dialog */} + <Dialog open={evaluationOpen} onOpenChange={setEvaluationOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>TBE Evaluation</DialogTitle> + <DialogDescription> + Enter evaluation result for {selectedSession?.sessionCode} + </DialogDescription> + </DialogHeader> + <div className="space-y-4 mt-4"> + {/* Evaluation form would go here */} + <p className="text-sm text-muted-foreground"> + Evaluation form to be implemented... + </p> + </div> + </DialogContent> + </Dialog> + </> + ) +}
\ No newline at end of file |
