diff options
Diffstat (limited to 'lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx')
| -rw-r--r-- | lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx | 512 |
1 files changed, 0 insertions, 512 deletions
diff --git a/lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx b/lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx deleted file mode 100644 index 79524f58..00000000 --- a/lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx +++ /dev/null @@ -1,512 +0,0 @@ -"use client" - -import * as React from "react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { toast } from "sonner" -import { Check, ChevronsUpDown, File, Upload, X } from "lucide-react" - -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { ProcurementRfqsView } from "@/db/schema" -import { addVendorToRfq } from "@/lib/procurement-rfqs/services" -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { cn } from "@/lib/utils" -import { ScrollArea } from "@/components/ui/scroll-area" - -// 필수 필드를 위한 커스텀 레이블 컴포넌트 -const RequiredLabel = ({ children }: { children: React.ReactNode }) => ( - <FormLabel> - {children} <span className="text-red-500">*</span> - </FormLabel> -); - -// 폼 유효성 검증 스키마 -const vendorFormSchema = z.object({ - vendorId: z.string().min(1, "벤더를 선택해주세요"), - currency: z.string().min(1, "통화를 선택해주세요"), - paymentTermsCode: z.string().min(1, "지불 조건을 선택해주세요"), - incotermsCode: z.string().min(1, "인코텀즈를 선택해주세요"), - incotermsDetail: z.string().optional(), - deliveryDate: z.string().optional(), - taxCode: z.string().optional(), - placeOfShipping: z.string().optional(), - placeOfDestination: z.string().optional(), - materialPriceRelatedYn: z.boolean().default(false), -}) - -type VendorFormValues = z.infer<typeof vendorFormSchema> - -interface AddVendorDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - selectedRfq: ProcurementRfqsView | null - // 벤더 및 기타 옵션 데이터를 prop으로 받음 - vendors?: { id: number; vendorName: string; vendorCode: string }[] - currencies?: { code: string; name: string }[] - paymentTerms?: { code: string; description: string }[] - incoterms?: { code: string; description: string }[] - onSuccess?: () => void - existingVendorIds?: number[] - -} - -export function AddVendorDialog({ - open, - onOpenChange, - selectedRfq, - vendors = [], - currencies = [], - paymentTerms = [], - incoterms = [], - onSuccess, - existingVendorIds = [], // 기본값 빈 배열 -}: AddVendorDialogProps) { - - - const availableVendors = React.useMemo(() => { - return vendors.filter(vendor => !existingVendorIds.includes(vendor.id)); - }, [vendors, existingVendorIds]); - - - // 파일 업로드 상태 관리 - const [attachments, setAttachments] = useState<File[]>([]) - const [isSubmitting, setIsSubmitting] = useState(false) - - // 벤더 선택을 위한 팝오버 상태 - const [vendorOpen, setVendorOpen] = useState(false) - - const form = useForm<VendorFormValues>({ - resolver: zodResolver(vendorFormSchema), - defaultValues: { - vendorId: "", - currency: "", - paymentTermsCode: "", - incotermsCode: "", - incotermsDetail: "", - deliveryDate: "", - taxCode: "", - placeOfShipping: "", - placeOfDestination: "", - materialPriceRelatedYn: false, - }, - }) - - // 폼 제출 핸들러 - async function onSubmit(values: VendorFormValues) { - if (!selectedRfq) { - toast.error("선택된 RFQ가 없습니다") - return - } - - try { - setIsSubmitting(true) - - // FormData 생성 - const formData = new FormData() - formData.append("rfqId", selectedRfq.id.toString()) - - // 폼 데이터 추가 - Object.entries(values).forEach(([key, value]) => { - formData.append(key, value.toString()) - }) - - // 첨부파일 추가 - attachments.forEach((file, index) => { - formData.append(`attachment-${index}`, file) - }) - - // 서버 액션 호출 - const result = await addVendorToRfq(formData) - - if (result.success) { - toast.success("벤더가 성공적으로 추가되었습니다") - onOpenChange(false) - form.reset() - setAttachments([]) - onSuccess?.() - } else { - toast.error(result.message || "벤더 추가 중 오류가 발생했습니다") - } - } catch (error) { - console.error("벤더 추가 오류:", error) - toast.error("벤더 추가 중 오류가 발생했습니다") - } finally { - setIsSubmitting(false) - } - } - - // 파일 업로드 핸들러 - const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { - if (event.target.files && event.target.files.length > 0) { - const newFiles = Array.from(event.target.files) - setAttachments((prev) => [...prev, ...newFiles]) - } - } - - // 파일 삭제 핸들러 - const handleRemoveFile = (index: number) => { - setAttachments((prev) => prev.filter((_, i) => i !== index)) - } - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - {/* 커스텀 DialogContent - 고정 헤더, 스크롤 가능한 콘텐츠, 고정 푸터 */} - <DialogContent className="sm:max-w-[600px] p-0 h-[85vh] flex flex-col overflow-hidden" style={{maxHeight:'85vh'}}> - {/* 고정 헤더 */} - <div className="p-6 border-b"> - <DialogHeader> - <DialogTitle>벤더 추가</DialogTitle> - <DialogDescription> - {selectedRfq ? ( - <> - <span className="font-medium">{selectedRfq.rfqCode}</span> RFQ에 벤더를 추가합니다. - </> - ) : ( - "RFQ에 벤더를 추가합니다." - )} - </DialogDescription> - </DialogHeader> - </div> - - {/* 스크롤 가능한 콘텐츠 영역 */} - <div className="flex-1 overflow-y-auto p-6"> - <Form {...form}> - <form id="vendor-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> - {/* 검색 가능한 벤더 선택 필드 */} - <FormField - control={form.control} - name="vendorId" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <RequiredLabel>벤더</RequiredLabel> - <Popover open={vendorOpen} onOpenChange={setVendorOpen}> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - role="combobox" - aria-expanded={vendorOpen} - className={cn( - "w-full justify-between", - !field.value && "text-muted-foreground" - )} - > - {field.value - ? vendors.find((vendor) => String(vendor.id) === field.value) - ? `${vendors.find((vendor) => String(vendor.id) === field.value)?.vendorName} (${vendors.find((vendor) => String(vendor.id) === field.value)?.vendorCode})` - : "벤더를 선택하세요" - : "벤더를 선택하세요"} - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-[400px] p-0"> - <Command> - <CommandInput placeholder="벤더 검색..." /> - <CommandEmpty>검색 결과가 없습니다</CommandEmpty> - <CommandList> - <ScrollArea className="h-60"> - <CommandGroup> - {availableVendors.length > 0 ? ( - availableVendors.map((vendor) => ( - <CommandItem - key={vendor.id} - value={`${vendor.vendorName} ${vendor.vendorCode}`} - onSelect={() => { - form.setValue("vendorId", String(vendor.id), { - shouldValidate: true, - }) - setVendorOpen(false) - }} - > - <Check - className={cn( - "mr-2 h-4 w-4", - String(vendor.id) === field.value - ? "opacity-100" - : "opacity-0" - )} - /> - {vendor.vendorName} ({vendor.vendorCode}) - </CommandItem> - )) - ) : ( - <CommandItem disabled>추가 가능한 벤더가 없습니다</CommandItem> - )} - </CommandGroup> - </ScrollArea> - </CommandList> - </Command> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="currency" - render={({ field }) => ( - <FormItem> - <RequiredLabel>통화</RequiredLabel> - <Select onValueChange={field.onChange} defaultValue={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="통화를 선택하세요" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {currencies.map((currency) => ( - <SelectItem key={currency.code} value={currency.code}> - {currency.name} ({currency.code}) - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="paymentTermsCode" - render={({ field }) => ( - <FormItem> - <RequiredLabel>지불 조건</RequiredLabel> - <Select onValueChange={field.onChange} defaultValue={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="지불 조건 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {paymentTerms.map((term) => ( - <SelectItem key={term.code} value={term.code}> - {term.description} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="incotermsCode" - render={({ field }) => ( - <FormItem> - <RequiredLabel>인코텀즈</RequiredLabel> - <Select onValueChange={field.onChange} defaultValue={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="인코텀즈 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {incoterms.map((incoterm) => ( - <SelectItem key={incoterm.code} value={incoterm.code}> - {incoterm.description} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - </div> - - {/* 나머지 필드들은 동일하게 유지 */} - <FormField - control={form.control} - name="incotermsDetail" - render={({ field }) => ( - <FormItem> - <FormLabel>인코텀즈 세부사항</FormLabel> - <FormControl> - <Input {...field} placeholder="인코텀즈 세부사항" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="deliveryDate" - render={({ field }) => ( - <FormItem> - <FormLabel>납품 예정일</FormLabel> - <FormControl> - <Input {...field} type="date" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="taxCode" - render={({ field }) => ( - <FormItem> - <FormLabel>세금 코드</FormLabel> - <FormControl> - <Input {...field} placeholder="세금 코드" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="placeOfShipping" - render={({ field }) => ( - <FormItem> - <FormLabel>선적지</FormLabel> - <FormControl> - <Input {...field} placeholder="선적지" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="placeOfDestination" - render={({ field }) => ( - <FormItem> - <FormLabel>도착지</FormLabel> - <FormControl> - <Input {...field} placeholder="도착지" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <FormField - control={form.control} - name="materialPriceRelatedYn" - render={({ field }) => ( - <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> - <FormControl> - <input - type="checkbox" - checked={field.value} - onChange={field.onChange} - className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" - /> - </FormControl> - <div className="space-y-1 leading-none"> - <FormLabel>하도급대금 연동제 여부</FormLabel> - </div> - </FormItem> - )} - /> - - {/* 파일 업로드 섹션 */} - <div className="space-y-2"> - <Label>첨부 파일</Label> - <div className="border rounded-md p-4"> - <div className="flex items-center justify-center w-full"> - <label - htmlFor="file-upload" - className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100" - > - <div className="flex flex-col items-center justify-center pt-5 pb-6"> - <Upload className="w-8 h-8 mb-2 text-gray-500" /> - <p className="mb-2 text-sm text-gray-500"> - <span className="font-semibold">클릭하여 파일 업로드</span> 또는 파일을 끌어 놓으세요 - </p> - <p className="text-xs text-gray-500">PDF, DOCX, XLSX, JPG, PNG (최대 10MB)</p> - </div> - <input - id="file-upload" - type="file" - className="hidden" - multiple - onChange={handleFileUpload} - /> - </label> - </div> - - {/* 업로드된 파일 목록 */} - {attachments.length > 0 && ( - <div className="mt-4 space-y-2"> - <h4 className="text-sm font-medium">업로드된 파일</h4> - <ul className="space-y-2"> - {attachments.map((file, index) => ( - <li - key={index} - className="flex items-center justify-between p-2 text-sm bg-gray-50 rounded-md" - > - <div className="flex items-center space-x-2"> - <File className="w-4 h-4 text-gray-500" /> - <span className="truncate max-w-[250px]">{file.name}</span> - <span className="text-gray-500 text-xs"> - ({(file.size / 1024).toFixed(1)} KB) - </span> - </div> - <Button - type="button" - variant="ghost" - size="sm" - onClick={() => handleRemoveFile(index)} - > - <X className="w-4 h-4 text-gray-500" /> - </Button> - </li> - ))} - </ul> - </div> - )} - </div> - </div> - </form> - </Form> - </div> - - {/* 고정 푸터 */} - <div className="p-6 border-t"> - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => onOpenChange(false)} - disabled={isSubmitting} - > - 취소 - </Button> - <Button - type="submit" - form="vendor-form" - disabled={isSubmitting} - > - {isSubmitting ? "처리 중..." : "벤더 추가"} - </Button> - </DialogFooter> - </div> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file |
