diff options
Diffstat (limited to 'lib/bidding/selection')
| -rw-r--r-- | lib/bidding/selection/actions.ts | 118 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-info-card.tsx | 14 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-item-table.tsx | 71 |
3 files changed, 109 insertions, 94 deletions
diff --git a/lib/bidding/selection/actions.ts b/lib/bidding/selection/actions.ts index 91550960..06dcbea1 100644 --- a/lib/bidding/selection/actions.ts +++ b/lib/bidding/selection/actions.ts @@ -237,12 +237,14 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { .where(eq(biddings.originalBiddingNumber, baseNumber)) .orderBy(biddings.createdAt) - // 각 bidding에 대한 벤더의 견적 정보 조회 + // 각 bidding에 대한 벤더의 견적 정보 및 상세 아이템 조회 const historyPromises = relatedBiddings.map(async (bidding) => { + // 1. 견적 헤더 정보 조회 (ID 포함) const biddingCompanyData = await db .select({ + id: biddingCompanies.id, finalQuoteAmount: biddingCompanies.finalQuoteAmount, - responseSubmittedAt: biddingCompanies.responseSubmittedAt, + responseSubmittedAt: biddingCompanies.finalQuoteSubmittedAt, isFinalSubmission: biddingCompanies.isFinalSubmission }) .from(biddingCompanies) @@ -256,84 +258,72 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { return null } - return { - biddingId: bidding.id, - biddingNumber: bidding.biddingNumber, - finalQuoteAmount: biddingCompanyData[0].finalQuoteAmount, - responseSubmittedAt: biddingCompanyData[0].responseSubmittedAt, - isFinalSubmission: biddingCompanyData[0].isFinalSubmission, - targetPrice: bidding.targetPrice, - currency: bidding.currency - } - }) - - const historyData = (await Promise.all(historyPromises)).filter(Boolean) - - // biddingNumber의 suffix를 기준으로 정렬 (-01, -02, -03 등) - const sortedHistory = historyData.sort((a, b) => { - const aSuffix = a!.biddingNumber.split('-')[1] ? parseInt(a!.biddingNumber.split('-')[1]) : 0 - const bSuffix = b!.biddingNumber.split('-')[1] ? parseInt(b!.biddingNumber.split('-')[1]) : 0 - return aSuffix - bSuffix - }) - - // PR 항목 정보 조회 (현재 bidding 기준) - const prItems = await db - .select({ - id: prItemsForBidding.id, - itemNumber: prItemsForBidding.itemNumber, - itemInfo: prItemsForBidding.itemInfo, - quantity: prItemsForBidding.quantity, - quantityUnit: prItemsForBidding.quantityUnit, - requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate - }) - .from(prItemsForBidding) - .where(eq(prItemsForBidding.biddingId, biddingId)) - - // 각 히스토리 항목에 대한 PR 아이템 견적 조회 - const history = await Promise.all(sortedHistory.map(async (item, index) => { - // 각 bidding에 대한 PR 아이템 견적 조회 + // 2. 아이템별 견적 및 상세 정보 조회 (Join 사용) const prItemBids = await db .select({ - prItemId: companyPrItemBids.prItemId, + // 견적 정보 bidUnitPrice: companyPrItemBids.bidUnitPrice, bidAmount: companyPrItemBids.bidAmount, - proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate + proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, + // 아이템 상세 정보 + prItemId: prItemsForBidding.id, + itemNumber: prItemsForBidding.itemNumber, + itemInfo: prItemsForBidding.itemInfo, + quantity: prItemsForBidding.quantity, + quantityUnit: prItemsForBidding.quantityUnit, + requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate }) .from(companyPrItemBids) - .where(and( - eq(companyPrItemBids.biddingId, item!.biddingId), - eq(companyPrItemBids.companyId, vendorId) - )) - - const targetPrice = item!.targetPrice ? parseFloat(item!.targetPrice.toString()) : null - const totalAmount = parseFloat(item!.finalQuoteAmount.toString()) + .innerJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id)) + .where(eq(companyPrItemBids.biddingCompanyId, biddingCompanyData[0].id)) + + // 아이템 매핑 + const items = prItemBids.map(bid => ({ + itemCode: bid.itemNumber || `ITEM${bid.prItemId}`, + itemName: bid.itemInfo || '품목 정보 없음', + quantity: bid.quantity ? parseFloat(bid.quantity.toString()) : 0, + unit: bid.quantityUnit || 'EA', + unitPrice: bid.bidUnitPrice ? parseFloat(bid.bidUnitPrice.toString()) : 0, + totalPrice: bid.bidAmount ? parseFloat(bid.bidAmount.toString()) : 0, + deliveryDate: bid.proposedDeliveryDate + ? new Date(bid.proposedDeliveryDate) + : bid.requestedDeliveryDate + ? new Date(bid.requestedDeliveryDate) + : new Date() + })) + + const targetPrice = bidding.targetPrice ? parseFloat(bidding.targetPrice.toString()) : null + const totalAmount = parseFloat(biddingCompanyData[0].finalQuoteAmount.toString()) const vsTargetPrice = targetPrice && targetPrice > 0 ? ((totalAmount - targetPrice) / targetPrice) * 100 : 0 - const items = prItemBids.map(bid => { - const prItem = prItems.find(p => p.id === bid.prItemId) - return { - itemCode: prItem?.itemNumber || `ITEM${bid.prItemId}`, - itemName: prItem?.itemInfo || '품목 정보 없음', - quantity: prItem?.quantity || 0, - unit: prItem?.quantityUnit || 'EA', - unitPrice: parseFloat(bid.bidUnitPrice.toString()), - totalPrice: parseFloat(bid.bidAmount.toString()), - deliveryDate: bid.proposedDeliveryDate ? new Date(bid.proposedDeliveryDate) : prItem?.requestedDeliveryDate ? new Date(prItem.requestedDeliveryDate) : new Date() - } - }) - return { - id: item!.biddingId, - round: index + 1, // 1차, 2차, 3차... - submittedAt: new Date(item!.responseSubmittedAt), + biddingId: bidding.id, + biddingNumber: bidding.biddingNumber, + submittedAt: new Date(biddingCompanyData[0].responseSubmittedAt), totalAmount, - currency: item!.currency || 'KRW', + currency: bidding.currency || 'KRW', vsTargetPrice: parseFloat(vsTargetPrice.toFixed(2)), items } + }) + + const historyData = (await Promise.all(historyPromises)).filter(Boolean) + + // biddingNumber의 suffix를 기준으로 정렬 (-01, -02, -03 등) + const sortedHistory = historyData.sort((a, b) => { + const aSuffix = a!.biddingNumber.split('-')[1] ? parseInt(a!.biddingNumber.split('-')[1]) : 0 + const bSuffix = b!.biddingNumber.split('-')[1] ? parseInt(b!.biddingNumber.split('-')[1]) : 0 + return aSuffix - bSuffix + }) + + // 회차 정보 추가 + const history = sortedHistory.map((item, index) => ({ + id: item!.biddingId, + round: index + 1, // 1차, 2차, 3차... + ...item! })) return { diff --git a/lib/bidding/selection/bidding-info-card.tsx b/lib/bidding/selection/bidding-info-card.tsx index b363f538..5904bf65 100644 --- a/lib/bidding/selection/bidding-info-card.tsx +++ b/lib/bidding/selection/bidding-info-card.tsx @@ -5,6 +5,18 @@ import { Badge } from '@/components/ui/badge' // import { formatDate } from '@/lib/utils' import { biddingStatusLabels, contractTypeLabels } from '@/db/schema' +// 입찰유형 라벨 맵 추가 +const biddingTypeLabels: Record<string, string> = { + equipment: '기자재', + construction: '공사', + service: '용역', + lease: '임차', + transport: '운송', + waste: '폐기물', + sale: '매각', + other: '기타(직접입력)', +} + interface BiddingInfoCardProps { bidding: Bidding } @@ -56,7 +68,7 @@ export function BiddingInfoCard({ bidding }: BiddingInfoCardProps) { 입찰유형 </label> <div className="text-sm font-medium"> - {bidding.biddingType} + {biddingTypeLabels[bidding.biddingType as keyof typeof biddingTypeLabels] || bidding.biddingType || '-'} </div> </div> diff --git a/lib/bidding/selection/bidding-item-table.tsx b/lib/bidding/selection/bidding-item-table.tsx index c101f7e7..aa2b34ec 100644 --- a/lib/bidding/selection/bidding-item-table.tsx +++ b/lib/bidding/selection/bidding-item-table.tsx @@ -2,10 +2,7 @@ import * as React from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { - getPRItemsForBidding, - getVendorPricesForBidding -} from '@/lib/bidding/detail/service' +import { getBiddingSelectionItemsAndPrices } from '@/lib/bidding/service' import { formatNumber } from '@/lib/utils' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' @@ -21,26 +18,55 @@ export function BiddingItemTable({ biddingId }: BiddingItemTableProps) { const [loading, setLoading] = React.useState(true) React.useEffect(() => { + let isMounted = true + const loadData = async () => { try { setLoading(true) - const [prItems, vendorPrices] = await Promise.all([ - getPRItemsForBidding(biddingId), - getVendorPricesForBidding(biddingId) - ]) - console.log('prItems', prItems) - console.log('vendorPrices', vendorPrices) - setData({ prItems, vendorPrices }) + const { prItems, vendorPrices } = await getBiddingSelectionItemsAndPrices(biddingId) + + if (isMounted) { + console.log('prItems', prItems) + console.log('vendorPrices', vendorPrices) + setData({ prItems, vendorPrices }) + } } catch (error) { console.error('Failed to load bidding items:', error) } finally { - setLoading(false) + if (isMounted) { + setLoading(false) + } } } loadData() + + return () => { + isMounted = false + } }, [biddingId]) + // Memoize calculations + const totals = React.useMemo(() => { + const { prItems } = data + return { + quantity: prItems.reduce((sum, item) => sum + Number(item.quantity || 0), 0), + weight: prItems.reduce((sum, item) => sum + Number(item.totalWeight || 0), 0), + targetAmount: prItems.reduce((sum, item) => sum + Number(item.targetAmount || 0), 0) + } + }, [data.prItems]) + + const vendorTotals = React.useMemo(() => { + const { vendorPrices } = data + return vendorPrices.map(vendor => { + const total = vendor.itemPrices.reduce((sum: number, item: any) => sum + Number(item.amount || 0), 0) + return { + companyId: vendor.companyId, + totalAmount: total + } + }) + }, [data.vendorPrices]) + if (loading) { return ( <Card> @@ -58,19 +84,6 @@ export function BiddingItemTable({ biddingId }: BiddingItemTableProps) { const { prItems, vendorPrices } = data - // Calculate Totals - const totalQuantity = prItems.reduce((sum, item) => sum + Number(item.quantity || 0), 0) - const totalWeight = prItems.reduce((sum, item) => sum + Number(item.totalWeight || 0), 0) - const totalTargetAmount = prItems.reduce((sum, item) => sum + Number(item.targetAmount || 0), 0) - - // Calculate Vendor Totals - const vendorTotals = vendorPrices.map(vendor => { - const total = vendor.itemPrices.reduce((sum: number, item: any) => sum + Number(item.amount || 0), 0) - return { - companyId: vendor.companyId, - totalAmount: total - } - }) return ( <Card> @@ -118,17 +131,17 @@ export function BiddingItemTable({ biddingId }: BiddingItemTableProps) { {/* Summary Row */} <tr className="border-b transition-colors hover:bg-muted/50 bg-muted/30 font-semibold"> <td className="p-4 align-middle text-center border-r" colSpan={4}>합계</td> - <td className="p-4 align-middle text-right border-r">{formatNumber(totalQuantity)}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totals.quantity)}</td> <td className="p-4 align-middle text-center border-r">-</td> - <td className="p-4 align-middle text-right border-r">{formatNumber(totalWeight)}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totals.weight)}</td> <td className="p-4 align-middle text-center border-r">-</td> <td className="p-4 align-middle text-center border-r">-</td> - <td className="p-4 align-middle text-right border-r">{formatNumber(totalTargetAmount)}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totals.targetAmount)}</td> <td className="p-4 align-middle text-center border-r">KRW</td> {vendorPrices.map((vendor) => { const vTotal = vendorTotals.find(t => t.companyId === vendor.companyId)?.totalAmount || 0 - const ratio = totalTargetAmount > 0 ? (vTotal / totalTargetAmount) * 100 : 0 + const ratio = totals.targetAmount > 0 ? (vTotal / totals.targetAmount) * 100 : 0 return ( <React.Fragment key={vendor.companyId}> <td className="p-4 align-middle text-center border-r">-</td> |
