summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response/quotation-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/vendor-response/quotation-editor.tsx')
-rw-r--r--lib/techsales-rfq/vendor-response/quotation-editor.tsx559
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