From b67e36df49f067cbd5ba899f9fbcc755f38d4b4f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 4 Sep 2025 08:31:31 +0000 Subject: (대표님, 최겸, 임수민) 작업사항 커밋 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor/components/pr-items-pricing-table.tsx | 347 +++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 lib/bidding/vendor/components/pr-items-pricing-table.tsx (limited to 'lib/bidding/vendor/components/pr-items-pricing-table.tsx') diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx new file mode 100644 index 00000000..320ed6eb --- /dev/null +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -0,0 +1,347 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { Badge } from '@/components/ui/badge' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Package, + FileText, + Download, + Calculator +} from 'lucide-react' +import { formatDate } from '@/lib/utils' +import { downloadFile } from '@/lib/file-download' +import { getSpecDocumentsForPrItem } from '../../pre-quote/service' + +interface PrItem { + id: number + itemNumber: string | null + prNumber: string | null + itemInfo: string | null + materialDescription: string | null + quantity: string | null + quantityUnit: string | null + currency: string | null + requestedDeliveryDate: string | null + hasSpecDocument: boolean | null +} + +interface PrItemQuotation { + prItemId: number + bidUnitPrice: number + bidAmount: number + proposedDeliveryDate?: string + technicalSpecification?: string +} + +interface SpecDocument { + id: number + fileName: string + originalFileName: string + fileSize: number | null + filePath: string + title: string | null + description: string | null + uploadedAt: string +} + +interface PrItemsPricingTableProps { + prItems: PrItem[] + initialQuotations?: PrItemQuotation[] + currency?: string + onQuotationsChange: (quotations: PrItemQuotation[]) => void + onTotalAmountChange: (total: number) => void + readOnly?: boolean +} + +export function PrItemsPricingTable({ + prItems, + initialQuotations = [], + currency = 'KRW', + onQuotationsChange, + onTotalAmountChange, + readOnly = false +}: PrItemsPricingTableProps) { + const [quotations, setQuotations] = React.useState([]) + const [specDocuments, setSpecDocuments] = React.useState>({}) + const [loadingSpecs, setLoadingSpecs] = React.useState>({}) + + // 초기 견적 데이터 설정 + React.useEffect(() => { + const initQuotations = prItems.map(item => { + const existing = initialQuotations.find(q => q.prItemId === item.id) + if (existing) { + return existing + } + return { + prItemId: item.id, + bidUnitPrice: 0, + bidAmount: 0, + proposedDeliveryDate: '', + technicalSpecification: '' + } + }) + setQuotations(initQuotations) + }, [prItems, initialQuotations]) + + // SPEC 문서 로드 + const loadSpecDocuments = async (prItemId: number) => { + if (loadingSpecs[prItemId]) return + + setLoadingSpecs(prev => ({ ...prev, [prItemId]: true })) + try { + const docs = await getSpecDocumentsForPrItem(prItemId) + // Date를 string으로 변환 + const mappedDocs = docs.map(doc => ({ + ...doc, + uploadedAt: doc.uploadedAt.toString() + })) + setSpecDocuments(prev => ({ ...prev, [prItemId]: mappedDocs })) + } catch (error) { + console.error('Failed to load spec documents:', error) + } finally { + setLoadingSpecs(prev => ({ ...prev, [prItemId]: false })) + } + } + + // 견적 데이터 업데이트 + const updateQuotation = (prItemId: number, field: keyof PrItemQuotation, value: any) => { + const updatedQuotations = quotations.map(q => { + if (q.prItemId === prItemId) { + const updated = { ...q, [field]: value } + + // 단가나 수량이 변경되면 금액 자동 계산 + if (field === 'bidUnitPrice') { + const prItem = prItems.find(item => item.id === prItemId) + const quantity = parseFloat(prItem?.quantity || '1') + updated.bidAmount = updated.bidUnitPrice * quantity + } + + return updated + } + return q + }) + + setQuotations(updatedQuotations) + onQuotationsChange(updatedQuotations) + + // 총 금액 계산 + const totalAmount = updatedQuotations.reduce((sum, q) => sum + q.bidAmount, 0) + onTotalAmountChange(totalAmount) + } + + // 파일 다운로드 + const handleDownloadSpec = async (document: SpecDocument) => { + try { + await downloadFile(document.filePath, document.originalFileName, { + showToast: true + }) + } catch (error) { + console.error('Failed to download spec document:', error) + } + } + + // 통화 포맷팅 + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + }).format(amount) + } + + // 총 금액 계산 + const totalAmount = quotations.reduce((sum, q) => sum + q.bidAmount, 0) + + return ( + + + + + 품목별 견적 작성 + + + +
+
+ + + + 아이템번호 + PR번호 + 품목정보 + 자재내역 + 수량 + 단위 + 견적단가 + 견적금액 + 납품예정일 + 기술사양 + SPEC + + + + {prItems.map((item) => { + const quotation = quotations.find(q => q.prItemId === item.id) || { + prItemId: item.id, + bidUnitPrice: 0, + bidAmount: 0, + proposedDeliveryDate: '', + technicalSpecification: '' + } + + return ( + + + {item.itemNumber || '-'} + + {item.prNumber || '-'} + +
+ {item.itemInfo || '-'} +
+
+ +
+ {item.materialDescription || '-'} +
+
+ + {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} + + {item.quantityUnit || '-'} + + {readOnly ? ( + + {quotation.bidUnitPrice.toLocaleString()} + + ) : ( + updateQuotation( + item.id, + 'bidUnitPrice', + parseFloat(e.target.value) || 0 + )} + className="w-32 text-right" + placeholder="단가" + /> + )} + + +
+ {formatCurrency(quotation.bidAmount)} +
+
+ + {readOnly ? ( + quotation.proposedDeliveryDate ? + formatDate(quotation.proposedDeliveryDate, 'KR') : '-' + ) : ( + updateQuotation( + item.id, + 'proposedDeliveryDate', + e.target.value + )} + className="w-40" + /> + )} + + + {readOnly ? ( +
+ {quotation.technicalSpecification || '-'} +
+ ) : ( +