From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-contact-selection-dialog.tsx | 343 +++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx (limited to 'lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx') diff --git a/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx new file mode 100644 index 00000000..aa6f6c2f --- /dev/null +++ b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx @@ -0,0 +1,343 @@ +"use client" + +import * as React from "react" +import { useState, useEffect, useCallback } from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { Mail, Phone, User, Send, Loader2 } from "lucide-react" +import { toast } from "sonner" + +interface VendorContact { + id: number + contactName: string + contactPosition: string | null + contactEmail: string + contactPhone: string | null + isPrimary: boolean +} + +interface VendorWithContacts { + vendor: { + id: number + vendorName: string + vendorCode: string | null + } + contacts: VendorContact[] +} + +interface SelectedContact { + vendorId: number + contactId: number + contactEmail: string + contactName: string +} + +interface VendorContactSelectionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vendorIds: number[] + onSendRfq: (selectedContacts: SelectedContact[]) => Promise +} + +export function VendorContactSelectionDialog({ + open, + onOpenChange, + vendorIds, + onSendRfq +}: VendorContactSelectionDialogProps) { + const [vendorsWithContacts, setVendorsWithContacts] = useState>({}) + const [selectedContacts, setSelectedContacts] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isSending, setIsSending] = useState(false) + + // 벤더 contact 정보 조회 + useEffect(() => { + if (open && vendorIds.length > 0) { + loadVendorsContacts() + } + }, [open, vendorIds]) + + // 다이얼로그 닫힐 때 상태 초기화 + useEffect(() => { + if (!open) { + setVendorsWithContacts({}) + setSelectedContacts([]) + setIsLoading(false) + } + }, [open]) + + const loadVendorsContacts = useCallback(async () => { + try { + setIsLoading(true) + const { getTechVendorsContacts } = await import("@/lib/techsales-rfq/service") + + const result = await getTechVendorsContacts(vendorIds) + + if (result.error) { + toast.error(result.error) + return + } + + setVendorsWithContacts(result.data) + + // 기본 선택: 모든 contact 선택 + const defaultSelected: SelectedContact[] = [] + Object.values(result.data).forEach(vendorData => { + vendorData.contacts.forEach(contact => { + defaultSelected.push({ + vendorId: vendorData.vendor.id, + contactId: contact.id, + contactEmail: contact.contactEmail, + contactName: contact.contactName + }) + }) + }) + setSelectedContacts(defaultSelected) + + } catch (error) { + console.error("벤더 contact 조회 오류:", error) + toast.error("벤더 연락처를 불러오는 중 오류가 발생했습니다") + } finally { + setIsLoading(false) + } + }, [vendorIds]) + + // contact 선택/해제 핸들러 + const handleContactToggle = (vendorId: number, contact: VendorContact) => { + const isSelected = selectedContacts.some( + sc => sc.vendorId === vendorId && sc.contactId === contact.id + ) + + if (isSelected) { + // 선택 해제 + setSelectedContacts(prev => + prev.filter(sc => !(sc.vendorId === vendorId && sc.contactId === contact.id)) + ) + } else { + // 선택 추가 + setSelectedContacts(prev => [ + ...prev, + { + vendorId, + contactId: contact.id, + contactEmail: contact.contactEmail, + contactName: contact.contactName + } + ]) + } + } + + // 벤더별 전체 선택/해제 + const handleVendorToggle = (vendorId: number, vendorData: VendorWithContacts) => { + const vendorContacts = vendorData.contacts + const selectedVendorContacts = selectedContacts.filter(sc => sc.vendorId === vendorId) + + if (selectedVendorContacts.length === vendorContacts.length) { + // 전체 해제 + setSelectedContacts(prev => prev.filter(sc => sc.vendorId !== vendorId)) + } else { + // 전체 선택 + const newSelected = vendorContacts.map(contact => ({ + vendorId, + contactId: contact.id, + contactEmail: contact.contactEmail, + contactName: contact.contactName + })) + + setSelectedContacts(prev => [ + ...prev.filter(sc => sc.vendorId !== vendorId), + ...newSelected + ]) + } + } + + // RFQ 발송 핸들러 + const handleSendRfq = async () => { + if (selectedContacts.length === 0) { + toast.warning("발송할 연락처를 선택해주세요.") + return + } + + try { + setIsSending(true) + await onSendRfq(selectedContacts) + onOpenChange(false) + } catch (error) { + console.error("RFQ 발송 오류:", error) + } finally { + setIsSending(false) + } + } + + // 선택된 contact가 있는지 확인 + const isContactSelected = (vendorId: number, contactId: number) => { + return selectedContacts.some(sc => sc.vendorId === vendorId && sc.contactId === contactId) + } + + // 벤더별 선택 상태 확인 + const getVendorSelectionState = (vendorId: number, vendorData: VendorWithContacts) => { + const selectedVendorContacts = selectedContacts.filter(sc => sc.vendorId === vendorId) + const totalContacts = vendorData.contacts.length + + if (selectedVendorContacts.length === 0) return "none" + if (selectedVendorContacts.length === totalContacts) return "all" + return "partial" + } + + return ( + + + + RFQ 발송 대상 선택 + + 각 벤더의 연락처를 선택하여 RFQ를 발송하세요. 기본적으로 모든 연락처가 선택되어 있습니다. + + + +
+ {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ +
+ + +
+
+ ))} +
+ ) : Object.keys(vendorsWithContacts).length === 0 ? ( +
+ +

연락처 정보가 없습니다.

+

벤더의 연락처를 먼저 등록해주세요.

+
+ ) : ( + Object.entries(vendorsWithContacts).map(([vendorId, vendorData]) => { + const selectionState = getVendorSelectionState(Number(vendorId), vendorData) + + return ( +
+
+
+ { + if (el) { + const input = el.querySelector('input[type="checkbox"]') as HTMLInputElement + if (input) { + input.indeterminate = selectionState === "partial" + } + } + }} + onCheckedChange={() => handleVendorToggle(Number(vendorId), vendorData)} + /> +
+

{vendorData.vendor.vendorName}

+ {vendorData.vendor.vendorCode && ( +

+ 코드: {vendorData.vendor.vendorCode} +

+ )} +
+
+ + {selectedContacts.filter(sc => sc.vendorId === Number(vendorId)).length} / {vendorData.contacts.length} 선택됨 + +
+ +
+ {vendorData.contacts.map((contact) => ( +
+
+ handleContactToggle(Number(vendorId), contact)} + /> +
+ +
+
+ {contact.contactName} +
+ {contact.contactPosition && ( +

+ {contact.contactPosition} +

+ )} +
+
+
+ +
+
+ + {contact.contactEmail} +
+ {contact.contactPhone && ( +
+ + {contact.contactPhone} +
+ )} +
+
+ ))} +
+
+ ) + }) + )} +
+ + +
+
+ 총 {selectedContacts.length}명의 연락처가 선택됨 +
+
+ + +
+
+
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3