diff options
Diffstat (limited to 'components/bidding/manage/bidding-companies-editor.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-companies-editor.tsx | 803 |
1 files changed, 803 insertions, 0 deletions
diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx new file mode 100644 index 00000000..1ce8b014 --- /dev/null +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -0,0 +1,803 @@ +'use client' + +import * as React from 'react' +import { Building, User, Plus, Trash2 } from 'lucide-react' +import { toast } from 'sonner' + +import { Button } from '@/components/ui/button' +import { + getBiddingVendors, + getBiddingCompanyContacts, + createBiddingCompanyContact, + deleteBiddingCompanyContact, + getVendorContactsByVendorId, + updateBiddingCompanyPriceAdjustmentQuestion +} 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' + +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 +} + +interface BiddingCompaniesEditorProps { + biddingId: number +} + +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 }: BiddingCompaniesEditorProps) { + const [vendors, setVendors] = React.useState<QuotationVendor[]>([]) + const [isLoading, setIsLoading] = React.useState(false) + const [addVendorDialogOpen, setAddVendorDialogOpen] = React.useState(false) + const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null) + const [biddingCompanyContacts, setBiddingCompanyContacts] = React.useState<BiddingCompanyContact[]>([]) + const [isLoadingContacts, setIsLoadingContacts] = React.useState(false) + // 각 업체별 첫 번째 담당자 정보 저장 (vendorId -> 첫 번째 담당자) + const [vendorFirstContacts, setVendorFirstContacts] = React.useState<Map<number, BiddingCompanyContact>>(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<VendorContact[]>([]) + const [isLoadingVendorContacts, setIsLoadingVendorContacts] = React.useState(false) + const [selectedContactFromVendor, setSelectedContactFromVendor] = React.useState<VendorContact | null>(null) + + // 업체 목록 다시 로딩 함수 + 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, + })) + setVendors(vendorsList) + + // 각 업체별 첫 번째 담당자 정보 로드 + const firstContactsMap = new Map<number, BiddingCompanyContact>() + 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, + })) + setVendors(vendorsList) + + // 각 업체별 첫 번째 담당자 정보 로드 + const firstContactsMap = new Map<number, BiddingCompanyContact>() + 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 + } + + 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 ( + <div className="flex items-center justify-center p-8"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div> + <span className="ml-2">업체 정보를 불러오는 중...</span> + </div> + ) + } + + return ( + <div className="space-y-6"> + {/* 참여 업체 목록 테이블 */} + <Card> + <CardHeader className="flex flex-row items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + <Building className="h-5 w-5" /> + 참여 업체 목록 + </CardTitle> + <p className="text-sm text-muted-foreground mt-1"> + 입찰에 참여하는 업체들을 관리합니다. 업체를 선택하면 하단에 담당자 목록이 표시됩니다. + </p> + </div> + <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + 업체 추가 + </Button> + </CardHeader> + <CardContent> + {vendors.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + 참여 업체가 없습니다. 업체를 추가해주세요. + </div> + ) : ( + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[50px]">선택</TableHead> + <TableHead>업체명</TableHead> + <TableHead>업체코드</TableHead> + <TableHead>담당자 이름</TableHead> + <TableHead>담당자 이메일</TableHead> + <TableHead>담당자 연락처</TableHead> + <TableHead>상태</TableHead> + <TableHead className="w-[180px]">연동제 적용요건 문의</TableHead> + <TableHead className="w-[100px]">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {vendors.map((vendor) => ( + <TableRow + key={vendor.id} + className={`cursor-pointer hover:bg-muted/50 ${selectedVendor?.id === vendor.id ? 'bg-muted/50' : ''}`} + onClick={() => handleVendorSelect(vendor)} + > + <TableCell onClick={(e) => e.stopPropagation()}> + <Checkbox + checked={selectedVendor?.id === vendor.id} + onCheckedChange={() => handleVendorSelect(vendor)} + /> + </TableCell> + <TableCell className="font-medium">{vendor.vendorName}</TableCell> + <TableCell>{vendor.vendorCode}</TableCell> + <TableCell> + {vendor.companyId && vendorFirstContacts.has(vendor.companyId) + ? vendorFirstContacts.get(vendor.companyId)!.contactName + : '-'} + </TableCell> + <TableCell> + {vendor.companyId && vendorFirstContacts.has(vendor.companyId) + ? vendorFirstContacts.get(vendor.companyId)!.contactEmail + : '-'} + </TableCell> + <TableCell> + {vendor.companyId && vendorFirstContacts.has(vendor.companyId) + ? vendorFirstContacts.get(vendor.companyId)!.contactNumber || '-' + : '-'} + </TableCell> + + <TableCell> + <span className="px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800"> + {vendor.invitationStatus} + </span> + </TableCell> + <TableCell> + <div className="flex items-center gap-2"> + <Checkbox + checked={vendor.isPriceAdjustmentApplicableQuestion || false} + onCheckedChange={(checked) => + handleTogglePriceAdjustmentQuestion(vendor.id, checked as boolean) + } + /> + <span className="text-sm text-muted-foreground"> + {vendor.isPriceAdjustmentApplicableQuestion ? '예' : '아니오'} + </span> + </div> + </TableCell> + <TableCell> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveVendor(vendor.id)} + className="text-red-600 hover:text-red-800" + > + <Trash2 className="h-4 w-4" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} + </CardContent> + </Card> + + {/* 선택한 업체의 담당자 목록 테이블 */} + {selectedVendor && ( + <Card> + <CardHeader className="flex flex-row items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + <User className="h-5 w-5" /> + {selectedVendor.vendorName} 담당자 목록 + </CardTitle> + <p className="text-sm text-muted-foreground mt-1"> + 선택한 업체의 선정된 담당자를 관리합니다. + </p> + </div> + <div className="flex gap-2"> + <Button + variant="outline" + onClick={handleOpenAddContactFromVendor} + className="flex items-center gap-2" + > + <User className="h-4 w-4" /> + 업체 담당자 추가 + </Button> + <Button + onClick={() => setAddContactDialogOpen(true)} + className="flex items-center gap-2" + > + <Plus className="h-4 w-4" /> + 담당자 수기 입력 + </Button> + </div> + </CardHeader> + <CardContent> + {isLoadingContacts ? ( + <div className="flex items-center justify-center py-8"> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + <span className="text-sm text-muted-foreground">담당자 목록을 불러오는 중...</span> + </div> + ) : biddingCompanyContacts.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + 등록된 담당자가 없습니다. 담당자를 추가해주세요. + </div> + ) : ( + <Table> + <TableHeader> + <TableRow> + <TableHead>이름</TableHead> + <TableHead>이메일</TableHead> + <TableHead>전화번호</TableHead> + <TableHead className="w-[100px]">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {biddingCompanyContacts.map((biddingCompanyContact) => ( + <TableRow key={biddingCompanyContact.id}> + <TableCell className="font-medium">{biddingCompanyContact.contactName}</TableCell> + <TableCell>{biddingCompanyContact.contactEmail}</TableCell> + <TableCell>{biddingCompanyContact.contactNumber || '-'}</TableCell> + <TableCell> + <Button + variant="ghost" + size="sm" + onClick={() => handleDeleteContact(biddingCompanyContact.id)} + className="text-red-600 hover:text-red-800" + > + <Trash2 className="h-4 w-4" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} + </CardContent> + </Card> + )} + + {/* 업체 추가 다이얼로그 */} + <BiddingDetailVendorCreateDialog + biddingId={biddingId} + open={addVendorDialogOpen} + onOpenChange={setAddVendorDialogOpen} + onSuccess={reloadVendors} + /> + + {/* 담당자 추가 다이얼로그 (직접 입력) */} + <Dialog open={addContactDialogOpen} onOpenChange={setAddContactDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>담당자 추가</DialogTitle> + <DialogDescription> + 새로운 담당자 정보를 입력하세요. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4 py-4"> + <div className="space-y-2"> + <Label htmlFor="contactName">이름 *</Label> + <Input + id="contactName" + value={newContact.contactName} + onChange={(e) => setNewContact(prev => ({ ...prev, contactName: e.target.value }))} + placeholder="담당자 이름" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="contactEmail">이메일 *</Label> + <Input + id="contactEmail" + type="email" + value={newContact.contactEmail} + onChange={(e) => setNewContact(prev => ({ ...prev, contactEmail: e.target.value }))} + placeholder="example@email.com" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="contactNumber">전화번호</Label> + <Input + id="contactNumber" + value={newContact.contactNumber} + onChange={(e) => setNewContact(prev => ({ ...prev, contactNumber: e.target.value }))} + placeholder="010-1234-5678" + /> + </div> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setAddContactDialogOpen(false) + setNewContact({ contactName: '', contactEmail: '', contactNumber: '' }) + }} + > + 취소 + </Button> + <Button onClick={handleAddContact}> + 추가 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 벤더 담당자에서 추가 다이얼로그 */} + <Dialog open={addContactFromVendorDialogOpen} onOpenChange={setAddContactFromVendorDialogOpen}> + <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle> + {selectedVendor ? `${selectedVendor.vendorName} 벤더 담당자에서 선택` : '벤더 담당자 선택'} + </DialogTitle> + <DialogDescription> + 벤더에 등록된 담당자 목록에서 선택하세요. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4 py-4"> + {isLoadingVendorContacts ? ( + <div className="flex items-center justify-center py-8"> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + <span className="text-sm text-muted-foreground">담당자 목록을 불러오는 중...</span> + </div> + ) : vendorContacts.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + 등록된 담당자가 없습니다. + </div> + ) : ( + <div className="space-y-2"> + {vendorContacts.map((contact) => ( + <div + key={contact.id} + className={`flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors ${ + selectedContactFromVendor?.id === contact.id ? 'bg-primary/10 border-primary' : '' + }`} + onClick={() => setSelectedContactFromVendor(contact)} + > + <div className="flex items-center gap-3 flex-1"> + <Checkbox + checked={selectedContactFromVendor?.id === contact.id} + onCheckedChange={() => setSelectedContactFromVendor(contact)} + className="shrink-0" + /> + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2"> + <span className="font-medium">{contact.contactName}</span> + {contact.isPrimary && ( + <span className="px-2 py-0.5 rounded-full text-xs bg-primary/10 text-primary"> + 주담당자 + </span> + )} + </div> + {contact.contactPosition && ( + <p className="text-sm text-muted-foreground">{contact.contactPosition}</p> + )} + <div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground"> + <span>{contact.contactEmail}</span> + {contact.contactPhone && <span>{contact.contactPhone}</span>} + </div> + </div> + </div> + </div> + ))} + </div> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setAddContactFromVendorDialogOpen(false) + setSelectedContactFromVendor(null) + }} + > + 취소 + </Button> + <Button + onClick={handleAddContactFromVendor} + disabled={!selectedContactFromVendor || isLoadingVendorContacts} + > + 추가 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ) +}
\ No newline at end of file |
