summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor/partners-bidding-pre-quote.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor/partners-bidding-pre-quote.tsx')
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx389
1 files changed, 303 insertions, 86 deletions
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
index 4cd0efdb..fdd05154 100644
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
@@ -30,7 +30,8 @@ import {
submitPreQuoteResponse,
getPrItemsForBidding,
getSavedPrItemQuotations,
- savePreQuoteDraft
+ savePreQuoteDraft,
+ setPreQuoteParticipation
} from '../pre-quote/service'
import { getBiddingConditions } from '../service'
import { getPriceAdjustmentFormByBiddingCompanyId } from '../detail/service'
@@ -80,6 +81,7 @@ interface BiddingDetail {
invitationStatus: string | null
preQuoteAmount: string | null
preQuoteSubmittedAt: string | null
+ preQuoteDeadline: string | null
isPreQuoteSelected: boolean | null
isAttendingMeeting: boolean | null
// companyConditionResponses에서 가져온 조건들 (제시된 조건과 응답 모두)
@@ -126,6 +128,9 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
isAttendingMeeting: false,
})
+ // 사전견적 참여의사 상태
+ const [participationDecision, setParticipationDecision] = React.useState<boolean | null>(null)
+
// 연동제 폼 상태
const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({
itemName: '',
@@ -211,6 +216,9 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
additionalProposals: result.additionalProposals || '',
isAttendingMeeting: result.isAttendingMeeting || false,
})
+
+ // 사전견적 참여의사 초기화
+ setParticipationDecision(result.isPreQuoteParticipated)
}
if (conditions) {
@@ -238,64 +246,131 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
// 임시저장 기능
const handleTempSave = () => {
- if (!biddingDetail) return
+ if (!biddingDetail || !biddingDetail.biddingCompanyId) {
+ toast({
+ title: '임시저장 실패',
+ description: '입찰 정보가 올바르지 않습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!userId) {
+ toast({
+ title: '임시저장 실패',
+ description: '사용자 정보를 확인할 수 없습니다. 다시 로그인해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
setIsSaving(true)
startTransition(async () => {
- const result = await savePreQuoteDraft(
+ try {
+ const result = await savePreQuoteDraft(
+ biddingDetail.biddingCompanyId!,
+ {
+ prItemQuotations,
+ 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
+ },
+ userId
+ )
+
+ if (result.success) {
+ toast({
+ title: '임시저장 완료',
+ description: result.message,
+ })
+ } else {
+ toast({
+ title: '임시저장 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ } catch (error) {
+ console.error('Temp save error:', error)
+ toast({
+ title: '임시저장 실패',
+ description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsSaving(false)
+ }
+ })
+ }
+
+ // 사전견적 참여의사 설정 함수
+ const handleParticipationDecision = async (participate: boolean) => {
+ if (!biddingDetail?.biddingCompanyId) return
+
+ startTransition(async () => {
+ const result = await setPreQuoteParticipation(
biddingDetail.biddingCompanyId!,
- {
- prItemQuotations,
- 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
+ participate,
+ userId
)
if (result.success) {
+ setParticipationDecision(participate)
toast({
- title: '임시저장 완료',
- description: result.message,
+ title: '설정 완료',
+ description: `사전견적 ${participate ? '참여' : '미참여'}로 설정되었습니다.`,
})
} else {
toast({
- title: '임시저장 실패',
+ title: '설정 실패',
description: result.error,
variant: 'destructive',
})
}
- setIsSaving(false)
})
}
const handleSubmitResponse = () => {
if (!biddingDetail) return
+ // 견적마감일 체크
+ if (biddingDetail.preQuoteDeadline) {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ if (deadline < now) {
+ toast({
+ title: '견적 마감',
+ description: '견적 마감일이 지나 제출할 수 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+ }
+
// 필수값 검증
if (prItemQuotations.length === 0 || totalAmount === 0) {
toast({
@@ -342,7 +417,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
const result = await submitPreQuoteResponse(
biddingDetail.biddingCompanyId!,
submissionData,
- 'current-user' // TODO: 실제 사용자 ID
+ userId
)
console.log('제출 결과:', result)
@@ -493,7 +568,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<span>{biddingDetail.itemName}</span>
</div>
</div>
- <div>
+ {/* <div>
<Label className="text-sm font-medium text-muted-foreground">계약구분</Label>
<div className="mt-1">{contractTypeLabels[biddingDetail.contractType]}</div>
</div>
@@ -504,7 +579,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<div>
<Label className="text-sm font-medium text-muted-foreground">낙찰수</Label>
<div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : '복수'}</div>
- </div>
+ </div> */}
<div>
<Label className="text-sm font-medium text-muted-foreground">담당자</Label>
<div className="flex items-center gap-2 mt-1">
@@ -514,7 +589,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</div>
</div>
- {biddingDetail.budget && (
+ {/* {biddingDetail.budget && (
<div>
<Label className="text-sm font-medium text-muted-foreground">예산</Label>
<div className="flex items-center gap-2 mt-1">
@@ -522,10 +597,10 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<span className="font-semibold">{formatCurrency(biddingDetail.budget)}</span>
</div>
</div>
- )}
+ )} */}
{/* 일정 정보 */}
- <div className="pt-4 border-t">
+ {/* <div className="pt-4 border-t">
<Label className="text-sm font-medium text-muted-foreground mb-2 block">일정 정보</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
{biddingDetail.submissionStartDate && biddingDetail.submissionEndDate && (
@@ -539,7 +614,60 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</div>
)}
</div>
- </div>
+ </div> */}
+
+ {/* 견적마감일 정보 */}
+ {biddingDetail.preQuoteDeadline && (
+ <div className="pt-4 border-t">
+ <Label className="text-sm font-medium text-muted-foreground mb-2 block">견적 마감 정보</Label>
+ {(() => {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ const isExpired = deadline < now
+ const timeLeft = deadline.getTime() - now.getTime()
+ const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
+ const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+
+ return (
+ <div className={`p-3 rounded-lg border-2 ${
+ isExpired
+ ? 'border-red-200 bg-red-50'
+ : daysLeft <= 1
+ ? 'border-orange-200 bg-orange-50'
+ : 'border-green-200 bg-green-50'
+ }`}>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Calendar className="w-5 h-5" />
+ <span className="font-medium">견적 마감일:</span>
+ <span className="text-lg font-semibold">
+ {formatDate(biddingDetail.preQuoteDeadline, 'KR')}
+ </span>
+ </div>
+ {isExpired ? (
+ <Badge variant="destructive" className="ml-2">
+ 마감됨
+ </Badge>
+ ) : daysLeft <= 1 ? (
+ <Badge variant="secondary" className="ml-2 bg-orange-100 text-orange-800">
+ {daysLeft === 0 ? `${hoursLeft}시간 남음` : `${daysLeft}일 남음`}
+ </Badge>
+ ) : (
+ <Badge variant="secondary" className="ml-2 bg-green-100 text-green-800">
+ {daysLeft}일 남음
+ </Badge>
+ )}
+ </div>
+ {isExpired && (
+ <div className="mt-2 text-sm text-red-600">
+ ⚠️ 견적 마감일이 지났습니다. 견적 제출이 불가능합니다.
+ </div>
+ )}
+ </div>
+ )
+ })()}
+ </div>
+ )}
</CardContent>
</Card>
@@ -617,28 +745,124 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</Card>
)}
- {/* 품목별 견적 작성 섹션 */}
- {prItems.length > 0 && (
- <PrItemsPricingTable
- prItems={prItems}
- initialQuotations={prItemQuotations}
- currency={biddingDetail?.currency || 'KRW'}
- onQuotationsChange={setPrItemQuotations}
- onTotalAmountChange={setTotalAmount}
- readOnly={false}
- />
- )}
+ {/* 사전견적 참여의사 결정 섹션 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Users className="w-5 h-5" />
+ 사전견적 참여의사 결정
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ {participationDecision === null ? (
+ <div className="space-y-4">
+ <p className="text-muted-foreground">
+ 해당 입찰의 사전견적에 참여하시겠습니까?
+ </p>
+ <div className="flex gap-3">
+ <Button
+ onClick={() => handleParticipationDecision(true)}
+ disabled={isPending}
+ className="flex items-center gap-2"
+ >
+ <CheckCircle className="w-4 h-4" />
+ 참여
+ </Button>
+ <Button
+ variant="outline"
+ onClick={() => handleParticipationDecision(false)}
+ disabled={isPending}
+ className="flex items-center gap-2"
+ >
+ <XCircle className="w-4 h-4" />
+ 미참여
+ </Button>
+ </div>
+ </div>
+ ) : (
+ <div className="space-y-4">
+ <div className={`flex items-center gap-2 p-3 rounded-lg ${
+ participationDecision ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
+ }`}>
+ {participationDecision ? (
+ <CheckCircle className="w-5 h-5" />
+ ) : (
+ <XCircle className="w-5 h-5" />
+ )}
+ <span className="font-medium">
+ 사전견적 {participationDecision ? '참여' : '미참여'}로 설정되었습니다.
+ </span>
+ </div>
+ {participationDecision === false && (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-muted-foreground">
+ 미참여로 설정되어 견적 작성 섹션이 숨겨집니다. 참여하시려면 아래 버튼을 클릭해주세요.
+ </p>
+ </div>
+ )}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setParticipationDecision(null)}
+ disabled={isPending}
+ >
+ 결정 변경하기
+ </Button>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 참여 결정 시에만 견적 작성 섹션들 표시 (단, 견적마감일이 지나지 않은 경우에만) */}
+ {participationDecision === true && (() => {
+ // 견적마감일 체크
+ if (biddingDetail?.preQuoteDeadline) {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ const isExpired = deadline < now
+
+ if (isExpired) {
+ return (
+ <Card>
+ <CardContent className="pt-6">
+ <div className="text-center py-8">
+ <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
+ <h3 className="text-lg font-semibold text-red-700 mb-2">견적 마감</h3>
+ <p className="text-muted-foreground">
+ 견적 마감일({formatDate(biddingDetail.preQuoteDeadline, 'KR')})이 지나 견적 제출이 불가능합니다.
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+ )
+ }
+ }
+
+ return true // 견적 작성 가능
+ })() && (
+ <>
+ {/* 품목별 견적 작성 섹션 */}
+ {prItems.length > 0 && (
+ <PrItemsPricingTable
+ prItems={prItems}
+ initialQuotations={prItemQuotations}
+ currency={biddingDetail?.currency || 'KRW'}
+ onQuotationsChange={setPrItemQuotations}
+ onTotalAmountChange={setTotalAmount}
+ readOnly={false}
+ />
+ )}
- {/* 견적 문서 업로드 섹션 */}
- <SimpleFileUpload
- biddingId={biddingId}
- companyId={companyId}
- userId={userId}
- readOnly={false}
- />
+ {/* 견적 문서 업로드 섹션 */}
+ <SimpleFileUpload
+ biddingId={biddingId}
+ companyId={companyId}
+ userId={userId}
+ readOnly={false}
+ />
- {/* 사전견적 폼 섹션 */}
- <Card>
+ {/* 사전견적 폼 섹션 */}
+ <Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Send className="w-5 h-5" />
@@ -952,30 +1176,23 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
)}
<div className="flex justify-end gap-2 pt-4">
- <>
- <Button
- variant="outline"
- onClick={handleTempSave}
- disabled={isSaving || isPending}
- >
- <Save className="w-4 h-4 mr-2" />
- {isSaving ? '저장중...' : '임시저장'}
- </Button>
- <Button onClick={handleSubmitResponse} disabled={isPending || isSaving}>
- <Send className="w-4 h-4 mr-2" />
- 사전견적 제출
- </Button>
- </>
-
- {/* {biddingDetail?.invitationStatus === 'submitted' && (
- <div className="flex items-center gap-2 text-green-600">
- <CheckCircle className="w-5 h-5" />
- <span className="font-medium">사전견적이 제출되었습니다</span>
- </div>
- )} */}
+ <Button
+ variant="outline"
+ onClick={handleTempSave}
+ disabled={isSaving || isPending}
+ >
+ <Save className="w-4 h-4 mr-2" />
+ {isSaving ? '저장중...' : '임시저장'}
+ </Button>
+ <Button onClick={handleSubmitResponse} disabled={isPending || isSaving}>
+ <Send className="w-4 h-4 mr-2" />
+ 사전견적 제출
+ </Button>
</div>
</CardContent>
</Card>
+ </>
+ )}
</div>
)
}