'use client' import * as React from 'react' import { Building, User, Plus, Trash2, Users } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { getBiddingVendors, getBiddingCompanyContacts, createBiddingCompanyContact, deleteBiddingCompanyContact, getVendorContactsByVendorId, updateBiddingCompanyPriceAdjustmentQuestion, getBiddingCompaniesByBidPicId, addBiddingCompanyFromOtherBidding } from '@/lib/bidding/service' import { deleteBiddingCompany } from '@/lib/bidding/pre-quote/service' import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Checkbox } from '@/components/ui/checkbox' import { Loader2 } from 'lucide-react' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { PurchaseGroupCodeSelector, PurchaseGroupCodeWithUser } from '@/components/common/selectors/purchase-group-code/purchase-group-code-selector' interface QuotationVendor { id: number // biddingCompanies.id companyId?: number // vendors.id (벤더 ID) vendorName: string vendorCode: string contactPerson?: string contactEmail?: string contactPhone?: string quotationAmount?: number currency: string invitationStatus: string isPriceAdjustmentApplicableQuestion?: boolean businessSize?: string | null } interface BiddingCompaniesEditorProps { biddingId: number readonly?: boolean } interface VendorContact { id: number vendorId: number contactName: string contactPosition: string | null contactDepartment: string | null contactTask: string | null contactEmail: string contactPhone: string | null isPrimary: boolean } interface BiddingCompanyContact { id: number biddingId: number vendorId: number contactName: string contactEmail: string contactNumber: string | null createdAt: Date updatedAt: Date } export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingCompaniesEditorProps) { const [vendors, setVendors] = React.useState([]) const [isLoading, setIsLoading] = React.useState(false) const [addVendorDialogOpen, setAddVendorDialogOpen] = React.useState(false) const [selectedVendor, setSelectedVendor] = React.useState(null) const [biddingCompanyContacts, setBiddingCompanyContacts] = React.useState([]) const [isLoadingContacts, setIsLoadingContacts] = React.useState(false) // 각 업체별 첫 번째 담당자 정보 저장 (vendorId -> 첫 번째 담당자) const [vendorFirstContacts, setVendorFirstContacts] = React.useState>(new Map()) // 담당자 추가 다이얼로그 const [addContactDialogOpen, setAddContactDialogOpen] = React.useState(false) const [newContact, setNewContact] = React.useState({ contactName: '', contactEmail: '', contactNumber: '', }) const [addContactFromVendorDialogOpen, setAddContactFromVendorDialogOpen] = React.useState(false) const [vendorContacts, setVendorContacts] = React.useState([]) const [isLoadingVendorContacts, setIsLoadingVendorContacts] = React.useState(false) const [selectedContactFromVendor, setSelectedContactFromVendor] = React.useState(null) // 협력사 멀티 선택 다이얼로그 const [multiSelectDialogOpen, setMultiSelectDialogOpen] = React.useState(false) const [selectedBidPic, setSelectedBidPic] = React.useState(undefined) const [biddingCompaniesList, setBiddingCompaniesList] = React.useState>([]) const [isLoadingBiddingCompanies, setIsLoadingBiddingCompanies] = React.useState(false) const [selectedBiddingCompany, setSelectedBiddingCompany] = React.useState<{ biddingId: number companyId: number } | null>(null) const [selectedBiddingCompanyContacts, setSelectedBiddingCompanyContacts] = React.useState([]) const [isLoadingCompanyContacts, setIsLoadingCompanyContacts] = React.useState(false) // 업체 목록 다시 로딩 함수 const reloadVendors = React.useCallback(async () => { try { const result = await getBiddingVendors(biddingId) if (result.success && result.data) { const vendorsList = result.data.map(v => ({ ...v, companyId: v.companyId || undefined, vendorName: v.vendorName || '', vendorCode: v.vendorCode || '', contactPerson: v.contactPerson ?? undefined, contactEmail: v.contactEmail ?? undefined, contactPhone: v.contactPhone ?? undefined, quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined, isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false, businessSize: v.businessSize ?? undefined, })) setVendors(vendorsList) // 각 업체별 첫 번째 담당자 정보 로드 const firstContactsMap = new Map() const contactPromises = vendorsList .filter(v => v.companyId) .map(async (vendor) => { try { const contactResult = await getBiddingCompanyContacts(biddingId, vendor.companyId!) if (contactResult.success && contactResult.data && contactResult.data.length > 0) { firstContactsMap.set(vendor.companyId!, contactResult.data[0]) } } catch (error) { console.error(`Failed to load contact for vendor ${vendor.companyId}:`, error) } }) await Promise.all(contactPromises) setVendorFirstContacts(firstContactsMap) } } catch (error) { console.error('Failed to reload vendors:', error) } }, [biddingId]) // 데이터 로딩 React.useEffect(() => { const loadVendors = async () => { setIsLoading(true) try { const result = await getBiddingVendors(biddingId) if (result.success && result.data) { const vendorsList = result.data.map(v => ({ id: v.id, companyId: v.companyId || undefined, vendorName: v.vendorName || '', vendorCode: v.vendorCode || '', contactPerson: v.contactPerson !== null ? v.contactPerson : undefined, contactEmail: v.contactEmail !== null ? v.contactEmail : undefined, contactPhone: v.contactPhone !== null ? v.contactPhone : undefined, quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined, currency: v.currency || 'KRW', invitationStatus: v.invitationStatus, isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false, businessSize: v.businessSize ?? undefined, })) setVendors(vendorsList) // 각 업체별 첫 번째 담당자 정보 로드 const firstContactsMap = new Map() const contactPromises = vendorsList .filter(v => v.companyId) .map(async (vendor) => { try { const contactResult = await getBiddingCompanyContacts(biddingId, vendor.companyId!) if (contactResult.success && contactResult.data && contactResult.data.length > 0) { firstContactsMap.set(vendor.companyId!, contactResult.data[0]) } } catch (error) { console.error(`Failed to load contact for vendor ${vendor.companyId}:`, error) } }) await Promise.all(contactPromises) setVendorFirstContacts(firstContactsMap) } else { toast.error(result.error || '업체 정보를 불러오는데 실패했습니다.') setVendors([]) } } catch (error) { console.error('Failed to load vendors:', error) toast.error('업체 정보를 불러오는데 실패했습니다.') setVendors([]) } finally { setIsLoading(false) } } loadVendors() }, [biddingId]) // 업체 선택 핸들러 (단일 선택) const handleVendorSelect = async (vendor: QuotationVendor) => { // 이미 선택된 업체를 다시 클릭하면 선택 해제 if (selectedVendor?.id === vendor.id) { setSelectedVendor(null) setBiddingCompanyContacts([]) return } // 새 업체 선택 setSelectedVendor(vendor) // 선택한 업체의 담당자 목록 로딩 if (vendor.companyId) { setIsLoadingContacts(true) try { const result = await getBiddingCompanyContacts(biddingId, vendor.companyId) if (result.success && result.data) { setBiddingCompanyContacts(result.data) } else { toast.error(result.error || '담당자 목록을 불러오는데 실패했습니다.') setBiddingCompanyContacts([]) } } catch (error) { console.error('Failed to load contacts:', error) toast.error('담당자 목록을 불러오는데 실패했습니다.') setBiddingCompanyContacts([]) } finally { setIsLoadingContacts(false) } } } // 업체 삭제 const handleRemoveVendor = async (vendorId: number) => { if (!confirm('정말로 이 업체를 삭제하시겠습니까?')) { return } try { const result = await deleteBiddingCompany(vendorId) if (result.success) { toast.success('업체가 삭제되었습니다.') // 업체 목록 다시 로딩 await reloadVendors() // 선택된 업체가 삭제된 경우 담당자 목록도 초기화 if (selectedVendor?.id === vendorId) { setSelectedVendor(null) setBiddingCompanyContacts([]) } } else { toast.error(result.error || '업체 삭제에 실패했습니다.') } } catch (error) { console.error('Failed to remove vendor:', error) toast.error('업체 삭제에 실패했습니다.') } } // 담당자 추가 (직접 입력) const handleAddContact = async () => { if (!selectedVendor || !selectedVendor.companyId) { toast.error('업체를 선택해주세요.') return } if (!newContact.contactName || !newContact.contactEmail) { toast.error('이름과 이메일은 필수입니다.') return } // 전화번호 형식 검증 (국제 표준) if (newContact.contactNumber && !newContact.contactNumber.startsWith('+')) { toast.error('전화번호는 국제 표준 형식(+)으로 시작해야 합니다. (예: +821012345678)') return } try { const result = await createBiddingCompanyContact( biddingId, selectedVendor.companyId, { contactName: newContact.contactName, contactEmail: newContact.contactEmail, contactNumber: newContact.contactNumber || undefined, } ) if (result.success) { toast.success('담당자가 추가되었습니다.') setAddContactDialogOpen(false) setNewContact({ contactName: '', contactEmail: '', contactNumber: '' }) // 담당자 목록 새로고침 const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId) if (contactsResult.success && contactsResult.data) { setBiddingCompanyContacts(contactsResult.data) // 첫 번째 담당자 정보 업데이트 if (contactsResult.data.length > 0) { setVendorFirstContacts(prev => { const newMap = new Map(prev) newMap.set(selectedVendor.companyId!, contactsResult.data[0]) return newMap }) } } } else { toast.error(result.error || '담당자 추가에 실패했습니다.') } } catch (error) { console.error('Failed to add contact:', error) toast.error('담당자 추가에 실패했습니다.') } } // 담당자 추가 (벤더 목록에서 선택) const handleOpenAddContactFromVendor = async () => { if (!selectedVendor || !selectedVendor.companyId) { toast.error('업체를 선택해주세요.') return } setIsLoadingVendorContacts(true) setAddContactFromVendorDialogOpen(true) setSelectedContactFromVendor(null) try { const result = await getVendorContactsByVendorId(selectedVendor.companyId) if (result.success && result.data) { setVendorContacts(result.data) } else { toast.error(result.error || '벤더 담당자 목록을 불러오는데 실패했습니다.') setVendorContacts([]) } } catch (error) { console.error('Failed to load vendor contacts:', error) toast.error('벤더 담당자 목록을 불러오는데 실패했습니다.') setVendorContacts([]) } finally { setIsLoadingVendorContacts(false) } } // 벤더 담당자 선택 후 저장 const handleAddContactFromVendor = async () => { if (!selectedContactFromVendor || !selectedVendor || !selectedVendor.companyId) { toast.error('담당자를 선택해주세요.') return } try { const result = await createBiddingCompanyContact( biddingId, selectedVendor.companyId, { contactName: selectedContactFromVendor.contactName, contactEmail: selectedContactFromVendor.contactEmail, contactNumber: selectedContactFromVendor.contactPhone || undefined, } ) if (result.success) { toast.success('담당자가 추가되었습니다.') setAddContactFromVendorDialogOpen(false) setSelectedContactFromVendor(null) // 담당자 목록 새로고침 const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId) if (contactsResult.success && contactsResult.data) { setBiddingCompanyContacts(contactsResult.data) // 첫 번째 담당자 정보 업데이트 if (contactsResult.data.length > 0) { setVendorFirstContacts(prev => { const newMap = new Map(prev) newMap.set(selectedVendor.companyId!, contactsResult.data[0]) return newMap }) } } } else { toast.error(result.error || '담당자 추가에 실패했습니다.') } } catch (error) { console.error('Failed to add contact:', error) toast.error('담당자 추가에 실패했습니다.') } } // 담당자 삭제 const handleDeleteContact = async (contactId: number) => { if (!confirm('정말로 이 담당자를 삭제하시겠습니까?')) { return } try { const result = await deleteBiddingCompanyContact(contactId) if (result.success) { toast.success('담당자가 삭제되었습니다.') // 담당자 목록 새로고침 if (selectedVendor && selectedVendor.companyId) { const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId) if (contactsResult.success && contactsResult.data) { setBiddingCompanyContacts(contactsResult.data) // 첫 번째 담당자 정보 업데이트 if (contactsResult.data.length > 0) { setVendorFirstContacts(prev => { const newMap = new Map(prev) newMap.set(selectedVendor.companyId!, contactsResult.data[0]) return newMap }) } else { // 담당자가 없으면 Map에서 제거 setVendorFirstContacts(prev => { const newMap = new Map(prev) newMap.delete(selectedVendor.companyId!) return newMap }) } } } } else { toast.error(result.error || '담당자 삭제에 실패했습니다.') } } catch (error) { console.error('Failed to delete contact:', error) toast.error('담당자 삭제에 실패했습니다.') } } // 연동제 적용요건 문의 체크박스 변경 const handleTogglePriceAdjustmentQuestion = async (vendorId: number, checked: boolean) => { try { const result = await updateBiddingCompanyPriceAdjustmentQuestion(vendorId, checked) if (result.success) { // 로컬 상태 업데이트 setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, isPriceAdjustmentApplicableQuestion: checked } : v )) // 선택된 업체 정보도 업데이트 if (selectedVendor?.id === vendorId) { setSelectedVendor(prev => prev ? { ...prev, isPriceAdjustmentApplicableQuestion: checked } : null) } // 담당자 목록 새로고침 (첫 번째 담당자 정보 업데이트를 위해) if (selectedVendor && selectedVendor.companyId) { const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId) if (contactsResult.success && contactsResult.data) { setBiddingCompanyContacts(contactsResult.data) if (contactsResult.data.length > 0) { setVendorFirstContacts(prev => { const newMap = new Map(prev) newMap.set(selectedVendor.companyId!, contactsResult.data[0]) return newMap }) } } } } else { toast.error(result.error || '연동제 적용요건 문의 여부 업데이트에 실패했습니다.') } } catch (error) { console.error('Failed to update price adjustment question:', error) toast.error('연동제 적용요건 문의 여부 업데이트에 실패했습니다.') } } if (isLoading) { return (
업체 정보를 불러오는 중...
) } return (
{/* 참여 업체 목록 테이블 */}
참여 업체 목록

입찰에 참여하는 업체들을 관리합니다. 업체를 선택하면 하단에 담당자 목록이 표시됩니다.

{!readonly && (
)}
{vendors.length === 0 ? (
참여 업체가 없습니다. 업체를 추가해주세요.
) : ( 선택 업체명 업체코드 기업규모 담당자 이름 담당자 이메일 담당자 연락처 상태 연동제 적용요건 문의 작업 {vendors.map((vendor) => ( handleVendorSelect(vendor)} > e.stopPropagation()}> handleVendorSelect(vendor)} disabled={readonly} /> {vendor.vendorName} {vendor.vendorCode} {(() => { switch (vendor.businessSize) { case 'A': return '대기업'; case 'B': return '중견기업'; case 'C': return '중소기업'; case 'D': return '소기업'; default: return '-'; } })()} {vendor.companyId && vendorFirstContacts.has(vendor.companyId) ? vendorFirstContacts.get(vendor.companyId)!.contactName : '-'} {vendor.companyId && vendorFirstContacts.has(vendor.companyId) ? vendorFirstContacts.get(vendor.companyId)!.contactEmail : '-'} {vendor.companyId && vendorFirstContacts.has(vendor.companyId) ? vendorFirstContacts.get(vendor.companyId)!.contactNumber || '-' : '-'} {vendor.invitationStatus}
handleTogglePriceAdjustmentQuestion(vendor.id, checked as boolean) } disabled={readonly} /> {vendor.isPriceAdjustmentApplicableQuestion ? '예' : '아니오'}
))}
)}
{/* 선택한 업체의 담당자 목록 테이블 */} {selectedVendor && (
{selectedVendor.vendorName} 담당자 목록

선택한 업체의 선정된 담당자를 관리합니다.

{isLoadingContacts ? (
담당자 목록을 불러오는 중...
) : biddingCompanyContacts.length === 0 ? (
등록된 담당자가 없습니다. 담당자를 추가해주세요.
) : ( 이름 이메일 전화번호 작업 {biddingCompanyContacts.map((biddingCompanyContact) => ( {biddingCompanyContact.contactName} {biddingCompanyContact.contactEmail} {biddingCompanyContact.contactNumber || '-'} ))}
)}
)} {/* 업체 추가 다이얼로그 */} {/* 담당자 추가 다이얼로그 (직접 입력) */} 담당자 추가 새로운 담당자 정보를 입력하세요.
setNewContact(prev => ({ ...prev, contactName: e.target.value }))} placeholder="담당자 이름" />
setNewContact(prev => ({ ...prev, contactEmail: e.target.value }))} placeholder="example@email.com" />
setNewContact(prev => ({ ...prev, contactNumber: e.target.value }))} placeholder="+821012345678" />

