diff options
Diffstat (limited to 'lib/rfqs-ship/table/add-rfq-dialog.tsx')
| -rw-r--r-- | lib/rfqs-ship/table/add-rfq-dialog.tsx | 468 |
1 files changed, 468 insertions, 0 deletions
diff --git a/lib/rfqs-ship/table/add-rfq-dialog.tsx b/lib/rfqs-ship/table/add-rfq-dialog.tsx new file mode 100644 index 00000000..67561b4f --- /dev/null +++ b/lib/rfqs-ship/table/add-rfq-dialog.tsx @@ -0,0 +1,468 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" + +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" + +import { useSession } from "next-auth/react" +import { createRfqSchema, type CreateRfqSchema, RfqType } from "../validations" +import { createRfq, generateNextRfqCode, getBudgetaryRfqs } from "../service" +import { ProjectSelector } from "@/components/ProjectSelector" +import { type Project } from "../service" +import { ParentRfqSelector } from "./ParentRfqSelector" +import { EstimateProjectSelector } from "@/components/BidProjectSelector" + +// 부모 RFQ 정보 타입 정의 +interface ParentRfq { + id: number; + rfqCode: string; + description: string | null; + rfqType: RfqType; + projectId: number | null; + projectCode: string | null; + projectName: string | null; +} + +interface AddRfqDialogProps { + rfqType?: RfqType; +} + +export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) { + const [open, setOpen] = React.useState(false) + const { data: session, status } = useSession() + const [parentRfqs, setParentRfqs] = React.useState<ParentRfq[]>([]) + const [isLoadingParents, setIsLoadingParents] = React.useState(false) + const [selectedParentRfq, setSelectedParentRfq] = React.useState<ParentRfq | null>(null) + const [isLoadingRfqCode, setIsLoadingRfqCode] = React.useState(false) + + // Get the user ID safely, ensuring it's a valid number + const userId = React.useMemo(() => { + const id = session?.user?.id ? Number(session.user.id) : null; + + return id; + }, [session, status]); + + // RfqType에 따른 타이틀 생성 + const getTitle = () => { + switch (rfqType) { + case RfqType.PURCHASE: + return "Purchase RFQ"; + case RfqType.BUDGETARY: + return "Budgetary RFQ"; + case RfqType.PURCHASE_BUDGETARY: + return "Purchase Budgetary RFQ"; + default: + return "RFQ"; + } + }; + + // RfqType 설명 가져오기 + const getTypeDescription = () => { + switch (rfqType) { + case RfqType.PURCHASE: + return "실제 구매 발주 전에 가격을 요청"; + case RfqType.BUDGETARY: + return "기술영업 단계에서 입찰가 산정을 위한 견적 요청"; + case RfqType.PURCHASE_BUDGETARY: + return "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 가격 요청"; + default: + return ""; + } + }; + + // RHF + Zod + const form = useForm<CreateRfqSchema>({ + resolver: zodResolver(createRfqSchema), + defaultValues: { + rfqCode: "", + description: "", + projectId: undefined, + parentRfqId: undefined, + dueDate: new Date(), + status: "DRAFT", + rfqType: rfqType, + // Don't set createdBy yet - we'll set it when the form is submitted + createdBy: undefined, + }, + }); + + // Update form values when session loads + React.useEffect(() => { + if (status === "authenticated" && userId) { + form.setValue("createdBy", userId); + } + }, [status, userId, form]); + + // 다이얼로그가 열릴 때 자동으로 RFQ 코드 생성 + React.useEffect(() => { + if (open) { + const generateRfqCode = async () => { + setIsLoadingRfqCode(true); + try { + // 서버 액션 호출 + const result = await generateNextRfqCode(rfqType); + + if (result.error) { + toast.error(`RFQ 코드 생성 실패: ${result.error}`); + return; + } + + // 생성된 코드를 폼에 설정 + form.setValue("rfqCode", result.code); + } catch (error) { + console.error("RFQ 코드 생성 오류:", error); + toast.error("RFQ 코드 생성에 실패했습니다"); + } finally { + setIsLoadingRfqCode(false); + } + }; + + generateRfqCode(); + } + }, [open, rfqType, form]); + + // 현재 RFQ 타입에 따라 선택 가능한 부모 RFQ 타입들 결정 + const getParentRfqTypes = (): RfqType[] => { + switch (rfqType) { + case RfqType.PURCHASE: + // PURCHASE는 BUDGETARY와 PURCHASE_BUDGETARY를 부모로 가질 수 있음 + return [RfqType.BUDGETARY, RfqType.PURCHASE_BUDGETARY]; + case RfqType.PURCHASE_BUDGETARY: + // PURCHASE_BUDGETARY는 BUDGETARY만 부모로 가질 수 있음 + return [RfqType.BUDGETARY]; + default: + return []; + } + }; + + // 선택 가능한 부모 RFQ 목록 로드 + React.useEffect(() => { + if ((rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_BUDGETARY) && open) { + const loadParentRfqs = async () => { + setIsLoadingParents(true); + try { + // 현재 RFQ 타입에 따라 선택 가능한, 부모가 될 수 있는 RFQ 타입들 가져오기 + const parentTypes = getParentRfqTypes(); + + // 부모 RFQ 타입이 있을 때만 API 호출 + if (parentTypes.length > 0) { + const result = await getBudgetaryRfqs({ + rfqTypes: parentTypes // 서비스에 rfqTypes 파라미터 추가 필요 + }); + + if ('rfqs' in result) { + setParentRfqs(result.rfqs as unknown as ParentRfq[]); + } else if ('error' in result) { + console.error("부모 RFQ 로드 오류:", result.error); + } + } + } catch (error) { + console.error("부모 RFQ 로드 오류:", error); + } finally { + setIsLoadingParents(false); + } + }; + + loadParentRfqs(); + } + }, [rfqType, open]); + + // 프로젝트 선택 처리 + const handleProjectSelect = (project: Project | null) => { + if (project === null) { + return; + } + + form.setValue("projectId", project.id); + }; + + const handleBidProjectSelect = (project: Project | null) => { + if (project === null) { + return; + } + + form.setValue("bidProjectId", project.id); + }; + + // 부모 RFQ 선택 처리 + const handleParentRfqSelect = (rfq: ParentRfq | null) => { + setSelectedParentRfq(rfq); + form.setValue("parentRfqId", rfq?.id); + }; + + async function onSubmit(data: CreateRfqSchema) { + // Check if user is authenticated before submitting + if (status !== "authenticated" || !userId) { + toast.error("사용자 인증이 필요합니다. 다시 로그인해주세요."); + return; + } + + // Make sure createdBy is set with the current user ID + const submitData = { + ...data, + createdBy: userId + }; + + console.log("Submitting form data:", submitData); + + const result = await createRfq(submitData); + if (result.error) { + toast.error(`에러: ${result.error}`); + return; + } + + toast.success("RFQ가 성공적으로 생성되었습니다."); + form.reset(); + setSelectedParentRfq(null); + setOpen(false); + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset(); + setSelectedParentRfq(null); + } + setOpen(nextOpen); + } + + // Return a message or disabled state if user is not authenticated + if (status === "loading") { + return <Button variant="outline" size="sm" disabled>Loading...</Button>; + } + + // 타입에 따라 부모 RFQ 선택 필드를 보여줄지 결정 + const shouldShowParentRfqSelector = rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_BUDGETARY; + const shouldShowEstimateSelector = rfqType === RfqType.BUDGETARY; + + // 부모 RFQ 선택기 레이블 및 설명 가져오기 + const getParentRfqSelectorLabel = () => { + if (rfqType === RfqType.PURCHASE) { + return "부모 RFQ (BUDGETARY/PURCHASE_BUDGETARY)"; + } else if (rfqType === RfqType.PURCHASE_BUDGETARY) { + return "부모 RFQ (BUDGETARY)"; + } + return "부모 RFQ"; + }; + + const getParentRfqDescription = () => { + if (rfqType === RfqType.PURCHASE) { + return "BUDGETARY 또는 PURCHASE_BUDGETARY 타입의 RFQ를 부모로 선택할 수 있습니다."; + } else if (rfqType === RfqType.PURCHASE_BUDGETARY) { + return "BUDGETARY 타입의 RFQ만 부모로 선택할 수 있습니다."; + } + return ""; + }; + + return ( + <Dialog open={open} onOpenChange={handleDialogOpenChange}> + {/* 모달을 열기 위한 버튼 */} + <DialogTrigger asChild> + <Button variant="default" size="sm"> + Add {getTitle()} + </Button> + </DialogTrigger> + + <DialogContent> + <DialogHeader> + <DialogTitle>Create New {getTitle()}</DialogTitle> + <DialogDescription> + 새 {getTitle()} 정보를 입력하고 <b>Create</b> 버튼을 누르세요. + <div className="mt-1 text-xs text-muted-foreground"> + {getTypeDescription()} + </div> + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="space-y-4 py-4"> + {/* rfqType - hidden field */} + <FormField + control={form.control} + name="rfqType" + render={({ field }) => ( + <input type="hidden" {...field} /> + )} + /> + + {/* Project Selector */} + <FormField + control={form.control} + name="projectId" + render={({ field }) => ( + <FormItem> + <FormLabel>Project</FormLabel> + <FormControl> + + {shouldShowEstimateSelector ? + <EstimateProjectSelector + selectedProjectId={field.value} + onProjectSelect={handleBidProjectSelect} + placeholder="견적 프로젝트 선택..." + /> : + <ProjectSelector + selectedProjectId={field.value} + onProjectSelect={handleProjectSelect} + placeholder="프로젝트 선택..." + />} + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* Parent RFQ Selector - PURCHASE 또는 PURCHASE_BUDGETARY 타입일 때만 표시 */} + {shouldShowParentRfqSelector && ( + <FormField + control={form.control} + name="parentRfqId" + render={({ field }) => ( + <FormItem> + <FormLabel>{getParentRfqSelectorLabel()}</FormLabel> + <FormControl> + <ParentRfqSelector + selectedRfqId={field.value as number | undefined} + onRfqSelect={handleParentRfqSelect} + rfqType={rfqType} + parentRfqTypes={getParentRfqTypes()} + placeholder={ + rfqType === RfqType.PURCHASE + ? "BUDGETARY 또는 PURCHASE_BUDGETARY RFQ 선택..." + : "BUDGETARY RFQ 선택..." + } + /> + </FormControl> + <div className="text-xs text-muted-foreground mt-1"> + {getParentRfqDescription()} + </div> + <FormMessage /> + </FormItem> + )} + /> + )} + + {/* rfqCode - 자동 생성되고 읽기 전용 */} + <FormField + control={form.control} + name="rfqCode" + render={({ field }) => ( + <FormItem> + <FormLabel>RFQ Code</FormLabel> + <FormControl> + <div className="flex"> + <Input + placeholder="자동으로 생성 중..." + {...field} + disabled={true} + className="bg-muted" + /> + {isLoadingRfqCode && ( + <div className="ml-2 flex items-center"> + <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div> + </div> + )} + </div> + </FormControl> + <div className="text-xs text-muted-foreground mt-1"> + RFQ 타입과 현재 날짜를 기준으로 자동 생성됩니다 + </div> + <FormMessage /> + </FormItem> + )} + /> + + {/* description */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>RFQ Description</FormLabel> + <FormControl> + <Input placeholder="e.g. 설명을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* dueDate */} + <FormField + control={form.control} + name="dueDate" + render={({ field }) => ( + <FormItem> + <FormLabel>Due Date</FormLabel> + <FormControl> + <Input + type="date" + value={field.value ? field.value.toISOString().slice(0, 10) : ""} + onChange={(e) => { + const val = e.target.value + if (val) { + const date = new Date(val); + // 날짜 1일씩 밀리는 문제로 우선 KTC로 입력 + // 추후 아래와 같이 수정 + // 1. 해당 유저 타임존 값으로 입력 + // 2. DB에는 UTC 타임존 값으로 저장 + // 3. 출력시 유저별 타임존 값으로 변환해 출력 + // 4. 어떤 타임존으로 나오는지도 함께 렌더링 + // field.onChange(new Date(val + "T00:00:00")) + field.onChange(date); + } + }} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* status (Read-only) */} + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>Status</FormLabel> + <FormControl> + <Input + disabled + className="capitalize" + {...field} + onChange={() => { }} // Prevent changes + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + > + Cancel + </Button> + <Button + type="submit" + disabled={form.formState.isSubmitting || status !== "authenticated"} + > + Create + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
