diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
| commit | a5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch) | |
| tree | 667ed8c5d6ec35b109190e9f976d66ae54def4ce /components/bidding/manage/bidding-detail-vendor-create-dialog.tsx | |
| parent | b0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff) | |
| parent | f8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff) | |
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'components/bidding/manage/bidding-detail-vendor-create-dialog.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-detail-vendor-create-dialog.tsx | 437 |
1 files changed, 437 insertions, 0 deletions
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<Vendor[]>([]) + const [selectedVendorsWithQuestion, setSelectedVendorsWithQuestion] = React.useState<SelectedVendorWithQuestion[]>([]) + 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 ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col"> + {/* 헤더 */} + <DialogHeader className="p-6 pb-0"> + <DialogTitle>협력업체 추가</DialogTitle> + <DialogDescription> + 입찰에 참여할 업체를 선택하고 연동제 적용요건 문의를 설정하세요. + </DialogDescription> + </DialogHeader> + + {/* 탭 네비게이션 */} + <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col px-6"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="select"> + 1. 입찰업체 선택 ({selectedVendors.length}개) + </TabsTrigger> + <TabsTrigger + value="question" + disabled={selectedVendors.length === 0} + > + 2. 연동제 적용요건 문의 + </TabsTrigger> + </TabsList> + + {/* Tab 1: 입찰업체 선택 */} + <TabsContent value="select" className="flex-1 overflow-y-auto mt-4 pb-4"> + <Card> + <CardHeader> + <CardTitle className="text-lg">업체 선택</CardTitle> + <CardDescription> + 입찰에 참여할 협력업체를 선택하세요. + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {/* 업체 추가 버튼 */} + <Popover open={vendorOpen} onOpenChange={setVendorOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={vendorOpen} + className="w-full justify-between" + disabled={vendorList.length === 0} + > + <span className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + 업체 선택하기 + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[500px] p-0" align="start"> + <Command> + <CommandInput placeholder="업체명 또는 코드로 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {vendorList + .filter(vendor => !isVendorSelected(vendor.id)) + .map((vendor) => ( + <CommandItem + key={vendor.id} + value={`${vendor.vendorCode} ${vendor.vendorName}`} + onSelect={() => handleAddVendor(vendor)} + > + <div className="flex items-center gap-2 w-full"> + <Badge variant="outline" className="shrink-0"> + {vendor.vendorCode} + </Badge> + <span className="truncate">{vendor.vendorName}</span> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + {/* 선택된 업체 목록 */} + {selectedVendors.length > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <h4 className="text-sm font-medium">선택된 업체 ({selectedVendors.length}개)</h4> + </div> + <div className="space-y-2"> + {selectedVendors.map((vendor, index) => ( + <div + key={vendor.id} + className="flex items-center justify-between p-3 rounded-lg bg-secondary/50" + > + <div className="flex items-center gap-3"> + <span className="text-sm text-muted-foreground"> + {index + 1}. + </span> + <Badge variant="outline"> + {vendor.vendorCode} + </Badge> + <span className="text-sm font-medium"> + {vendor.vendorName} + </span> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveVendor(vendor.id)} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + )} + + {selectedVendors.length === 0 && ( + <div className="text-center py-8 text-muted-foreground"> + <p className="text-sm">아직 선택된 업체가 없습니다.</p> + <p className="text-xs mt-1">위 버튼을 클릭하여 업체를 추가하세요.</p> + </div> + )} + </div> + </CardContent> + </Card> + </TabsContent> + + {/* Tab 2: 연동제 적용요건 문의 체크 */} + <TabsContent value="question" className="flex-1 overflow-y-auto mt-4 pb-4"> + <Card> + <CardHeader> + <CardTitle className="text-lg">연동제 적용요건 문의</CardTitle> + <CardDescription> + 선택한 업체별로 연동제 적용요건 문의 여부를 체크하세요. + </CardDescription> + </CardHeader> + <CardContent> + {selectedVendorsWithQuestion.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + <p className="text-sm">선택된 업체가 없습니다.</p> + <p className="text-xs mt-1">먼저 입찰업체 선택 탭에서 업체를 선택해주세요.</p> + </div> + ) : ( + <div className="space-y-4"> + {selectedVendorsWithQuestion.map((item, index) => ( + <div + key={item.vendor.id} + className="flex items-center justify-between p-4 rounded-lg border" + > + <div className="flex items-center gap-4 flex-1"> + <span className="text-sm text-muted-foreground w-8"> + {index + 1}. + </span> + <div className="flex-1"> + <div className="flex items-center gap-2 mb-1"> + <Badge variant="outline"> + {item.vendor.vendorCode} + </Badge> + <span className="font-medium">{item.vendor.vendorName}</span> + </div> + </div> + </div> + <div className="flex items-center gap-2"> + <Checkbox + id={`question-${item.vendor.id}`} + checked={item.isPriceAdjustmentApplicableQuestion} + onCheckedChange={(checked) => + handleTogglePriceAdjustmentQuestion(item.vendor.id, checked as boolean) + } + /> + <Label + htmlFor={`question-${item.vendor.id}`} + className="text-sm cursor-pointer" + > + 연동제 적용요건 문의 + </Label> + </div> + </div> + ))} + </div> + )} + </CardContent> + </Card> + </TabsContent> + </Tabs> + + {/* 푸터 */} + <DialogFooter className="p-6 pt-0 border-t"> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isPending} + > + 취소 + </Button> + {activeTab === 'select' ? ( + <Button + onClick={() => { + if (selectedVendors.length > 0) { + setActiveTab('question') + } else { + toast({ + title: '오류', + description: '업체를 선택해주세요.', + variant: 'destructive', + }) + } + }} + disabled={isPending || selectedVendors.length === 0} + > + 다음 단계 + </Button> + ) : ( + <Button + onClick={handleCreate} + disabled={isPending || selectedVendorsWithQuestion.length === 0} + > + {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {selectedVendorsWithQuestion.length > 0 + ? `${selectedVendorsWithQuestion.length}개 업체 추가` + : '업체 추가' + } + </Button> + )} + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
