'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 { Badge } from '@/components/ui/badge' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Package, Download, Calculator } from 'lucide-react' import { formatDate } from '@/lib/utils' import { downloadFile, formatFileSize, getFileInfo } from '@/lib/file-download' import { getSpecDocumentsForPrItem } from '../../pre-quote/service' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' interface PrItem { id: number biddingId: number itemNumber: string | null projectId: number | null projectInfo: string | null itemInfo: string | null shi: string | null materialGroupNumber: string | null materialGroupInfo: string | null materialNumber: string | null materialInfo: string | null requestedDeliveryDate: Date | string | null annualUnitPrice: string | null currency: string | null quantity: string | null quantityUnit: string | null totalWeight: string | null weightUnit: string | null priceUnit: string | null purchaseUnit: string | null materialWeight: string | null prNumber: string | null hasSpecDocument: boolean | null specification: string | null bidUnitPrice?: string | number | null bidAmount?: string | number | null proposedDeliveryDate?: string | Date | null technicalSpecification?: string | 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 } // 파일 다운로드 훅 const useFileDownload = () => { const [downloadingFiles, setDownloadingFiles] = React.useState>(new Set()) const handleDownload = async (filePath: string, fileName: string, options?: { action?: 'download' | 'preview' }) => { const fileKey = `${filePath}_${fileName}` if (downloadingFiles.has(fileKey)) return setDownloadingFiles(prev => new Set(prev).add(fileKey)) try { await downloadFile(filePath, fileName, { action: options?.action || 'download', showToast: true, showSuccessToast: true, onError: (error) => { console.error("파일 다운로드 실패:", error) }, onSuccess: (fileName, fileSize) => { console.log("파일 다운로드 성공:", fileName, fileSize ? formatFileSize(fileSize) : '') } }) } catch (error) { console.error("다운로드 처리 중 오류:", error) } finally { setDownloadingFiles(prev => { const newSet = new Set(prev) newSet.delete(fileKey) return newSet }) } } return { handleDownload, downloadingFiles } } // 파일 다운로드 링크 컴포넌트 interface FileDownloadLinkProps { filePath: string fileName: string fileSize?: number | null title?: string | null className?: string } const FileDownloadLink: React.FC = ({ filePath, fileName, fileSize, title, className = "" }) => { const { handleDownload, downloadingFiles } = useFileDownload() const fileInfo = getFileInfo(fileName) const fileKey = `${filePath}_${fileName}` const isDownloading = downloadingFiles.has(fileKey) return (
{fileName}
{fileSize &&
{formatFileSize(fileSize)}
}
클릭하여 다운로드
) } 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>({}) // 초기 견적 데이터 설정 및 SPEC 문서 로드 React.useEffect(() => { const initQuotations = prItems.map(item => { const existing = initialQuotations.find(q => q.prItemId === item.id) if (existing) { return existing } // prItems 자체에 견적 정보가 있는 경우 활용 if (item.bidUnitPrice !== undefined || item.bidAmount !== undefined) { return { prItemId: item.id, bidUnitPrice: item.bidUnitPrice ? Number(item.bidUnitPrice) : 0, bidAmount: item.bidAmount ? Number(item.bidAmount) : 0, proposedDeliveryDate: item.proposedDeliveryDate ? (item.proposedDeliveryDate instanceof Date ? item.proposedDeliveryDate.toISOString().split('T')[0] : String(item.proposedDeliveryDate)) : '', technicalSpecification: item.technicalSpecification || '' } } return { prItemId: item.id, bidUnitPrice: 0, bidAmount: 0, proposedDeliveryDate: '', technicalSpecification: '' } }) setQuotations(initQuotations) // SPEC 문서가 있는 모든 PR 아이템의 문서를 미리 로드 const loadAllSpecDocuments = async () => { const itemsWithSpecs = prItems.filter(item => item.hasSpecDocument) console.log('Loading spec documents for items:', itemsWithSpecs.map(item => ({ id: item.id, itemNumber: item.itemNumber }))) for (const item of itemsWithSpecs) { try { console.log('Loading spec documents for prItemId:', item.id) const docs = await getSpecDocumentsForPrItem(item.id) console.log('Loaded spec documents for item', item.id, ':', docs) // Date를 string으로 변환 const mappedDocs = docs.map(doc => ({ ...doc, uploadedAt: doc.uploadedAt.toString() })) setSpecDocuments(prev => ({ ...prev, [item.id]: mappedDocs })) } catch (error) { console.error('Failed to load spec documents for item', item.id, ':', error) } } } loadAllSpecDocuments() }, [prItems, initialQuotations]) // 견적 데이터 업데이트 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) let multiplier = 1 if (prItem?.quantity && parseFloat(prItem.quantity) > 0) { // 수량이 있으면 수량 기준 multiplier = parseFloat(prItem.quantity) } else if (prItem?.totalWeight && parseFloat(prItem.totalWeight) > 0) { // 수량이 없으면 중량 기준 multiplier = parseFloat(prItem.totalWeight) } updated.bidAmount = updated.bidUnitPrice * multiplier } return updated } return q }) setQuotations(updatedQuotations) onQuotationsChange(updatedQuotations) // 총 금액 계산 const totalAmount = updatedQuotations.reduce((sum, q) => sum + q.bidAmount, 0) onTotalAmountChange(totalAmount) } // 통화 포맷팅 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 ( 품목별 입찰 작성
자재번호 자재명 SHI 납품예정일 업체 납품예정일 수량 구매단위 가격단위 구매단위 총중량 중량단위 입찰단가 입찰금액 업체 통화 자재내역상세 스팩 P/R번호 {prItems.map((item) => { const quotation = quotations.find(q => q.prItemId === item.id) || { prItemId: item.id, bidUnitPrice: 0, bidAmount: 0, proposedDeliveryDate: '', technicalSpecification: '' } return ( {item.materialNumber || '-'}
{item.materialInfo || '-'}
{item.requestedDeliveryDate ? formatDate(new Date(item.requestedDeliveryDate), 'KR') : '-' } {readOnly ? ( quotation.proposedDeliveryDate ? formatDate(quotation.proposedDeliveryDate, 'KR') : '-' ) : ( updateQuotation( item.id, 'proposedDeliveryDate', e.target.value )} className="w-40" /> )} {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} {item.quantityUnit || '-'} {item.priceUnit || '-'} {item.purchaseUnit || '-'} {item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'} {item.weightUnit || '-'} {readOnly ? ( {quotation.bidUnitPrice.toLocaleString()} ) : ( { let value = e.target.value if (/^0[0-9]+/.test(value)) { value = value.replace(/^0+/, '') if (value === '') value = '0' } const numericValue = parseFloat(value) updateQuotation( item.id, 'bidUnitPrice', isNaN(numericValue) ? 0 : numericValue ) }} className="w-32 text-right" placeholder="단가" /> )}
{formatCurrency(quotation.bidAmount)}
{currency}
{item.specification || '-'}
{item.hasSpecDocument ? (
{specDocuments[item.id] && specDocuments[item.id].length > 0 ? (
{specDocuments[item.id].map((doc) => (
))}
) : (
문서 없음 로딩 중...
)}
) : ( SPEC 없음 )}
{item.prNumber || '-'}
) })}
{/* 총 금액 표시 */}
{formatCurrency(totalAmount)}
) }