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.tsx227
1 files changed, 161 insertions, 66 deletions
diff --git a/lib/rfqs/table/add-rfq-dialog.tsx b/lib/rfqs/table/add-rfq-dialog.tsx
index 45390cd0..41055608 100644
--- a/lib/rfqs/table/add-rfq-dialog.tsx
+++ b/lib/rfqs/table/add-rfq-dialog.tsx
@@ -3,38 +3,29 @@
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 { createRfq, generateNextRfqCode, 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";
+import { ParentRfqSelector } from "./ParentRfqSelector"
// 부모 RFQ 정보 타입 정의
-interface BudgetaryRfq {
+interface ParentRfq {
id: number;
rfqCode: string;
description: string | null;
+ rfqType: RfqType;
+ projectId: number | null;
+ projectCode: string | null;
+ projectName: string | null;
}
interface AddRfqDialogProps {
@@ -44,11 +35,10 @@ interface AddRfqDialogProps {
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)
+ 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(() => {
@@ -64,9 +54,30 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
// RfqType에 따른 타이틀 생성
const getTitle = () => {
- return rfqType === RfqType.PURCHASE
- ? "Purchase RFQ"
- : "Budgetary RFQ";
+ 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
@@ -92,40 +103,79 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
}
}, [status, userId, form]);
- // Budgetary RFQ 목록 로드 (Purchase RFQ 생성 시만)
+ // 다이얼로그가 열릴 때 자동으로 RFQ 코드 생성
React.useEffect(() => {
- if (rfqType === RfqType.PURCHASE && open) {
- const loadBudgetaryRfqs = async () => {
- setIsLoadingBudgetary(true);
+ if (open) {
+ const generateRfqCode = async () => {
+ setIsLoadingRfqCode(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);
+ // 서버 액션 호출
+ const result = await generateNextRfqCode(rfqType);
+
+ if (result.error) {
+ toast.error(`RFQ 코드 생성 실패: ${result.error}`);
+ return;
}
+
+ // 생성된 코드를 폼에 설정
+ form.setValue("rfqCode", result.code);
} catch (error) {
- console.error("Budgetary RFQs 로드 오류:", error);
+ console.error("RFQ 코드 생성 오류:", error);
+ toast.error("RFQ 코드 생성에 실패했습니다");
} finally {
- setIsLoadingBudgetary(false);
+ setIsLoadingRfqCode(false);
}
};
-
- loadBudgetaryRfqs();
+
+ generateRfqCode();
}
- }, [rfqType, open]);
+ }, [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 [];
+ }
+ };
- // 검색어로 필터링된 Budgetary RFQ 목록
- const filteredBudgetaryRfqs = React.useMemo(() => {
- if (!budgetarySearchTerm.trim()) return budgetaryRfqs;
+ // 선택 가능한 부모 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);
+ }
+ };
- const lowerSearch = budgetarySearchTerm.toLowerCase();
- return budgetaryRfqs.filter(
- rfq =>
- rfq.rfqCode.toLowerCase().includes(lowerSearch) ||
- (rfq.description && rfq.description.toLowerCase().includes(lowerSearch))
- );
- }, [budgetaryRfqs, budgetarySearchTerm]);
+ loadParentRfqs();
+ }
+ }, [rfqType, open]);
// 프로젝트 선택 처리
const handleProjectSelect = (project: Project | null) => {
@@ -136,11 +186,10 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
form.setValue("projectId", project.id);
};
- // Budgetary RFQ 선택 처리
- const handleBudgetaryRfqSelect = (rfq: BudgetaryRfq) => {
- setSelectedBudgetaryRfq(rfq);
- form.setValue("parentRfqId", rfq.id);
- setBudgetarySearchOpen(false);
+ // 부모 RFQ 선택 처리
+ const handleParentRfqSelect = (rfq: ParentRfq | null) => {
+ setSelectedParentRfq(rfq);
+ form.setValue("parentRfqId", rfq?.id);
};
async function onSubmit(data: CreateRfqSchema) {
@@ -166,14 +215,14 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
toast.success("RFQ가 성공적으로 생성되었습니다.");
form.reset();
- setSelectedBudgetaryRfq(null);
+ setSelectedParentRfq(null);
setOpen(false);
}
function handleDialogOpenChange(nextOpen: boolean) {
if (!nextOpen) {
form.reset();
- setSelectedBudgetaryRfq(null);
+ setSelectedParentRfq(null);
}
setOpen(nextOpen);
}
@@ -183,6 +232,28 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
return <Button variant="outline" size="sm" disabled>Loading...</Button>;
}
+ // 타입에 따라 부모 RFQ 선택 필드를 보여줄지 결정
+ const shouldShowParentRfqSelector = rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_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}>
{/* 모달을 열기 위한 버튼 */}
@@ -197,6 +268,9 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
<DialogTitle>Create New {getTitle()}</DialogTitle>
<DialogDescription>
새 {getTitle()} 정보를 입력하고 <b>Create</b> 버튼을 누르세요.
+ <div className="mt-1 text-xs text-muted-foreground">
+ {getTypeDescription()}
+ </div>
</DialogDescription>
</DialogHeader>
@@ -231,31 +305,37 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
)}
/>
- {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */}
- {rfqType === RfqType.PURCHASE && (
+ {/* Parent RFQ Selector - PURCHASE 또는 PURCHASE_BUDGETARY 타입일 때만 표시 */}
+ {shouldShowParentRfqSelector && (
<FormField
control={form.control}
name="parentRfqId"
render={({ field }) => (
<FormItem>
- <FormLabel>Budgetary RFQ (Optional)</FormLabel>
+ <FormLabel>{getParentRfqSelectorLabel()}</FormLabel>
<FormControl>
- <BudgetaryRfqSelector
+ <ParentRfqSelector
selectedRfqId={field.value as number | undefined}
- onRfqSelect={(rfq) => {
- setSelectedBudgetaryRfq(rfq as any);
- form.setValue("parentRfqId", rfq?.id);
- }}
- placeholder="Budgetary RFQ 선택..."
+ 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 */}
+ {/* rfqCode - 자동 생성되고 읽기 전용 */}
<FormField
control={form.control}
name="rfqCode"
@@ -263,8 +343,23 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps)
<FormItem>
<FormLabel>RFQ Code</FormLabel>
<FormControl>
- <Input placeholder="e.g. RFQ-2025-001" {...field} />
+ <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>
)}