diff options
Diffstat (limited to 'components/bidding/manage/bidding-companies-editor.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-companies-editor.tsx | 262 |
1 files changed, 256 insertions, 6 deletions
diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index 6634f528..4c3e6bbc 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { Building, User, Plus, Trash2 } from 'lucide-react' +import { Building, User, Plus, Trash2, Users } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -11,7 +11,9 @@ import { createBiddingCompanyContact, deleteBiddingCompanyContact, getVendorContactsByVendorId, - updateBiddingCompanyPriceAdjustmentQuestion + updateBiddingCompanyPriceAdjustmentQuestion, + getBiddingCompaniesByBidPicId, + addBiddingCompanyFromOtherBidding } from '@/lib/bidding/service' import { deleteBiddingCompany } from '@/lib/bidding/pre-quote/service' import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog' @@ -36,6 +38,7 @@ import { } 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 @@ -102,6 +105,26 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC const [isLoadingVendorContacts, setIsLoadingVendorContacts] = React.useState(false) const [selectedContactFromVendor, setSelectedContactFromVendor] = React.useState<VendorContact | null>(null) + // 협력사 멀티 선택 다이얼로그 + const [multiSelectDialogOpen, setMultiSelectDialogOpen] = React.useState(false) + const [selectedBidPic, setSelectedBidPic] = React.useState<PurchaseGroupCodeWithUser | undefined>(undefined) + const [biddingCompaniesList, setBiddingCompaniesList] = React.useState<Array<{ + biddingId: number + biddingNumber: string + biddingTitle: string + companyId: number + vendorCode: string + vendorName: string + updatedAt: Date + }>>([]) + const [isLoadingBiddingCompanies, setIsLoadingBiddingCompanies] = React.useState(false) + const [selectedBiddingCompany, setSelectedBiddingCompany] = React.useState<{ + biddingId: number + companyId: number + } | null>(null) + const [selectedBiddingCompanyContacts, setSelectedBiddingCompanyContacts] = React.useState<BiddingCompanyContact[]>([]) + const [isLoadingCompanyContacts, setIsLoadingCompanyContacts] = React.useState(false) + // 업체 목록 다시 로딩 함수 const reloadVendors = React.useCallback(async () => { try { @@ -494,10 +517,16 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC </p> </div> {!readonly && ( - <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2" disabled={readonly}> - <Plus className="h-4 w-4" /> - 업체 추가 - </Button> + <div className="flex gap-2"> + <Button onClick={() => setMultiSelectDialogOpen(true)} className="flex items-center gap-2" disabled={readonly} variant="outline"> + <Users className="h-4 w-4" /> + 협력사 멀티 선택 + </Button> + <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2" disabled={readonly}> + <Plus className="h-4 w-4" /> + 업체 추가 + </Button> + </div> )} </CardHeader> <CardContent> @@ -740,6 +769,227 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC </DialogContent> </Dialog> + {/* 협력사 멀티 선택 다이얼로그 */} + <Dialog open={multiSelectDialogOpen} onOpenChange={setMultiSelectDialogOpen}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>참여협력사 선택</DialogTitle> + <DialogDescription> + 입찰담당자를 선택하여 해당 담당자의 입찰 업체를 조회하고 선택할 수 있습니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4 py-4"> + {/* 입찰담당자 선택 */} + <div className="space-y-2"> + <Label>입찰담당자 선택</Label> + <PurchaseGroupCodeSelector + selectedCode={selectedBidPic} + onCodeSelect={async (code) => { + 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} + /> + </div> + + {/* 입찰 업체 목록 */} + {isLoadingBiddingCompanies ? ( + <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> + ) : biddingCompaniesList.length === 0 && selectedBidPic ? ( + <div className="text-center py-8 text-muted-foreground"> + 해당 입찰담당자의 입찰 업체가 없습니다. + </div> + ) : biddingCompaniesList.length > 0 ? ( + <div className="space-y-2"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[50px]">선택</TableHead> + <TableHead>입찰번호</TableHead> + <TableHead>입찰명</TableHead> + <TableHead>협력사코드</TableHead> + <TableHead>협력사명</TableHead> + <TableHead>입찰 업데이트일</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {biddingCompaniesList.map((company) => { + const isSelected = selectedBiddingCompany?.biddingId === company.biddingId && + selectedBiddingCompany?.companyId === company.companyId + return ( + <TableRow + key={`${company.biddingId}-${company.companyId}`} + className={`cursor-pointer hover:bg-muted/50 ${ + isSelected ? 'bg-muted/50' : '' + }`} + onClick={async () => { + 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) + } + }} + > + <TableCell onClick={(e) => e.stopPropagation()}> + <Checkbox + checked={isSelected} + onCheckedChange={() => { + // 클릭 이벤트는 TableRow의 onClick에서 처리 + }} + disabled={readonly} + /> + </TableCell> + <TableCell className="font-medium">{company.biddingNumber}</TableCell> + <TableCell>{company.biddingTitle}</TableCell> + <TableCell>{company.vendorCode}</TableCell> + <TableCell>{company.vendorName}</TableCell> + <TableCell> + {company.updatedAt ? new Date(company.updatedAt).toLocaleDateString('ko-KR') : '-'} + </TableCell> + </TableRow> + ) + })} + </TableBody> + </Table> + + {/* 선택한 입찰 업체의 담당자 정보 */} + {selectedBiddingCompany !== null && ( + <div className="mt-4 p-4 border rounded-lg"> + <h4 className="font-medium mb-2">담당자 정보</h4> + {isLoadingCompanyContacts ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + <span className="text-sm text-muted-foreground">담당자 정보를 불러오는 중...</span> + </div> + ) : selectedBiddingCompanyContacts.length === 0 ? ( + <div className="text-sm text-muted-foreground">등록된 담당자가 없습니다.</div> + ) : ( + <div className="space-y-2"> + {selectedBiddingCompanyContacts.map((contact) => ( + <div key={contact.id} className="text-sm"> + <span className="font-medium">{contact.contactName}</span> + <span className="text-muted-foreground ml-2">{contact.contactEmail}</span> + {contact.contactNumber && ( + <span className="text-muted-foreground ml-2">{contact.contactNumber}</span> + )} + </div> + ))} + </div> + )} + </div> + )} + </div> + ) : null} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setMultiSelectDialogOpen(false) + setSelectedBidPic(undefined) + setBiddingCompaniesList([]) + setSelectedBiddingCompany(null) + setSelectedBiddingCompanyContacts([]) + }} + > + 취소 + </Button> + <Button + onClick={async () => { + if (!selectedBiddingCompany) { + toast.error('입찰 업체를 선택해주세요.') + return + } + + const selectedCompany = biddingCompaniesList.find( + c => c.biddingId === selectedBiddingCompany.biddingId && + c.companyId === selectedBiddingCompany.companyId + ) + + if (!selectedCompany) { + toast.error('선택한 입찰 업체 정보를 찾을 수 없습니다.') + return + } + + try { + const contacts = selectedBiddingCompanyContacts.map(c => ({ + contactName: c.contactName, + contactEmail: c.contactEmail, + contactNumber: c.contactNumber || undefined, + })) + + const result = await addBiddingCompanyFromOtherBidding( + biddingId, + selectedCompany.biddingId, + selectedCompany.companyId, + contacts.length > 0 ? contacts : undefined + ) + + if (result.success) { + toast.success('업체가 성공적으로 추가되었습니다.') + setMultiSelectDialogOpen(false) + setSelectedBidPic(undefined) + setBiddingCompaniesList([]) + setSelectedBiddingCompany(null) + setSelectedBiddingCompanyContacts([]) + await reloadVendors() + } else { + toast.error(result.error || '업체 추가에 실패했습니다.') + } + } catch (error) { + console.error('Failed to add bidding company:', error) + toast.error('업체 추가에 실패했습니다.') + } + }} + disabled={!selectedBiddingCompany || readonly} + > + 추가 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + {/* 벤더 담당자에서 추가 다이얼로그 */} <Dialog open={addContactFromVendorDialogOpen} onOpenChange={setAddContactFromVendorDialogOpen}> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> |
