From 95866a13ba4e1c235373834460aa284b763fe0d9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 23 Jun 2025 09:03:29 +0000 Subject: (최겸) 기술영업 RFQ 개발(0620 요구사항, 첨부파일, REV 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/detail-table/rfq-detail-table.tsx | 178 ++++++++++++++++----- 1 file changed, 134 insertions(+), 44 deletions(-) (limited to 'lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx') diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index f2eda8d9..1d701bd5 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -12,12 +12,14 @@ import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { Loader2, UserPlus, BarChart2, Send, Trash2 } from "lucide-react" +import { Loader2, UserPlus, Send, Trash2, CheckCircle } from "lucide-react" import { ClientDataTable } from "@/components/client-data-table/data-table" import { AddVendorDialog } from "./add-vendor-dialog" import { VendorCommunicationDrawer } from "./vendor-communication-drawer" -import { VendorQuotationComparisonDialog } from "./vendor-quotation-comparison-dialog" import { DeleteVendorsDialog } from "../delete-vendors-dialog" +import { QuotationHistoryDialog } from "@/lib/techsales-rfq/table/detail-table/quotation-history-dialog" +import { TechSalesQuotationAttachmentsSheet, type QuotationAttachment } from "../tech-sales-quotation-attachments-sheet" +import type { QuotationInfo } from "./rfq-detail-column" // 기본적인 RFQ 타입 정의 interface TechSalesRfq { @@ -30,6 +32,8 @@ interface TechSalesRfq { rfqSendDate?: Date | null dueDate?: Date | null createdByName?: string | null + rfqType: "SHIP" | "TOP" | "HULL" | null + ptypeNm?: string | null } // 프로퍼티 정의 @@ -58,9 +62,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps // 읽지 않은 메시지 개수 const [unreadMessages, setUnreadMessages] = useState>({}) - // 견적 비교 다이얼로그 상태 관리 - const [comparisonDialogOpen, setComparisonDialogOpen] = useState(false) - // 테이블 선택 상태 관리 const [selectedRows, setSelectedRows] = useState([]) const [isSendingRfq, setIsSendingRfq] = useState(false) @@ -69,6 +70,16 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps // 벤더 삭제 확인 다이얼로그 상태 추가 const [deleteConfirmDialogOpen, setDeleteConfirmDialogOpen] = useState(false) + // 견적 히스토리 다이얼로그 상태 관리 + const [historyDialogOpen, setHistoryDialogOpen] = useState(false) + const [selectedQuotationId, setSelectedQuotationId] = useState(null) + + // 견적서 첨부파일 sheet 상태 관리 + const [quotationAttachmentsSheetOpen, setQuotationAttachmentsSheetOpen] = useState(false) + const [selectedQuotationInfo, setSelectedQuotationInfo] = useState(null) + const [quotationAttachments, setQuotationAttachments] = useState([]) + const [isLoadingAttachments, setIsLoadingAttachments] = useState(false) + // selectedRfq ID 메모이제이션 (객체 참조 변경 방지) const selectedRfqId = useMemo(() => selectedRfq?.id, [selectedRfq?.id]) @@ -108,6 +119,8 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps detailId: item.id, rfqId: selectedRfqId, rfqCode: selectedRfq?.rfqCode || null, + rfqType: selectedRfq?.rfqType || null, + ptypeNm: selectedRfq?.ptypeNm || null, vendorId: item.vendorId ? Number(item.vendorId) : undefined, })) || [] @@ -121,7 +134,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps console.error("데이터 새로고침 오류:", error) toast.error("데이터를 새로고침하는 중 오류가 발생했습니다") } - }, [selectedRfqId, selectedRfq?.rfqCode, loadUnreadMessages]) + }, [selectedRfqId, selectedRfq?.rfqCode, selectedRfq?.rfqType, selectedRfq?.ptypeNm, loadUnreadMessages]) // 벤더 추가 핸들러 메모이제이션 const handleAddVendor = useCallback(async () => { @@ -180,6 +193,54 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps } }, [selectedRows, selectedRfqId, handleRefreshData]); + // 벤더 선택 핸들러 추가 + const [isAcceptingVendors, setIsAcceptingVendors] = useState(false); + + const handleAcceptVendors = useCallback(async () => { + if (selectedRows.length === 0) { + toast.warning("선택할 벤더를 선택해주세요."); + return; + } + + if (selectedRows.length > 1) { + toast.warning("하나의 벤더만 선택할 수 있습니다."); + return; + } + + const selectedQuotation = selectedRows[0]; + if (selectedQuotation.status !== "Submitted") { + toast.warning("제출된 견적서만 선택할 수 있습니다."); + return; + } + + try { + setIsAcceptingVendors(true); + + // 벤더 견적 승인 서비스 함수 호출 + const { acceptTechSalesVendorQuotationAction } = await import("@/lib/techsales-rfq/actions"); + + const result = await acceptTechSalesVendorQuotationAction(selectedQuotation.id); + + if (result.success) { + toast.success(result.message || "벤더가 성공적으로 선택되었습니다."); + } else { + toast.error(result.error || "벤더 선택 중 오류가 발생했습니다."); + } + + // 선택 해제 + setSelectedRows([]); + + // 데이터 새로고침 + await handleRefreshData(); + + } catch (error) { + console.error("벤더 선택 오류:", error); + toast.error("벤더 선택 중 오류가 발생했습니다."); + } finally { + setIsAcceptingVendors(false); + } + }, [selectedRows, handleRefreshData]); + // 벤더 삭제 핸들러 메모이제이션 const handleDeleteVendors = useCallback(async () => { if (selectedRows.length === 0) { @@ -246,27 +307,47 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps await handleDeleteVendors(); }, [handleDeleteVendors]); - // 견적 비교 다이얼로그 열기 핸들러 메모이제이션 - const handleOpenComparisonDialog = useCallback(() => { - // 제출된 견적이 있는 벤더가 최소 1개 이상 있는지 확인 - const hasSubmittedQuotations = details.some(detail => - detail.status === "Submitted" // RfqDetailView의 실제 필드 사용 - ); - if (!hasSubmittedQuotations) { - toast.warning("제출된 견적이 없습니다."); - return; - } + // 견적 히스토리 다이얼로그 열기 핸들러 메모이제이션 + const handleOpenHistoryDialog = useCallback((quotationId: number) => { + setSelectedQuotationId(quotationId); + setHistoryDialogOpen(true); + }, []) - setComparisonDialogOpen(true); - }, [details]) + // 견적서 첨부파일 sheet 열기 핸들러 메모이제이션 + const handleOpenQuotationAttachmentsSheet = useCallback(async (quotationId: number, quotationInfo: QuotationInfo) => { + try { + setIsLoadingAttachments(true); + setSelectedQuotationInfo(quotationInfo); + setQuotationAttachmentsSheetOpen(true); + + // 견적서 첨부파일 조회 + const { getTechSalesVendorQuotationAttachments } = await import("@/lib/techsales-rfq/service"); + const result = await getTechSalesVendorQuotationAttachments(quotationId); + + if (result.error) { + toast.error(result.error); + setQuotationAttachments([]); + } else { + setQuotationAttachments(result.data || []); + } + } catch (error) { + console.error("견적서 첨부파일 조회 오류:", error); + toast.error("견적서 첨부파일을 불러오는 중 오류가 발생했습니다."); + setQuotationAttachments([]); + } finally { + setIsLoadingAttachments(false); + } + }, []) // 칼럼 정의 - unreadMessages 상태 전달 (메모이제이션) const columns = useMemo(() => getRfqDetailColumns({ setRowAction, - unreadMessages - }), [unreadMessages]) + unreadMessages, + onQuotationClick: handleOpenHistoryDialog, + openQuotationAttachmentsSheet: handleOpenQuotationAttachmentsSheet + }), [unreadMessages, handleOpenHistoryDialog, handleOpenQuotationAttachmentsSheet]) // 필터 필드 정의 (메모이제이션) const advancedFilterFields = useMemo( @@ -493,6 +574,22 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps )}
+ {/* 벤더 선택 버튼 */} + + {/* RFQ 발송 버튼 */} - {/* 견적 비교 버튼 */} - - {/* 벤더 추가 버튼 */}
) } \ No newline at end of file -- cgit v1.2.3