summaryrefslogtreecommitdiff
path: root/lib/rfqs/table/add-rfq-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/table/add-rfq-dialog.tsx')
-rw-r--r--lib/rfqs/table/add-rfq-dialog.tsx349
1 files changed, 349 insertions, 0 deletions
diff --git a/lib/rfqs/table/add-rfq-dialog.tsx b/lib/rfqs/table/add-rfq-dialog.tsx
new file mode 100644
index 00000000..1d824bc0
--- /dev/null
+++ b/lib/rfqs/table/add-rfq-dialog.tsx
@@ -0,0 +1,349 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Check, ChevronsUpDown } from "lucide-react"
+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 {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
+import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"
+
+import { useSession } from "next-auth/react"
+import { createRfqSchema, type CreateRfqSchema, RfqType } from "../validations"
+import { createRfq, getBudgetaryRfqs } from "../service"
+import { ProjectSelector } from "@/components/ProjectSelector"
+import { type Project } from "../service"
+import { cn } from "@/lib/utils"
+import { BudgetaryRfqSelector } from "./BudgetaryRfqSelector"
+import { type BudgetaryRfq as ServiceBudgetaryRfq } from "../service";
+
+// 부모 RFQ 정보 타입 정의
+interface BudgetaryRfq {
+ id: number;
+ rfqCode: string;
+ description: 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 [budgetaryRfqs, setBudgetaryRfqs] = React.useState<BudgetaryRfq[]>([])
+ const [isLoadingBudgetary, setIsLoadingBudgetary] = React.useState(false)
+ const [budgetarySearchOpen, setBudgetarySearchOpen] = React.useState(false)
+ const [budgetarySearchTerm, setBudgetarySearchTerm] = React.useState("")
+ const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState<BudgetaryRfq | null>(null)
+
+ // 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;
+
+ // Debug logging - remove in production
+ console.log("Session status:", status);
+ console.log("Session data:", session);
+ console.log("User ID:", id);
+
+ return id;
+ }, [session, status]);
+
+ // RfqType에 따른 타이틀 생성
+ const getTitle = () => {
+ return rfqType === RfqType.PURCHASE
+ ? "Purchase RFQ"
+ : "Budgetary RFQ";
+ };
+
+ // 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]);
+
+ // Budgetary RFQ 목록 로드 (Purchase RFQ 생성 시만)
+ React.useEffect(() => {
+ if (rfqType === RfqType.PURCHASE && open) {
+ const loadBudgetaryRfqs = async () => {
+ setIsLoadingBudgetary(true);
+ try {
+ const result = await getBudgetaryRfqs();
+ if ('rfqs' in result) {
+ setBudgetaryRfqs(result.rfqs as unknown as BudgetaryRfq[]);
+ } else if ('error' in result) {
+ console.error("Budgetary RFQs 로드 오류:", result.error);
+ }
+ } catch (error) {
+ console.error("Budgetary RFQs 로드 오류:", error);
+ } finally {
+ setIsLoadingBudgetary(false);
+ }
+ };
+
+ loadBudgetaryRfqs();
+ }
+ }, [rfqType, open]);
+
+ // 검색어로 필터링된 Budgetary RFQ 목록
+ const filteredBudgetaryRfqs = React.useMemo(() => {
+ if (!budgetarySearchTerm.trim()) return budgetaryRfqs;
+
+ const lowerSearch = budgetarySearchTerm.toLowerCase();
+ return budgetaryRfqs.filter(
+ rfq =>
+ rfq.rfqCode.toLowerCase().includes(lowerSearch) ||
+ (rfq.description && rfq.description.toLowerCase().includes(lowerSearch))
+ );
+ }, [budgetaryRfqs, budgetarySearchTerm]);
+
+ // 프로젝트 선택 처리
+ const handleProjectSelect = (project: Project) => {
+ form.setValue("projectId", project.id);
+ };
+
+ // Budgetary RFQ 선택 처리
+ const handleBudgetaryRfqSelect = (rfq: BudgetaryRfq) => {
+ setSelectedBudgetaryRfq(rfq);
+ form.setValue("parentRfqId", rfq.id);
+ setBudgetarySearchOpen(false);
+ };
+
+ 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();
+ setSelectedBudgetaryRfq(null);
+ setOpen(false);
+ }
+
+ function handleDialogOpenChange(nextOpen: boolean) {
+ if (!nextOpen) {
+ form.reset();
+ setSelectedBudgetaryRfq(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>;
+ }
+
+ 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> 버튼을 누르세요.
+ </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>
+ <ProjectSelector
+ selectedProjectId={field.value}
+ onProjectSelect={handleProjectSelect}
+ placeholder="프로젝트 선택..."
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */}
+ {rfqType === RfqType.PURCHASE && (
+ <FormField
+ control={form.control}
+ name="parentRfqId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Budgetary RFQ (Optional)</FormLabel>
+ <FormControl>
+ <BudgetaryRfqSelector
+ selectedRfqId={field.value as number | undefined}
+ onRfqSelect={(rfq) => {
+ setSelectedBudgetaryRfq(rfq as any);
+ form.setValue("parentRfqId", rfq?.id);
+ }}
+ placeholder="Budgetary RFQ 선택..."
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* rfqCode */}
+ <FormField
+ control={form.control}
+ name="rfqCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>RFQ Code</FormLabel>
+ <FormControl>
+ <Input placeholder="e.g. RFQ-2025-001" {...field} />
+ </FormControl>
+ <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) {
+ field.onChange(new Date(val + "T00:00:00"))
+ }
+ }}
+ />
+ </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