"use client" import * as React from "react" import { useForm, useFieldArray } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { format } from "date-fns" import { CalendarIcon, Loader2, Trash2, PlusCircle } from "lucide-react" import { useSession } from "next-auth/react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Calendar } from "@/components/ui/calendar" import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { toast } from "sonner" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { createPreQuoteRfqAction } from "@/lib/bidding/pre-quote/service" import { previewGeneralRfqCode } from "@/lib/rfq-last/service" import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/material-group-selector-dialog-single" import { MaterialSearchItem } from "@/lib/material/material-group-service" import { MaterialSelectorDialogSingle } from "@/components/common/selectors/material/material-selector-dialog-single" import { MaterialSearchItem as SAPMaterialSearchItem } from "@/components/common/selectors/material/material-service" import { PurchaseGroupCodeSelector } from "@/components/common/selectors/purchase-group-code/purchase-group-code-selector" import type { PurchaseGroupCodeWithUser } from "@/components/common/selectors/purchase-group-code" import { getBiddingById } from "@/lib/bidding/service" // 아이템 스키마 const itemSchema = z.object({ itemCode: z.string().optional(), itemName: z.string().optional(), materialCode: z.string().optional(), materialName: z.string().optional(), quantity: z.number().min(1, "수량은 1 이상이어야 합니다"), uom: z.string().min(1, "단위를 입력해주세요"), remark: z.string().optional(), }) // 사전견적용 일반견적 생성 폼 스키마 const createPreQuoteRfqSchema = z.object({ rfqType: z.string().optional(), rfqTitle: z.string().min(1, "견적명을 입력해주세요"), dueDate: z.date({ required_error: "제출마감일을 선택해주세요", }).optional(), // 필수값 해제 picUserId: z.number().optional(), projectId: z.number().optional(), remark: z.string().optional(), biddingNumber: z.string().optional(), // 입찰 No. 추가 contractStartDate: z.date().optional(), // 계약기간 시작 contractEndDate: z.date().optional(), // 계약기간 종료 items: z.array(itemSchema).min(1, "최소 하나의 자재를 추가해주세요"), }) type CreatePreQuoteRfqFormValues = z.infer interface CreatePreQuoteRfqDialogProps { open: boolean onOpenChange: (open: boolean) => void biddingId: number biddingItems: Array<{ id: number materialGroupNumber?: string | null materialGroupInfo?: string | null materialNumber?: string | null materialInfo?: string | null quantity?: string | null quantityUnit?: string | null totalWeight?: string | null weightUnit?: string | null }> picUserId?: number | null biddingConditions?: { paymentTerms?: string | null taxConditions?: string | null incoterms?: string | null incotermsOption?: string | null contractDeliveryDate?: string | null shippingPort?: string | null destinationPort?: string | null isPriceAdjustmentApplicable?: boolean | null sparePartOptions?: string | null } | null onSuccess?: () => void } export function CreatePreQuoteRfqDialog({ open, onOpenChange, biddingId, biddingItems, picUserId, biddingConditions, onSuccess }: CreatePreQuoteRfqDialogProps) { const [isLoading, setIsLoading] = React.useState(false) const [previewCode, setPreviewCode] = React.useState("") const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) const [selectedBidPic, setSelectedBidPic] = React.useState(undefined) const { data: session } = useSession() const userId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null; }, [session]); // 입찰품목을 일반견적 아이템으로 매핑 const initialItems = React.useMemo(() => { return biddingItems.map((item) => ({ itemCode: item.materialGroupNumber || "", itemName: item.materialGroupInfo || "", materialCode: item.materialNumber || "", materialName: item.materialInfo || "", quantity: item.quantity ? parseFloat(item.quantity) : 1, uom: item.quantityUnit || item.weightUnit || "EA", remark: "", })) }, [biddingItems]) const form = useForm({ resolver: zodResolver(createPreQuoteRfqSchema), defaultValues: { rfqType: "", rfqTitle: "", dueDate: undefined, picUserId: undefined, projectId: undefined, remark: "", items: initialItems.length > 0 ? initialItems : [ { itemCode: "", itemName: "", materialCode: "", materialName: "", quantity: 1, uom: "", remark: "", }, ], }, }) /* const { fields, append, remove } = useFieldArray({ control: form.control, name: "items", }) */ // 견적담당자 정보 로드 React.useEffect(() => { const loadBiddingInfo = async () => { if (!biddingId || !open) return try { const bidding = await getBiddingById(biddingId) if (bidding) { setSelectedBidPic({ DISPLAY_NAME: bidding.bidPicName || '', PURCHASE_GROUP_CODE: bidding.bidPicCode || '', EMPLOYEE_NUMBER: '', user: bidding.bidPicId ? { id: bidding.bidPicId, name: bidding.bidPicName || '', email: '', employeeNumber: null } : undefined }) } } catch (error) { console.error('Failed to load bidding info:', error) } } loadBiddingInfo() }, [biddingId, open]) // 프로젝트 정보 상태 추가 const [projectInfo, setProjectInfo] = React.useState("") // 다이얼로그가 열릴 때 폼 초기화 React.useEffect(() => { if (open) { // 입찰 정보를 기반으로 기본값 설정 let rfqTitle = ""; let projectId: number | undefined = undefined; let contractStartDate: Date | undefined = undefined; let contractEndDate: Date | undefined = undefined; let biddingNumber = ""; const loadDetailedBiddingInfo = async () => { if (biddingId) { try { const bidding = await getBiddingById(biddingId) if (bidding) { rfqTitle = bidding.title; biddingNumber = bidding.biddingNumber; // 프로젝트 정보 설정 const pCode = bidding.projectCode || ""; const pName = bidding.projectName || ""; setProjectInfo(pCode && pName ? `${pCode} - ${pName}` : pCode || pName || ""); // 폼 값 설정 form.setValue("rfqTitle", rfqTitle); form.setValue("rfqType", "pre_bidding"); // 기본값 설정 if (biddingNumber) form.setValue("biddingNumber", biddingNumber); if (bidding.contractStartDate) form.setValue("contractStartDate", new Date(bidding.contractStartDate)); if (bidding.contractEndDate) form.setValue("contractEndDate", new Date(bidding.contractEndDate)); } } catch (e) { console.error(e); } } }; loadDetailedBiddingInfo(); form.reset({ rfqType: "pre_bidding", // 기본값 rfqTitle: "", dueDate: undefined, // 필수값 해제되었으므로 undefined 가능 picUserId: selectedBidPic?.user?.id, projectId: undefined, remark: "", biddingNumber: "", contractStartDate: undefined, contractEndDate: undefined, items: initialItems.length > 0 ? initialItems : [ { itemCode: "", itemName: "", materialCode: "", materialName: "", quantity: 1, uom: "", remark: "", }, ], }) setPreviewCode("") } }, [open, initialItems, form, selectedBidPic, biddingId]) // 견적담당자 선택 시 RFQ 코드 미리보기 생성 React.useEffect(() => { if (!selectedBidPic?.user?.id) { setPreviewCode("") return } // 즉시 실행 함수 패턴 사용 (async () => { setIsLoadingPreview(true) try { const code = await previewGeneralRfqCode(selectedBidPic.user!.id) setPreviewCode(code) } catch (error) { console.error("코드 미리보기 오류:", error) setPreviewCode("") } finally { setIsLoadingPreview(false) } })() }, [selectedBidPic]) // 견적 종류 변경 const handleRfqTypeChange = (value: string) => { form.setValue("rfqType", value) } const handleCancel = () => { form.reset({ rfqType: "", rfqTitle: "", dueDate: undefined, picUserId: undefined, projectId: undefined, remark: "", items: initialItems.length > 0 ? initialItems : [ { itemCode: "", itemName: "", materialCode: "", materialName: "", quantity: 1, uom: "", remark: "", }, ], }) setSelectedBidPic(undefined) setPreviewCode("") onOpenChange(false) } const onSubmit = async (data: CreatePreQuoteRfqFormValues) => { if (!userId) { toast.error("로그인이 필요합니다") return } const picUserId = selectedBidPic?.user?.id || session?.user?.id setIsLoading(true) try { // 서버 액션 호출 (입찰 조건 포함) const result = await createPreQuoteRfqAction({ // biddingId, // createPreQuoteRfqAction 인터페이스 변경됨 biddingId, rfqType: data.rfqType || "pre_bidding", rfqTitle: data.rfqTitle, dueDate: data.dueDate ? new Date(data.dueDate) : undefined, // optional이지만 submit시에는 값이 있을 수 있음 (없으면 서비스에서 처리) picUserId, projectId: data.projectId, remark: data.remark || "", biddingNumber: data.biddingNumber, // 추가 contractStartDate: data.contractStartDate, // 추가 contractEndDate: data.contractEndDate, // 추가 items: data.items as Array<{ itemCode: string; itemName: string; materialCode?: string; materialName?: string; quantity: number; uom: string; remark?: string; }>, biddingConditions: biddingConditions || undefined, createdBy: userId, updatedBy: userId, }) if (result.success) { toast.success(result.message, { description: result.data?.rfqCode ? `RFQ 코드: ${result.data.rfqCode}` : undefined, }) // 다이얼로그 닫기 onOpenChange(false) // 성공 콜백 실행 if (onSuccess) { onSuccess() } } else { toast.error(result.error || "사전견적용 일반견적 생성에 실패했습니다") } } catch (error) { console.error('사전견적용 일반견적 생성 오류:', error) toast.error("사전견적용 일반견적 생성에 실패했습니다", { description: "알 수 없는 오류가 발생했습니다", }) } finally { setIsLoading(false) } } // 아이템 추가 (사용안함) /* const handleAddItem = () => { append({ itemCode: "", itemName: "", materialCode: "", materialName: "", quantity: 1, uom: "", remark: "", }) } */ return ( {/* 고정된 헤더 */} 사전견적용 일반견적 생성 입찰의 사전견적을 위한 일반견적을 생성합니다. 입찰품목이 자재정보로 매핑되어 있습니다. {/* 스크롤 가능한 컨텐츠 영역 */}
{/* 기본 정보 섹션 */}

