summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
-rw-r--r--lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx228
-rw-r--r--lib/techsales-rfq/vendor-response/quotation-editor.tsx559
-rw-r--r--lib/techsales-rfq/vendor-response/quotation-item-editor.tsx664
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx32
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx22
5 files changed, 222 insertions, 1283 deletions
diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
index 3449dcb6..20b2703c 100644
--- a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
+++ b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
-import { CalendarIcon, Save, Send, AlertCircle } from "lucide-react"
+import { CalendarIcon, Send, AlertCircle, Upload, X, FileText, Download } from "lucide-react"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
@@ -26,6 +26,13 @@ interface QuotationResponseTabProps {
currency: string | null
validUntil: Date | null
remark: string | null
+ quotationAttachments?: Array<{
+ id: number
+ fileName: string
+ fileSize: number
+ filePath: string
+ description?: string | null
+ }>
rfq: {
id: number
rfqCode: string | null
@@ -58,38 +65,93 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
)
const [remark, setRemark] = useState(quotation.remark || "")
const [isLoading, setIsLoading] = useState(false)
+ const [attachments, setAttachments] = useState<Array<{
+ id?: number
+ fileName: string
+ fileSize: number
+ filePath: string
+ isNew?: boolean
+ file?: File
+ }>>([])
+ const [isUploadingFiles, setIsUploadingFiles] = useState(false)
const router = useRouter()
+ // // 초기 첨부파일 데이터 로드
+ // useEffect(() => {
+ // if (quotation.quotationAttachments) {
+ // setAttachments(quotation.quotationAttachments.map(att => ({
+ // id: att.id,
+ // fileName: att.fileName,
+ // fileSize: att.fileSize,
+ // filePath: att.filePath,
+ // isNew: false
+ // })))
+ // }
+ // }, [quotation.quotationAttachments])
+
const rfq = quotation.rfq
const isDueDatePassed = rfq?.dueDate ? new Date(rfq.dueDate) < new Date() : false
- const canSubmit = quotation.status === "Draft" && !isDueDatePassed
- const canEdit = ["Draft", "Revised"].includes(quotation.status) && !isDueDatePassed
+ const canSubmit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
+ const canEdit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
+
+ // 파일 업로드 핸들러
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files
+ if (!files) return
+
+ Array.from(files).forEach(file => {
+ setAttachments(prev => [
+ ...prev,
+ {
+ fileName: file.name,
+ fileSize: file.size,
+ filePath: '',
+ isNew: true,
+ file
+ }
+ ])
+ })
+ }
+
+ // 첨부파일 제거
+ const removeAttachment = (index: number) => {
+ setAttachments(prev => prev.filter((_, i) => i !== index))
+ }
+
+ // 파일 업로드 함수
+ const uploadFiles = async () => {
+ const newFiles = attachments.filter(att => att.isNew && att.file)
+ if (newFiles.length === 0) return []
+
+ setIsUploadingFiles(true)
+ const uploadedFiles = []
- const handleSaveDraft = async () => {
- setIsLoading(true)
try {
- const { updateTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service")
-
- const result = await updateTechSalesVendorQuotation({
- id: quotation.id,
- currency,
- totalPrice,
- validUntil: validUntil!,
- remark,
- updatedBy: 1 // TODO: 실제 사용자 ID로 변경
- })
+ for (const attachment of newFiles) {
+ const formData = new FormData()
+ formData.append('file', attachment.file!)
+
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData
+ })
- if (result.error) {
- toast.error(result.error)
- } else {
- toast.success("임시 저장되었습니다.")
- // 페이지 새로고침 대신 router.refresh() 사용
- router.refresh()
+ if (!response.ok) throw new Error('파일 업로드 실패')
+
+ const result = await response.json()
+ uploadedFiles.push({
+ fileName: result.fileName,
+ filePath: result.url,
+ fileSize: attachment.fileSize
+ })
}
- } catch {
- toast.error("저장 중 오류가 발생했습니다.")
+ return uploadedFiles
+ } catch (error) {
+ console.error('파일 업로드 오류:', error)
+ toast.error('파일 업로드 중 오류가 발생했습니다.')
+ return []
} finally {
- setIsLoading(false)
+ setIsUploadingFiles(false)
}
}
@@ -101,6 +163,9 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
setIsLoading(true)
try {
+ // 파일 업로드 먼저 처리
+ const uploadedFiles = await uploadFiles()
+
const { submitTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service")
const result = await submitTechSalesVendorQuotation({
@@ -109,6 +174,7 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
totalPrice,
validUntil: validUntil!,
remark,
+ attachments: uploadedFiles,
updatedBy: 1 // TODO: 실제 사용자 ID로 변경
})
@@ -116,8 +182,10 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
toast.error(result.error)
} else {
toast.success("견적서가 제출되었습니다.")
- // 페이지 새로고침 대신 router.refresh() 사용
- router.refresh()
+ // // 페이지 새로고침 대신 router.refresh() 사용
+ // router.refresh()
+ // 페이지 새로고침
+ window.location.reload()
}
} catch {
toast.error("제출 중 오류가 발생했습니다.")
@@ -312,28 +380,98 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
/>
</div>
+ {/* 첨부파일 */}
+ <div className="space-y-4">
+ <Label>첨부파일</Label>
+
+ {/* 파일 업로드 버튼 */}
+ {canEdit && (
+ <div className="flex items-center gap-2">
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ disabled={isUploadingFiles}
+ onClick={() => document.getElementById('file-input')?.click()}
+ >
+ <Upload className="h-4 w-4 mr-2" />
+ 파일 선택
+ </Button>
+ <input
+ id="file-input"
+ type="file"
+ multiple
+ onChange={handleFileSelect}
+ className="hidden"
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.zip"
+ />
+ <span className="text-sm text-muted-foreground">
+ PDF, 문서파일, 이미지파일, 압축파일 등
+ </span>
+ </div>
+ )}
+
+ {/* 첨부파일 목록 */}
+ {attachments.length > 0 && (
+ <div className="space-y-2">
+ {attachments.map((attachment, index) => (
+ <div
+ key={index}
+ className="flex items-center justify-between p-3 border rounded-lg bg-muted/50"
+ >
+ <div className="flex items-center gap-2">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <div className="text-sm font-medium">{attachment.fileName}</div>
+ <div className="text-xs text-muted-foreground">
+ {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB
+ {attachment.isNew && (
+ <Badge variant="secondary" className="ml-2">
+ 새 파일
+ </Badge>
+ )}
+ </div>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ {!attachment.isNew && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => window.open(attachment.filePath, '_blank')}
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ )}
+ {canEdit && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeAttachment(index)}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+
{/* 액션 버튼 */}
- {canEdit && (
- <div className="flex gap-2 pt-4">
+ {canEdit && canSubmit && (
+ <div className="flex justify-center pt-4">
<Button
- variant="outline"
- onClick={handleSaveDraft}
- disabled={isLoading}
- className="flex-1"
+ onClick={handleSubmit}
+ disabled={isLoading || !totalPrice || !currency || !validUntil}
+ className="w-full "
>
- <Save className="mr-2 h-4 w-4" />
- 임시 저장
+ <Send className="mr-2 h-4 w-4" />
+ 견적서 제출
</Button>
- {canSubmit && (
- <Button
- onClick={handleSubmit}
- disabled={isLoading || !totalPrice || !currency || !validUntil}
- className="flex-1"
- >
- <Send className="mr-2 h-4 w-4" />
- 견적서 제출
- </Button>
- )}
</div>
)}
</CardContent>
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
diff --git a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx
deleted file mode 100644
index 92bec96a..00000000
--- a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx
+++ /dev/null
@@ -1,664 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useState, useEffect, useRef } from "react"
-import { toast } from "sonner"
-import { format } from "date-fns"
-
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Checkbox } from "@/components/ui/checkbox"
-import { DatePicker } from "@/components/ui/date-picker"
-import {
- Table,
- TableBody,
- TableCaption,
- TableCell,
- TableHead,
- TableHeader,
- TableRow
-} from "@/components/ui/table"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger
-} from "@/components/ui/tooltip"
-import {
- Info,
- Clock,
- CalendarIcon,
- ClipboardCheck,
- AlertTriangle,
- CheckCircle2,
- RefreshCw,
- Save,
- FileText,
- Sparkles
-} from "lucide-react"
-
-import { formatCurrency } from "@/lib/utils"
-import { updateQuotationItem } from "../services"
-import { Textarea } from "@/components/ui/textarea"
-
-// 견적 아이템 타입
-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 // 남겨두지만 UI에서는 사용하지 않음
- createdAt: Date
- updatedAt: Date
- prItem?: {
- id: number
- materialCode: string | null
- materialDescription: string | null
- // 기타 필요한 정보
- }
-}
-
-// debounce 함수 구현
-function debounce<T extends (...args: any[]) => any>(
- func: T,
- wait: number
-): (...args: Parameters<T>) => void {
- let timeout: NodeJS.Timeout | null = null;
-
- return function (...args: Parameters<T>) {
- if (timeout) clearTimeout(timeout);
- timeout = setTimeout(() => func(...args), wait);
- };
-}
-
-interface QuotationItemEditorProps {
- items: QuotationItem[]
- onItemsChange: (items: QuotationItem[]) => void
- disabled?: boolean
- currency: string
-}
-
-export function QuotationItemEditor({
- items,
- onItemsChange,
- disabled = false,
- currency
-}: QuotationItemEditorProps) {
- const [editingItem, setEditingItem] = useState<number | null>(null)
- const [isSaving, setIsSaving] = useState(false)
-
- // 저장이 필요한 항목들을 추적
- const [pendingChanges, setPendingChanges] = useState<Set<number>>(new Set())
-
- // 로컬 상태 업데이트 함수 - 화면에 즉시 반영하지만 서버에는 즉시 저장하지 않음
- const updateLocalItem = <K extends keyof QuotationItem>(
- index: number,
- field: K,
- value: QuotationItem[K]
- ) => {
- // 로컬 상태 업데이트
- const updatedItems = [...items]
- const item = { ...updatedItems[index] }
-
- // 필드 업데이트
- item[field] = value
-
- // 대체품 체크 해제 시 관련 필드 초기화
- if (field === 'isAlternative' && value === false) {
- item.vendorMaterialCode = null;
- item.vendorMaterialDescription = null;
- item.remark = null;
- }
-
- // 단가나 수량이 변경되면 총액 계산
- if (field === 'unitPrice' || field === 'quantity') {
- item.totalPrice = Number(item.unitPrice) * Number(item.quantity)
-
- // 세금이 있으면 세액 계산
- if (item.taxRate) {
- item.taxAmount = item.totalPrice * (item.taxRate / 100)
- }
-
- // 할인이 있으면 할인액 계산
- if (item.discountRate) {
- item.discountAmount = item.totalPrice * (item.discountRate / 100)
- }
- }
-
- // 세율이 변경되면 세액 계산
- if (field === 'taxRate') {
- item.taxAmount = item.totalPrice * (value as number / 100)
- }
-
- // 할인율이 변경되면 할인액 계산
- if (field === 'discountRate') {
- item.discountAmount = item.totalPrice * (value as number / 100)
- }
-
- // 변경된 아이템으로 교체
- updatedItems[index] = item
-
- // 미저장 항목으로 표시
- setPendingChanges(prev => new Set(prev).add(item.id))
-
- // 부모 컴포넌트에 변경 사항 알림
- onItemsChange(updatedItems)
-
- // 저장 필요함을 표시
- return item
- }
-
- // 서버에 저장하는 함수
- const saveItemToServer = async (item: QuotationItem, field: keyof QuotationItem, value: any) => {
- if (disabled) return
-
- try {
- setIsSaving(true)
-
- const result = await updateQuotationItem({
- id: item.id,
- [field]: value,
- totalPrice: item.totalPrice,
- taxAmount: item.taxAmount ?? 0,
- discountAmount: item.discountAmount ?? 0
- })
-
- // 저장 완료 후 pendingChanges에서 제거
- setPendingChanges(prev => {
- const newSet = new Set(prev)
- newSet.delete(item.id)
- return newSet
- })
-
- if (!result.success) {
- toast.error(result.message || "항목 저장 중 오류가 발생했습니다")
- }
- } catch (error) {
- console.error("항목 저장 오류:", error)
- toast.error("항목 저장 중 오류가 발생했습니다")
- } finally {
- setIsSaving(false)
- }
- }
-
- // debounce된 저장 함수
- const debouncedSave = useRef(debounce(
- (item: QuotationItem, field: keyof QuotationItem, value: any) => {
- saveItemToServer(item, field, value)
- },
- 800 // 800ms 지연
- )).current
-
- // 견적 항목 업데이트 함수
- const handleItemUpdate = (index: number, field: keyof QuotationItem, value: any) => {
- const updatedItem = updateLocalItem(index, field, value)
-
- // debounce를 통해 서버 저장 지연
- if (!disabled) {
- debouncedSave(updatedItem, field, value)
- }
- }
-
- // 모든 변경 사항 저장
- const saveAllChanges = async () => {
- if (disabled || pendingChanges.size === 0) return
-
- setIsSaving(true)
- toast.info(`${pendingChanges.size}개 항목 저장 중...`)
-
- try {
- // 변경된 모든 항목 저장
- for (const itemId of pendingChanges) {
- const index = items.findIndex(item => item.id === itemId)
- if (index !== -1) {
- const item = items[index]
- await updateQuotationItem({
- id: item.id,
- unitPrice: item.unitPrice,
- totalPrice: item.totalPrice,
- taxRate: item.taxRate ?? 0,
- taxAmount: item.taxAmount ?? 0,
- discountRate: item.discountRate ?? 0,
- discountAmount: item.discountAmount ?? 0,
- deliveryDate: item.deliveryDate,
- leadTimeInDays: item.leadTimeInDays ?? 0,
- vendorMaterialCode: item.vendorMaterialCode ?? "",
- vendorMaterialDescription: item.vendorMaterialDescription ?? "",
- isAlternative: item.isAlternative,
- isRecommended: false, // 항상 false로 설정 (사용하지 않음)
- remark: item.remark ?? ""
- })
- }
- }
-
- // 모든 변경 사항 저장 완료
- setPendingChanges(new Set())
- toast.success("모든 변경 사항이 저장되었습니다")
- } catch (error) {
- console.error("변경 사항 저장 오류:", error)
- toast.error("변경 사항 저장 중 오류가 발생했습니다")
- } finally {
- setIsSaving(false)
- }
- }
-
- // blur 이벤트로 저장 트리거 (사용자가 입력 완료 후)
- const handleBlur = (index: number, field: keyof QuotationItem, value: any) => {
- const itemId = items[index].id
-
- // 해당 항목이 pendingChanges에 있다면 즉시 저장
- if (pendingChanges.has(itemId)) {
- const item = items[index]
- saveItemToServer(item, field, value)
- }
- }
-
- // 전체 단가 업데이트 (일괄 반영)
- const handleBulkUnitPriceUpdate = () => {
- if (items.length === 0) return
-
- // 첫 번째 아이템의 단가 가져오기
- const firstUnitPrice = items[0].unitPrice
-
- if (!firstUnitPrice) {
- toast.error("첫 번째 항목의 단가를 먼저 입력해주세요")
- return
- }
-
- // 모든 아이템에 동일한 단가 적용
- const updatedItems = items.map(item => ({
- ...item,
- unitPrice: firstUnitPrice,
- totalPrice: firstUnitPrice * item.quantity,
- taxAmount: item.taxRate ? (firstUnitPrice * item.quantity) * (item.taxRate / 100) : item.taxAmount,
- discountAmount: item.discountRate ? (firstUnitPrice * item.quantity) * (item.discountRate / 100) : item.discountAmount
- }))
-
- // 모든 아이템을 변경 필요 항목으로 표시
- setPendingChanges(new Set(updatedItems.map(item => item.id)))
-
- // 부모 컴포넌트에 변경 사항 알림
- onItemsChange(updatedItems)
-
- toast.info("모든 항목의 단가가 업데이트되었습니다. 변경 사항을 저장하려면 '저장' 버튼을 클릭하세요.")
- }
-
- // 입력 핸들러
- const handleNumberInputChange = (
- index: number,
- field: keyof QuotationItem,
- e: React.ChangeEvent<HTMLInputElement>
- ) => {
- const value = e.target.value === '' ? 0 : parseFloat(e.target.value)
- handleItemUpdate(index, field, value)
- }
-
- const handleTextInputChange = (
- index: number,
- field: keyof QuotationItem,
- e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
- ) => {
- handleItemUpdate(index, field, e.target.value)
- }
-
- const handleDateChange = (
- index: number,
- field: keyof QuotationItem,
- date: Date | undefined
- ) => {
- handleItemUpdate(index, field, date || null)
- }
-
- const handleCheckboxChange = (
- index: number,
- field: keyof QuotationItem,
- checked: boolean
- ) => {
- handleItemUpdate(index, field, checked)
- }
-
- // 날짜 형식 지정
- const formatDeliveryDate = (date: Date | null) => {
- if (!date) return "-"
- return format(date, "yyyy-MM-dd")
- }
-
- // 입력 폼 필드 렌더링
- const renderInputField = (item: QuotationItem, index: number, field: keyof QuotationItem) => {
- if (field === 'unitPrice' || field === 'taxRate' || field === 'discountRate' || field === 'leadTimeInDays') {
- return (
- <Input
- type="number"
- min={0}
- step={field === 'unitPrice' ? 0.01 : field === 'taxRate' || field === 'discountRate' ? 0.1 : 1}
- value={item[field] as number || 0}
- onChange={(e) => handleNumberInputChange(index, field, e)}
- onBlur={(e) => handleBlur(index, field, parseFloat(e.target.value) || 0)}
- disabled={disabled || isSaving}
- className="w-full"
- />
- )
- } else if (field === 'vendorMaterialCode' || field === 'vendorMaterialDescription') {
- return (
- <Input
- type="text"
- value={item[field] as string || ''}
- onChange={(e) => handleTextInputChange(index, field, e)}
- onBlur={(e) => handleBlur(index, field, e.target.value)}
- disabled={disabled || isSaving || !item.isAlternative}
- className="w-full"
- placeholder={field === 'vendorMaterialCode' ? "벤더 자재그룹" : "벤더 자재명"}
- />
- )
- } else if (field === 'deliveryDate') {
- return (
- <DatePicker
- date={item.deliveryDate ? new Date(item.deliveryDate) : undefined}
- onSelect={(date) => {
- handleDateChange(index, field, date);
- // DatePicker는 blur 이벤트가 없으므로 즉시 저장 트리거
- if (date) handleBlur(index, field, date);
- }}
- disabled={disabled || isSaving}
- />
- )
- } else if (field === 'isAlternative') {
- return (
- <div className="flex items-center gap-1">
- <Checkbox
- checked={item.isAlternative}
- onCheckedChange={(checked) => {
- handleCheckboxChange(index, field, checked as boolean);
- handleBlur(index, field, checked as boolean);
- }}
- disabled={disabled || isSaving}
- />
- <span className="text-xs">대체품</span>
- </div>
- )
- }
-
- return null
- }
-
- // 대체품 필드 렌더링
- const renderAlternativeFields = (item: QuotationItem, index: number) => {
- if (!item.isAlternative) return null;
-
- return (
- <div className="mt-2 p-3 bg-blue-50 rounded-md space-y-2 text-sm">
- {/* <div className="flex flex-col gap-2">
- <label className="text-xs font-medium text-blue-700">벤더 자재그룹</label>
- <Input
- value={item.vendorMaterialCode || ""}
- onChange={(e) => handleTextInputChange(index, 'vendorMaterialCode', e)}
- onBlur={(e) => handleBlur(index, 'vendorMaterialCode', e.target.value)}
- disabled={disabled || isSaving}
- className="h-8 text-sm"
- placeholder="벤더 자재그룹 입력"
- />
- </div> */}
-
- <div className="flex flex-col gap-2">
- <label className="text-xs font-medium text-blue-700">벤더 자재명</label>
- <Input
- value={item.vendorMaterialDescription || ""}
- onChange={(e) => handleTextInputChange(index, 'vendorMaterialDescription', e)}
- onBlur={(e) => handleBlur(index, 'vendorMaterialDescription', e.target.value)}
- disabled={disabled || isSaving}
- className="h-8 text-sm"
- placeholder="벤더 자재명 입력"
- />
- </div>
-
- <div className="flex flex-col gap-2">
- <label className="text-xs font-medium text-blue-700">대체품 설명</label>
- <Textarea
- value={item.remark || ""}
- onChange={(e) => handleTextInputChange(index, 'remark', e)}
- onBlur={(e) => handleBlur(index, 'remark', e.target.value)}
- disabled={disabled || isSaving}
- className="min-h-[60px] text-sm"
- placeholder="원본과의 차이점, 대체 사유, 장점 등을 설명해주세요"
- />
- </div>
- </div>
- );
- };
-
- // 항목의 저장 상태 아이콘 표시
- const renderSaveStatus = (itemId: number) => {
- if (pendingChanges.has(itemId)) {
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger>
- <RefreshCw className="h-4 w-4 text-yellow-500 animate-spin" />
- </TooltipTrigger>
- <TooltipContent>
- <p>저장되지 않은 변경 사항이 있습니다</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )
- }
-
- return null
- }
-
- return (
- <div className="space-y-4">
- <div className="flex justify-between items-center">
- <div className="flex items-center gap-2">
- <h3 className="text-lg font-medium">항목 목록 ({items.length}개)</h3>
- {pendingChanges.size > 0 && (
- <Badge variant="outline" className="bg-yellow-50">
- 변경 {pendingChanges.size}개
- </Badge>
- )}
- </div>
-
- <div className="flex items-center gap-2">
- {pendingChanges.size > 0 && !disabled && (
- <Button
- variant="default"
- size="sm"
- onClick={saveAllChanges}
- disabled={isSaving}
- >
- {isSaving ? (
- <RefreshCw className="h-4 w-4 mr-2 animate-spin" />
- ) : (
- <Save className="h-4 w-4 mr-2" />
- )}
- 변경사항 저장 ({pendingChanges.size}개)
- </Button>
- )}
-
- {!disabled && (
- <Button
- variant="outline"
- size="sm"
- onClick={handleBulkUnitPriceUpdate}
- disabled={items.length === 0 || isSaving}
- >
- 첫 항목 단가로 일괄 적용
- </Button>
- )}
- </div>
- </div>
-
- <ScrollArea className="h-[500px] rounded-md border">
- <Table>
- <TableHeader className="sticky top-0 bg-background">
- <TableRow>
- <TableHead className="w-[50px]">번호</TableHead>
- <TableHead>자재그룹</TableHead>
- <TableHead>자재명</TableHead>
- <TableHead>수량</TableHead>
- <TableHead>단위</TableHead>
- <TableHead>단가</TableHead>
- <TableHead>금액</TableHead>
- <TableHead>
- <div className="flex items-center gap-1">
- 세율(%)
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger>
- <Info className="h-4 w-4" />
- </TooltipTrigger>
- <TooltipContent>
- <p>세율을 입력하면 자동으로 세액이 계산됩니다.</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- </TableHead>
- <TableHead>
- <div className="flex items-center gap-1">
- 납품일
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger>
- <Info className="h-4 w-4" />
- </TooltipTrigger>
- <TooltipContent>
- <p>납품 가능한 날짜를 선택해주세요.</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- </TableHead>
- <TableHead>리드타임(일)</TableHead>
- <TableHead>
- <div className="flex items-center gap-1">
- 대체품
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger>
- <Info className="h-4 w-4" />
- </TooltipTrigger>
- <TooltipContent>
- <p>요청된 제품의 대체품을 제안할 경우 선택하세요.</p>
- <p>대체품을 선택하면 추가 정보를 입력할 수 있습니다.</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- </TableHead>
- <TableHead className="w-[50px]">상태</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {items.length === 0 ? (
- <TableRow>
- <TableCell colSpan={12} className="text-center py-10">
- 견적 항목이 없습니다
- </TableCell>
- </TableRow>
- ) : (
- items.map((item, index) => (
- <React.Fragment key={item.id}>
- <TableRow className={pendingChanges.has(item.id) ? "bg-yellow-50/30" : ""}>
- <TableCell>
- {index + 1}
- </TableCell>
- <TableCell>
- {item.materialCode || "-"}
- </TableCell>
- <TableCell>
- <div className="font-medium max-w-xs truncate">
- {item.materialDescription || "-"}
- </div>
- </TableCell>
- <TableCell>
- {item.quantity}
- </TableCell>
- <TableCell>
- {item.uom || "-"}
- </TableCell>
- <TableCell>
- {renderInputField(item, index, 'unitPrice')}
- </TableCell>
- <TableCell>
- {formatCurrency(item.totalPrice, currency)}
- </TableCell>
- <TableCell>
- {renderInputField(item, index, 'taxRate')}
- </TableCell>
- <TableCell>
- {renderInputField(item, index, 'deliveryDate')}
- </TableCell>
- <TableCell>
- {renderInputField(item, index, 'leadTimeInDays')}
- </TableCell>
- <TableCell>
- {renderInputField(item, index, 'isAlternative')}
- </TableCell>
- <TableCell>
- {renderSaveStatus(item.id)}
- </TableCell>
- </TableRow>
-
- {/* 대체품으로 선택된 경우 추가 정보 행 표시 */}
- {item.isAlternative && (
- <TableRow className={pendingChanges.has(item.id) ? "bg-blue-50/40" : "bg-blue-50/20"}>
- <TableCell colSpan={1}></TableCell>
- <TableCell colSpan={10}>
- {renderAlternativeFields(item, index)}
- </TableCell>
- <TableCell colSpan={1}></TableCell>
- </TableRow>
- )}
- </React.Fragment>
- ))
- )}
- </TableBody>
- </Table>
- </ScrollArea>
-
- {isSaving && (
- <div className="flex items-center justify-center text-sm text-muted-foreground">
- <Clock className="h-4 w-4 animate-spin mr-2" />
- 변경 사항을 저장 중입니다...
- </div>
- )}
-
- <div className="bg-muted p-4 rounded-md">
- <h4 className="text-sm font-medium mb-2">안내 사항</h4>
- <ul className="text-sm space-y-1 text-muted-foreground">
- <li className="flex items-start gap-2">
- <AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
- <span>단가와 납품일은 필수로 입력해야 합니다.</span>
- </li>
- <li className="flex items-start gap-2">
- <ClipboardCheck className="h-4 w-4 mt-0.5 flex-shrink-0" />
- <span>입력 후 다른 필드로 이동하면 자동으로 저장됩니다. 여러 항목을 변경한 후 '저장' 버튼을 사용할 수도 있습니다.</span>
- </li>
- <li className="flex items-start gap-2">
- <FileText className="h-4 w-4 mt-0.5 flex-shrink-0" />
- <span><strong>대체품</strong>으로 제안하는 경우 자재명, 대체품 설명을 입력해주세요.</span>
- </li>
- </ul>
- </div>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
index b89f8953..39de94ed 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
@@ -30,7 +30,6 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations {
// 아이템 정보
itemName?: string;
-
itemCount?: number;
// 프로젝트 정보
@@ -38,6 +37,9 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations {
pspid?: string;
sector?: string;
+ // RFQ 정보
+ description?: string;
+
// 벤더 정보
vendorName?: string;
vendorCode?: string;
@@ -194,6 +196,33 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
// enableHiding: true,
// },
{
+ accessorKey: "description",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="RFQ title" />
+ ),
+ cell: ({ row }) => {
+ const description = row.getValue("description") as string;
+ return (
+ <div className="min-w-48 max-w-64">
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <span className="truncate block text-sm">
+ {description || "N/A"}
+ </span>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="max-w-xs">{description || "N/A"}</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </div>
+ );
+ },
+ enableSorting: true,
+ enableHiding: true,
+ },
+ {
accessorKey: "projNm",
header: ({ column }) => (
<DataTableColumnHeaderSimple column={column} title="프로젝트명" />
@@ -313,7 +342,6 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
cell: ({ row }) => {
const quotation = row.original
const attachmentCount = quotation.attachmentCount || 0
-
const handleClick = () => {
openAttachmentsSheet(quotation.rfqId)
}
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
index 5e5d4f39..4c5cdf8e 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
@@ -38,12 +38,15 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations {
rfqStatus?: string;
itemName?: string | null;
projNm?: string | null;
- quotationCode?: string | null;
-
- rejectionReason?: string | null;
- acceptedAt?: Date | null;
+ description?: string | null;
attachmentCount?: number;
itemCount?: number;
+ pspid?: string | null;
+ sector?: string | null;
+ vendorName?: string | null;
+ vendorCode?: string | null;
+ createdByName?: string | null;
+ updatedByName?: string | null;
}
interface VendorQuotationsTableProps {
@@ -380,7 +383,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
// useDataTable 훅 사용
const { table } = useDataTable({
data: stableData,
- columns,
+ columns: columns as any,
pageCount,
rowCount: total,
filterFields,
@@ -391,7 +394,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
enableRowSelection: true, // 행 선택 활성화
initialState: {
sorting: initialSettings.sort,
- columnPinning: { right: ["actions"] },
+ columnPinning: { right: ["actions", "items", "attachments"] },
},
getRowId: (originalRow) => String(originalRow.id),
shallow: false,
@@ -417,13 +420,6 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
<div className="w-full">
<div className="overflow-x-auto">
<div className="relative">
- {/* 로딩 오버레이 (재로딩 시) */}
- {/* {!isInitialLoad && isLoading && (
- <div className="absolute h-full w-full inset-0 bg-background/90 backdrop-blur-md z-10 flex items-center justify-center">
- <CenterLoadingIndicator />
- </div>
- )} */}
-
<DataTable
table={table}
className="min-w-full"