From 50adedf48ee4674ebe00f1ee72d93485183cdc51 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 5 Sep 2025 11:44:32 +0000 Subject: (대표님, 최겸, 임수민) EDP 입력 진행률, 견적목록관리, EDP excel import 오류수정, GTC-Contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor/batch-update-conditions-dialog.tsx | 1121 ++++++++++++++++++++ 1 file changed, 1121 insertions(+) create mode 100644 lib/rfq-last/vendor/batch-update-conditions-dialog.tsx (limited to 'lib/rfq-last/vendor/batch-update-conditions-dialog.tsx') diff --git a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx new file mode 100644 index 00000000..1b8fa528 --- /dev/null +++ b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx @@ -0,0 +1,1121 @@ +"use client"; + +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +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 { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@/components/ui/popover"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Calendar } from "@/components/ui/calendar"; +import { CalendarIcon, Loader2, Info, Package, Check, ChevronsUpDown } from "lucide-react"; +import { format } from "date-fns"; +import { ko } from "date-fns/locale"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import { updateVendorConditionsBatch } from "../service"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + getIncotermsForSelection, + getPaymentTermsForSelection, + getPlaceOfShippingForSelection, + getPlaceOfDestinationForSelection +} from "@/lib/procurement-select/service"; + +interface BatchUpdateConditionsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + rfqId: number; + rfqCode: string; + selectedVendors: Array<{ + id: number; + vendorName: string; + vendorCode: string; + }>; + onSuccess: () => void; +} + +// 타입 정의 +interface SelectOption { + id: number; + code: string; + description: string; +} + +// 폼 스키마 +const formSchema = z.object({ + currency: z.string().optional(), + paymentTermsCode: z.string().optional(), + incotermsCode: z.string().optional(), + incotermsDetail: z.string().optional(), + contractDuration: z.string().optional(), + taxCode: z.string().optional(), + placeOfShipping: z.string().optional(), + placeOfDestination: z.string().optional(), + deliveryDate: z.date().optional(), + materialPriceRelatedYn: z.boolean().default(false), + sparepartYn: z.boolean().default(false), + firstYn: z.boolean().default(false), + firstDescription: z.string().optional(), + sparepartDescription: z.string().optional(), +}); + +type FormValues = z.infer; + +const currencies = ["USD", "EUR", "KRW", "JPY", "CNY"]; + +export function BatchUpdateConditionsDialog({ + open, + onOpenChange, + rfqId, + rfqCode, + selectedVendors, + onSuccess, +}: BatchUpdateConditionsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false); + + // Select 옵션들 상태 + const [incoterms, setIncoterms] = React.useState([]); + const [paymentTerms, setPaymentTerms] = React.useState([]); + const [shippingPlaces, setShippingPlaces] = React.useState([]); + const [destinationPlaces, setDestinationPlaces] = React.useState([]); + + // 로딩 상태 + const [incotermsLoading, setIncotermsLoading] = React.useState(false); + const [paymentTermsLoading, setPaymentTermsLoading] = React.useState(false); + const [shippingLoading, setShippingLoading] = React.useState(false); + const [destinationLoading, setDestinationLoading] = React.useState(false); + + // Popover 열림 상태 + const [incotermsOpen, setIncotermsOpen] = React.useState(false); + const [paymentTermsOpen, setPaymentTermsOpen] = React.useState(false); + const [shippingOpen, setShippingOpen] = React.useState(false); + const [destinationOpen, setDestinationOpen] = React.useState(false); + const [calendarOpen, setCalendarOpen] = React.useState(false); + + // 체크박스로 각 필드 업데이트 여부 관리 + const [fieldsToUpdate, setFieldsToUpdate] = React.useState({ + currency: false, + paymentTermsCode: false, + incoterms: false, + deliveryDate: false, + contractDuration: false, + taxCode: false, + shipping: false, + materialPrice: false, + sparepart: false, + first: false, + }); + + // 폼 초기화 + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + currency: "", + paymentTermsCode: "", + incotermsCode: "", + incotermsDetail: "", + contractDuration: "", + taxCode: "", + placeOfShipping: "", + placeOfDestination: "", + materialPriceRelatedYn: false, + sparepartYn: false, + firstYn: false, + firstDescription: "", + sparepartDescription: "", + }, + }); + + // 데이터 로드 함수들 + const loadIncoterms = React.useCallback(async () => { + setIncotermsLoading(true); + try { + const data = await getIncotermsForSelection(); + setIncoterms(data); + } catch (error) { + console.error("Failed to load incoterms:", error); + toast.error("Incoterms 목록을 불러오는데 실패했습니다."); + } finally { + setIncotermsLoading(false); + } + }, []); + + const loadPaymentTerms = React.useCallback(async () => { + setPaymentTermsLoading(true); + try { + const data = await getPaymentTermsForSelection(); + setPaymentTerms(data); + } catch (error) { + console.error("Failed to load payment terms:", error); + toast.error("결제조건 목록을 불러오는데 실패했습니다."); + } finally { + setPaymentTermsLoading(false); + } + }, []); + + const loadShippingPlaces = React.useCallback(async () => { + setShippingLoading(true); + try { + const data = await getPlaceOfShippingForSelection(); + setShippingPlaces(data); + } catch (error) { + console.error("Failed to load shipping places:", error); + toast.error("선적지 목록을 불러오는데 실패했습니다."); + } finally { + setShippingLoading(false); + } + }, []); + + const loadDestinationPlaces = React.useCallback(async () => { + setDestinationLoading(true); + try { + const data = await getPlaceOfDestinationForSelection(); + setDestinationPlaces(data); + } catch (error) { + console.error("Failed to load destination places:", error); + toast.error("도착지 목록을 불러오는데 실패했습니다."); + } finally { + setDestinationLoading(false); + } + }, []); + + // 초기 데이터 로드 + React.useEffect(() => { + if (open) { + loadIncoterms(); + loadPaymentTerms(); + loadShippingPlaces(); + loadDestinationPlaces(); + } + }, [open, loadIncoterms, loadPaymentTerms, loadShippingPlaces, loadDestinationPlaces]); + + // 다이얼로그 닫힐 때 초기화 + React.useEffect(() => { + if (!open) { + form.reset(); + setFieldsToUpdate({ + currency: false, + paymentTermsCode: false, + incoterms: false, + deliveryDate: false, + contractDuration: false, + taxCode: false, + shipping: false, + materialPrice: false, + sparepart: false, + first: false, + }); + } + }, [open, form]); + + // 제출 처리 + const onSubmit = async (data: FormValues) => { + const hasFieldsToUpdate = Object.values(fieldsToUpdate).some(v => v); + if (!hasFieldsToUpdate) { + toast.error("최소 1개 이상의 변경할 항목을 선택해주세요."); + return; + } + + // 선택된 필드만 포함하여 conditions 객체 생성 + const conditions: any = {}; + + if (fieldsToUpdate.currency && data.currency) { + conditions.currency = data.currency; + } + if (fieldsToUpdate.paymentTermsCode && data.paymentTermsCode) { + conditions.paymentTermsCode = data.paymentTermsCode; + } + if (fieldsToUpdate.incoterms) { + if (data.incotermsCode) conditions.incotermsCode = data.incotermsCode; + if (data.incotermsDetail) conditions.incotermsDetail = data.incotermsDetail; + } + if (fieldsToUpdate.deliveryDate && data.deliveryDate) { + conditions.deliveryDate = data.deliveryDate; + } + if (fieldsToUpdate.contractDuration) { + conditions.contractDuration = data.contractDuration; + } + if (fieldsToUpdate.taxCode) { + conditions.taxCode = data.taxCode; + } + if (fieldsToUpdate.shipping) { + conditions.placeOfShipping = data.placeOfShipping; + conditions.placeOfDestination = data.placeOfDestination; + } + if (fieldsToUpdate.materialPrice) { + conditions.materialPriceRelatedYn = data.materialPriceRelatedYn; + } + if (fieldsToUpdate.sparepart) { + conditions.sparepartYn = data.sparepartYn; + if (data.sparepartYn) { + conditions.sparepartDescription = data.sparepartDescription; + } + } + if (fieldsToUpdate.first) { + conditions.firstYn = data.firstYn; + if (data.firstYn) { + conditions.firstDescription = data.firstDescription; + } + } + + setIsLoading(true); + + try { + const vendorIds = selectedVendors.map(v => v.id); + const result = await updateVendorConditionsBatch({ + rfqId, + vendorIds, + conditions, + }); + + if (result.success) { + toast.success(result.data?.message || "조건이 성공적으로 업데이트되었습니다."); + onSuccess(); + onOpenChange(false); + } else { + toast.error(result.error || "조건 업데이트에 실패했습니다."); + } + } catch (error) { + console.error("Submit error:", error); + toast.error("오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const getUpdateCount = () => { + return Object.values(fieldsToUpdate).filter(v => v).length; + }; + + // 선택된 옵션 찾기 헬퍼 함수들 + const selectedIncoterm = incoterms.find(i => i.code === form.watch("incotermsCode")); + const selectedPaymentTerm = paymentTerms.find(p => p.code === form.watch("paymentTermsCode")); + const selectedShipping = shippingPlaces.find(s => s.code === form.watch("placeOfShipping")); + const selectedDestination = destinationPlaces.find(d => d.code === form.watch("placeOfDestination")); + + return ( + + + {/* 헤더 */} + + 조건 일괄 설정 + + 선택한 {selectedVendors.length}개 벤더에 동일한 조건을 적용합니다. + 변경하려는 항목만 체크하고 값을 입력하세요. + + + +
+ + {/* 스크롤 가능한 컨텐츠 영역 */} + +
+ {/* 선택된 벤더 정보 */} + + +
+ + + 대상 벤더 + + {selectedVendors.length}개 +
+
+ +
+ {selectedVendors.map((vendor) => ( + + {vendor.vendorCode} - {vendor.vendorName} + + ))} +
+
+
+ + {/* 안내 메시지 */} + + + + 체크박스를 선택한 항목만 업데이트됩니다. + 선택하지 않은 항목은 기존 값이 유지됩니다. + + + + {/* 기본 조건 설정 */} + + + 기본 조건 + + + {/* 통화 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, currency: !!checked }) + } + /> + ( + + + 통화 + +
+ + + + + + + + + + 검색 결과가 없습니다. + + {currencies.map((currency) => ( + field.onChange(currency)} + > + {currency} + + + ))} + + + + + + + +
+
+ )} + /> +
+ + {/* 결제 조건 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, paymentTermsCode: !!checked }) + } + /> + ( + + + 결제 조건 + +
+ + + + + + + + + + + 검색 결과가 없습니다. + + {paymentTerms.map((term) => ( + { + field.onChange(term.code); + setPaymentTermsOpen(false); + }} + > +
+ {term.code} + - + {term.description} + +
+
+ ))} +
+
+
+
+
+ +
+
+ )} + /> +
+ + {/* 인코텀즈 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, incoterms: !!checked }) + } + /> +
+ +
+ ( + + + + + + + + + + + + 검색 결과가 없습니다. + + {incoterms.map((incoterm) => ( + { + field.onChange(incoterm.code); + setIncotermsOpen(false); + }} + > +
+ {incoterm.code} + - + {incoterm.description} + +
+
+ ))} +
+
+
+
+
+ +
+ )} + /> + {/* ( + + + + + + + )} + /> */} +
+
+
+ + {/* 납기일 */} + {!rfqCode.startsWith("F") && ( +
+ + setFieldsToUpdate({ ...fieldsToUpdate, deliveryDate: !!checked }) + } + /> + ( + + + 납기일 + +
+ + + + + + + + { + field.onChange(date); + setCalendarOpen(false); + }} + initialFocus + /> + + + +
+
+ )} + /> +
+ )} + + {/* 계약 기간 */} + {rfqCode.startsWith("F") && ( +
+ + setFieldsToUpdate({ ...fieldsToUpdate, contractDuration: !!checked }) + } + /> + ( + + + 계약 기간 + +
+ + + + +
+
+ )} + /> +
+ )} + + {/* 세금 코드 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, taxCode: !!checked }) + } + /> + ( + + + 세금 코드 + +
+ + + + +
+
+ )} + /> +
+ + {/* 선적지/도착지 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, shipping: !!checked }) + } + /> +
+ ( + + + 선적지 + +
+ + + + + + + + + + + 검색 결과가 없습니다. + + {shippingPlaces.map((place) => ( + { + field.onChange(place.code); + setShippingOpen(false); + }} + > +
+ {place.code} + - + {place.description} + +
+
+ ))} +
+
+
+
+
+ +
+
+ )} + /> + + ( + + + 도착지 + +
+ + + + + + + + + + + 검색 결과가 없습니다. + + {destinationPlaces.map((place) => ( + { + field.onChange(place.code); + setDestinationOpen(false); + }} + > +
+ {place.code} + - + {place.description} + +
+
+ ))} +
+
+
+
+
+ +
+
+ )} + /> +
+
+
+
+ + {/* 추가 옵션 */} + + + 추가 옵션 + + + {/* 연동제 적용 */} +
+ + setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked }) + } + /> + ( + +
+ + 연동제 적용 + +
+ 원자재 가격 연동 여부 +
+
+ + + +
+ )} + /> +
+ + {/* Spare Part */} +
+
+ + setFieldsToUpdate({ ...fieldsToUpdate, sparepart: !!checked }) + } + /> + ( + +
+ + Spare Part + +
+ 예비 부품 요구사항 +
+
+ + + +
+ )} + /> +
+ {form.watch("sparepartYn") && fieldsToUpdate.sparepart && ( + ( + + +