"use client" import * as React from "react" import { useState, useEffect, useMemo } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { format } from "date-fns" import { toast } from "sonner" import { MessageSquare, Paperclip } from "lucide-react" import { Button } from "@/components/ui/button" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" import { DatePicker } from "@/components/ui/date-picker" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { ClientDataTable } from "@/components/client-data-table/data-table" import { Skeleton } from "@/components/ui/skeleton" import { formatCurrency, formatDate } from "@/lib/utils" import { QuotationItemEditor } from "./quotation-item-editor" import { submitVendorQuotation, updateVendorQuotation, fetchCurrencies, fetchPaymentTerms, fetchIncoterms, fetchBuyerVendorComments, Comment } from "../services" import { BuyerCommunicationDrawer } from "./buyer-communication-drawer" // 견적서 폼 스키마 const quotationFormSchema = z.object({ quotationVersion: z.number().min(1), // 필수값 표시됨 currency: z.string().min(1, "통화를 선택해주세요"), // 필수값 표시됨 validUntil: z.date({ required_error: "견적 유효기간을 선택해주세요", invalid_type_error: "유효한 날짜를 선택해주세요", }), // 필수값 표시됨 estimatedDeliveryDate: z.date({ required_error: "예상 납품일을 선택해주세요", invalid_type_error: "유효한 날짜를 선택해주세요", }), // 필수값 표시됨 paymentTermsCode: z.string({ required_error: "지불 조건을 선택해주세요", }).min(1, "지불 조건을 선택해주세요"), // 필수값 표시됨 incotermsCode: z.string({ required_error: "인코텀즈를 선택해주세요", }).min(1, "인코텀즈를 선택해주세요"), // 필수값 아님 incotermsDetail: z.string().optional(), // 필수값 아님 remark: z.string().optional(), }) type QuotationFormValues = z.infer // 데이터 타입 정의 interface Currency { code: string name: string } interface PaymentTerm { code: string description: string } interface Incoterm { code: string description: string } // 이 컴포넌트에 전달되는 견적서 데이터 타입 interface VendorQuotation { id: number rfqId: number vendorId: number quotationCode: string | null quotationVersion: number | null totalItemsCount: number | null subTotal: string| null taxTotal: string| null discountTotal: string| null totalPrice: string| null currency: string| null validUntil: Date | null estimatedDeliveryDate: Date | null paymentTermsCode: string | null incotermsCode: string | null incotermsDetail: string | null status: "Draft" | "Submitted" | "Revised" | "Rejected" | "Accepted" remark: string | null rejectionReason: string | null submittedAt: Date | null acceptedAt: Date | null createdAt: Date updatedAt: Date rfq: { id: number rfqCode: string| null dueDate: Date | null status: string| null // 기타 필요한 정보 } vendor: { id: number vendorName: string vendorCode: string| null // 기타 필요한 정보 } items: QuotationItem[] } // 견적 아이템 타입 interface QuotationItem { id: number quotationId: number prItemId: number materialCode: string | null materialDescription: string | null quantity: number uom: string | null unitPrice: number totalPrice: number currency: string vendorMaterialCode: string | null vendorMaterialDescription: string | null deliveryDate: Date | null leadTimeInDays: number | null taxRate: number | null taxAmount: number | null discountRate: number | null discountAmount: number | null remark: string | null isAlternative: boolean isRecommended: boolean createdAt: Date updatedAt: Date prItem?: { id: number materialCode: string | null materialDescription: string | null // 기타 필요한 정보 } } // 견적서 편집 컴포넌트 프롭스 interface VendorQuotationEditorProps { quotation: VendorQuotation } export default function VendorQuotationEditor({ quotation }: VendorQuotationEditorProps) { console.log(quotation) const [activeTab, setActiveTab] = useState("items") const [isSubmitting, setIsSubmitting] = useState(false) const [isSaving, setIsSaving] = useState(false) const [items, setItems] = useState(quotation.items || []) // 서버에서 가져온 데이터 상태 const [currencies, setCurrencies] = useState([]) const [paymentTerms, setPaymentTerms] = useState([]) const [incoterms, setIncoterms] = useState([]) // 데이터 로딩 상태 const [loadingCurrencies, setLoadingCurrencies] = useState(true) const [loadingPaymentTerms, setLoadingPaymentTerms] = useState(true) const [loadingIncoterms, setLoadingIncoterms] = useState(true) // 커뮤니케이션 드로어 상태 const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false) const [comments, setComments] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [loadingComments, setLoadingComments] = useState(false); // 컴포넌트 마운트 시 메시지 미리 로드 useEffect(() => { if (quotation) { loadCommunicationData(); } }, [quotation]); // 메시지 데이터 로드 함수 const loadCommunicationData = async () => { try { setLoadingComments(true); const commentsData = await fetchBuyerVendorComments(quotation.rfqId, quotation.vendorId); setComments(commentsData); // 읽지 않은 메시지 수 계산 const unread = commentsData.filter( comment => !comment.isVendorComment && !comment.isRead ).length; setUnreadCount(unread); } catch (error) { console.error("메시지 데이터 로드 오류:", error); } finally { setLoadingComments(false); } }; // 커뮤니케이션 드로어가 닫힐 때 데이터 새로고침 const handleCommunicationDrawerChange = (open: boolean) => { setCommunicationDrawerOpen(open); if (!open) { loadCommunicationData(); // 드로어가 닫힐 때 데이터 새로고침 } }; // 버튼 비활성화 const isBeforeDueDate = () => { if (!quotation.rfq.dueDate) { // dueDate가 null인 경우 기본적으로 수정 불가능하도록 설정 (false 반환) return false; } const now = new Date(); const dueDate = new Date(quotation.rfq.dueDate); return now < dueDate; }; // 수정된 isDisabled 조건 const isDisabled = (quotation.status === "Accepted") || ((quotation.status === "Submitted" || quotation.status === "Revised") && !isBeforeDueDate()); // 견적서 총합 계산 const totals = useMemo(() => { const subTotal = items.reduce((sum, item) => sum + Number(item.totalPrice), 0) const taxTotal = items.reduce((sum, item) => sum + (Number(item.taxAmount) || 0), 0) const discountTotal = items.reduce((sum, item) => sum + (Number(item.discountAmount) || 0), 0) const totalPrice = subTotal + taxTotal - discountTotal return { subTotal, taxTotal, discountTotal, totalPrice } }, [items]) // 폼 설정 const form = useForm({ resolver: zodResolver(quotationFormSchema), defaultValues: { quotationVersion: quotation.quotationVersion || 0, currency: quotation.currency || "KRW", validUntil: quotation.validUntil || undefined, estimatedDeliveryDate: quotation.estimatedDeliveryDate || undefined, paymentTermsCode: quotation.paymentTermsCode || "", incotermsCode: quotation.incotermsCode || "", incotermsDetail: quotation.incotermsDetail || "", remark: quotation.remark || "", }, mode: "onChange", // 실시간 검증 활성화 }) // 마운트 시 데이터 로드 useEffect(() => { // 통화 데이터 로드 const loadCurrencies = async () => { try { setLoadingCurrencies(true) const result = await fetchCurrencies() if (result.success) { setCurrencies(result.data) } else { toast.error(result.message || "통화 데이터 로드 실패") } } catch (error) { console.error("통화 데이터 로드 오류:", error) toast.error("통화 데이터를 불러오는 중 오류가 발생했습니다") } finally { setLoadingCurrencies(false) } } // 지불 조건 데이터 로드 const loadPaymentTerms = async () => { try { setLoadingPaymentTerms(true) const result = await fetchPaymentTerms() if (result.success) { setPaymentTerms(result.data) } else { toast.error(result.message || "지불 조건 데이터 로드 실패") } } catch (error) { console.error("지불 조건 데이터 로드 오류:", error) toast.error("지불 조건 데이터를 불러오는 중 오류가 발생했습니다") } finally { setLoadingPaymentTerms(false) } } // 인코텀즈 데이터 로드 const loadIncoterms = async () => { try { setLoadingIncoterms(true) const result = await fetchIncoterms() if (result.success) { setIncoterms(result.data) } else { toast.error(result.message || "인코텀즈 데이터 로드 실패") } } catch (error) { console.error("인코텀즈 데이터 로드 오류:", error) toast.error("인코텀즈 데이터를 불러오는 중 오류가 발생했습니다") } finally { setLoadingIncoterms(false) } } // 함수 호출 loadCurrencies() loadPaymentTerms() loadIncoterms() }, []) // 견적서 저장 const handleSave = async () => { try { setIsSaving(true) // 기본 검증 (통화는 필수) const validationResult = await form.trigger(['currency']); if (!validationResult) { toast.warning("통화는 필수 항목입니다"); return; } const values = form.getValues() const result = await updateVendorQuotation({ id: quotation.id, ...values, subTotal: totals.subTotal.toString(), taxTotal: totals.taxTotal.toString(), discountTotal: totals.discountTotal.toString(), totalPrice: totals.totalPrice.toString(), totalItemsCount: items.length, }) if (result.success) { toast.success("견적서가 저장되었습니다") // 견적서 제출 준비 상태 점검 const formValid = await form.trigger(); const itemsValid = !items.some(item => item.unitPrice <= 0 || !item.deliveryDate); const alternativeItemsValid = !items.some(item => item.isAlternative && (!item.vendorMaterialDescription || !item.remark) ); if (formValid && itemsValid && alternativeItemsValid) { toast.info("모든 필수 정보가 입력되었습니다. '견적서 제출' 버튼을 클릭하여 제출하세요."); } else { const missingFields = []; if (!formValid) missingFields.push("견적서 기본 정보"); if (!itemsValid) missingFields.push("견적 항목의 단가/납품일"); if (!alternativeItemsValid) missingFields.push("대체품 정보"); toast.info(`제출하기 전에 다음 정보를 입력해주세요: ${missingFields.join(', ')}`); } } else { toast.error(result.message || "견적서 저장 중 오류가 발생했습니다") } } catch (error) { console.error("견적서 저장 오류:", error) toast.error("견적서 저장 중 오류가 발생했습니다") } finally { setIsSaving(false) } } // 견적서 제출 const handleSubmit = async () => { try { setIsSubmitting(true) // 1. 폼 스키마 검증 (기본 정보) const formValid = await form.trigger(); if (!formValid) { const formState = form.getFieldState("validUntil"); const estimatedDeliveryState = form.getFieldState("estimatedDeliveryDate"); const paymentTermsState = form.getFieldState("paymentTermsCode"); const incotermsState = form.getFieldState("incotermsCode"); // 주요 필드별 오류 메시지 표시 if (!form.getValues("validUntil")) { toast.error("견적 유효기간을 선택해주세요"); } else if (!form.getValues("estimatedDeliveryDate")) { toast.error("예상 납품일을 선택해주세요"); } else if (!form.getValues("paymentTermsCode")) { toast.error("지불 조건을 선택해주세요"); } else if (!form.getValues("incotermsCode")) { toast.error("인코텀즈를 선택해주세요"); } else { toast.error("견적서 기본 정보를 모두 입력해주세요"); } // 견적 정보 탭으로 이동 setActiveTab("details"); return; } // 2. 견적 항목 검증 const emptyItems = items.filter(item => item.unitPrice <= 0 || !item.deliveryDate ); if (emptyItems.length > 0) { toast.error(`${emptyItems.length}개 항목의 단가와 납품일을 입력해주세요`); setActiveTab("items"); return; } // 3. 대체품 정보 검증 const invalidAlternativeItems = items.filter(item => item.isAlternative && (!item.vendorMaterialDescription || !item.remark) ); if (invalidAlternativeItems.length > 0) { toast.error(`${invalidAlternativeItems.length}개의 대체품 항목에 정보를 모두 입력해주세요`); setActiveTab("items"); return; } // 모든 검증 통과 - 제출 진행 const values = form.getValues(); const result = await submitVendorQuotation({ id: quotation.id, ...values, subTotal: totals.subTotal.toString(), taxTotal: totals.taxTotal.toString(), discountTotal: totals.discountTotal.toString(), totalPrice: totals.totalPrice.toString(), totalItemsCount: items.length, }); if (result.success && isBeforeDueDate()) { toast.success("견적서가 제출되었습니다. 마감일 전까지 수정 가능합니다."); // 페이지 새로고침 window.location.reload(); } else { toast.error(result.message || "견적서 제출 중 오류가 발생했습니다"); } } catch (error) { console.error("견적서 제출 오류:", error); toast.error("견적서 제출 중 오류가 발생했습니다"); } finally { setIsSubmitting(false); } } const isSubmitReady = () => { // 폼 유효성 const formValid = !Object.keys(form.formState.errors).length; // 항목 유효성 const itemsValid = !items.some(item => item.unitPrice <= 0 || !item.deliveryDate ); // 대체품 유효성 const alternativeItemsValid = !items.some(item => item.isAlternative && (!item.vendorMaterialDescription || !item.remark) ); // 유효하지 않은 항목 또는 대체품이 있으면 제출 불가 return formValid && itemsValid && alternativeItemsValid; } // 아이템 업데이트 핸들러 const handleItemsUpdate = (updatedItems: QuotationItem[]) => { setItems(updatedItems) } // 상태에 따른 배지 색상 const getStatusBadge = (status: string) => { switch (status) { case "Draft": return 초안 case "Submitted": return 제출됨 case "Revised": return 수정됨 case "Rejected": return 반려됨 case "Accepted": return 승인됨 default: return {status} } } // 셀렉트 로딩 상태 표시 컴포넌트 const SelectSkeleton = () => (
) return (

견적서 작성

RFQ 번호: {quotation.rfq.rfqCode} | 견적서 번호: {quotation.quotationCode}

{quotation.rfq.dueDate ? (

마감일: {formatDate(new Date(quotation.rfq.dueDate))} {isBeforeDueDate() ? ' (마감 전: 수정 가능)' : ' (마감 됨: 수정 불가)'}

) : (

마감일이 설정되지 않았습니다

)}
{getStatusBadge(quotation.status)} {quotation.status === "Rejected" && (
반려 사유: {quotation.rejectionReason || "사유 없음"}
)}
견적 항목 견적 정보 커뮤니케이션 {/* 견적 항목 탭 */} 견적 항목 정보 각 항목에 대한 가격, 납품일 등을 입력해주세요
소계: {formatCurrency(totals.subTotal, quotation.currency)}
세액: {formatCurrency(totals.taxTotal, quotation.currency)}
할인액: {formatCurrency(totals.discountTotal, quotation.currency)}
총액: {formatCurrency(totals.totalPrice, quotation.currency)}
{/* 견적 정보 탭 */}
견적서 기본 정보 견적서의 일반 정보를 입력해주세요 {/* 통화 필드 */} {loadingCurrencies ? ( ) : ( ( 통화 * )} /> )} ( 견적 유효기간 * {/* 필수값 표시 */} )} /> ( 예상 납품일 * )} /> {/* 지불 조건 필드 */} {loadingPaymentTerms ? ( ) : ( ( 지불 조건 * )} /> )} {/* 인코텀즈 필드 */} {loadingIncoterms ? ( ) : ( ( 인코텀즈 * )} /> )} ( 인코텀즈 상세 )} /> ( 비고