From 9eb8e80f4f736c4edffa650c685d1f170ca51aa1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 15 May 2025 01:19:49 +0000 Subject: (대표님) 구매 요청사항 반영한 통합 rfq / 필터 개인화 / po-rfq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../po-rfq/detail-table/add-vendor-dialog.tsx | 512 +++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 components/po-rfq/detail-table/add-vendor-dialog.tsx (limited to 'components/po-rfq/detail-table/add-vendor-dialog.tsx') diff --git a/components/po-rfq/detail-table/add-vendor-dialog.tsx b/components/po-rfq/detail-table/add-vendor-dialog.tsx new file mode 100644 index 00000000..5e83ad8f --- /dev/null +++ b/components/po-rfq/detail-table/add-vendor-dialog.tsx @@ -0,0 +1,512 @@ +"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 }) => ( + + {children} * + +); + +// 폼 유효성 검증 스키마 +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 + +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([]) + const [isSubmitting, setIsSubmitting] = useState(false) + + // 벤더 선택을 위한 팝오버 상태 + const [vendorOpen, setVendorOpen] = useState(false) + + const form = useForm({ + 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) => { + 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 ( + + {/* 커스텀 DialogContent - 고정 헤더, 스크롤 가능한 콘텐츠, 고정 푸터 */} + + {/* 고정 헤더 */} +
+ + 벤더 추가 + + {selectedRfq ? ( + <> + {selectedRfq.rfqCode} RFQ에 벤더를 추가합니다. + + ) : ( + "RFQ에 벤더를 추가합니다." + )} + + +
+ + {/* 스크롤 가능한 콘텐츠 영역 */} +
+
+ + {/* 검색 가능한 벤더 선택 필드 */} + ( + + 벤더 + + + + + + + + + + 검색 결과가 없습니다 + + + + {availableVendors.length > 0 ? ( + availableVendors.map((vendor) => ( + { + form.setValue("vendorId", String(vendor.id), { + shouldValidate: true, + }) + setVendorOpen(false) + }} + > + + {vendor.vendorName} ({vendor.vendorCode}) + + )) + ) : ( + 추가 가능한 벤더가 없습니다 + )} + + + + + + + + + )} + /> + + ( + + 통화 + + + + )} + /> + +
+ ( + + 지불 조건 + + + + )} + /> + + ( + + 인코텀즈 + + + + )} + /> +
+ + {/* 나머지 필드들은 동일하게 유지 */} + ( + + 인코텀즈 세부사항 + + + + + + )} + /> + +
+ ( + + 납품 예정일 + + + + + + )} + /> + + ( + + 세금 코드 + + + + + + )} + /> +
+ +
+ ( + + 선적지 + + + + + + )} + /> + + ( + + 도착지 + + + + + + )} + /> +
+ + ( + + + + +
+ 자재 가격 관련 여부 +
+
+ )} + /> + + {/* 파일 업로드 섹션 */} +
+ +
+
+ +
+ + {/* 업로드된 파일 목록 */} + {attachments.length > 0 && ( +
+

업로드된 파일

+
    + {attachments.map((file, index) => ( +
  • +
    + + {file.name} + + ({(file.size / 1024).toFixed(1)} KB) + +
    + +
  • + ))} +
+
+ )} +
+
+ + +
+ + {/* 고정 푸터 */} +
+ + + + +
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3