summaryrefslogtreecommitdiff
path: root/lib/bidding/selection
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/selection')
-rw-r--r--lib/bidding/selection/actions.ts156
-rw-r--r--lib/bidding/selection/bidding-info-card.tsx4
-rw-r--r--lib/bidding/selection/biddings-selection-columns.tsx46
-rw-r--r--lib/bidding/selection/biddings-selection-table.tsx22
4 files changed, 115 insertions, 113 deletions
diff --git a/lib/bidding/selection/actions.ts b/lib/bidding/selection/actions.ts
index 16b2c083..0d2a8a75 100644
--- a/lib/bidding/selection/actions.ts
+++ b/lib/bidding/selection/actions.ts
@@ -115,20 +115,17 @@ export async function saveSelectionResult(data: SaveSelectionResultData) {
// 견적 히스토리 조회
export async function getQuotationHistory(biddingId: number, vendorId: number) {
try {
- // biddingCompanies에서 해당 벤더의 스냅샷 데이터 조회
- const companyData = await db
+ // 현재 bidding의 biddingNumber와 originalBiddingNumber 조회
+ const currentBiddingInfo = await db
.select({
- quotationSnapshots: biddingCompanies.quotationSnapshots
+ biddingNumber: biddings.biddingNumber,
+ originalBiddingNumber: biddings.originalBiddingNumber
})
- .from(biddingCompanies)
- .where(and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.companyId, vendorId)
- ))
+ .from(biddings)
+ .where(eq(biddings.id, biddingId))
.limit(1)
- // 데이터 존재 여부 및 유효성 체크
- if (!companyData.length || !companyData[0]?.quotationSnapshots) {
+ if (!currentBiddingInfo.length) {
return {
success: true,
data: {
@@ -137,40 +134,62 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) {
}
}
- let snapshots = companyData[0].quotationSnapshots
+ const baseNumber = currentBiddingInfo[0].originalBiddingNumber || currentBiddingInfo[0].biddingNumber.split('-')[0]
- // quotationSnapshots가 JSONB 타입이므로 파싱이 필요할 수 있음
- if (typeof snapshots === 'string') {
- try {
- snapshots = JSON.parse(snapshots)
- } catch (parseError) {
- console.error('Failed to parse quotationSnapshots:', parseError)
- return {
- success: true,
- data: {
- history: []
- }
- }
+ // 동일한 originalBiddingNumber를 가진 모든 bidding 조회
+ const relatedBiddings = await db
+ .select({
+ id: biddings.id,
+ biddingNumber: biddings.biddingNumber,
+ targetPrice: biddings.targetPrice,
+ currency: biddings.currency,
+ createdAt: biddings.createdAt
+ })
+ .from(biddings)
+ .where(eq(biddings.originalBiddingNumber, baseNumber))
+ .orderBy(biddings.createdAt)
+
+ // 각 bidding에 대한 벤더의 견적 정보 조회
+ const historyPromises = relatedBiddings.map(async (bidding) => {
+ const biddingCompanyData = await db
+ .select({
+ finalQuoteAmount: biddingCompanies.finalQuoteAmount,
+ responseSubmittedAt: biddingCompanies.responseSubmittedAt,
+ isFinalSubmission: biddingCompanies.isFinalSubmission
+ })
+ .from(biddingCompanies)
+ .where(and(
+ eq(biddingCompanies.biddingId, bidding.id),
+ eq(biddingCompanies.companyId, vendorId)
+ ))
+ .limit(1)
+
+ if (!biddingCompanyData.length || !biddingCompanyData[0].finalQuoteAmount || !biddingCompanyData[0].responseSubmittedAt) {
+ return null
}
- }
- // snapshots가 배열인지 확인
- if (!Array.isArray(snapshots)) {
- console.error('quotationSnapshots is not an array:', typeof snapshots)
return {
- success: true,
- data: {
- history: []
- }
+ biddingId: bidding.id,
+ biddingNumber: bidding.biddingNumber,
+ finalQuoteAmount: biddingCompanyData[0].finalQuoteAmount,
+ responseSubmittedAt: biddingCompanyData[0].responseSubmittedAt,
+ isFinalSubmission: biddingCompanyData[0].isFinalSubmission,
+ targetPrice: bidding.targetPrice,
+ currency: bidding.currency
}
- }
+ })
- // PR 항목 정보 조회 (스냅샷의 prItemId로 매핑하기 위해)
- const prItemIds = snapshots.flatMap(snapshot =>
- snapshot.items?.map((item: any) => item.prItemId) || []
- ).filter((id: number, index: number, arr: number[]) => arr.indexOf(id) === index)
+ 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 prItems = prItemIds.length > 0 ? await db
+ // PR 항목 정보 조회 (현재 bidding 기준)
+ const prItems = await db
.select({
id: prItemsForBidding.id,
itemNumber: prItemsForBidding.itemNumber,
@@ -180,53 +199,54 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) {
requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate
})
.from(prItemsForBidding)
- .where(sql`${prItemsForBidding.id} IN ${prItemIds}`) : []
+ .where(eq(prItemsForBidding.biddingId, biddingId))
- // PR 항목을 Map으로 변환하여 빠른 조회를 위해
- const prItemMap = new Map(prItems.map(item => [item.id, item]))
-
- // bidding 정보 조회 (targetPrice, currency)
- const biddingInfo = await db
- .select({
- targetPrice: biddings.targetPrice,
- currency: biddings.currency
- })
- .from(biddings)
- .where(eq(biddings.id, biddingId))
- .limit(1)
+ // 각 히스토리 항목에 대한 PR 아이템 견적 조회
+ const history = await Promise.all(sortedHistory.map(async (item, index) => {
+ // 각 bidding에 대한 PR 아이템 견적 조회
+ const prItemBids = await db
+ .select({
+ prItemId: companyPrItemBids.prItemId,
+ bidUnitPrice: companyPrItemBids.bidUnitPrice,
+ bidAmount: companyPrItemBids.bidAmount,
+ proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate
+ })
+ .from(companyPrItemBids)
+ .where(and(
+ eq(companyPrItemBids.biddingId, item!.biddingId),
+ eq(companyPrItemBids.companyId, vendorId)
+ ))
- const targetPrice = biddingInfo[0]?.targetPrice ? parseFloat(biddingInfo[0].targetPrice.toString()) : null
- const currency = biddingInfo[0]?.currency || 'KRW'
+ const targetPrice = item!.targetPrice ? parseFloat(item!.targetPrice.toString()) : null
+ const totalAmount = parseFloat(item!.finalQuoteAmount.toString())
- // 스냅샷 데이터를 변환
- const history = snapshots.map((snapshot: any) => {
const vsTargetPrice = targetPrice && targetPrice > 0
- ? ((snapshot.totalAmount - targetPrice) / targetPrice) * 100
+ ? ((totalAmount - targetPrice) / targetPrice) * 100
: 0
- const items = snapshot.items?.map((item: any) => {
- const prItem = prItemMap.get(item.prItemId)
+ const items = prItemBids.map(bid => {
+ const prItem = prItems.find(p => p.id === bid.prItemId)
return {
- itemCode: prItem?.itemNumber || `ITEM${item.prItemId}`,
+ itemCode: prItem?.itemNumber || `ITEM${bid.prItemId}`,
itemName: prItem?.itemInfo || '품목 정보 없음',
quantity: prItem?.quantity || 0,
unit: prItem?.quantityUnit || 'EA',
- unitPrice: item.bidUnitPrice,
- totalPrice: item.bidAmount,
- deliveryDate: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate) : prItem?.requestedDeliveryDate ? new Date(prItem.requestedDeliveryDate) : new Date()
+ 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: snapshot.id,
- round: snapshot.round,
- submittedAt: new Date(snapshot.submittedAt),
- totalAmount: snapshot.totalAmount,
- currency: snapshot.currency || currency,
+ id: item!.biddingId,
+ round: index + 1, // 1차, 2차, 3차...
+ submittedAt: new Date(item!.responseSubmittedAt),
+ totalAmount,
+ currency: item!.currency || 'KRW',
vsTargetPrice: parseFloat(vsTargetPrice.toFixed(2)),
items
}
- })
+ }))
return {
success: true,
diff --git a/lib/bidding/selection/bidding-info-card.tsx b/lib/bidding/selection/bidding-info-card.tsx
index f6f0bc69..8864e7db 100644
--- a/lib/bidding/selection/bidding-info-card.tsx
+++ b/lib/bidding/selection/bidding-info-card.tsx
@@ -65,9 +65,9 @@ export function BiddingInfoCard({ bidding }: BiddingInfoCardProps) {
<label className="text-sm font-medium text-muted-foreground">
진행상태
</label>
- <Badge variant="secondary">
+ <div className="text-sm font-medium">
{biddingStatusLabels[bidding.status as keyof typeof biddingStatusLabels] || bidding.status}
- </Badge>
+ </div>
</div>
{/* 입찰담당자 */}
diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx
index 8351a0dd..9efa849b 100644
--- a/lib/bidding/selection/biddings-selection-columns.tsx
+++ b/lib/bidding/selection/biddings-selection-columns.tsx
@@ -221,20 +221,20 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps):
},
// ░░░ 참여업체수 ░░░
- {
- id: "participantCount",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여업체수" />,
- cell: ({ row }) => {
- const count = row.original.participantCount || 0
- return (
- <div className="flex items-center gap-1">
- <span className="text-sm font-medium">{count}</span>
- </div>
- )
- },
- size: 100,
- meta: { excelHeader: "참여업체수" },
- },
+ // {
+ // id: "participantCount",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여업체수" />,
+ // cell: ({ row }) => {
+ // const count = row.original.participantCount || 0
+ // return (
+ // <div className="flex items-center gap-1">
+ // <span className="text-sm font-medium">{count}</span>
+ // </div>
+ // )
+ // },
+ // size: 100,
+ // meta: { excelHeader: "참여업체수" },
+ // },
// ═══════════════════════════════════════════════════════════════
@@ -256,24 +256,6 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps):
<Eye className="mr-2 h-4 w-4" />
상세보기
</DropdownMenuItem>
- {/* {row.original.status === 'bidding_opened' && (
- <>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "close_bidding" })}>
- <Calendar className="mr-2 h-4 w-4" />
- 입찰마감
- </DropdownMenuItem>
- </>
- )} */}
- {row.original.status === 'bidding_closed' && (
- <>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "evaluate_bidding" })}>
- <DollarSign className="mr-2 h-4 w-4" />
- 평가하기
- </DropdownMenuItem>
- </>
- )}
</DropdownMenuContent>
</DropdownMenu>
),
diff --git a/lib/bidding/selection/biddings-selection-table.tsx b/lib/bidding/selection/biddings-selection-table.tsx
index 9545fe09..c3990e7b 100644
--- a/lib/bidding/selection/biddings-selection-table.tsx
+++ b/lib/bidding/selection/biddings-selection-table.tsx
@@ -19,6 +19,7 @@ import {
contractTypeLabels,
} from "@/db/schema"
import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs"
+import { toast } from "@/hooks/use-toast"
type BiddingSelectionItem = {
id: number
@@ -83,17 +84,16 @@ export function BiddingsSelectionTable({ promises }: BiddingsSelectionTableProps
switch (rowAction.type) {
case "view":
// 상세 페이지로 이동
- router.push(`/evcp/bid-selection/${rowAction.row.original.id}/detail`)
- break
- case "close_bidding":
- // 입찰마감 (추후 구현)
- console.log('입찰마감:', rowAction.row.original)
- break
- case "evaluate_bidding":
- // 평가하기 (추후 구현)
- console.log('평가하기:', rowAction.row.original)
- break
- default:
+ // 입찰평가중일때만 상세보기 가능
+ if (rowAction.row.original.status === 'evaluation_of_bidding') {
+ router.push(`/evcp/bid-selection/${rowAction.row.original.id}/detail`)
+ } else {
+ toast({
+ title: '접근 제한',
+ description: '입찰평가중이 아닙니다.',
+ variant: 'destructive',
+ })
+ }
break
}
}