summaryrefslogtreecommitdiff
path: root/lib/b-rfq/initial/add-initial-rfq-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/initial/add-initial-rfq-dialog.tsx')
-rw-r--r--lib/b-rfq/initial/add-initial-rfq-dialog.tsx584
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