summaryrefslogtreecommitdiff
path: root/lib/tbe-last/table/tbe-last-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tbe-last/table/tbe-last-table.tsx')
-rw-r--r--lib/tbe-last/table/tbe-last-table.tsx419
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