diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/bidding/actions.ts | 372 | ||||
| -rw-r--r-- | lib/bidding/list/biddings-transmission-dialog.tsx | 144 | ||||
| -rw-r--r-- | lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx | 5 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-pre-quote.tsx | 51 |
4 files changed, 446 insertions, 126 deletions
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index 65ff3138..ae1fb54f 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -1,11 +1,12 @@ "use server"
import db from "@/db/db"
-import { eq, and, sql } from "drizzle-orm"
+import { eq, and } from "drizzle-orm"
import {
biddings,
biddingCompanies,
prItemsForBidding,
+ companyPrItemBids,
vendors,
generalContracts,
generalContractItems,
@@ -38,51 +39,23 @@ export async function transmitToContract(biddingId: number, userId: number) { const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null
- // 3. 낙찰된 업체들 조회 (별도 쿼리)
- let winnerCompaniesData: { companyId: number; finalQuoteAmount: string | null; vendorCode: string | null; vendorName: string | null; }[] = []
- try {
- // 2.1 biddingCompanies만 먼저 조회 (join 제거)
- const biddingCompaniesRaw = await db.select()
- .from(biddingCompanies)
- .where(
- and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.isWinner, true)
- )
- )
-
-
- // 2.2 각 company에 대한 vendor 정보 개별 조회
- for (const bc of biddingCompaniesRaw) {
- try {
- const vendorData = await db.select()
- .from(vendors)
- .where(eq(vendors.id, bc.companyId))
- .limit(1)
-
- const vendor = vendorData.length > 0 ? vendorData[0] : null
- winnerCompaniesData.push({
- companyId: bc.companyId,
- finalQuoteAmount: bc.finalQuoteAmount,
- vendorCode: vendor?.vendorCode || null,
- vendorName: vendor?.vendorName || null as string | null,
- })
- } catch (vendorError) {
- console.error('Vendor query error for', bc.companyId, ':', vendorError)
- // vendor 정보 없이도 진행
- winnerCompaniesData.push({
- companyId: bc.companyId,
- finalQuoteAmount: bc.finalQuoteAmount,
- vendorCode: null as string | null,
- vendorName: null as string | null,
- })
- }
- }
-
- } catch (queryError) {
- console.error('Query error:', queryError)
- throw new Error(`biddingCompanies 쿼리 실패: ${queryError}`)
- }
+ // 3. 낙찰된 업체들 조회 (biddingCompanies.id 포함)
+ const winnerCompaniesData = await db.select({
+ id: biddingCompanies.id,
+ companyId: biddingCompanies.companyId,
+ finalQuoteAmount: biddingCompanies.finalQuoteAmount,
+ awardRatio: biddingCompanies.awardRatio,
+ vendorCode: vendors.vendorCode,
+ vendorName: vendors.vendorName
+ })
+ .from(biddingCompanies)
+ .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .where(
+ and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.isWinner, true)
+ )
+ )
// 상태 검증
if (biddingData.status !== 'vendor_selected') {
@@ -95,10 +68,47 @@ export async function transmitToContract(biddingId: number, userId: number) { }
for (const winnerCompany of winnerCompaniesData) {
+ // winnerCompany에서 직접 정보 사용
+ const awardRatio = (Number(winnerCompany.awardRatio) || 100) / 100
+ const biddingCompanyId = winnerCompany.id
+
+ // 현재 winnerCompany의 입찰 데이터 조회
+ const companyBids = await db.select({
+ prItemId: companyPrItemBids.prItemId,
+ proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate,
+ bidUnitPrice: companyPrItemBids.bidUnitPrice,
+ bidAmount: companyPrItemBids.bidAmount,
+ currency: companyPrItemBids.currency,
+ // PR 아이템 정보도 함께 조회
+ itemNumber: prItemsForBidding.itemNumber,
+ itemInfo: prItemsForBidding.itemInfo,
+ materialDescription: prItemsForBidding.materialDescription,
+ quantity: prItemsForBidding.quantity,
+ quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
+ })
+ .from(companyPrItemBids)
+ .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id))
+ .where(eq(companyPrItemBids.biddingCompanyId, biddingCompanyId))
+
+ // 발주비율에 따른 최종 계약금액 계산
+ let totalContractAmount = 0
+ if (companyBids.length > 0) {
+ for (const bid of companyBids) {
+ const originalQuantity = Number(bid.quantity) || 0
+ const bidUnitPrice = Number(bid.bidUnitPrice) || 0
+ const finalQuantity = originalQuantity * awardRatio
+ const finalAmount = finalQuantity * bidUnitPrice
+ totalContractAmount += finalAmount
+ }
+ }
+
// 계약 번호 자동 생성 (실제 규칙에 맞게)
const contractNumber = await generateContractNumber(userId.toString(), biddingData.contractType)
console.log('Generated contractNumber:', contractNumber)
- // general-contract 생성
+
+ // general-contract 생성 (발주비율 계산된 최종 금액 사용)
const contractResult = await db.insert(generalContracts).values({
contractNumber,
revision: 0,
@@ -108,9 +118,9 @@ export async function transmitToContract(biddingId: number, userId: number) { name: biddingData.title,
vendorId: winnerCompany.companyId,
linkedBidNumber: biddingData.biddingNumber,
- contractAmount: winnerCompany.finalQuoteAmount || null,
- contractStartDate: biddingData.contractStartDate || null,
- contractEndDate: biddingData.contractEndDate || null,
+ contractAmount: totalContractAmount || null, // 발주비율 계산된 최종 금액 사용
+ startDate: biddingData.contractStartDate || null,
+ endDate: biddingData.contractEndDate || null,
currency: biddingData.currency || 'KRW',
// 계약 조건 정보 추가
paymentTerm: biddingCondition?.paymentTerms || null,
@@ -124,33 +134,39 @@ export async function transmitToContract(biddingId: number, userId: number) { console.log('contractResult', contractResult)
const contractId = contractResult[0].id
- // 4. PR 아이템들로 general-contract-items 생성
- const prItems = await db.select()
- .from(prItemsForBidding)
- .where(eq(prItemsForBidding.biddingId, biddingId))
+ // 현재 winnerCompany의 품목정보 생성 (발주비율 적용)
+ if (companyBids.length > 0) {
+ console.log(`Creating ${companyBids.length} contract items for winner company ${winnerCompany.companyId} with award ratio ${awardRatio}`)
+ for (const bid of companyBids) {
+ // 발주비율에 따른 최종 수량/중량 계산
+ const originalQuantity = Number(bid.quantity) || 0
+ const originalWeight = Number(bid.totalWeight) || 0
+ const bidUnitPrice = Number(bid.bidUnitPrice) || 0
+
+ const finalQuantity = originalQuantity * awardRatio
+ const finalWeight = originalWeight * awardRatio
+ const finalAmount = finalQuantity > 0
+ ? finalQuantity * bidUnitPrice
+ : finalWeight * bidUnitPrice
- if (prItems.length > 0) {
- console.log(`Creating ${prItems.length} contract items for contract ${contractId}`)
- for (const prItem of prItems) {
await db.insert(generalContractItems).values({
- contractId,
- project: prItem.projectInfo || '',
- itemCode: prItem.itemNumber || '',
- itemInfo: prItem.itemInfo || '',
- specification: prItem.materialDescription || '',
- quantity: prItem.quantity || null,
- quantityUnit: prItem.quantityUnit || '',
- contractUnitPrice: prItem.annualUnitPrice || null,
- contractAmount: prItem.annualUnitPrice && prItem.quantity
- ? (prItem.annualUnitPrice * prItem.quantity)
- : null,
- contractCurrency: biddingData.currency || 'KRW',
- contractDeliveryDate: prItem.requestedDeliveryDate || null,
- })
+ contractId: contractId,
+ itemCode: bid.itemNumber || '',
+ itemInfo: bid.itemInfo || '',
+ specification: bid.materialDescription || '',
+ quantity: finalQuantity || null,
+ quantityUnit: bid.quantityUnit || '',
+ totalWeight: finalWeight || null,
+ weightUnit: bid.weightUnit || '',
+ contractDeliveryDate: bid.proposedDeliveryDate || null,
+ contractUnitPrice: bid.bidUnitPrice || null,
+ contractAmount: finalAmount || null,
+ contractCurrency: bid.currency || biddingData.currency || 'KRW',
+ } as any)
}
- console.log(`Created ${prItems.length} contract items`)
+ console.log(`Created ${companyBids.length} contract items for winner company ${winnerCompany.companyId}`)
} else {
- console.log('No PR items found for this bidding')
+ console.log(`No bid data found for winner company ${winnerCompany.companyId}`)
}
}
@@ -189,34 +205,101 @@ export async function transmitToPO(biddingId: number) { const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null
- // 3. 낙찰된 업체들 조회
+ // 3. 낙찰된 업체들 조회 (발주비율 포함)
const winnerCompaniesRaw = await db.select({
+ id: biddingCompanies.id,
companyId: biddingCompanies.companyId,
finalQuoteAmount: biddingCompanies.finalQuoteAmount,
+ awardRatio: biddingCompanies.awardRatio,
vendorCode: vendors.vendorCode,
vendorName: vendors.vendorName
})
- .from(biddingCompanies)
- .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
- .where(
- and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.isWinner, true)
- )
+ .from(biddingCompanies)
+ .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .where(
+ and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.isWinner, true)
)
+ )
if (winnerCompaniesRaw.length === 0) {
throw new Error("낙찰된 업체가 없습니다.")
}
- // 4. PR 아이템 조회
- const prItems = await db.select()
- .from(prItemsForBidding)
- .where(eq(prItemsForBidding.biddingId, biddingId))
+ // 4. 낙찰된 업체들의 입찰 데이터 조회 (발주비율 적용)
+ type POItem = {
+ prItemId: number
+ proposedDeliveryDate: string | null
+ bidUnitPrice: string | null
+ bidAmount: string | null
+ currency: string | null
+ itemNumber: string | null
+ itemInfo: string | null
+ materialDescription: string | null
+ quantity: string | null
+ quantityUnit: string | null
+ totalWeight: string | null
+ weightUnit: string | null
+ finalQuantity: number
+ finalWeight: number
+ finalAmount: number
+ awardRatio: number
+ vendorCode: string | null
+ vendorName: string | null
+ companyId: number
+ }
+ const poItems: POItem[] = []
+ for (const winner of winnerCompaniesRaw) {
+ const awardRatio = (Number(winner.awardRatio) || 100) / 100
+
+ const companyBids = await db.select({
+ prItemId: companyPrItemBids.prItemId,
+ proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate,
+ bidUnitPrice: companyPrItemBids.bidUnitPrice,
+ bidAmount: companyPrItemBids.bidAmount,
+ currency: companyPrItemBids.currency,
+ // PR 아이템 정보
+ itemNumber: prItemsForBidding.itemNumber,
+ itemInfo: prItemsForBidding.itemInfo,
+ materialDescription: prItemsForBidding.materialDescription,
+ quantity: prItemsForBidding.quantity,
+ quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
+ })
+ .from(companyPrItemBids)
+ .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id))
+ .where(eq(companyPrItemBids.biddingCompanyId, winner.id))
+
+ // 발주비율 적용하여 PO 아이템 생성
+ for (const bid of companyBids) {
+ const originalQuantity = Number(bid.quantity) || 0
+ const originalWeight = Number(bid.totalWeight) || 0
+ const bidUnitPrice = Number(bid.bidUnitPrice) || 0
+
+ const finalQuantity = originalQuantity * awardRatio
+ const finalWeight = originalWeight * awardRatio
+ const finalAmount = finalQuantity > 0
+ ? finalQuantity * bidUnitPrice
+ : finalWeight * bidUnitPrice
+
+ poItems.push({
+ ...bid,
+ finalQuantity,
+ finalWeight,
+ finalAmount,
+ awardRatio,
+ vendorCode: winner.vendorCode,
+ vendorName: winner.vendorName,
+ companyId: winner.companyId,
+ } as POItem)
+ }
+ }
- // 5. PO 데이터 구성 (bidding condition 정보 사용)
+ // 5. PO 데이터 구성 (bidding condition 정보와 발주비율 적용된 데이터 사용)
const poData = {
- T_Bidding_HEADER: winnerCompaniesRaw.map((company, index) => ({
+ T_Bidding_HEADER: winnerCompaniesRaw.map((company) => ({
ANFNR: bidding.biddingNumber,
LIFNR: company.vendorCode || `VENDOR${company.companyId}`,
ZPROC_IND: 'A', // 구매 처리 상태
@@ -232,20 +315,16 @@ export async function transmitToPO(biddingId: number) { IHRAN: getCurrentSAPDate(),
TEXT: `PO from Bidding: ${bidding.title}`,
})),
- T_Bidding_ITEM: prItems.map((item, index) => ({
+ T_Bidding_ITEM: poItems.map((item, index) => ({
ANFNR: bidding.biddingNumber,
ANFPS: (index + 1).toString().padStart(5, '0'),
- LIFNR: winnerCompaniesRaw[0]?.vendorCode || `VENDOR${winnerCompaniesRaw[0]?.companyId}`,
- NETPR: item.annualUnitPrice?.toString() || '0',
+ LIFNR: item.vendorCode || `VENDOR${item.companyId}`,
+ NETPR: item.bidUnitPrice?.toString() || '0',
PEINH: '1',
BPRME: item.quantityUnit || 'EA',
- NETWR: item.annualUnitPrice && item.quantity
- ? (item.annualUnitPrice * item.quantity).toString()
- : '0',
- BRTWR: item.annualUnitPrice && item.quantity
- ? ((item.annualUnitPrice * item.quantity) * 1.1).toString() // 10% 부가세 가정
- : '0',
- LFDAT: item.requestedDeliveryDate?.toISOString().split('T')[0] || getCurrentSAPDate(),
+ NETWR: item.finalAmount?.toString() || '0',
+ BRTWR: (Number(item.finalAmount || 0) * 1.1).toString(), // 10% 부가세 가정
+ LFDAT: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate).toISOString().split('T')[0] : getCurrentSAPDate(),
})),
T_PR_RETURN: [{
ANFNR: bidding.biddingNumber,
@@ -272,3 +351,102 @@ export async function transmitToPO(biddingId: number) { throw new Error(error instanceof Error ? error.message : 'PO 전송에 실패했습니다.')
}
}
+
+// 낙찰된 업체들의 상세 정보 조회 (발주비율에 따른 계산 포함)
+export async function getWinnerDetails(biddingId: number) {
+ try {
+ // 1. 낙찰된 업체들 조회
+ const winnerCompanies = await db.select({
+ id: biddingCompanies.id,
+ companyId: biddingCompanies.companyId,
+ finalQuoteAmount: biddingCompanies.finalQuoteAmount,
+ awardRatio: biddingCompanies.awardRatio,
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ })
+ .from(biddingCompanies)
+ .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .where(
+ and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.isWinner, true)
+ )
+ )
+
+ if (winnerCompanies.length === 0) {
+ return { success: false, error: '낙찰된 업체가 없습니다.' }
+ }
+
+ // 2. 각 낙찰 업체의 입찰 품목 정보 조회
+ const winnerDetails = []
+
+ for (const winner of winnerCompanies) {
+ // 업체의 입찰 품목 정보 조회
+ const companyBids = await db.select({
+ prItemId: companyPrItemBids.prItemId,
+ proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate,
+ bidUnitPrice: companyPrItemBids.bidUnitPrice,
+ bidAmount: companyPrItemBids.bidAmount,
+ currency: companyPrItemBids.currency,
+ // PR 아이템 정보
+ itemNumber: prItemsForBidding.itemNumber,
+ itemInfo: prItemsForBidding.itemInfo,
+ materialDescription: prItemsForBidding.materialDescription,
+ quantity: prItemsForBidding.quantity,
+ quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
+ })
+ .from(companyPrItemBids)
+ .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id))
+ .where(eq(companyPrItemBids.biddingCompanyId, winner.id))
+
+ // 발주비율에 따른 계산 (백분율을 실제 비율로 변환)
+ const awardRatio = (Number(winner.awardRatio) || 100) / 100
+ const calculatedItems = companyBids.map(bid => {
+ const originalQuantity = Number(bid.quantity) || 0
+ const originalWeight = Number(bid.totalWeight) || 0
+ const bidUnitPrice = Number(bid.bidUnitPrice) || 0
+
+ // 발주비율에 따른 최종 수량/중량 계산
+ const finalQuantity = originalQuantity * awardRatio
+ const finalWeight = originalWeight * awardRatio
+
+ // 최종 견적가 계산 (수량 또는 중량에 따른)
+ const finalAmount = finalQuantity > 0
+ ? finalQuantity * bidUnitPrice
+ : finalWeight * bidUnitPrice
+
+ return {
+ ...bid,
+ finalQuantity,
+ finalWeight,
+ finalAmount,
+ awardRatio,
+ }
+ })
+
+ // 업체 총 견적가 계산
+ const totalFinalAmount = calculatedItems.reduce((sum, item) => sum + item.finalAmount, 0)
+
+ winnerDetails.push({
+ ...winner,
+ items: calculatedItems,
+ totalFinalAmount,
+ awardRatio: Number(winner.awardRatio) || 1,
+ })
+ }
+
+ return {
+ success: true,
+ data: winnerDetails
+ }
+
+ } catch (error) {
+ console.error('Winner details 조회 실패:', error)
+ return {
+ success: false,
+ error: '낙찰 업체 상세 정보 조회에 실패했습니다.'
+ }
+ }
+}
diff --git a/lib/bidding/list/biddings-transmission-dialog.tsx b/lib/bidding/list/biddings-transmission-dialog.tsx index 035ab583..61207327 100644 --- a/lib/bidding/list/biddings-transmission-dialog.tsx +++ b/lib/bidding/list/biddings-transmission-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react"
import {
- Send, CheckCircle, FileText, Truck
+ Send, CheckCircle, FileText, Truck, Calculator, Package
} from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
@@ -16,8 +16,10 @@ import { } from "@/components/ui/dialog"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
import { BiddingListItem } from "@/db/schema"
-import { transmitToContract, transmitToPO } from "@/lib/bidding/actions"
+import { transmitToContract, transmitToPO, getWinnerDetails } from "@/lib/bidding/actions"
interface TransmissionDialogProps {
open: boolean
@@ -26,8 +28,60 @@ interface TransmissionDialogProps { userId: number
}
+interface WinnerDetail {
+ id: number
+ companyId: number
+ vendorName: string | null
+ vendorCode: string | null
+ awardRatio: number
+ totalFinalAmount: number
+ items: Array<{
+ itemNumber: string | null
+ itemInfo: string | null
+ materialDescription: string | null
+ quantity: string | null
+ quantityUnit: string | null
+ totalWeight: string | null
+ weightUnit: string | null
+ bidUnitPrice: string | null
+ currency: string | null
+ finalQuantity: number
+ finalWeight: number
+ finalAmount: number
+ awardRatio: number
+ }>
+}
+
export function TransmissionDialog({ open, onOpenChange, bidding, userId }: TransmissionDialogProps) {
const [isLoading, setIsLoading] = React.useState(false)
+ const [winnerDetails, setWinnerDetails] = React.useState<WinnerDetail[]>([])
+ const [isLoadingDetails, setIsLoadingDetails] = React.useState(false)
+
+ // 낙찰 업체 상세 정보 로드
+ const loadWinnerDetails = React.useCallback(async () => {
+ if (!bidding) return
+
+ try {
+ setIsLoadingDetails(true)
+ const result = await getWinnerDetails(bidding.id)
+ if (result.success) {
+ setWinnerDetails(result.data || [])
+ } else {
+ toast.error(result.error || '낙찰 업체 정보를 불러오는데 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to load winner details:', error)
+ toast.error('낙찰 업체 정보를 불러오는데 실패했습니다.')
+ } finally {
+ setIsLoadingDetails(false)
+ }
+ }, [bidding])
+
+ React.useEffect(() => {
+ if (open && bidding) {
+ loadWinnerDetails()
+ }
+ }, [open, bidding, loadWinnerDetails])
if (!bidding) return null
@@ -102,19 +156,85 @@ export function TransmissionDialog({ open, onOpenChange, bidding, userId }: Tran </CardContent>
</Card>
- {/* 선정된 업체 정보 (임시로 표시) */}
+ {/* 선정된 업체 상세 정보 */}
<Card>
<CardHeader className="pb-3">
- <CardTitle className="text-base">선정된 업체</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="flex items-center gap-2">
+ <CardTitle className="text-base flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600" />
- <span className="text-sm">업체 선정이 완료되었습니다.</span>
- </div>
- <p className="text-xs text-muted-foreground mt-2">
- 자세한 업체 정보는 전송 후 확인할 수 있습니다.
- </p>
+ 선정된 업체 ({winnerDetails.length}개)
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {isLoadingDetails ? (
+ <div className="text-center py-4">
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto"></div>
+ <p className="text-sm text-muted-foreground mt-2">업체 정보를 불러오는 중...</p>
+ </div>
+ ) : winnerDetails.length === 0 ? (
+ <p className="text-sm text-muted-foreground">선정된 업체가 없습니다.</p>
+ ) : (
+ winnerDetails.map((winner) => (
+ <div key={winner.id} className="border rounded-lg p-4 space-y-3">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Package className="w-4 h-4 text-primary" />
+ <span className="font-medium">{winner.vendorName || `업체 ${winner.companyId}`}</span>
+ <Badge variant="outline">{winner.vendorCode}</Badge>
+ </div>
+ <div className="text-right">
+ <div className="text-sm font-medium">
+ 발주비율: {winner.awardRatio.toFixed(1)}%
+ </div>
+ <div className="text-sm text-muted-foreground">
+ 최종 견적가: {winner.totalFinalAmount.toLocaleString()} {winner.items[0]?.currency || 'KRW'}
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="space-y-2">
+ <div className="text-sm font-medium flex items-center gap-2">
+ <Calculator className="w-4 h-4" />
+ 품목별 상세 ({winner.items.length}개 품목)
+ </div>
+
+ <div className="space-y-2 max-h-40 overflow-y-auto">
+ {winner.items.map((item, itemIndex) => (
+ <div key={itemIndex} className="bg-muted/50 rounded p-2 text-xs">
+ <div className="grid grid-cols-2 gap-2">
+ <div>
+ <span className="font-medium">품목:</span> {item.itemInfo || item.itemNumber}
+ </div>
+ <div>
+ <span className="font-medium">규격:</span> {item.materialDescription}
+ </div>
+ <div>
+ <span className="font-medium">원래 수량:</span> {Number(item.quantity).toLocaleString()} {item.quantityUnit}
+ </div>
+ <div>
+ <span className="font-medium">발주 수량:</span> {item.finalQuantity.toLocaleString()} {item.quantityUnit}
+ </div>
+ <div>
+ <span className="font-medium">원래 중량:</span> {Number(item.totalWeight).toLocaleString()} {item.weightUnit}
+ </div>
+ <div>
+ <span className="font-medium">발주 중량:</span> {item.finalWeight.toLocaleString()} {item.weightUnit}
+ </div>
+ <div>
+ <span className="font-medium">단가:</span> {Number(item.bidUnitPrice).toLocaleString()} {item.currency}
+ </div>
+ <div>
+ <span className="font-medium">최종 금액:</span> {item.finalAmount.toLocaleString()} {item.currency}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ ))
+ )}
</CardContent>
</Card>
</div>
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx index 5fc0a0ee..3205df08 100644 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx +++ b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx @@ -474,7 +474,7 @@ export function BiddingPreQuoteInvitationDialog({ <div className="mb-6 p-4 border rounded-lg bg-muted/30"> <Label htmlFor="preQuoteDeadline" className="text-sm font-medium mb-2 flex items-center gap-2"> <Calendar className="w-4 h-4" /> - 견적 마감일 (선택사항) + 견적 마감일 </Label> <Input id="preQuoteDeadline" @@ -483,9 +483,6 @@ export function BiddingPreQuoteInvitationDialog({ onChange={(e) => setPreQuoteDeadline(e.target.value)} className="w-full" /> - <p className="text-xs text-muted-foreground mt-1"> - 설정하지 않으면 마감일 없이 초대가 발송됩니다. - </p> </div> {/* 기존 계약 정보 알림 */} diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx index 6a76ffa1..6b9f956b 100644 --- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx +++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx @@ -325,6 +325,17 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin return } + // 사전견적 상태 체크 + const isPreQuoteStatus = biddingStatus === 'request_for_quotation' || biddingStatus === 'received_quotation' + if (!isPreQuoteStatus) { + toast({ + title: "접근 제한", + description: "사전견적 단계가 아니므로 임시저장이 불가능합니다.", + variant: "destructive", + }) + return + } + if (!userId) { toast({ title: '임시저장 실패', @@ -347,11 +358,11 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin proposedContractDeliveryDate: responseData.proposedContractDeliveryDate, proposedShippingPort: responseData.proposedShippingPort, proposedDestinationPort: responseData.proposedDestinationPort, - priceAdjustmentResponse: responseData.priceAdjustmentResponse, - isInitialResponse: responseData.isInitialResponse, + priceAdjustmentResponse: responseData.priceAdjustmentResponse || false, // 체크 안하면 false로 설정 + isInitialResponse: responseData.isInitialResponse || false, // 체크 안하면 false로 설정 sparePartResponse: responseData.sparePartResponse, additionalProposals: responseData.additionalProposals, - priceAdjustmentForm: responseData.priceAdjustmentResponse ? { + priceAdjustmentForm: (responseData.priceAdjustmentResponse || false) ? { itemName: priceAdjustmentForm.itemName, adjustmentReflectionPoint: priceAdjustmentForm.adjustmentReflectionPoint, majorApplicableRawMaterial: priceAdjustmentForm.majorApplicableRawMaterial, @@ -440,6 +451,17 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin return } + // 사전견적 상태 체크 + const isPreQuoteStatus = biddingStatus === 'request_for_quotation' || biddingStatus === 'received_quotation' + if (!isPreQuoteStatus) { + toast({ + title: "접근 제한", + description: "사전견적 단계가 아니므로 견적 제출이 불가능합니다.", + variant: "destructive", + }) + return + } + // 견적마감일 체크 if (biddingDetail.preQuoteDeadline) { const now = new Date() @@ -509,11 +531,11 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin proposedContractDeliveryDate: responseData.proposedContractDeliveryDate, proposedShippingPort: responseData.proposedShippingPort, proposedDestinationPort: responseData.proposedDestinationPort, - priceAdjustmentResponse: responseData.priceAdjustmentResponse, - isInitialResponse: responseData.isInitialResponse, + priceAdjustmentResponse: responseData.priceAdjustmentResponse || false, // 체크 안하면 false로 설정 + isInitialResponse: responseData.isInitialResponse || false, // 체크 안하면 false로 설정 sparePartResponse: responseData.sparePartResponse, additionalProposals: responseData.additionalProposals, - priceAdjustmentForm: responseData.priceAdjustmentResponse ? { + priceAdjustmentForm: (responseData.priceAdjustmentResponse || false) ? { itemName: priceAdjustmentForm.itemName, adjustmentReflectionPoint: priceAdjustmentForm.adjustmentReflectionPoint, majorApplicableRawMaterial: priceAdjustmentForm.majorApplicableRawMaterial, @@ -1145,12 +1167,12 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="additionalProposals">사유</Label> + <Label htmlFor="additionalProposals">변경사유</Label> <Textarea id="additionalProposals" value={responseData.additionalProposals} onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})} - placeholder="사유를 입력하세요" + placeholder="변경사유를 입력하세요" rows={4} /> </div> @@ -1351,15 +1373,18 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin )} <div className="flex justify-end gap-2 pt-4"> - <Button - variant="outline" - onClick={handleTempSave} - disabled={isSaving || isPending} + <Button + variant="outline" + onClick={handleTempSave} + disabled={isSaving || isPending || (biddingDetail && !['request_for_quotation', 'received_quotation'].includes(biddingDetail.status))} > <Save className="w-4 h-4 mr-2" /> {isSaving ? '저장중...' : '임시저장'} </Button> - <Button onClick={handleSubmitResponse} disabled={isPending || isSaving}> + <Button + onClick={handleSubmitResponse} + disabled={isPending || isSaving || (biddingDetail && !['request_for_quotation', 'received_quotation'].includes(biddingDetail.status))} + > <Send className="w-4 h-4 mr-2" /> 사전견적 제출 </Button> |
