summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-companies-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage/bidding-companies-editor.tsx')
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx262
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">