diff options
Diffstat (limited to 'lib/bidding/vendor/partners-bidding-pre-quote.tsx')
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-pre-quote.tsx | 389 |
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> ) } |
