diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response/quotation-editor.tsx')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/quotation-editor.tsx | 559 |
1 files changed, 0 insertions, 559 deletions
diff --git a/lib/techsales-rfq/vendor-response/quotation-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-editor.tsx deleted file mode 100644 index 54058214..00000000 --- a/lib/techsales-rfq/vendor-response/quotation-editor.tsx +++ /dev/null @@ -1,559 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect } from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import * as z from "zod" -import { toast } from "sonner" -import { useRouter } from "next/navigation" -import { CalendarIcon, Save, Send, ArrowLeft } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Separator } from "@/components/ui/separator" -import { DatePicker } from "@/components/ui/date-picker" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Skeleton } from "@/components/ui/skeleton" - -import { formatCurrency, formatDate } from "@/lib/utils" -import { - updateTechSalesVendorQuotation, - submitTechSalesVendorQuotation, - fetchCurrencies -} from "../service" - -// 견적서 폼 스키마 (techsales용 단순화) -const quotationFormSchema = z.object({ - currency: z.string().min(1, "통화를 선택해주세요"), - totalPrice: z.string().min(1, "총액을 입력해주세요"), - validUntil: z.date({ - required_error: "견적 유효기간을 선택해주세요", - invalid_type_error: "유효한 날짜를 선택해주세요", - }), - remark: z.string().optional(), -}) - -type QuotationFormValues = z.infer<typeof quotationFormSchema> - -// 통화 타입 -interface Currency { - code: string - name: string -} - -// 이 컴포넌트에 전달되는 견적서 데이터 타입 (techsales용 단순화) -interface TechSalesVendorQuotation { - id: number - rfqId: number - vendorId: number - quotationCode: string | null - quotationVersion: number | null - totalPrice: string | null - currency: string | null - validUntil: Date | 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 - materialCode: string | null - remark: string | null - projectSnapshot?: { - pspid?: string - projNm?: string - sector?: string - projMsrm?: number - kunnr?: string - kunnrNm?: string - ptypeNm?: string - } | null - seriesSnapshot?: Array<{ - pspid: string - sersNo: string - scDt?: string - klDt?: string - lcDt?: string - dlDt?: string - dockNo?: string - dockNm?: string - projNo?: string - post1?: string - }> | null - item?: { - id: number - itemCode: string | null - itemList: string | null - } | null - biddingProject?: { - id: number - pspid: string | null - projNm: string | null - } | null - createdByUser?: { - id: number - name: string | null - email: string | null - } | null - } - vendor: { - id: number - vendorName: string - vendorCode: string | null - } -} - -interface TechSalesQuotationEditorProps { - quotation: TechSalesVendorQuotation -} - -export default function TechSalesQuotationEditor({ quotation }: TechSalesQuotationEditorProps) { - const router = useRouter() - const [isSubmitting, setIsSubmitting] = useState(false) - const [isSaving, setIsSaving] = useState(false) - const [currencies, setCurrencies] = useState<Currency[]>([]) - const [loadingCurrencies, setLoadingCurrencies] = useState(true) - - // 폼 초기화 - const form = useForm<QuotationFormValues>({ - resolver: zodResolver(quotationFormSchema), - defaultValues: { - currency: quotation.currency || "USD", - totalPrice: quotation.totalPrice || "", - validUntil: quotation.validUntil || undefined, - remark: quotation.remark || "", - }, - }) - - // 통화 목록 로드 - useEffect(() => { - const loadCurrencies = async () => { - try { - const { data, error } = await fetchCurrencies() - if (error) { - toast.error("통화 목록을 불러오는데 실패했습니다") - return - } - setCurrencies(data || []) - } catch (error) { - console.error("Error loading currencies:", error) - toast.error("통화 목록을 불러오는데 실패했습니다") - } finally { - setLoadingCurrencies(false) - } - } - - loadCurrencies() - }, []) - - // 마감일 확인 - const isBeforeDueDate = () => { - if (!quotation.rfq.dueDate) return true - return new Date() <= new Date(quotation.rfq.dueDate) - } - - // 편집 가능 여부 확인 - const isEditable = () => { - return quotation.status === "Draft" || quotation.status === "Rejected" - } - - // 제출 가능 여부 확인 - const isSubmitReady = () => { - const values = form.getValues() - return values.currency && - values.totalPrice && - parseFloat(values.totalPrice) > 0 && - values.validUntil && - isBeforeDueDate() - } - - // 저장 핸들러 - const handleSave = async () => { - if (!isEditable()) { - toast.error("편집할 수 없는 상태입니다") - return - } - - setIsSaving(true) - try { - const values = form.getValues() - const { data, error } = await updateTechSalesVendorQuotation({ - id: quotation.id, - currency: values.currency, - totalPrice: values.totalPrice, - validUntil: values.validUntil, - remark: values.remark, - updatedBy: quotation.vendorId, // 임시로 vendorId 사용 - }) - - if (error) { - toast.error(error) - return - } - - toast.success("견적서가 저장되었습니다") - router.refresh() - } catch (error) { - console.error("Error saving quotation:", error) - toast.error("견적서 저장 중 오류가 발생했습니다") - } finally { - setIsSaving(false) - } - } - - // 제출 핸들러 - const handleSubmit = async () => { - if (!isEditable()) { - toast.error("제출할 수 없는 상태입니다") - return - } - - if (!isSubmitReady()) { - toast.error("필수 항목을 모두 입력해주세요") - return - } - - if (!isBeforeDueDate()) { - toast.error("마감일이 지났습니다") - return - } - - setIsSubmitting(true) - try { - const values = form.getValues() - const { data, error } = await submitTechSalesVendorQuotation({ - id: quotation.id, - currency: values.currency, - totalPrice: values.totalPrice, - validUntil: values.validUntil, - remark: values.remark, - updatedBy: quotation.vendorId, // 임시로 vendorId 사용 - }) - - if (error) { - toast.error(error) - return - } - - toast.success("견적서가 제출되었습니다") - router.push("/ko/partners/techsales/rfq-ship") - } catch (error) { - console.error("Error submitting quotation:", error) - toast.error("견적서 제출 중 오류가 발생했습니다") - } finally { - setIsSubmitting(false) - } - } - - // 상태 배지 - const getStatusBadge = (status: string) => { - const statusConfig = { - "Draft": { label: "초안", variant: "secondary" as const }, - "Submitted": { label: "제출됨", variant: "default" as const }, - "Revised": { label: "수정됨", variant: "outline" as const }, - "Rejected": { label: "반려됨", variant: "destructive" as const }, - "Accepted": { label: "승인됨", variant: "success" as const }, - } - - const config = statusConfig[status as keyof typeof statusConfig] || { - label: status, - variant: "secondary" as const - } - - return <Badge variant={config.variant}>{config.label}</Badge> - } - - return ( - <div className="container max-w-4xl mx-auto py-6 space-y-6"> - {/* 헤더 */} - <div className="flex items-center justify-between"> - <div className="flex items-center space-x-4"> - <Button - variant="ghost" - size="sm" - onClick={() => router.back()} - > - <ArrowLeft className="h-4 w-4 mr-2" /> - 뒤로가기 - </Button> - <div> - <h1 className="text-2xl font-bold">기술영업 견적서</h1> - <p className="text-muted-foreground"> - RFQ: {quotation.rfq.rfqCode} | {getStatusBadge(quotation.status)} - </p> - </div> - </div> - <div className="flex items-center space-x-2"> - {isEditable() && ( - <> - <Button - variant="outline" - onClick={handleSave} - disabled={isSaving} - > - <Save className="h-4 w-4 mr-2" /> - {isSaving ? "저장 중..." : "저장"} - </Button> - <Button - onClick={handleSubmit} - disabled={isSubmitting || !isSubmitReady()} - > - <Send className="h-4 w-4 mr-2" /> - {isSubmitting ? "제출 중..." : "제출"} - </Button> - </> - )} - </div> - </div> - - <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> - {/* 왼쪽: RFQ 정보 */} - <div className="lg:col-span-1 space-y-6"> - {/* RFQ 기본 정보 */} - <Card> - <CardHeader> - <CardTitle>RFQ 정보</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - <div> - <label className="text-sm font-medium text-muted-foreground">RFQ 번호</label> - <p className="font-mono">{quotation.rfq.rfqCode}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">자재 그룹</label> - <p>{quotation.rfq.materialCode || "N/A"}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">자재명</label> - <p>{quotation.rfq.item?.itemList || "N/A"}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">마감일</label> - <p className={!isBeforeDueDate() ? "text-red-600 font-medium" : ""}> - {quotation.rfq.dueDate ? formatDate(quotation.rfq.dueDate) : "N/A"} - </p> - </div> - {quotation.rfq.remark && ( - <div> - <label className="text-sm font-medium text-muted-foreground">비고</label> - <p className="text-sm">{quotation.rfq.remark}</p> - </div> - )} - </CardContent> - </Card> - - {/* 프로젝트 정보 */} - {quotation.rfq.projectSnapshot && ( - <Card> - <CardHeader> - <CardTitle>프로젝트 정보</CardTitle> - </CardHeader> - <CardContent className="space-y-3"> - <div> - <label className="text-sm font-medium text-muted-foreground">프로젝트 번호</label> - <p className="font-mono">{quotation.rfq.projectSnapshot.pspid}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">프로젝트명</label> - <p>{quotation.rfq.projectSnapshot.projNm || "N/A"}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">선종</label> - <p>{quotation.rfq.projectSnapshot.ptypeNm || "N/A"}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">척수</label> - <p>{quotation.rfq.projectSnapshot.projMsrm || "N/A"}</p> - </div> - <div> - <label className="text-sm font-medium text-muted-foreground">선주</label> - <p>{quotation.rfq.projectSnapshot.kunnrNm || "N/A"}</p> - </div> - </CardContent> - </Card> - )} - - {/* 시리즈 정보 */} - {quotation.rfq.seriesSnapshot && quotation.rfq.seriesSnapshot.length > 0 && ( - <Card> - <CardHeader> - <CardTitle>시리즈 일정</CardTitle> - </CardHeader> - <CardContent> - <div className="space-y-3"> - {quotation.rfq.seriesSnapshot.map((series, index) => ( - <div key={index} className="border rounded p-3"> - <div className="font-medium mb-2">시리즈 {series.sersNo}</div> - <div className="grid grid-cols-2 gap-2 text-sm"> - {series.klDt && ( - <div> - <span className="text-muted-foreground">K/L:</span> {formatDate(series.klDt)} - </div> - )} - {series.dlDt && ( - <div> - <span className="text-muted-foreground">인도:</span> {formatDate(series.dlDt)} - </div> - )} - </div> - </div> - ))} - </div> - </CardContent> - </Card> - )} - </div> - - {/* 오른쪽: 견적서 입력 폼 */} - <div className="lg:col-span-2"> - <Card> - <CardHeader> - <CardTitle>견적서 작성</CardTitle> - <CardDescription> - 총액 기반으로 견적을 작성해주세요. - </CardDescription> - </CardHeader> - <CardContent> - <Form {...form}> - <form className="space-y-6"> - {/* 통화 선택 */} - <FormField - control={form.control} - name="currency" - render={({ field }) => ( - <FormItem> - <FormLabel>통화 *</FormLabel> - <Select - onValueChange={field.onChange} - defaultValue={field.value} - disabled={!isEditable()} - > - <FormControl> - <SelectTrigger> - <SelectValue placeholder="통화를 선택하세요" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {loadingCurrencies ? ( - <div className="p-2"> - <Skeleton className="h-4 w-full" /> - </div> - ) : ( - currencies.map((currency) => ( - <SelectItem key={currency.code} value={currency.code}> - {currency.code} - {currency.name} - </SelectItem> - )) - )} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - {/* 총액 입력 */} - <FormField - control={form.control} - name="totalPrice" - render={({ field }) => ( - <FormItem> - <FormLabel>총액 *</FormLabel> - <FormControl> - <Input - type="number" - step="0.01" - placeholder="총액을 입력하세요" - disabled={!isEditable()} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 유효기간 */} - <FormField - control={form.control} - name="validUntil" - render={({ field }) => ( - <FormItem> - <FormLabel>견적 유효기간 *</FormLabel> - <FormControl> - <DatePicker - date={field.value} - onDateChange={field.onChange} - disabled={!isEditable()} - placeholder="유효기간을 선택하세요" - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 비고 */} - <FormField - control={form.control} - name="remark" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="추가 설명이나 특이사항을 입력하세요" - disabled={!isEditable()} - rows={4} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 반려 사유 (반려된 경우에만 표시) */} - {quotation.status === "Rejected" && quotation.rejectionReason && ( - <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> - <label className="text-sm font-medium text-red-800">반려 사유</label> - <p className="text-sm text-red-700 mt-1">{quotation.rejectionReason}</p> - </div> - )} - - {/* 제출 정보 */} - {quotation.submittedAt && ( - <div className="p-4 bg-blue-50 border border-blue-200 rounded-lg"> - <label className="text-sm font-medium text-blue-800">제출 정보</label> - <p className="text-sm text-blue-700 mt-1"> - 제출일: {formatDate(quotation.submittedAt)} - </p> - </div> - )} - </form> - </Form> - </CardContent> - </Card> - </div> - </div> - </div> - ) -}
\ No newline at end of file |
