summaryrefslogtreecommitdiff
path: root/lib/procurement-rfqs/vendor-response/quotation-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/procurement-rfqs/vendor-response/quotation-editor.tsx')
-rw-r--r--lib/procurement-rfqs/vendor-response/quotation-editor.tsx955
1 files changed, 0 insertions, 955 deletions
diff --git a/lib/procurement-rfqs/vendor-response/quotation-editor.tsx b/lib/procurement-rfqs/vendor-response/quotation-editor.tsx
deleted file mode 100644
index 66bb2613..00000000
--- a/lib/procurement-rfqs/vendor-response/quotation-editor.tsx
+++ /dev/null
@@ -1,955 +0,0 @@
-"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<typeof quotationFormSchema>
-
-// 데이터 타입 정의
-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<QuotationItem[]>(quotation.items || [])
-
- // 서버에서 가져온 데이터 상태
- const [currencies, setCurrencies] = useState<Currency[]>([])
- const [paymentTerms, setPaymentTerms] = useState<PaymentTerm[]>([])
- const [incoterms, setIncoterms] = useState<Incoterm[]>([])
-
- // 데이터 로딩 상태
- const [loadingCurrencies, setLoadingCurrencies] = useState(true)
- const [loadingPaymentTerms, setLoadingPaymentTerms] = useState(true)
- const [loadingIncoterms, setLoadingIncoterms] = useState(true)
-
- // 커뮤니케이션 드로어 상태
- const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false)
-
- const [comments, setComments] = useState<Comment[]>([]);
- 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<QuotationFormValues>({
- 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 <Badge variant="outline">초안</Badge>
- case "Submitted":
- return <Badge variant="default">제출됨</Badge>
- case "Revised":
- return <Badge variant="secondary">수정됨</Badge>
- case "Rejected":
- return <Badge variant="destructive">반려됨</Badge>
- case "Accepted":
- return <Badge variant="default">승인됨</Badge>
- default:
- return <Badge>{status}</Badge>
- }
- }
-
- // 셀렉트 로딩 상태 표시 컴포넌트
- const SelectSkeleton = () => (
- <div className="flex flex-col gap-2">
- <Skeleton className="h-4 w-[40%]" />
- <Skeleton className="h-10 w-full" />
- </div>
- )
-
- return (
- <div className="space-y-6">
- <div className="flex justify-between items-start">
- <div>
- <h1 className="text-2xl font-bold tracking-tight">견적서 작성</h1>
- <p className="text-muted-foreground">
- RFQ 번호: {quotation.rfq.rfqCode} | 견적서 번호: {quotation.quotationCode}
- </p>
- {quotation.rfq.dueDate ? (
- <p className={`text-sm ${isBeforeDueDate() ? 'text-green-600' : 'text-red-600'}`}>
- 마감일: {formatDate(new Date(quotation.rfq.dueDate))}
- {isBeforeDueDate()
- ? ' (마감 전: 수정 가능)'
- : ' (마감 됨: 수정 불가)'}
- </p>
- ) : (
- <p className="text-sm text-amber-600">
- 마감일이 설정되지 않았습니다
- </p>
- )}
- </div>
- <div className="flex items-center gap-2">
- {getStatusBadge(quotation.status)}
- {quotation.status === "Rejected" && (
- <div className="text-sm text-destructive">
- <span className="font-medium">반려 사유:</span> {quotation.rejectionReason || "사유 없음"}
- </div>
- )}
- </div>
- </div>
-
- <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
- <TabsList>
- <TabsTrigger value="items">견적 항목</TabsTrigger>
- <TabsTrigger value="details">견적 정보</TabsTrigger>
- <TabsTrigger value="communication">커뮤니케이션</TabsTrigger>
- </TabsList>
-
- {/* 견적 항목 탭 */}
- <TabsContent value="items" className="p-0 pt-4">
- <Card>
- <CardHeader>
- <CardTitle>견적 항목 정보</CardTitle>
- <CardDescription>
- 각 항목에 대한 가격, 납품일 등을 입력해주세요
- </CardDescription>
- </CardHeader>
- <CardContent>
- <QuotationItemEditor
- items={items}
- onItemsChange={handleItemsUpdate}
- disabled={isDisabled}
- currency={form.watch("currency")}
- />
- </CardContent>
- <CardFooter className="flex justify-between border-t p-4">
- <div className="space-y-1">
- <div className="text-sm text-muted-foreground">
- <span className="font-medium">소계:</span> {formatCurrency(totals.subTotal, quotation.currency)}
- </div>
- <div className="text-sm text-muted-foreground">
- <span className="font-medium">세액:</span> {formatCurrency(totals.taxTotal, quotation.currency)}
- </div>
- <div className="text-sm text-muted-foreground">
- <span className="font-medium">할인액:</span> {formatCurrency(totals.discountTotal, quotation.currency)}
- </div>
- <div className="text-base font-bold">
- <span>총액:</span> {formatCurrency(totals.totalPrice, quotation.currency)}
- </div>
- </div>
- <div className="flex space-x-2">
- <Button
- variant="outline"
- onClick={handleSave}
- disabled={isDisabled || isSaving}
- >
- {isSaving ? "저장 중..." : "저장"}
- </Button>
- <Button
- onClick={handleSubmit}
- disabled={isDisabled || isSubmitting || !isSubmitReady()}
- >
- {isSubmitting ? "제출 중..." : "견적서 제출"}
- </Button>
- </div>
- </CardFooter>
- </Card>
- </TabsContent>
-
- {/* 견적 정보 탭 */}
- <TabsContent value="details" className="p-0 pt-4">
- <Form {...form}>
- <form className="space-y-6">
- <Card>
- <CardHeader>
- <CardTitle>견적서 기본 정보</CardTitle>
- <CardDescription>
- 견적서의 일반 정보를 입력해주세요
- </CardDescription>
- </CardHeader>
- <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 통화 필드 */}
- {loadingCurrencies ? (
- <SelectSkeleton />
- ) : (
- <FormField
- control={form.control}
- name="currency"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 통화
- <span className="text-destructive ml-1">*</span>
- </FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- disabled={isDisabled}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="통화 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {currencies.map((currency) => (
- <SelectItem key={currency.code} value={currency.code}>
- {currency.code} ({currency.name})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- <FormField
- control={form.control}
- name="validUntil"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 견적 유효기간
- <span className="text-destructive ml-1">*</span> {/* 필수값 표시 */}
- </FormLabel>
- <FormControl>
- <DatePicker
- date={field.value}
- onSelect={field.onChange}
- disabled={isDisabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="estimatedDeliveryDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 예상 납품일
- <span className="text-destructive ml-1">*</span>
- </FormLabel>
- <FormControl>
- <DatePicker
- date={field.value}
- onSelect={field.onChange}
- disabled={isDisabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 지불 조건 필드 */}
- {loadingPaymentTerms ? (
- <SelectSkeleton />
- ) : (
- <FormField
- control={form.control}
- name="paymentTermsCode"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 지불 조건
- <span className="text-destructive ml-1">*</span>
- </FormLabel>
- <Select
- value={field.value || ""}
- onValueChange={field.onChange}
- disabled={isDisabled}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="지불 조건 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {paymentTerms.map((term) => (
- <SelectItem key={term.code} value={term.code}>
- {term.description}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- {/* 인코텀즈 필드 */}
- {loadingIncoterms ? (
- <SelectSkeleton />
- ) : (
- <FormField
- control={form.control}
- name="incotermsCode"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 인코텀즈
- <span className="text-destructive ml-1">*</span>
- </FormLabel>
- <Select
- value={field.value || ""}
- onValueChange={field.onChange}
- disabled={isDisabled}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="인코텀즈 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {incoterms.map((term) => (
- <SelectItem key={term.code} value={term.code}>
- {term.code} ({term.description})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- <FormField
- control={form.control}
- name="incotermsDetail"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center">
- 인코텀즈 상세
- <span className="text-destructive ml-1"></span>
- </FormLabel>
- <FormControl>
- <Input
- placeholder="인코텀즈 상세 정보 입력"
- {...field}
- value={field.value || ""}
- disabled={isDisabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="remark"
- render={({ field }) => (
- <FormItem className="col-span-2">
- <FormLabel className="flex items-center">
- 비고
- <span className="text-destructive ml-1"></span>
- </FormLabel>
- <FormControl>
- <Textarea
- placeholder="추가 정보나 특이사항을 입력해주세요"
- className="resize-none min-h-[100px]"
- {...field}
- value={field.value || ""}
- disabled={isDisabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </CardContent>
- <CardFooter className="flex justify-end">
- <Button
- variant="outline"
- onClick={handleSave}
- disabled={isDisabled || isSaving}
- >
- {isSaving ? "저장 중..." : "저장"}
- </Button>
- </CardFooter>
- </Card>
- </form>
- </Form>
- </TabsContent>
-
- {/* 커뮤니케이션 탭 */}
- <TabsContent value="communication" className="p-0 pt-4">
- <Card>
- <CardHeader className="flex flex-row items-center justify-between">
- <div>
- <CardTitle className="flex items-center gap-2">
- 커뮤니케이션
- {unreadCount > 0 && (
- <Badge variant="destructive" className="ml-2">
- 새 메시지 {unreadCount}
- </Badge>
- )}
- </CardTitle>
- <CardDescription>
- 구매자와의 메시지 및 첨부파일
- </CardDescription>
- </div>
- <Button
- onClick={() => setCommunicationDrawerOpen(true)}
- variant="outline"
- size="sm"
- >
- <MessageSquare className="h-4 w-4 mr-2" />
- {unreadCount > 0 ? "새 메시지 확인" : "메시지 보내기"}
- </Button>
- </CardHeader>
- <CardContent>
- {loadingComments ? (
- <div className="flex items-center justify-center p-8">
- <div className="text-center">
- <Skeleton className="h-4 w-32 mx-auto mb-2" />
- <Skeleton className="h-4 w-48 mx-auto" />
- </div>
- </div>
- ) : comments.length === 0 ? (
- <div className="min-h-[200px] flex flex-col items-center justify-center text-center p-8">
- <div className="max-w-md">
- <div className="mx-auto bg-primary/10 rounded-full w-12 h-12 flex items-center justify-center mb-4">
- <MessageSquare className="h-6 w-6 text-primary" />
- </div>
- <h3 className="text-lg font-medium mb-2">아직 메시지가 없습니다</h3>
- <p className="text-muted-foreground mb-4">
- 견적서에 대한 질문이나 의견이 있으신가요? 구매자와 메시지를 주고받으세요.
- </p>
- <Button
- onClick={() => setCommunicationDrawerOpen(true)}
- className="mx-auto"
- >
- 메시지 보내기
- </Button>
- </div>
- </div>
- ) : (
- <div className="space-y-4">
- {/* 최근 메시지 3개 미리보기 */}
- <div className="space-y-2">
- <h3 className="text-sm font-medium">최근 메시지</h3>
- <ScrollArea className="h-[250px] rounded-md border p-4">
- {comments.slice(-3).map(comment => (
- <div
- key={comment.id}
- className={`p-3 mb-3 rounded-lg ${!comment.isVendorComment && !comment.isRead
- ? 'bg-primary/10 border-l-4 border-primary'
- : 'bg-muted/50'
- }`}
- >
- <div className="flex justify-between items-center mb-1">
- <span className="text-sm font-medium">
- {comment.isVendorComment
- ? '나'
- : comment.userName || '구매 담당자'}
- </span>
- <span className="text-xs text-muted-foreground">
- {new Date(comment.createdAt).toLocaleDateString()}
- </span>
- </div>
- <p className="text-sm line-clamp-2">{comment.content}</p>
- {comment.attachments.length > 0 && (
- <div className="mt-1 text-xs text-muted-foreground">
- <Paperclip className="h-3 w-3 inline mr-1" />
- 첨부파일 {comment.attachments.length}개
- </div>
- )}
- </div>
- ))}
- </ScrollArea>
- </div>
-
- <div className="flex justify-center">
- <Button
- onClick={() => setCommunicationDrawerOpen(true)}
- className="w-full"
- >
- 전체 메시지 보기 ({comments.length}개)
- </Button>
- </div>
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 커뮤니케이션 드로어 */}
- <BuyerCommunicationDrawer
- open={communicationDrawerOpen}
- onOpenChange={handleCommunicationDrawerChange}
- quotation={quotation}
- onSuccess={loadCommunicationData}
- />
- </TabsContent>
- </Tabs>
- </div>
- )
-} \ No newline at end of file