diff options
Diffstat (limited to 'lib/bidding/selection')
| -rw-r--r-- | lib/bidding/selection/actions.ts | 156 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-info-card.tsx | 4 | ||||
| -rw-r--r-- | lib/bidding/selection/biddings-selection-columns.tsx | 46 | ||||
| -rw-r--r-- | lib/bidding/selection/biddings-selection-table.tsx | 22 |
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
}
}
|
