"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, getVendorConditions } from "../service"; import { Badge } from "@/components/ui/badge"; import { TAX_CONDITIONS } from "@/lib/tax-conditions/types"; 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; } // 타입 정의 type 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 [currencyOpen, setCurrencyOpen] = 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 as unknown as SelectOption[]); } 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 as unknown as SelectOption[]); } 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 as unknown as SelectOption[]); } 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 as unknown as SelectOption[]); } catch (error) { console.error("Failed to load destination places:", error); toast.error("도착지 목록을 불러오는데 실패했습니다."); } finally { setDestinationLoading(false); } }, []); // 벤더별 조건 로드 함수 const loadVendorConditions = React.useCallback(async (vendorId: number) => { try { const conditions = await getVendorConditions(rfqId, vendorId); // 가져온 조건으로 폼 초기화 form.reset({ currency: conditions.currency || "", paymentTermsCode: conditions.paymentTermsCode || "", incotermsCode: conditions.incotermsCode || "", incotermsDetail: conditions.incotermsDetail || "", deliveryDate: conditions.deliveryDate || undefined, contractDuration: conditions.contractDuration || "", taxCode: conditions.taxCode || "", placeOfShipping: conditions.placeOfShipping || "", placeOfDestination: conditions.placeOfDestination || "", materialPriceRelatedYn: conditions.materialPriceRelatedYn || false, sparepartYn: conditions.sparepartYn || false, firstYn: conditions.firstYn || false, firstDescription: conditions.firstDescription || "", sparepartDescription: conditions.sparepartDescription || "", }); } catch (error) { console.error("Failed to load vendor conditions:", error); toast.error("벤더 조건을 불러오는데 실패했습니다."); } }, [rfqId, form]); // 초기 데이터 로드 React.useEffect(() => { if (open) { loadIncoterms(); loadPaymentTerms(); loadShippingPlaces(); loadDestinationPlaces(); // 선택된 벤더가 1개일 때만 해당 벤더의 조건을 가져옴 if (selectedVendors.length === 1) { loadVendorConditions(selectedVendors[0].id); } } }, [open, loadIncoterms, loadPaymentTerms, loadShippingPlaces, loadDestinationPlaces, selectedVendors, loadVendorConditions]); // 다이얼로그 닫힐 때 초기화 React.useEffect(() => { if (!open) { // 선택된 벤더가 2개 이상이거나 없다면 기본값으로 초기화 if (selectedVendors.length !== 1) { form.reset({ currency: "", paymentTermsCode: "", incotermsCode: "", incotermsDetail: "", contractDuration: "", taxCode: "", placeOfShipping: "", placeOfDestination: "", materialPriceRelatedYn: false, sparepartYn: false, firstYn: false, firstDescription: "", sparepartDescription: "", }); } setFieldsToUpdate({ currency: false, paymentTermsCode: false, incoterms: false, deliveryDate: false, contractDuration: false, taxCode: false, shipping: false, materialPrice: false, sparepart: false, first: false, }); } }, [open, form, selectedVendors]); // 제출 처리 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 }) } /> ( 통화
{ e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }} > 검색 결과가 없습니다. {currencies.map((currency) => ( { field.onChange(currency); setCurrencyOpen(false); }} > {currency} ))}
)} />
{/* 결제 조건 */}
setFieldsToUpdate({ ...fieldsToUpdate, paymentTermsCode: !!checked }) } /> ( 결제 조건
{ e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }} > 검색 결과가 없습니다. {paymentTerms.map((term) => ( { field.onChange(term.code); setPaymentTermsOpen(false); }} >
{term.code} - {term.description}
))}
)} />
{/* 인코텀즈 */}
setFieldsToUpdate({ ...fieldsToUpdate, incoterms: !!checked }) } />
( { e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }}> 검색 결과가 없습니다. {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 }) } /> ( 세금 코드
{ e.stopPropagation(); const target = e.currentTarget; target.scrollTop += e.deltaY; }} > 검색 결과가 없습니다. {TAX_CONDITIONS.map((condition) => ( field.onChange(condition.code)} >
{condition.code} - {condition.name}
))}
)} />
{/* 선적지/도착지 */}
setFieldsToUpdate({ ...fieldsToUpdate, shipping: !!checked }) } />
( 선적지
{ e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }} > 검색 결과가 없습니다. {shippingPlaces.map((place) => ( { field.onChange(place.code); setShippingOpen(false); }} >
{place.code} - {place.description}
))}
)} /> ( 도착지
{ e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }} > 검색 결과가 없습니다. {destinationPlaces.map((place) => ( { field.onChange(place.code); setDestinationOpen(false); }} >
{place.code} - {place.description}
))}
)} />
{/* 추가 옵션 */} 추가 옵션 {/* 연동제 적용 */}
{ setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked }); if (checked) { form.setValue("materialPriceRelatedYn", true); } }} /> (
연동제 적용
원자재 가격 연동 여부
)} />
{/* Spare Part */}
{ setFieldsToUpdate({ ...fieldsToUpdate, sparepart: !!checked }); if (checked) { form.setValue("sparepartYn", true); } }} /> (
Spare Part
예비 부품 요구사항
)} />
{form.watch("sparepartYn") && fieldsToUpdate.sparepart && ( (