From cf8dac0c6490469dab88a560004b0c07dbd48612 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 18 Sep 2025 00:23:40 +0000 Subject: (대표님) rfq, 계약, 서명 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tbe-last/table/tbe-last-table-columns.tsx | 49 +++++++++++ lib/tbe-last/table/tbe-last-table.tsx | 118 ++++++++++++++++++++++---- 2 files changed, 149 insertions(+), 18 deletions(-) (limited to 'lib/tbe-last/table') diff --git a/lib/tbe-last/table/tbe-last-table-columns.tsx b/lib/tbe-last/table/tbe-last-table-columns.tsx index 726d8925..b18e51c0 100644 --- a/lib/tbe-last/table/tbe-last-table-columns.tsx +++ b/lib/tbe-last/table/tbe-last-table-columns.tsx @@ -86,6 +86,55 @@ export function getColumns({ cell: ({ row }) => row.original.rfqCode, size: 120, }, + + { + id: "tbeRequired", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const rfqCode = row.original.rfqCode; + const sessionStatus = row.original.sessionStatus; + + // 같은 RFQ의 첫 번째 행에만 체크박스 표시 + const allRows = table.getRowModel().rows; + const isFirstInGroup = allRows.findIndex( + r => r.original.rfqCode === rfqCode + ) === allRows.indexOf(row); + + if (!isFirstInGroup) return null; + + // 같은 RFQ의 모든 row + const rfqRows = allRows.filter( + r => r.original.rfqCode === rfqCode + ); + + const vendorCount = rfqRows.length; + + // 같은 RFQ의 row들이 선택되었는지 확인 + const isChecked = rfqRows.every(r => r.getIsSelected()); + const isIndeterminate = rfqRows.some(r => r.getIsSelected()) && !isChecked; + + return ( +
+ { + // RFQ의 모든 벤더 선택/해제 + rfqRows.forEach(r => { + r.toggleSelected(!!checked); + }); + }} + /> + + ({vendorCount} vendors) + +
+ ); + }, + size: 120, + }, // RFQ Title { diff --git a/lib/tbe-last/table/tbe-last-table.tsx b/lib/tbe-last/table/tbe-last-table.tsx index a9328bdf..c18768cd 100644 --- a/lib/tbe-last/table/tbe-last-table.tsx +++ b/lib/tbe-last/table/tbe-last-table.tsx @@ -10,7 +10,7 @@ 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 { getAllTBELast, getTBESessionDetail, requestTBEForRFQ } from "@/lib/tbe-last/service" import { Button } from "@/components/ui/button" import { Download, RefreshCw } from "lucide-react" import { exportTableToExcel } from "@/lib/export" @@ -20,6 +20,7 @@ import { SessionDetailDialog } from "./session-detail-dialog" import { DocumentsSheet } from "./documents-sheet" import { PrItemsDialog } from "./pr-items-dialog" import { EvaluationDialog } from "./evaluation-dialog" +import { toast } from "sonner" interface TbeLastTableProps { promises: Promise<[ @@ -30,21 +31,21 @@ interface TbeLastTableProps { export function TbeLastTable({ promises }: TbeLastTableProps) { const router = useRouter() const [{ data, pageCount }] = React.use(promises) - - console.log(data,"data") + + console.log(data, "data") // 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(null) const [selectedRfqId, setSelectedRfqId] = React.useState(null) const [selectedSession, setSelectedSession] = React.useState(null) const [sessionDetail, setSessionDetail] = React.useState(null) const [isLoadingDetail, setIsLoadingDetail] = React.useState(false) - + // Load session detail when needed const loadSessionDetail = React.useCallback(async (sessionId: number) => { setIsLoadingDetail(true) @@ -57,37 +58,37 @@ export function TbeLastTable({ promises }: TbeLastTableProps) { 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) loadSessionDetail(session.rfqId) }, []) - + const handleRefresh = React.useCallback(() => { router.refresh() }, [router]) - + // Table columns const columns = React.useMemo( () => @@ -99,7 +100,7 @@ export function TbeLastTable({ promises }: TbeLastTableProps) { }), [handleOpenSessionDetail, handleOpenDocuments, handleOpenPrItems, handleOpenEvaluation] ) - + // Filter fields const filterFields: DataTableAdvancedFilterField[] = [ { @@ -125,7 +126,7 @@ export function TbeLastTable({ promises }: TbeLastTableProps) { ], }, ] - + // Data table const { table } = useDataTable({ data, @@ -142,7 +143,64 @@ export function TbeLastTable({ promises }: TbeLastTableProps) { shallow: false, clearOnDefault: true, }) - + + const handleBulkTBERequest = React.useCallback(async (rfqGroups: Map) => { + try { + const promises = Array.from(rfqGroups.entries()).map(async ([rfqCode, sessions]) => { + // 준비중 상태인 세션만 필터링 + const pendingSessions = sessions.filter(s => s.sessionStatus === "준비중"); + + if (pendingSessions.length === 0) { + toast.info(`RFQ ${rfqCode}: 이미 TBE가 요청되었습니다.`); + return null; + } + + const vendors = pendingSessions.map(session => ({ + sessionId: session.tbeSessionId, + vendorId: session.vendorId, // vendor ID 추가 + vendorCode: session.vendorCode, + vendorName: session.vendorName, + })); + + const rfqInfo = { + rfqId: sessions[0].rfqId, // rfqLastId 추가 + rfqCode: sessions[0].rfqCode, + rfqTitle: sessions[0].rfqTitle || "", + rfqDueDate: sessions[0].rfqDueDate, + projectCode: sessions[0].projectCode || "", + projectName: sessions[0].projectName || "", + packageNo: sessions[0].packageNo || "", + packageName: sessions[0].packageName || "", + picName: sessions[0].picName || "", + }; + + return requestTBEForRFQ(rfqInfo, vendors); + }); + + const results = await Promise.allSettled(promises); + + const successCount = results.filter(r => r.status === "fulfilled" && r.value?.success).length; + const failCount = results.filter(r => r.status === "rejected" || (r.status === "fulfilled" && !r.value?.success)).length; + + if (successCount > 0) { + toast.success(`${successCount}개 RFQ에 대한 TBE 요청이 완료되었습니다.`); + } + + if (failCount > 0) { + toast.error(`${failCount}개 RFQ에 대한 TBE 요청이 실패했습니다.`); + } + + // 테이블 새로고침 + router.refresh(); + table.resetRowSelection(); + + } catch (error) { + console.error("TBE 요청 처리 중 오류:", error); + toast.error("TBE 요청 처리 중 오류가 발생했습니다."); + } + }, [router, table]); + + return ( <> @@ -152,6 +210,30 @@ export function TbeLastTable({ promises }: TbeLastTableProps) { shallow={false} >
+ + {table.getFilteredSelectedRowModel().rows.length > 0 && ( + + )}