diff options
Diffstat (limited to 'lib/rfq-last/vendor/batch-update-conditions-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/vendor/batch-update-conditions-dialog.tsx | 1121 |
1 files changed, 1121 insertions, 0 deletions
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<typeof formSchema>; + +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<SelectOption[]>([]); + const [paymentTerms, setPaymentTerms] = React.useState<SelectOption[]>([]); + const [shippingPlaces, setShippingPlaces] = React.useState<SelectOption[]>([]); + const [destinationPlaces, setDestinationPlaces] = React.useState<SelectOption[]>([]); + + // 로딩 상태 + 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<FormValues>({ + 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 ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-3xl h-[90vh] p-0 flex flex-col"> + {/* 헤더 */} + <DialogHeader className="p-6 pb-0"> + <DialogTitle>조건 일괄 설정</DialogTitle> + <DialogDescription> + 선택한 {selectedVendors.length}개 벤더에 동일한 조건을 적용합니다. + 변경하려는 항목만 체크하고 값을 입력하세요. + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0"> + {/* 스크롤 가능한 컨텐츠 영역 */} + <ScrollArea className="flex-1 px-6"> + <div className="grid gap-4 py-4"> + {/* 선택된 벤더 정보 */} + <Card> + <CardHeader className="pb-3"> + <div className="flex items-center justify-between"> + <CardTitle className="text-lg flex items-center gap-2"> + <Package className="h-5 w-5" /> + 대상 벤더 + </CardTitle> + <Badge>{selectedVendors.length}개</Badge> + </div> + </CardHeader> + <CardContent> + <div className="flex flex-wrap gap-2"> + {selectedVendors.map((vendor) => ( + <Badge key={vendor.id} variant="secondary"> + {vendor.vendorCode} - {vendor.vendorName} + </Badge> + ))} + </div> + </CardContent> + </Card> + + {/* 안내 메시지 */} + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + 체크박스를 선택한 항목만 업데이트됩니다. + 선택하지 않은 항목은 기존 값이 유지됩니다. + </AlertDescription> + </Alert> + + {/* 기본 조건 설정 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">기본 조건</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {/* 통화 */} + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.currency} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, currency: !!checked }) + } + /> + <FormField + control={form.control} + name="currency" + render={({ field }) => ( + <FormItem className="flex-1 grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.currency && "text-muted-foreground" + )}> + 통화 + </FormLabel> + <div className="col-span-2"> + <FormControl> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className="w-full justify-between" + disabled={!fieldsToUpdate.currency} + > + {field.value || "통화 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput placeholder="통화 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {currencies.map((currency) => ( + <CommandItem + key={currency} + value={currency} + onSelect={() => field.onChange(currency)} + > + {currency} + <Check + className={cn( + "ml-auto h-4 w-4", + currency === field.value ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + </FormControl> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + + {/* 결제 조건 */} + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.paymentTermsCode} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, paymentTermsCode: !!checked }) + } + /> + <FormField + control={form.control} + name="paymentTermsCode" + render={({ field }) => ( + <FormItem className="flex-1 grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.paymentTermsCode && "text-muted-foreground" + )}> + 결제 조건 + </FormLabel> + <div className="col-span-2"> + <Popover open={paymentTermsOpen} onOpenChange={setPaymentTermsOpen}> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + aria-expanded={paymentTermsOpen} + className="w-full justify-between" + disabled={!fieldsToUpdate.paymentTermsCode || paymentTermsLoading} + > + {selectedPaymentTerm ? ( + <span className="truncate"> + {selectedPaymentTerm.code} - {selectedPaymentTerm.description} + </span> + ) : ( + <span className="text-muted-foreground"> + {paymentTermsLoading ? "로딩 중..." : "결제조건 선택"} + </span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput placeholder="코드 또는 설명으로 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {paymentTerms.map((term) => ( + <CommandItem + key={term.id} + value={`${term.code} ${term.description}`} + onSelect={() => { + field.onChange(term.code); + setPaymentTermsOpen(false); + }} + > + <div className="flex items-center gap-2 w-full"> + <span className="font-medium">{term.code}</span> + <span className="text-muted-foreground">-</span> + <span className="truncate">{term.description}</span> + <Check + className={cn( + "ml-auto h-4 w-4", + term.code === field.value ? "opacity-100" : "opacity-0" + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + + {/* 인코텀즈 */} + <div className="flex items-start gap-4"> + <Checkbox + className="mt-3" + checked={fieldsToUpdate.incoterms} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, incoterms: !!checked }) + } + /> + <div className="flex-1 grid grid-cols-3 gap-4"> + <Label className={cn( + "text-right pt-2", + !fieldsToUpdate.incoterms && "text-muted-foreground" + )}> + 인코텀즈 + </Label> + <div className="col-span-2 space-y-2"> + <FormField + control={form.control} + name="incotermsCode" + render={({ field }) => ( + <FormItem> + <Popover open={incotermsOpen} onOpenChange={setIncotermsOpen}> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + aria-expanded={incotermsOpen} + className="w-full justify-between" + disabled={!fieldsToUpdate.incoterms || incotermsLoading} + > + {selectedIncoterm ? ( + <span className="truncate"> + {selectedIncoterm.code} - {selectedIncoterm.description} + </span> + ) : ( + <span className="text-muted-foreground"> + {incotermsLoading ? "로딩 중..." : "인코텀즈 선택"} + </span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput placeholder="코드 또는 설명으로 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {incoterms.map((incoterm) => ( + <CommandItem + key={incoterm.id} + value={`${incoterm.code} ${incoterm.description}`} + onSelect={() => { + field.onChange(incoterm.code); + setIncotermsOpen(false); + }} + > + <div className="flex items-center gap-2 w-full"> + <span className="font-medium">{incoterm.code}</span> + <span className="text-muted-foreground">-</span> + <span className="truncate">{incoterm.description}</span> + <Check + className={cn( + "ml-auto h-4 w-4", + incoterm.code === field.value ? "opacity-100" : "opacity-0" + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + {/* <FormField + control={form.control} + name="incotermsDetail" + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="인코텀즈 상세 (예: 부산항)" + {...field} + disabled={!fieldsToUpdate.incoterms} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> */} + </div> + </div> + </div> + + {/* 납기일 */} + {!rfqCode.startsWith("F") && ( + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.deliveryDate} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, deliveryDate: !!checked }) + } + /> + <FormField + control={form.control} + name="deliveryDate" + render={({ field }) => ( + <FormItem className="flex-1 grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.deliveryDate && "text-muted-foreground" + )}> + 납기일 + </FormLabel> + <div className="col-span-2"> + <Popover open={calendarOpen} onOpenChange={setCalendarOpen}> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={cn( + "w-full justify-start text-left font-normal", + !field.value && "text-muted-foreground" + )} + disabled={!fieldsToUpdate.deliveryDate} + > + <CalendarIcon className="mr-2 h-4 w-4" /> + {field.value ? ( + format(field.value, "yyyy-MM-dd", { locale: ko }) + ) : ( + <span>날짜를 선택하세요</span> + )} + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0"> + <Calendar + mode="single" + selected={field.value} + onSelect={(date) => { + field.onChange(date); + setCalendarOpen(false); + }} + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + )} + + {/* 계약 기간 */} + {rfqCode.startsWith("F") && ( + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.contractDuration} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, contractDuration: !!checked }) + } + /> + <FormField + control={form.control} + name="contractDuration" + render={({ field }) => ( + <FormItem className="flex-1 grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.contractDuration && "text-muted-foreground" + )}> + 계약 기간 + </FormLabel> + <div className="col-span-2"> + <FormControl> + <Input + placeholder="예: 12개월" + {...field} + disabled={!fieldsToUpdate.contractDuration} + /> + </FormControl> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + )} + + {/* 세금 코드 */} + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.taxCode} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, taxCode: !!checked }) + } + /> + <FormField + control={form.control} + name="taxCode" + render={({ field }) => ( + <FormItem className="flex-1 grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.taxCode && "text-muted-foreground" + )}> + 세금 코드 + </FormLabel> + <div className="col-span-2"> + <FormControl> + <Input + {...field} + disabled={!fieldsToUpdate.taxCode} + /> + </FormControl> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + + {/* 선적지/도착지 */} + <div className="flex items-start gap-4"> + <Checkbox + className="mt-3" + checked={fieldsToUpdate.shipping} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, shipping: !!checked }) + } + /> + <div className="flex-1 space-y-2"> + <FormField + control={form.control} + name="placeOfShipping" + render={({ field }) => ( + <FormItem className="grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.shipping && "text-muted-foreground" + )}> + 선적지 + </FormLabel> + <div className="col-span-2"> + <Popover open={shippingOpen} onOpenChange={setShippingOpen}> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + aria-expanded={shippingOpen} + className="w-full justify-between" + disabled={!fieldsToUpdate.shipping || shippingLoading} + > + {selectedShipping ? ( + <span className="truncate"> + {selectedShipping.code} - {selectedShipping.description} + </span> + ) : ( + <span className="text-muted-foreground"> + {shippingLoading ? "로딩 중..." : "선적지 선택"} + </span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput placeholder="선적지 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {shippingPlaces.map((place) => ( + <CommandItem + key={place.id} + value={`${place.code} ${place.description}`} + onSelect={() => { + field.onChange(place.code); + setShippingOpen(false); + }} + > + <div className="flex items-center gap-2 w-full"> + <span className="font-medium">{place.code}</span> + <span className="text-muted-foreground">-</span> + <span className="truncate">{place.description}</span> + <Check + className={cn( + "ml-auto h-4 w-4", + place.code === field.value ? "opacity-100" : "opacity-0" + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormMessage /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="placeOfDestination" + render={({ field }) => ( + <FormItem className="grid grid-cols-3 items-center gap-4"> + <FormLabel className={cn( + "text-right", + !fieldsToUpdate.shipping && "text-muted-foreground" + )}> + 도착지 + </FormLabel> + <div className="col-span-2"> + <Popover open={destinationOpen} onOpenChange={setDestinationOpen}> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + aria-expanded={destinationOpen} + className="w-full justify-between" + disabled={!fieldsToUpdate.shipping || destinationLoading} + > + {selectedDestination ? ( + <span className="truncate"> + {selectedDestination.code} - {selectedDestination.description} + </span> + ) : ( + <span className="text-muted-foreground"> + {destinationLoading ? "로딩 중..." : "도착지 선택"} + </span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput placeholder="도착지 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {destinationPlaces.map((place) => ( + <CommandItem + key={place.id} + value={`${place.code} ${place.description}`} + onSelect={() => { + field.onChange(place.code); + setDestinationOpen(false); + }} + > + <div className="flex items-center gap-2 w-full"> + <span className="font-medium">{place.code}</span> + <span className="text-muted-foreground">-</span> + <span className="truncate">{place.description}</span> + <Check + className={cn( + "ml-auto h-4 w-4", + place.code === field.value ? "opacity-100" : "opacity-0" + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormMessage /> + </div> + </FormItem> + )} + /> + </div> + </div> + </CardContent> + </Card> + + {/* 추가 옵션 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">추가 옵션</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {/* 연동제 적용 */} + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.materialPrice} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked }) + } + /> + <FormField + control={form.control} + name="materialPriceRelatedYn" + render={({ field }) => ( + <FormItem className="flex-1 flex items-center justify-between"> + <div className="space-y-0.5"> + <FormLabel className={cn( + !fieldsToUpdate.materialPrice && "text-muted-foreground" + )}> + 연동제 적용 + </FormLabel> + <div className="text-sm text-muted-foreground"> + 원자재 가격 연동 여부 + </div> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + disabled={!fieldsToUpdate.materialPrice} + /> + </FormControl> + </FormItem> + )} + /> + </div> + + {/* Spare Part */} + <div className="space-y-2"> + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.sparepart} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, sparepart: !!checked }) + } + /> + <FormField + control={form.control} + name="sparepartYn" + render={({ field }) => ( + <FormItem className="flex-1 flex items-center justify-between"> + <div className="space-y-0.5"> + <FormLabel className={cn( + !fieldsToUpdate.sparepart && "text-muted-foreground" + )}> + Spare Part + </FormLabel> + <div className="text-sm text-muted-foreground"> + 예비 부품 요구사항 + </div> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + disabled={!fieldsToUpdate.sparepart} + /> + </FormControl> + </FormItem> + )} + /> + </div> + {form.watch("sparepartYn") && fieldsToUpdate.sparepart && ( + <FormField + control={form.control} + name="sparepartDescription" + render={({ field }) => ( + <FormItem className="ml-7"> + <FormControl> + <Textarea + placeholder="Spare Part 요구사항을 입력하세요..." + {...field} + disabled={!fieldsToUpdate.sparepart} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + {/* 초도품 관리 */} + <div className="space-y-2"> + <div className="flex items-center gap-4"> + <Checkbox + checked={fieldsToUpdate.first} + onCheckedChange={(checked) => + setFieldsToUpdate({ ...fieldsToUpdate, first: !!checked }) + } + /> + <FormField + control={form.control} + name="firstYn" + render={({ field }) => ( + <FormItem className="flex-1 flex items-center justify-between"> + <div className="space-y-0.5"> + <FormLabel className={cn( + !fieldsToUpdate.first && "text-muted-foreground" + )}> + 초도품 관리 + </FormLabel> + <div className="text-sm text-muted-foreground"> + 초도품 관리 요구사항 + </div> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + disabled={!fieldsToUpdate.first} + /> + </FormControl> + </FormItem> + )} + /> + </div> + {form.watch("firstYn") && fieldsToUpdate.first && ( + <FormField + control={form.control} + name="firstDescription" + render={({ field }) => ( + <FormItem className="ml-7"> + <FormControl> + <Textarea + placeholder="초도품 관리 요구사항을 입력하세요..." + {...field} + disabled={!fieldsToUpdate.first} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + </CardContent> + </Card> + </div> + </ScrollArea> + + {/* 푸터 */} + <DialogFooter className="p-6 pt-4 border-t"> + <div className="flex items-center justify-between w-full"> + <div className="text-sm text-muted-foreground"> + {getUpdateCount() > 0 + ? `${getUpdateCount()}개 항목 선택됨` + : '변경할 항목을 선택하세요' + } + </div> + <div className="flex gap-2"> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="submit" + disabled={isLoading || getUpdateCount() === 0} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {getUpdateCount() > 0 + ? `${getUpdateCount()}개 항목 업데이트` + : '조건 업데이트' + } + </Button> + </div> + </div> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