기본 정보

{/* 견적 종류 */} {/*
( 견적 종류 * )} />
*/} {/* 제출마감일 */} ( 제출마감일 { if (!date) { field.onChange(undefined) return } const newDate = new Date(date) if (field.value) { newDate.setHours(field.value.getHours(), field.value.getMinutes()) } else { newDate.setHours(0, 0, 0, 0) } field.onChange(newDate) }} disabled={(date) => { const today = new Date() today.setHours(0, 0, 0, 0) return date < today || date < new Date("1900-01-01") }} initialFocus />
{ if (field.value) { const [hours, minutes] = e.target.value.split(':').map(Number) const newDate = new Date(field.value) newDate.setHours(hours, minutes) field.onChange(newDate) } }} />
)} />
{/* 견적명 */} ( 견적명 * 견적의 목적이나 내용을 간단명료하게 입력해주세요 )} /> {/* 프로젝트 선택 */}
프로젝트
( )} /> {/* 담당자 정보 */} ( 견적담당자 * { setSelectedBidPic(code) field.onChange(code.user?.id) }} placeholder="입찰담당자 선택" /> 사전견적용 일반견적의 담당자를 선택합니다 )} /> {/* RFQ 코드 미리보기 */} {previewCode && (
예상 RFQ 코드: {previewCode} {isLoadingPreview && ( )}
)} {/* 계약기간 */}
( 계약기간 시작 )} /> ( 계약기간 종료 )} />
{/* 비고 */} ( 비고