diff options
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx')
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx new file mode 100644 index 00000000..9229b09c --- /dev/null +++ b/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx @@ -0,0 +1,335 @@ +'use client' + +import * as React from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { Check, ChevronsUpDown, Search } from 'lucide-react' +import { cn } from '@/lib/utils' +import { createQuotationVendor } from '@/lib/bidding/detail/service' +import { createQuotationVendorSchema } from '@/lib/bidding/validation' +import { searchVendors } from '@/lib/vendors/service' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' + +interface BiddingDetailVendorCreateDialogProps { + biddingId: number + open: boolean + onOpenChange: (open: boolean) => void + onSuccess: () => void +} + +interface Vendor { + id: number + vendorName: string + vendorCode: string + status: string +} + +export function BiddingDetailVendorCreateDialog({ + biddingId, + open, + onOpenChange, + onSuccess +}: BiddingDetailVendorCreateDialogProps) { + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + + // Vendor 검색 상태 + const [vendors, setVendors] = React.useState<Vendor[]>([]) + const [selectedVendor, setSelectedVendor] = React.useState<Vendor | null>(null) + const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false) + const [vendorSearchValue, setVendorSearchValue] = React.useState('') + + // 폼 상태 + const [formData, setFormData] = React.useState({ + quotationAmount: 0, + currency: 'KRW', + paymentTerms: '', + taxConditions: '', + deliveryDate: '', + awardRatio: 0, + status: 'pending' as const, + }) + + // Vendor 검색 + React.useEffect(() => { + const search = async () => { + if (vendorSearchValue.trim().length < 2) { + setVendors([]) + return + } + + try { + const result = await searchVendors(vendorSearchValue.trim(), 10) + setVendors(result) + } catch (error) { + console.error('Vendor search failed:', error) + setVendors([]) + } + } + + const debounceTimer = setTimeout(search, 300) + return () => clearTimeout(debounceTimer) + }, [vendorSearchValue]) + + const handleVendorSelect = (vendor: Vendor) => { + setSelectedVendor(vendor) + setVendorSearchValue(`${vendor.vendorName} (${vendor.vendorCode})`) + setVendorSearchOpen(false) + } + + const handleCreate = () => { + if (!selectedVendor) { + toast({ + title: '오류', + description: '업체를 선택해주세요.', + variant: 'destructive', + }) + return + } + + const result = createQuotationVendorSchema.safeParse({ + biddingId, + vendorId: selectedVendor.id, + vendorName: selectedVendor.vendorName, + vendorCode: selectedVendor.vendorCode, + contactPerson: '', + contactEmail: '', + contactPhone: '', + ...formData, + }) + + if (!result.success) { + toast({ + title: '유효성 오류', + description: result.error.issues[0]?.message || '입력값을 확인해주세요.', + variant: 'destructive', + }) + return + } + + startTransition(async () => { + const response = await createQuotationVendor(result.data, 'current-user') + + if (response.success) { + toast({ + title: '성공', + description: response.message, + }) + onOpenChange(false) + resetForm() + onSuccess() + } else { + toast({ + title: '오류', + description: response.error, + variant: 'destructive', + }) + } + }) + } + + const resetForm = () => { + setSelectedVendor(null) + setVendorSearchValue('') + setFormData({ + quotationAmount: 0, + currency: 'KRW', + paymentTerms: '', + taxConditions: '', + deliveryDate: '', + awardRatio: 0, + status: 'pending', + }) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[600px]"> + <DialogHeader> + <DialogTitle>협력업체 추가</DialogTitle> + <DialogDescription> + 검색해서 업체를 선택하고 견적 정보를 입력해주세요. + </DialogDescription> + </DialogHeader> + <div className="grid gap-4 py-4"> + {/* Vendor 검색 */} + <div className="space-y-2"> + <Label htmlFor="vendor-search">업체 검색</Label> + <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={vendorSearchOpen} + className="w-full justify-between" + > + {selectedVendor + ? `${selectedVendor.vendorName} (${selectedVendor.vendorCode})` + : "업체를 검색해서 선택하세요..."} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-full p-0"> + <Command> + <CommandInput + placeholder="업체명 또는 코드를 입력하세요..." + value={vendorSearchValue} + onValueChange={setVendorSearchValue} + /> + <CommandEmpty> + {vendorSearchValue.length < 2 + ? "최소 2자 이상 입력해주세요" + : "검색 결과가 없습니다"} + </CommandEmpty> + <CommandGroup className="max-h-64 overflow-auto"> + {vendors.map((vendor) => ( + <CommandItem + key={vendor.id} + value={`${vendor.vendorName} ${vendor.vendorCode}`} + onSelect={() => handleVendorSelect(vendor)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedVendor?.id === vendor.id ? "opacity-100" : "opacity-0" + )} + /> + <div className="flex flex-col"> + <span className="font-medium">{vendor.vendorName}</span> + <span className="text-sm text-muted-foreground">{vendor.vendorCode}</span> + </div> + </CommandItem> + ))} + </CommandGroup> + </Command> + </PopoverContent> + </Popover> + </div> + + {/* 견적 정보 입력 */} + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="quotationAmount">견적금액</Label> + <Input + id="quotationAmount" + type="number" + value={formData.quotationAmount} + onChange={(e) => setFormData({ ...formData, quotationAmount: Number(e.target.value) })} + placeholder="견적금액을 입력하세요" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="currency">통화</Label> + <Select value={formData.currency} onValueChange={(value) => setFormData({ ...formData, currency: value })}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="KRW">KRW</SelectItem> + <SelectItem value="USD">USD</SelectItem> + <SelectItem value="EUR">EUR</SelectItem> + </SelectContent> + </Select> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="awardRatio">발주비율 (%)</Label> + <Input + id="awardRatio" + type="number" + min="0" + max="100" + value={formData.awardRatio} + onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })} + placeholder="발주비율을 입력하세요" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="status">상태</Label> + <Select value={formData.status} onValueChange={(value: any) => setFormData({ ...formData, status: value })}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="pending">대기</SelectItem> + <SelectItem value="submitted">제출</SelectItem> + <SelectItem value="selected">선정</SelectItem> + <SelectItem value="rejected">거절</SelectItem> + </SelectContent> + </Select> + </div> + </div> + + <div className="space-y-2"> + <Label htmlFor="paymentTerms">지급조건</Label> + <Input + id="paymentTerms" + value={formData.paymentTerms} + onChange={(e) => setFormData({ ...formData, paymentTerms: e.target.value })} + placeholder="지급조건을 입력하세요" + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="taxConditions">세금조건</Label> + <Input + id="taxConditions" + value={formData.taxConditions} + onChange={(e) => setFormData({ ...formData, taxConditions: e.target.value })} + placeholder="세금조건을 입력하세요" + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="deliveryDate">납품일</Label> + <Input + id="deliveryDate" + type="date" + value={formData.deliveryDate} + onChange={(e) => setFormData({ ...formData, deliveryDate: e.target.value })} + /> + </div> + </div> + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button onClick={handleCreate} disabled={isPending || !selectedVendor}> + 추가 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} |
