From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/bidding-detail-vendor-create-dialog.tsx | 437 +++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 components/bidding/manage/bidding-detail-vendor-create-dialog.tsx (limited to 'components/bidding/manage/bidding-detail-vendor-create-dialog.tsx') diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx new file mode 100644 index 00000000..ed3e2be6 --- /dev/null +++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx @@ -0,0 +1,437 @@ +'use client' + +import * as React from 'react' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { Checkbox } from '@/components/ui/checkbox' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { ChevronsUpDown, Loader2, X, Plus } from 'lucide-react' +import { createBiddingDetailVendor } from '@/lib/bidding/detail/service' +import { searchVendorsForBidding } from '@/lib/bidding/service' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +interface BiddingDetailVendorCreateDialogProps { + biddingId: number + open: boolean + onOpenChange: (open: boolean) => void + onSuccess: () => void +} + +interface Vendor { + id: number + vendorName: string + vendorCode: string + status: string +} + +interface SelectedVendorWithQuestion { + vendor: Vendor + isPriceAdjustmentApplicableQuestion: boolean +} + +export function BiddingDetailVendorCreateDialog({ + biddingId, + open, + onOpenChange, + onSuccess +}: BiddingDetailVendorCreateDialogProps) { + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + const [activeTab, setActiveTab] = React.useState('select') + + // Vendor 검색 상태 + const [vendorList, setVendorList] = React.useState([]) + const [selectedVendorsWithQuestion, setSelectedVendorsWithQuestion] = React.useState([]) + const [vendorOpen, setVendorOpen] = React.useState(false) + + // 벤더 로드 + const loadVendors = React.useCallback(async () => { + try { + const result = await searchVendorsForBidding('', biddingId) // 빈 검색어로 모든 벤더 로드 + setVendorList(result || []) + } catch (error) { + console.error('Failed to load vendors:', error) + toast({ + title: '오류', + description: '벤더 목록을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + setVendorList([]) + } + }, [biddingId, toast]) + + React.useEffect(() => { + if (open) { + loadVendors() + } + }, [open, loadVendors]) + + // 초기화 + React.useEffect(() => { + if (!open) { + setSelectedVendorsWithQuestion([]) + setActiveTab('select') + } + }, [open]) + + // 벤더 추가 + const handleAddVendor = (vendor: Vendor) => { + if (!selectedVendorsWithQuestion.find(v => v.vendor.id === vendor.id)) { + setSelectedVendorsWithQuestion([ + ...selectedVendorsWithQuestion, + { + vendor, + isPriceAdjustmentApplicableQuestion: false + } + ]) + } + setVendorOpen(false) + } + + // 벤더 제거 + const handleRemoveVendor = (vendorId: number) => { + setSelectedVendorsWithQuestion( + selectedVendorsWithQuestion.filter(v => v.vendor.id !== vendorId) + ) + } + + // 이미 선택된 벤더인지 확인 + const isVendorSelected = (vendorId: number) => { + return selectedVendorsWithQuestion.some(v => v.vendor.id === vendorId) + } + + // 연동제 적용요건 문의 체크박스 토글 + const handleTogglePriceAdjustmentQuestion = (vendorId: number, checked: boolean) => { + setSelectedVendorsWithQuestion(prev => + prev.map(item => + item.vendor.id === vendorId + ? { ...item, isPriceAdjustmentApplicableQuestion: checked } + : item + ) + ) + } + + const handleCreate = () => { + if (selectedVendorsWithQuestion.length === 0) { + toast({ + title: '오류', + description: '업체를 선택해주세요.', + variant: 'destructive', + }) + return + } + + // Tab 2로 이동하여 연동제 적용요건 문의를 확인하도록 유도 + if (activeTab === 'select') { + setActiveTab('question') + toast({ + title: '확인 필요', + description: '선택한 업체들의 연동제 적용요건 문의를 확인해주세요.', + }) + return + } + + startTransition(async () => { + let successCount = 0 + const errorMessages: string[] = [] + + for (const item of selectedVendorsWithQuestion) { + try { + const response = await createBiddingDetailVendor( + biddingId, + item.vendor.id, + item.isPriceAdjustmentApplicableQuestion + ) + + if (response.success) { + successCount++ + } else { + errorMessages.push(`${item.vendor.vendorName}: ${response.error}`) + } + } catch { + errorMessages.push(`${item.vendor.vendorName}: 처리 중 오류가 발생했습니다.`) + } + } + + if (successCount > 0) { + toast({ + title: '성공', + description: `${successCount}개의 업체가 성공적으로 추가되었습니다.${errorMessages.length > 0 ? ` ${errorMessages.length}개는 실패했습니다.` : ''}`, + }) + onOpenChange(false) + resetForm() + onSuccess() + } + + if (errorMessages.length > 0 && successCount === 0) { + toast({ + title: '오류', + description: `업체 추가에 실패했습니다: ${errorMessages.join(', ')}`, + variant: 'destructive', + }) + } + }) + } + + const resetForm = () => { + setSelectedVendorsWithQuestion([]) + setActiveTab('select') + } + + const selectedVendors = selectedVendorsWithQuestion.map(item => item.vendor) + + return ( + + + {/* 헤더 */} + + 협력업체 추가 + + 입찰에 참여할 업체를 선택하고 연동제 적용요건 문의를 설정하세요. + + + + {/* 탭 네비게이션 */} + + + + 1. 입찰업체 선택 ({selectedVendors.length}개) + + + 2. 연동제 적용요건 문의 + + + + {/* Tab 1: 입찰업체 선택 */} + + + + 업체 선택 + + 입찰에 참여할 협력업체를 선택하세요. + + + +
+ {/* 업체 추가 버튼 */} + + + + + + + + + 검색 결과가 없습니다. + + {vendorList + .filter(vendor => !isVendorSelected(vendor.id)) + .map((vendor) => ( + handleAddVendor(vendor)} + > +
+ + {vendor.vendorCode} + + {vendor.vendorName} +
+
+ ))} +
+
+
+
+
+ + {/* 선택된 업체 목록 */} + {selectedVendors.length > 0 && ( +
+
+

선택된 업체 ({selectedVendors.length}개)

+
+
+ {selectedVendors.map((vendor, index) => ( +
+
+ + {index + 1}. + + + {vendor.vendorCode} + + + {vendor.vendorName} + +
+ +
+ ))} +
+
+ )} + + {selectedVendors.length === 0 && ( +
+

아직 선택된 업체가 없습니다.

+

위 버튼을 클릭하여 업체를 추가하세요.

+
+ )} +
+
+
+
+ + {/* Tab 2: 연동제 적용요건 문의 체크 */} + + + + 연동제 적용요건 문의 + + 선택한 업체별로 연동제 적용요건 문의 여부를 체크하세요. + + + + {selectedVendorsWithQuestion.length === 0 ? ( +
+

선택된 업체가 없습니다.

+

먼저 입찰업체 선택 탭에서 업체를 선택해주세요.

+
+ ) : ( +
+ {selectedVendorsWithQuestion.map((item, index) => ( +
+
+ + {index + 1}. + +
+
+ + {item.vendor.vendorCode} + + {item.vendor.vendorName} +
+
+
+
+ + handleTogglePriceAdjustmentQuestion(item.vendor.id, checked as boolean) + } + /> + +
+
+ ))} +
+ )} +
+
+
+
+ + {/* 푸터 */} + + + {activeTab === 'select' ? ( + + ) : ( + + )} + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3