diff options
Diffstat (limited to 'lib/b-rfq/initial/add-initial-rfq-dialog.tsx')
| -rw-r--r-- | lib/b-rfq/initial/add-initial-rfq-dialog.tsx | 584 |
1 files changed, 0 insertions, 584 deletions
diff --git a/lib/b-rfq/initial/add-initial-rfq-dialog.tsx b/lib/b-rfq/initial/add-initial-rfq-dialog.tsx deleted file mode 100644 index 58a091ac..00000000 --- a/lib/b-rfq/initial/add-initial-rfq-dialog.tsx +++ /dev/null @@ -1,584 +0,0 @@ -"use client" - -import * as React from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { Plus, Check, ChevronsUpDown, Search, Building, CalendarIcon } from "lucide-react" -import { toast } from "sonner" - -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -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 { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Checkbox } from "@/components/ui/checkbox" -import { cn, formatDate } from "@/lib/utils" -import { addInitialRfqRecord, getIncotermsForSelection, getVendorsForSelection } from "../service" -import { Calendar } from "@/components/ui/calendar" -import { InitialRfqDetailView } from "@/db/schema" - -// Initial RFQ 추가 폼 스키마 -const addInitialRfqSchema = z.object({ - vendorId: z.number({ - required_error: "벤더를 선택해주세요.", - }), - initialRfqStatus: z.enum(["DRAFT", "Init. RFQ Sent", "S/L Decline", "Init. RFQ Answered"], { - required_error: "초기 RFQ 상태를 선택해주세요.", - }).default("DRAFT"), - dueDate: z.date({ - required_error: "마감일을 선택해주세요.", - }), - validDate: z.date().optional(), - incotermsCode: z.string().optional(), - gtc: z.string().optional(), - gtcValidDate: z.string().optional(), - classification: z.string().optional(), - sparepart: z.string().optional(), - shortList: z.boolean().default(false), - returnYn: z.boolean().default(false), - cpRequestYn: z.boolean().default(false), - prjectGtcYn: z.boolean().default(false), - returnRevision: z.number().default(0), -}) - -export type AddInitialRfqFormData = z.infer<typeof addInitialRfqSchema> - -interface Vendor { - id: number - vendorName: string - vendorCode: string - country: string - taxId: string - status: string -} - -interface Incoterm { - id: number - code: string - description: string -} - -interface AddInitialRfqDialogProps { - rfqId: number - onSuccess?: () => void - defaultValues?: InitialRfqDetailView // 선택된 항목의 기본값 -} - -export function AddInitialRfqDialog({ rfqId, onSuccess, defaultValues }: AddInitialRfqDialogProps) { - const [open, setOpen] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) - const [vendors, setVendors] = React.useState<Vendor[]>([]) - const [vendorsLoading, setVendorsLoading] = React.useState(false) - const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false) - const [incoterms, setIncoterms] = React.useState<Incoterm[]>([]) - const [incotermsLoading, setIncotermsLoading] = React.useState(false) - const [incotermsSearchOpen, setIncotermsSearchOpen] = React.useState(false) - - // 기본값 설정 (선택된 항목이 있으면 해당 값 사용, 없으면 일반 기본값) - const getDefaultFormValues = React.useCallback((): Partial<AddInitialRfqFormData> => { - if (defaultValues) { - return { - vendorId: defaultValues.vendorId, - initialRfqStatus: "DRAFT", // 새로 추가할 때는 항상 DRAFT로 시작 - dueDate: defaultValues.dueDate || new Date(), - validDate: defaultValues.validDate, - incotermsCode: defaultValues.incotermsCode || "", - classification: defaultValues.classification || "", - sparepart: defaultValues.sparepart || "", - shortList: false, // 새로 추가할 때는 기본적으로 false - returnYn: false, - cpRequestYn: defaultValues.cpRequestYn || false, - prjectGtcYn: defaultValues.prjectGtcYn || false, - returnRevision: 0, - } - } - - return { - initialRfqStatus: "DRAFT", - shortList: false, - returnYn: false, - cpRequestYn: false, - prjectGtcYn: false, - returnRevision: 0, - } - }, [defaultValues]) - - const form = useForm<AddInitialRfqFormData>({ - resolver: zodResolver(addInitialRfqSchema), - defaultValues: getDefaultFormValues(), - }) - - // 벤더 목록 로드 - const loadVendors = React.useCallback(async () => { - setVendorsLoading(true) - try { - const vendorList = await getVendorsForSelection() - setVendors(vendorList) - } catch (error) { - console.error("Failed to load vendors:", error) - toast.error("벤더 목록을 불러오는데 실패했습니다.") - } finally { - setVendorsLoading(false) - } - }, []) - - // Incoterms 목록 로드 - const loadIncoterms = React.useCallback(async () => { - setIncotermsLoading(true) - try { - const incotermsList = await getIncotermsForSelection() - setIncoterms(incotermsList) - } catch (error) { - console.error("Failed to load incoterms:", error) - toast.error("Incoterms 목록을 불러오는데 실패했습니다.") - } finally { - setIncotermsLoading(false) - } - }, []) - - // 다이얼로그 열릴 때 실행 - React.useEffect(() => { - if (open) { - // 폼을 기본값으로 리셋 - form.reset(getDefaultFormValues()) - - // 데이터 로드 - if (vendors.length === 0) { - loadVendors() - } - if (incoterms.length === 0) { - loadIncoterms() - } - } - }, [open, vendors.length, incoterms.length, loadVendors, loadIncoterms, form, getDefaultFormValues]) - - // 다이얼로그 닫기 핸들러 - const handleOpenChange = (newOpen: boolean) => { - if (!newOpen && !isSubmitting) { - form.reset(getDefaultFormValues()) - } - setOpen(newOpen) - } - - // 폼 제출 - const onSubmit = async (data: AddInitialRfqFormData) => { - setIsSubmitting(true) - - try { - const result = await addInitialRfqRecord({ - ...data, - rfqId, - }) - - if (result.success) { - toast.success(result.message || "초기 RFQ가 성공적으로 추가되었습니다.") - form.reset(getDefaultFormValues()) - handleOpenChange(false) - onSuccess?.() - } else { - toast.error(result.message || "초기 RFQ 추가에 실패했습니다.") - } - - } catch (error) { - console.error("Submit error:", error) - toast.error("초기 RFQ 추가 중 오류가 발생했습니다.") - } finally { - setIsSubmitting(false) - } - } - - // 선택된 벤더 정보 - const selectedVendor = vendors.find(vendor => vendor.id === form.watch("vendorId")) - const selectedIncoterm = incoterms.find(incoterm => incoterm.code === form.watch("incotermsCode")) - - // 기본값이 있을 때 버튼 텍스트 변경 - const buttonText = defaultValues ? "유사 항목 추가" : "초기 RFQ 추가" - const dialogTitle = defaultValues ? "유사 초기 RFQ 추가" : "초기 RFQ 추가" - const dialogDescription = defaultValues - ? "선택된 항목을 기본값으로 하여 새로운 초기 RFQ를 추가합니다." - : "새로운 벤더를 대상으로 하는 초기 RFQ를 추가합니다." - - return ( - <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogTrigger asChild> - <Button variant="outline" size="sm" className="gap-2"> - <Plus className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">{buttonText}</span> - </Button> - </DialogTrigger> - - <DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>{dialogTitle}</DialogTitle> - <DialogDescription> - {dialogDescription} - {defaultValues && ( - <div className="mt-2 p-2 bg-muted rounded-md text-sm"> - <strong>기본값 출처:</strong> {defaultValues.vendorName} ({defaultValues.vendorCode}) - </div> - )} - </DialogDescription> - </DialogHeader> - - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> - {/* 벤더 선택 */} - <FormField - control={form.control} - name="vendorId" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>벤더 선택 *</FormLabel> - <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - role="combobox" - aria-expanded={vendorSearchOpen} - className="justify-between" - disabled={vendorsLoading} - > - {selectedVendor ? ( - <div className="flex items-center gap-2"> - <Building className="h-4 w-4" /> - <span className="truncate"> - {selectedVendor.vendorName} ({selectedVendor.vendorCode}) - </span> - </div> - ) : ( - <span className="text-muted-foreground"> - {vendorsLoading ? "로딩 중..." : "벤더를 선택하세요"} - </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="벤더명 또는 코드로 검색..." - className="h-9" - /> - <CommandList> - <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> - <CommandGroup> - {vendors.map((vendor) => ( - <CommandItem - key={vendor.id} - value={`${vendor.vendorName} ${vendor.vendorCode}`} - onSelect={() => { - field.onChange(vendor.id) - setVendorSearchOpen(false) - }} - > - <div className="flex items-center gap-2 w-full"> - <Building className="h-4 w-4" /> - <div className="flex-1 min-w-0"> - <div className="font-medium truncate"> - {vendor.vendorName} - </div> - <div className="text-sm text-muted-foreground"> - {vendor.vendorCode} • {vendor.country} • {vendor.taxId} - </div> - </div> - <Check - className={cn( - "ml-auto h-4 w-4", - vendor.id === field.value ? "opacity-100" : "opacity-0" - )} - /> - </div> - </CommandItem> - ))} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - {/* 날짜 필드들 */} - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="dueDate" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>견적 마감일 *</FormLabel> - <Popover> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - className={cn( - "w-full pl-3 text-left font-normal", - !field.value && "text-muted-foreground" - )} - > - {field.value ? ( - formatDate(field.value, "KR") - ) : ( - <span>견적 마감일을 선택하세요</span> - )} - <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar - mode="single" - selected={field.value} - onSelect={field.onChange} - disabled={(date) => - date < new Date() || date < new Date("1900-01-01") - } - initialFocus - /> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="validDate" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>견적 유효일</FormLabel> - <Popover> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - className={cn( - "w-full pl-3 text-left font-normal", - !field.value && "text-muted-foreground" - )} - > - {field.value ? ( - formatDate(field.value, "KR") - ) : ( - <span>견적 유효일을 선택하세요</span> - )} - <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar - mode="single" - selected={field.value} - onSelect={field.onChange} - disabled={(date) => - date < new Date() || date < new Date("1900-01-01") - } - initialFocus - /> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - </div> - - {/* Incoterms 선택 */} - <FormField - control={form.control} - name="incotermsCode" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>Incoterms</FormLabel> - <Popover open={incotermsSearchOpen} onOpenChange={setIncotermsSearchOpen}> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - role="combobox" - aria-expanded={incotermsSearchOpen} - className="justify-between" - disabled={incotermsLoading} - > - {selectedIncoterm ? ( - <div className="flex items-center gap-2"> - <span className="truncate"> - {selectedIncoterm.code} - {selectedIncoterm.description} - </span> - </div> - ) : ( - <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="코드 또는 내용으로 검색..." - className="h-9" - /> - <CommandList> - <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> - <CommandGroup> - {incoterms.map((incoterm) => ( - <CommandItem - key={incoterm.id} - value={`${incoterm.code} ${incoterm.description}`} - onSelect={() => { - field.onChange(incoterm.code) - setIncotermsSearchOpen(false) - }} - > - <div className="flex items-center gap-2 w-full"> - <div className="flex-1 min-w-0"> - <div className="font-medium truncate"> - {incoterm.code} - {incoterm.description} - </div> - </div> - <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> - )} - /> - - {/* 옵션 체크박스 */} - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="cpRequestYn" - render={({ field }) => ( - <FormItem className="flex flex-row items-start space-x-3 space-y-0"> - <FormControl> - <Checkbox - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <div className="space-y-1 leading-none ml-2"> - <FormLabel>CP 요청</FormLabel> - </div> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="prjectGtcYn" - render={({ field }) => ( - <FormItem className="flex flex-row items-start space-x-3 space-y-0"> - <FormControl> - <Checkbox - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <div className="space-y-1 leading-none ml-2"> - <FormLabel>Project용 GTC 사용</FormLabel> - </div> - </FormItem> - )} - /> - </div> - - {/* 분류 정보 */} - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="classification" - render={({ field }) => ( - <FormItem> - <FormLabel>선급</FormLabel> - <FormControl> - <Input placeholder="선급" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="sparepart" - render={({ field }) => ( - <FormItem> - <FormLabel>Spare part</FormLabel> - <FormControl> - <Input placeholder="O1, O2" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => handleOpenChange(false)} - disabled={isSubmitting} - > - 취소 - </Button> - <Button type="submit" disabled={isSubmitting}> - {isSubmitting ? "추가 중..." : "추가"} - </Button> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file |
