summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx
diff options
context:
space:
mode:
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.tsx335
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>
+ )
+}