* SMS 발송을 위해 국제 표준 형식으로 입력해주세요. (예: +821012345678)

{/* 협력사 멀티 선택 다이얼로그 */} 참여협력사 선택 입찰담당자를 선택하여 해당 담당자의 입찰 업체를 조회하고 선택할 수 있습니다.
{/* 입찰담당자 선택 */}
{ setSelectedBidPic(code) if (code.user?.id) { setIsLoadingBiddingCompanies(true) try { const result = await getBiddingCompaniesByBidPicId(code.user.id) if (result.success && result.data) { setBiddingCompaniesList(result.data) } else { toast.error(result.error || '입찰 업체 조회에 실패했습니다.') setBiddingCompaniesList([]) } } catch (error) { console.error('Failed to load bidding companies:', error) toast.error('입찰 업체 조회에 실패했습니다.') setBiddingCompaniesList([]) } finally { setIsLoadingBiddingCompanies(false) } } }} placeholder="입찰담당자 선택" disabled={readonly} />
{/* 입찰 업체 목록 */} {isLoadingBiddingCompanies ? (
입찰 업체를 불러오는 중...
) : biddingCompaniesList.length === 0 && selectedBidPic ? (
해당 입찰담당자의 입찰 업체가 없습니다.
) : biddingCompaniesList.length > 0 ? (
선택 입찰번호 입찰명 협력사코드 협력사명 입찰 업데이트일 {biddingCompaniesList.map((company) => { const isSelected = selectedBiddingCompany?.biddingId === company.biddingId && selectedBiddingCompany?.companyId === company.companyId return ( { if (isSelected) { setSelectedBiddingCompany(null) setSelectedBiddingCompanyContacts([]) return } setSelectedBiddingCompany({ biddingId: company.biddingId, companyId: company.companyId }) setIsLoadingCompanyContacts(true) try { const contactsResult = await getBiddingCompanyContacts(company.biddingId, company.companyId) if (contactsResult.success && contactsResult.data) { setSelectedBiddingCompanyContacts(contactsResult.data) } else { setSelectedBiddingCompanyContacts([]) } } catch (error) { console.error('Failed to load company contacts:', error) setSelectedBiddingCompanyContacts([]) } finally { setIsLoadingCompanyContacts(false) } }} > e.stopPropagation()}> { // 클릭 이벤트는 TableRow의 onClick에서 처리 }} disabled={readonly} /> {company.biddingNumber} {company.biddingTitle} {company.vendorCode} {company.vendorName} {company.updatedAt ? new Date(company.updatedAt).toLocaleDateString('ko-KR') : '-'} ) })}
{/* 선택한 입찰 업체의 담당자 정보 */} {selectedBiddingCompany !== null && (

담당자 정보

{isLoadingCompanyContacts ? (
담당자 정보를 불러오는 중...
) : selectedBiddingCompanyContacts.length === 0 ? (
등록된 담당자가 없습니다.
) : (
{selectedBiddingCompanyContacts.map((contact) => (
{contact.contactName} {contact.contactEmail} {contact.contactNumber && ( {contact.contactNumber} )}
))}
)}
)}
) : null}
{/* 벤더 담당자에서 추가 다이얼로그 */} {selectedVendor ? `${selectedVendor.vendorName} 벤더 담당자에서 선택` : '벤더 담당자 선택'} 벤더에 등록된 담당자 목록에서 선택하세요.
{isLoadingVendorContacts ? (
담당자 목록을 불러오는 중...
) : vendorContacts.length === 0 ? (
등록된 담당자가 없습니다.
) : (
{vendorContacts.map((contact) => (
setSelectedContactFromVendor(contact)} >
setSelectedContactFromVendor(contact)} className="shrink-0" />
{contact.contactName} {contact.isPrimary && ( 주담당자 )}
{contact.contactPosition && (

{contact.contactPosition}

)}
{contact.contactEmail} {contact.contactPhone && {contact.contactPhone}}
))}
)}
) }