summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx')
-rw-r--r--lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx343
1 files changed, 343 insertions, 0 deletions
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<void>
+}
+
+export function VendorContactSelectionDialog({
+ open,
+ onOpenChange,
+ vendorIds,
+ onSendRfq
+}: VendorContactSelectionDialogProps) {
+ const [vendorsWithContacts, setVendorsWithContacts] = useState<Record<number, VendorWithContacts>>({})
+ const [selectedContacts, setSelectedContacts] = useState<SelectedContact[]>([])
+ 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 (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
+ <DialogHeader>
+ <DialogTitle>RFQ 발송 대상 선택</DialogTitle>
+ <DialogDescription>
+ 각 벤더의 연락처를 선택하여 RFQ를 발송하세요. 기본적으로 모든 연락처가 선택되어 있습니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 overflow-y-auto space-y-4">
+ {isLoading ? (
+ <div className="space-y-4">
+ {[1, 2, 3].map((i) => (
+ <div key={i} className="space-y-3">
+ <Skeleton className="h-6 w-40" />
+ <div className="space-y-2 pl-4">
+ <Skeleton className="h-16 w-full" />
+ <Skeleton className="h-16 w-full" />
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : Object.keys(vendorsWithContacts).length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ <Mail className="size-12 mx-auto mb-2 opacity-50" />
+ <p>연락처 정보가 없습니다.</p>
+ <p className="text-sm">벤더의 연락처를 먼저 등록해주세요.</p>
+ </div>
+ ) : (
+ Object.entries(vendorsWithContacts).map(([vendorId, vendorData]) => {
+ const selectionState = getVendorSelectionState(Number(vendorId), vendorData)
+
+ return (
+ <div key={vendorId} className="border rounded-lg p-4">
+ <div className="flex items-center justify-between mb-3">
+ <div className="flex items-center gap-3">
+ <Checkbox
+ checked={selectionState === "all"}
+ ref={(el) => {
+ if (el) {
+ const input = el.querySelector('input[type="checkbox"]') as HTMLInputElement
+ if (input) {
+ input.indeterminate = selectionState === "partial"
+ }
+ }
+ }}
+ onCheckedChange={() => handleVendorToggle(Number(vendorId), vendorData)}
+ />
+ <div>
+ <h3 className="font-medium">{vendorData.vendor.vendorName}</h3>
+ {vendorData.vendor.vendorCode && (
+ <p className="text-sm text-muted-foreground">
+ 코드: {vendorData.vendor.vendorCode}
+ </p>
+ )}
+ </div>
+ </div>
+ <Badge variant="outline">
+ {selectedContacts.filter(sc => sc.vendorId === Number(vendorId)).length} / {vendorData.contacts.length} 선택됨
+ </Badge>
+ </div>
+
+ <div className="space-y-2 pl-6">
+ {vendorData.contacts.map((contact) => (
+ <div
+ key={contact.id}
+ className={`flex items-center justify-between p-3 rounded border ${
+ isContactSelected(Number(vendorId), contact.id)
+ ? "bg-blue-50 border-blue-200"
+ : "bg-gray-50 border-gray-200"
+ }`}
+ >
+ <div className="flex items-center gap-3">
+ <Checkbox
+ checked={isContactSelected(Number(vendorId), contact.id)}
+ onCheckedChange={() => handleContactToggle(Number(vendorId), contact)}
+ />
+ <div className="flex items-center gap-2">
+ <User className="size-4 text-muted-foreground" />
+ <div>
+ <div className="flex items-center gap-2">
+ <span className="font-medium">{contact.contactName}</span>
+ </div>
+ {contact.contactPosition && (
+ <p className="text-sm text-muted-foreground">
+ {contact.contactPosition}
+ </p>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-4 text-sm">
+ <div className="flex items-center gap-1">
+ <Mail className="size-4 text-muted-foreground" />
+ <span>{contact.contactEmail}</span>
+ </div>
+ {contact.contactPhone && (
+ <div className="flex items-center gap-1">
+ <Phone className="size-4 text-muted-foreground" />
+ <span>{contact.contactPhone}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+ })
+ )}
+ </div>
+
+ <DialogFooter>
+ <div className="flex items-center justify-between w-full">
+ <div className="text-sm text-muted-foreground">
+ 총 {selectedContacts.length}명의 연락처가 선택됨
+ </div>
+ <div className="flex gap-2">
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ 취소
+ </Button>
+ <Button
+ onClick={handleSendRfq}
+ disabled={selectedContacts.length === 0 || isSending}
+ className="flex items-center gap-2"
+ >
+ {isSending ? (
+ <>
+ <Loader2 className="size-4 animate-spin" />
+ 발송 중...
+ </>
+ ) : (
+ <>
+ <Send className="size-4" />
+ RFQ 발송 ({selectedContacts.length}명)
+ </>
+ )}
+ </Button>
+ </div>
+ </div>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file