summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor')
-rw-r--r--lib/bidding/vendor/components/pr-items-pricing-table.tsx23
-rw-r--r--lib/bidding/vendor/partners-bidding-detail.tsx821
-rw-r--r--lib/bidding/vendor/partners-bidding-list.tsx13
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx4
4 files changed, 405 insertions, 456 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
index 01885f7a..1dee7adb 100644
--- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -39,6 +39,8 @@ interface PrItem {
materialDescription: string | null
quantity: string | null
quantityUnit: string | null
+ totalWeight: string | null
+ weightUnit: string | null
currency: string | null
requestedDeliveryDate: string | null
hasSpecDocument: boolean | null
@@ -221,11 +223,20 @@ export function PrItemsPricingTable({
if (q.prItemId === prItemId) {
const updated = { ...q, [field]: value }
- // 단가나 수량이 변경되면 금액 자동 계산
+ // 단가가 변경되면 금액 자동 계산 (수량 우선, 없으면 중량 사용)
if (field === 'bidUnitPrice') {
const prItem = prItems.find(item => item.id === prItemId)
- const quantity = parseFloat(prItem?.quantity || '1')
- updated.bidAmount = updated.bidUnitPrice * quantity
+ 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
@@ -273,6 +284,8 @@ export function PrItemsPricingTable({
<TableHead>자재내역</TableHead>
<TableHead>수량</TableHead>
<TableHead>단위</TableHead>
+ <TableHead>중량</TableHead>
+ <TableHead>중량단위</TableHead>
<TableHead>견적단가</TableHead>
<TableHead>견적금액</TableHead>
<TableHead>납품예정일</TableHead>
@@ -310,6 +323,10 @@ export function PrItemsPricingTable({
{item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'}
</TableCell>
<TableCell>{item.quantityUnit || '-'}</TableCell>
+ <TableCell className="text-right">
+ {item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'}
+ </TableCell>
+ <TableCell>{item.weightUnit || '-'}</TableCell>
<TableCell>
{readOnly ? (
<span className="font-medium">
diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx
index 1e6ae479..8d24ca66 100644
--- a/lib/bidding/vendor/partners-bidding-detail.tsx
+++ b/lib/bidding/vendor/partners-bidding-detail.tsx
@@ -20,15 +20,21 @@ import {
Users,
Send,
CheckCircle,
- XCircle
+ XCircle,
+ Save
} from 'lucide-react'
import { formatDate } from '@/lib/utils'
import {
getBiddingDetailsForPartners,
submitPartnerResponse,
- updatePartnerAttendance
+ updatePartnerAttendance,
+ updatePartnerBiddingParticipation,
+ saveBiddingDraft
} from '../detail/service'
+import { getPrItemsForBidding, getSavedPrItemQuotations } from '@/lib/bidding/pre-quote/service'
+import { PrItemsPricingTable } from './components/pr-items-pricing-table'
+import { SimpleFileUpload } from './components/simple-file-upload'
import {
biddingStatusLabels,
contractTypeLabels,
@@ -36,6 +42,7 @@ import {
} from '@/db/schema'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
+import { useSession } from 'next-auth/react'
interface PartnersBiddingDetailProps {
biddingId: number
@@ -45,115 +52,144 @@ interface PartnersBiddingDetailProps {
interface BiddingDetail {
id: number
biddingNumber: string
- revision: number
- projectName: string
- itemName: string
+ revision: number | null
+ projectName: string | null
+ itemName: string | null
title: string
- description: string
- content: string
+ description: string | null
+ content: string | null
contractType: string
biddingType: string
awardCount: string
- contractPeriod: string
- preQuoteDate: string
- biddingRegistrationDate: string
- submissionStartDate: string
- submissionEndDate: string
- evaluationDate: string
+ contractPeriod: string | null
+ preQuoteDate: string | null
+ biddingRegistrationDate: string | null
+ submissionStartDate: string | null
+ submissionEndDate: string | null
+ evaluationDate: string | null
currency: string
- budget: number
- targetPrice: number
+ budget: number | null
+ targetPrice: number | null
status: string
- managerName: string
- managerEmail: string
- managerPhone: string
+ managerName: string | null
+ managerEmail: string | null
+ managerPhone: string | null
biddingCompanyId: number
- biddingId: number // bidding의 ID 추가
+ biddingId: number
invitationStatus: string
- finalQuoteAmount: number
- finalQuoteSubmittedAt: string
+ finalQuoteAmount: number | null
+ finalQuoteSubmittedAt: string | null
isWinner: boolean
isAttendingMeeting: boolean | null
- // companyConditionResponses에서 가져온 조건들 (제시된 조건과 응답 모두)
- paymentTermsResponse: string
- taxConditionsResponse: string
- incotermsResponse: string
- proposedContractDeliveryDate: string
- proposedShippingPort: string
- proposedDestinationPort: string
- priceAdjustmentResponse: boolean
- sparePartResponse: string
- additionalProposals: string
- responseSubmittedAt: string
+ isBiddingParticipated: boolean | null
+ additionalProposals: string | null
+ responseSubmittedAt: string | null
+}
+
+interface PrItem {
+ id: number
+ itemNumber: string | null
+ prNumber: string | null
+ itemInfo: string | null
+ materialDescription: string | null
+ quantity: string | null
+ quantityUnit: string | null
+ totalWeight: string | null
+ weightUnit: string | null
+ currency: string | null
+ requestedDeliveryDate: string | null
+ hasSpecDocument: boolean | null
+}
+
+interface PrItemQuotation {
+ prItemId: number
+ bidUnitPrice: number
+ bidAmount: number
+ proposedDeliveryDate?: string | null
+ technicalSpecification?: string
}
export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingDetailProps) {
const router = useRouter()
const { toast } = useToast()
+ const session = useSession()
const [isPending, startTransition] = useTransition()
const [biddingDetail, setBiddingDetail] = React.useState<BiddingDetail | null>(null)
const [isLoading, setIsLoading] = React.useState(true)
+ const [isUpdatingParticipation, setIsUpdatingParticipation] = React.useState(false)
+ const [isSavingDraft, setIsSavingDraft] = React.useState(false)
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+
+ // 품목별 견적 관련 상태
+ const [prItems, setPrItems] = React.useState<PrItem[]>([])
+ const [prItemQuotations, setPrItemQuotations] = React.useState<PrItemQuotation[]>([])
+ const [totalQuotationAmount, setTotalQuotationAmount] = React.useState(0)
// 응찰 폼 상태
const [responseData, setResponseData] = React.useState({
finalQuoteAmount: '',
- paymentTermsResponse: '',
- taxConditionsResponse: '',
- incotermsResponse: '',
proposedContractDeliveryDate: '',
- proposedShippingPort: '',
- proposedDestinationPort: '',
- priceAdjustmentResponse: false,
- isInitialResponse: false,
- sparePartResponse: '',
additionalProposals: '',
- isAttendingMeeting: false,
})
+ const userId = session.data?.user?.id || ''
- // 연동제 폼 상태
- const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({
- itemName: '',
- adjustmentReflectionPoint: '',
- majorApplicableRawMaterial: '',
- adjustmentFormula: '',
- rawMaterialPriceIndex: '',
- referenceDate: '',
- comparisonDate: '',
- adjustmentRatio: '',
- notes: '',
- adjustmentConditions: '',
- majorNonApplicableRawMaterial: '',
- adjustmentPeriod: '',
- contractorWriter: '',
- adjustmentDate: '',
- nonApplicableReason: '',
- })
// 데이터 로드
React.useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
- const result = await getBiddingDetailsForPartners(biddingId, companyId)
+ const [result, prItemsResult] = await Promise.all([
+ getBiddingDetailsForPartners(biddingId, companyId),
+ getPrItemsForBidding(biddingId)
+ ])
+
if (result) {
setBiddingDetail(result)
// 기존 응답 데이터로 폼 초기화
setResponseData({
finalQuoteAmount: result.finalQuoteAmount?.toString() || '',
- paymentTermsResponse: result.paymentTermsResponse || '',
- taxConditionsResponse: result.taxConditionsResponse || '',
- incotermsResponse: result.incotermsResponse || '',
proposedContractDeliveryDate: result.proposedContractDeliveryDate || '',
- proposedShippingPort: result.proposedShippingPort || '',
- proposedDestinationPort: result.proposedDestinationPort || '',
- priceAdjustmentResponse: result.priceAdjustmentResponse || false,
- isInitialResponse: result.isInitialResponse || false,
- sparePartResponse: result.sparePartResponse || '',
additionalProposals: result.additionalProposals || '',
- isAttendingMeeting: result.isAttendingMeeting || false,
})
}
+
+ // PR 아이템 설정
+ setPrItems(prItemsResult)
+
+ // 사전견적 데이터를 본입찰용으로 로드 (응찰 확정 시 또는 사전견적이 있는 경우)
+ if (result?.biddingCompanyId) {
+ try {
+ // 사전견적 데이터를 가져와서 본입찰용으로 변환
+ const preQuoteData = await getSavedPrItemQuotations(result.biddingCompanyId)
+
+ // 사전견적 데이터를 본입찰 포맷으로 변환
+ const convertedQuotations = preQuoteData.map(item => ({
+ prItemId: item.prItemId,
+ bidUnitPrice: item.bidUnitPrice,
+ bidAmount: item.bidAmount,
+ proposedDeliveryDate: item.proposedDeliveryDate || '',
+ technicalSpecification: item.technicalSpecification || undefined
+ }))
+
+ setPrItemQuotations(convertedQuotations)
+
+ // 총 금액 계산
+ const total = convertedQuotations.reduce((sum, q) => sum + q.bidAmount, 0)
+ setTotalQuotationAmount(total)
+
+ // 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정
+ if (total > 0 && result.isBiddingParticipated === true) {
+ setResponseData(prev => ({
+ ...prev,
+ finalQuoteAmount: total.toString()
+ }))
+ }
+ } catch (error) {
+ console.error('Failed to load pre-quote data:', error)
+ }
+ }
} catch (error) {
console.error('Failed to load bidding detail:', error)
toast({
@@ -169,53 +205,16 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
loadData()
}, [biddingId, companyId, toast])
- const handleSubmitResponse = () => {
+ // 입찰 참여여부 결정 핸들러
+ const handleParticipationDecision = async (participated: boolean) => {
if (!biddingDetail) return
- // 필수값 검증
- if (!responseData.finalQuoteAmount.trim()) {
- toast({
- title: '유효성 오류',
- description: '견적 금액을 입력해주세요.',
- variant: 'destructive',
- })
- return
- }
-
- startTransition(async () => {
- const result = await submitPartnerResponse(
+ setIsUpdatingParticipation(true)
+ try {
+ const result = await updatePartnerBiddingParticipation(
biddingDetail.biddingCompanyId,
- {
- finalQuoteAmount: parseFloat(responseData.finalQuoteAmount),
- paymentTermsResponse: responseData.paymentTermsResponse,
- taxConditionsResponse: responseData.taxConditionsResponse,
- incotermsResponse: responseData.incotermsResponse,
- proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
- proposedShippingPort: responseData.proposedShippingPort,
- proposedDestinationPort: responseData.proposedDestinationPort,
- priceAdjustmentResponse: responseData.priceAdjustmentResponse,
- isInitialResponse: responseData.isInitialResponse,
- sparePartResponse: responseData.sparePartResponse,
- additionalProposals: responseData.additionalProposals,
- priceAdjustmentForm: responseData.priceAdjustmentResponse ? {
- itemName: priceAdjustmentForm.itemName,
- adjustmentReflectionPoint: priceAdjustmentForm.adjustmentReflectionPoint,
- majorApplicableRawMaterial: priceAdjustmentForm.majorApplicableRawMaterial,
- adjustmentFormula: priceAdjustmentForm.adjustmentFormula,
- rawMaterialPriceIndex: priceAdjustmentForm.rawMaterialPriceIndex,
- referenceDate: priceAdjustmentForm.referenceDate,
- comparisonDate: priceAdjustmentForm.comparisonDate,
- adjustmentRatio: priceAdjustmentForm.adjustmentRatio ? parseFloat(priceAdjustmentForm.adjustmentRatio) : undefined,
- notes: priceAdjustmentForm.notes,
- adjustmentConditions: priceAdjustmentForm.adjustmentConditions,
- majorNonApplicableRawMaterial: priceAdjustmentForm.majorNonApplicableRawMaterial,
- adjustmentPeriod: priceAdjustmentForm.adjustmentPeriod,
- contractorWriter: priceAdjustmentForm.contractorWriter,
- adjustmentDate: priceAdjustmentForm.adjustmentDate,
- nonApplicableReason: priceAdjustmentForm.nonApplicableReason,
- } : undefined
- },
- 'current-user' // TODO: 실제 사용자 ID
+ participated,
+ userId
)
if (result.success) {
@@ -236,6 +235,169 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
variant: 'destructive',
})
}
+ } catch (error) {
+ console.error('Failed to update participation:', error)
+ toast({
+ title: '오류',
+ description: '참여여부 업데이트에 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsUpdatingParticipation(false)
+ }
+ }
+
+ // 품목별 견적 변경 핸들러
+ const handleQuotationsChange = (quotations: PrItemQuotation[]) => {
+ console.log('견적 변경:', quotations)
+ setPrItemQuotations(quotations)
+ }
+
+ // 총 금액 변경 핸들러
+ const handleTotalAmountChange = (total: number) => {
+ setTotalQuotationAmount(total)
+ // 자동으로 총 견적 금액도 업데이트
+ setResponseData(prev => ({
+ ...prev,
+ finalQuoteAmount: total.toString()
+ }))
+ }
+
+ // 임시 저장 핸들러
+ const handleSaveDraft = async () => {
+ if (!biddingDetail || !userId) return
+
+ if (prItemQuotations.length === 0) {
+ toast({
+ title: '저장할 데이터 없음',
+ description: '저장할 품목별 견적이 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ setIsSavingDraft(true)
+ try {
+ const quotationsForSave = prItemQuotations.map(q => ({
+ prItemId: q.prItemId,
+ bidUnitPrice: q.bidUnitPrice,
+ bidAmount: q.bidAmount,
+ proposedDeliveryDate: q.proposedDeliveryDate || undefined,
+ technicalSpecification: q.technicalSpecification
+ }))
+
+ console.log('임시저장 - prItemQuotations:', prItemQuotations)
+ console.log('임시저장 - quotationsForSave:', quotationsForSave)
+
+ const result = await saveBiddingDraft(
+ biddingDetail.biddingCompanyId,
+ quotationsForSave,
+ userId
+ )
+
+ if (result.success) {
+ toast({
+ title: '임시 저장 완료',
+ description: '품목별 견적이 임시 저장되었습니다.',
+ })
+ } else {
+ toast({
+ title: '임시 저장 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ } catch (error) {
+ console.error('Failed to save draft:', error)
+ toast({
+ title: '오류',
+ description: '임시 저장에 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsSavingDraft(false)
+ }
+ }
+
+ const handleSubmitResponse = () => {
+ if (!biddingDetail) return
+
+ // 필수값 검증
+ if (!responseData.finalQuoteAmount.trim()) {
+ toast({
+ title: '유효성 오류',
+ description: '견적 금액을 입력해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ // 품목별 견적이 있는지 확인
+ if (prItems.length > 0 && prItemQuotations.length === 0) {
+ toast({
+ title: '유효성 오류',
+ description: '품목별 견적을 작성해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ setIsSubmitting(true)
+ startTransition(async () => {
+ try {
+ // 1. 입찰 참여 상태를 응찰로 변경
+ const participationResult = await updatePartnerBiddingParticipation(
+ biddingDetail.biddingCompanyId,
+ true, // 응찰
+ userId
+ )
+
+ if (!participationResult.success) {
+ throw new Error(participationResult.error)
+ }
+
+ // 2. 최종 견적 응답 제출 (PR 아이템별 견적 포함)
+ const result = await submitPartnerResponse(
+ biddingDetail.biddingCompanyId,
+ {
+ finalQuoteAmount: parseFloat(responseData.finalQuoteAmount),
+ proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
+ additionalProposals: responseData.additionalProposals,
+ prItemQuotations: prItemQuotations.length > 0 ? prItemQuotations.map(q => ({
+ prItemId: q.prItemId,
+ bidUnitPrice: q.bidUnitPrice,
+ bidAmount: q.bidAmount,
+ proposedDeliveryDate: q.proposedDeliveryDate || undefined,
+ technicalSpecification: q.technicalSpecification
+ })) : undefined,
+ },
+ userId
+ )
+
+ if (result.success) {
+ toast({
+ title: '응찰 완료',
+ description: '견적이 성공적으로 제출되었습니다.',
+ })
+
+ // 데이터 새로고침
+ const updatedDetail = await getBiddingDetailsForPartners(biddingId, companyId)
+ if (updatedDetail) {
+ setBiddingDetail(updatedDetail)
+ }
+ } else {
+ throw new Error(result.error)
+ }
+ } catch (error) {
+ console.error('Failed to submit response:', error)
+ toast({
+ title: '오류',
+ description: error instanceof Error ? error.message : '응찰 제출에 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsSubmitting(false)
+ }
})
}
@@ -296,6 +458,28 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</div>
</div>
+ {/* 입찰 참여여부 상태 표시 */}
+ <div className="flex items-center gap-2">
+ {biddingDetail.isBiddingParticipated === null ? (
+ <div className="flex items-center gap-2">
+ <Badge variant="outline">참여 결정 대기</Badge>
+ <Button
+ onClick={() => handleParticipationDecision(false)}
+ disabled={isUpdatingParticipation}
+ variant="destructive"
+ size="sm"
+ >
+ <XCircle className="w-4 h-4 mr-1" />
+ 미응찰
+ </Button>
+ </div>
+ ) : (
+ <Badge variant={biddingDetail.isBiddingParticipated ? 'default' : 'destructive'}>
+ {biddingDetail.isBiddingParticipated ? '응찰' : '미응찰'}
+ </Badge>
+ )}
+ </div>
+
</div>
{/* 입찰 공고 섹션 */}
@@ -312,14 +496,14 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<Label className="text-sm font-medium text-muted-foreground">프로젝트</Label>
<div className="flex items-center gap-2 mt-1">
<Building2 className="w-4 h-4" />
- <span>{biddingDetail.projectName}</span>
+ <span>{biddingDetail.projectName || '미설정'}</span>
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">품목</Label>
<div className="flex items-center gap-2 mt-1">
<Package className="w-4 h-4" />
- <span>{biddingDetail.itemName}</span>
+ <span>{biddingDetail.itemName || '미설정'}</span>
</div>
</div>
<div>
@@ -338,7 +522,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<Label className="text-sm font-medium text-muted-foreground">담당자</Label>
<div className="flex items-center gap-2 mt-1">
<User className="w-4 h-4" />
- <span>{biddingDetail.managerName}</span>
+ <span>{biddingDetail.managerName || '미설정'}</span>
</div>
</div>
</div>
@@ -372,73 +556,29 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</CardContent>
</Card>
- {/* 현재 설정된 조건 섹션 */}
+
+ {/* 참여 상태에 따른 섹션 표시 */}
+ {biddingDetail.isBiddingParticipated === false ? (
+ /* 미응찰 상태 표시 */
<Card>
<CardHeader>
- <CardTitle>현재 설정된 입찰 조건</CardTitle>
+ <CardTitle className="flex items-center gap-2">
+ <XCircle className="w-5 h-5 text-destructive" />
+ 입찰 참여 거절
+ </CardTitle>
</CardHeader>
<CardContent>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <Label className="text-sm font-medium">지급조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.paymentTermsResponse}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">세금조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.taxConditionsResponse}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">운송조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.incotermsResponse}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">제안 계약납기일</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.proposedContractDeliveryDate ? formatDate(biddingDetail.proposedContractDeliveryDate, 'KR') : '미설정'}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">제안 선적지</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.proposedShippingPort}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">제안 도착지</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.proposedDestinationPort}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">스페어파트 응답</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.sparePartResponse}
- </div>
- </div>
-
- <div>
- <Label className="text-sm font-medium">연동제 적용</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- {biddingDetail.priceAdjustmentResponse ? '적용' : '미적용'}
- </div>
- </div>
+ <div className="text-center py-8">
+ <XCircle className="w-16 h-16 text-destructive mx-auto mb-4" />
+ <h3 className="text-lg font-semibold text-destructive mb-2">입찰에 참여하지 않기로 결정했습니다</h3>
+ <p className="text-muted-foreground">
+ 해당 입찰에 대한 견적 제출 및 관련 기능은 이용할 수 없습니다.
+ </p>
</div>
</CardContent>
</Card>
-
- {/* 응찰 폼 섹션 */}
+ ) : biddingDetail.isBiddingParticipated === true || biddingDetail.isBiddingParticipated === null ? (
+ /* 응찰 폼 섹션 */
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -447,19 +587,19 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="finalQuoteAmount">견적금액 *</Label>
+ {/* 품목별 견적 섹션 */}
+ {/* <div className="space-y-2">
+ <Label htmlFor="finalQuoteAmount">총 견적금액 *</Label>
<Input
id="finalQuoteAmount"
type="number"
value={responseData.finalQuoteAmount}
onChange={(e) => setResponseData({...responseData, finalQuoteAmount: e.target.value})}
- placeholder="견적금액을 입력하세요"
+ placeholder="총 견적금액을 입력하세요"
/>
- </div>
+ </div> */}
- <div className="space-y-2">
+ {/* <div className="space-y-2">
<Label htmlFor="proposedContractDeliveryDate">제안 납품일</Label>
<Input
id="proposedContractDeliveryDate"
@@ -467,289 +607,68 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
value={responseData.proposedContractDeliveryDate}
onChange={(e) => setResponseData({...responseData, proposedContractDeliveryDate: e.target.value})}
/>
+ </div> */}
+
+ {/* 품목별 상세 견적 테이블 */}
+ {prItems.length > 0 ? (
+ <PrItemsPricingTable
+ prItems={prItems}
+ initialQuotations={prItemQuotations}
+ currency={biddingDetail?.currency || 'KRW'}
+ onQuotationsChange={handleQuotationsChange}
+ onTotalAmountChange={handleTotalAmountChange}
+ readOnly={false}
+ />
+ ) : (
+ <div className="border rounded-lg p-4 bg-muted/20">
+ <p className="text-sm text-muted-foreground text-center py-4">
+ 등록된 품목이 없습니다.
+ </p>
</div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="paymentTermsResponse">응답 지급조건</Label>
- <Input
- id="paymentTermsResponse"
- value={responseData.paymentTermsResponse}
- onChange={(e) => setResponseData({...responseData, paymentTermsResponse: e.target.value})}
- placeholder="지급조건에 대한 의견을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="taxConditionsResponse">응답 세금조건</Label>
- <Input
- id="taxConditionsResponse"
- value={responseData.taxConditionsResponse}
- onChange={(e) => setResponseData({...responseData, taxConditionsResponse: e.target.value})}
- placeholder="세금조건에 대한 의견을 입력하세요"
- />
- </div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="incotermsResponse">응답 운송조건</Label>
- <Input
- id="incotermsResponse"
- value={responseData.incotermsResponse}
- onChange={(e) => setResponseData({...responseData, incotermsResponse: e.target.value})}
- placeholder="운송조건에 대한 의견을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="proposedShippingPort">제안 선적지</Label>
- <Input
- id="proposedShippingPort"
- value={responseData.proposedShippingPort}
- onChange={(e) => setResponseData({...responseData, proposedShippingPort: e.target.value})}
- placeholder="선적지를 입력하세요"
- />
- </div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="proposedDestinationPort">제안 도착지</Label>
- <Input
- id="proposedDestinationPort"
- value={responseData.proposedDestinationPort}
- onChange={(e) => setResponseData({...responseData, proposedDestinationPort: e.target.value})}
- placeholder="도착지를 입력하세요"
- />
- </div>
+ )}
- <div className="space-y-2">
- <Label htmlFor="sparePartResponse">스페어파트 응답</Label>
- <Input
- id="sparePartResponse"
- value={responseData.sparePartResponse}
- onChange={(e) => setResponseData({...responseData, sparePartResponse: e.target.value})}
- placeholder="스페어파트 관련 응답을 입력하세요"
- />
- </div>
- </div>
+ {/* 견적 첨부파일 섹션 */}
+ {biddingDetail && userId && (
+ <SimpleFileUpload
+ biddingId={biddingId}
+ companyId={companyId}
+ userId={userId}
+ readOnly={false}
+ />
+ )}
- <div className="space-y-2">
- <Label htmlFor="additionalProposals">추가 제안사항</Label>
+ {/* 기타 사항 */}
+ {/* <div className="space-y-2">
+ <Label htmlFor="additionalProposals">기타 사항</Label>
<Textarea
id="additionalProposals"
value={responseData.additionalProposals}
onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})}
- placeholder="추가 제안사항을 입력하세요"
+ placeholder="기타 특이사항이나 제안사항을 입력하세요"
rows={4}
/>
- </div>
-
- <div className="space-y-4">
- <div className="flex items-center space-x-2">
- <Checkbox
- id="isInitialResponse"
- checked={responseData.isInitialResponse}
- onCheckedChange={(checked) =>
- setResponseData({...responseData, isInitialResponse: !!checked})
- }
- />
- <Label htmlFor="isInitialResponse">초도 공급입니다</Label>
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id="priceAdjustmentResponse"
- checked={responseData.priceAdjustmentResponse}
- onCheckedChange={(checked) =>
- setResponseData({...responseData, priceAdjustmentResponse: !!checked})
- }
- />
- <Label htmlFor="priceAdjustmentResponse">연동제 적용에 동의합니다</Label>
- </div>
- </div>
-
- {/* 연동제 상세 정보 (연동제 적용 시에만 표시) */}
- {responseData.priceAdjustmentResponse && (
- <Card className="mt-6">
- <CardHeader>
- <CardTitle className="text-lg">하도급대금등 연동표</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-2">
- <Label htmlFor="itemName">품목등의 명칭</Label>
- <Input
- id="itemName"
- value={priceAdjustmentForm.itemName}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, itemName: e.target.value})}
- placeholder="품목명을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentReflectionPoint">조정대금 반영시점</Label>
- <Input
- id="adjustmentReflectionPoint"
- value={priceAdjustmentForm.adjustmentReflectionPoint}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentReflectionPoint: e.target.value})}
- placeholder="반영시점을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentRatio">연동 비율 (%)</Label>
- <Input
- id="adjustmentRatio"
- type="number"
- step="0.01"
- value={priceAdjustmentForm.adjustmentRatio}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentRatio: e.target.value})}
- placeholder="비율을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentPeriod">조정주기</Label>
- <Input
- id="adjustmentPeriod"
- value={priceAdjustmentForm.adjustmentPeriod}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentPeriod: e.target.value})}
- placeholder="조정주기를 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="referenceDate">기준시점</Label>
- <Input
- id="referenceDate"
- type="date"
- value={priceAdjustmentForm.referenceDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, referenceDate: e.target.value})}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="comparisonDate">비교시점</Label>
- <Input
- id="comparisonDate"
- type="date"
- value={priceAdjustmentForm.comparisonDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, comparisonDate: e.target.value})}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="contractorWriter">수탁기업(협력사) 작성자</Label>
- <Input
- id="contractorWriter"
- value={priceAdjustmentForm.contractorWriter}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, contractorWriter: e.target.value})}
- placeholder="작성자명을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentDate">조정일</Label>
- <Input
- id="adjustmentDate"
- type="date"
- value={priceAdjustmentForm.adjustmentDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentDate: e.target.value})}
- />
- </div>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="majorApplicableRawMaterial">연동대상 주요 원재료</Label>
- <Textarea
- id="majorApplicableRawMaterial"
- value={priceAdjustmentForm.majorApplicableRawMaterial}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorApplicableRawMaterial: e.target.value})}
- placeholder="연동 대상 원재료를 입력하세요"
- rows={3}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentFormula">하도급대금등 연동 산식</Label>
- <Textarea
- id="adjustmentFormula"
- value={priceAdjustmentForm.adjustmentFormula}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentFormula: e.target.value})}
- placeholder="연동 산식을 입력하세요"
- rows={3}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="rawMaterialPriceIndex">원재료 가격 기준지표</Label>
- <Textarea
- id="rawMaterialPriceIndex"
- value={priceAdjustmentForm.rawMaterialPriceIndex}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, rawMaterialPriceIndex: e.target.value})}
- placeholder="가격 기준지표를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentConditions">조정요건</Label>
- <Textarea
- id="adjustmentConditions"
- value={priceAdjustmentForm.adjustmentConditions}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentConditions: e.target.value})}
- placeholder="조정요건을 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="majorNonApplicableRawMaterial">연동 미적용 주요 원재료</Label>
- <Textarea
- id="majorNonApplicableRawMaterial"
- value={priceAdjustmentForm.majorNonApplicableRawMaterial}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorNonApplicableRawMaterial: e.target.value})}
- placeholder="연동 미적용 원재료를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="nonApplicableReason">연동 미적용 사유</Label>
- <Textarea
- id="nonApplicableReason"
- value={priceAdjustmentForm.nonApplicableReason}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, nonApplicableReason: e.target.value})}
- placeholder="미적용 사유를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="priceAdjustmentNotes">기타 사항</Label>
- <Textarea
- id="priceAdjustmentNotes"
- value={priceAdjustmentForm.notes}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, notes: e.target.value})}
- placeholder="기타 사항을 입력하세요"
- rows={2}
- />
- </div>
- </CardContent>
- </Card>
- )}
-
- <div className="flex justify-end pt-4">
- <Button onClick={handleSubmitResponse} disabled={isPending}>
+ </div> */}
+ {/* 응찰 제출 버튼 - 미응찰 상태가 아닐 때만 표시 */}
+ {(biddingDetail.isBiddingParticipated === true || biddingDetail.isBiddingParticipated === null) && (
+ <div className="flex justify-end pt-4 gap-2">
+ <Button
+ variant="outline"
+ onClick={handleSaveDraft}
+ disabled={isSavingDraft || isSubmitting}
+ className="min-w-[100px]"
+ >
+ <Save className="w-4 h-4 mr-2" />
+ {isSavingDraft ? '저장 중...' : '임시 저장'}
+ </Button>
+ <Button onClick={handleSubmitResponse} disabled={isSubmitting || isSavingDraft} className="min-w-[100px]">
<Send className="w-4 h-4 mr-2" />
- 응찰 제출
+ {isSubmitting ? '제출 중...' : '응찰 제출'}
</Button>
</div>
+ )}
</CardContent>
</Card>
+ ) : null}
</div>
)
}
diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx
index 9f182911..2e8d4164 100644
--- a/lib/bidding/vendor/partners-bidding-list.tsx
+++ b/lib/bidding/vendor/partners-bidding-list.tsx
@@ -9,6 +9,7 @@ import type {
} from '@/types/table'
import { useDataTable } from '@/hooks/use-data-table'
+import { useToast } from '@/hooks/use-toast'
import { DataTable } from '@/components/data-table/data-table'
import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar'
import { getPartnersBiddingListColumns } from './partners-bidding-list-columns'
@@ -32,6 +33,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
const [selectedBiddingForPreQuoteParticipation, setSelectedBiddingForPreQuoteParticipation] = React.useState<any | null>(null)
const router = useRouter()
+ const { toast } = useToast()
// 데이터 새로고침 함수
const refreshData = React.useCallback(async () => {
@@ -89,6 +91,17 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
if (rowAction) {
switch (rowAction.type) {
case 'view':
+ // 본입찰 초대 여부 확인
+ const bidding = rowAction.row.original
+ if (bidding.status === 'bidding_opened' && !bidding.isBiddingInvited) {
+ // 본입찰이 오픈되었지만 초대받지 않은 경우
+ toast({
+ title: '접근 제한',
+ description: '본입찰에 초대받지 않은 업체입니다.',
+ variant: 'destructive',
+ })
+ return
+ }
// 상세 페이지로 이동 (biddingId 사용)
router.push(`/partners/bid/${rowAction.row.original.biddingId}`)
break
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
index 94b76f58..4cd0efdb 100644
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
@@ -746,12 +746,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>