diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-11 11:21:35 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-11 11:21:35 +0000 |
| commit | 47e527f5f763658600696ee58451fb666e692f5a (patch) | |
| tree | 67f159fd0cbad5e0553c7958caa3075127121e76 /lib/bidding/vendor | |
| parent | ee77f36b1ceece1236d45fba102c3ea410acebc1 (diff) | |
(최겸) 구매 입찰 세부기능 수정
Diffstat (limited to 'lib/bidding/vendor')
| -rw-r--r-- | lib/bidding/vendor/components/pr-items-pricing-table.tsx | 6 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-detail.tsx | 190 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-pre-quote.tsx | 51 |
3 files changed, 172 insertions, 75 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index 1dee7adb..13804251 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -286,6 +286,7 @@ export function PrItemsPricingTable({ <TableHead>단위</TableHead> <TableHead>중량</TableHead> <TableHead>중량단위</TableHead> + <TableHead>SHI 납품요청일</TableHead> <TableHead>견적단가</TableHead> <TableHead>견적금액</TableHead> <TableHead>납품예정일</TableHead> @@ -328,6 +329,11 @@ export function PrItemsPricingTable({ </TableCell> <TableCell>{item.weightUnit || '-'}</TableCell> <TableCell> + {item.requestedDeliveryDate ? + formatDate(item.requestedDeliveryDate, 'KR') : '-' + } + </TableCell> + <TableCell> {readOnly ? ( <span className="font-medium"> {quotation.bidUnitPrice.toLocaleString()} diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx index 8d24ca66..4b316eee 100644 --- a/lib/bidding/vendor/partners-bidding-detail.tsx +++ b/lib/bidding/vendor/partners-bidding-detail.tsx @@ -60,13 +60,13 @@ interface BiddingDetail { content: string | null contractType: string biddingType: string - awardCount: string + awardCount: string | null contractPeriod: string | null - preQuoteDate: string | null - biddingRegistrationDate: string | null - submissionStartDate: string | null - submissionEndDate: string | null - evaluationDate: string | null + preQuoteDate: Date | null + biddingRegistrationDate: Date | null + submissionStartDate: Date | null + submissionEndDate: Date | null + evaluationDate: Date | null currency: string budget: number | null targetPrice: number | null @@ -78,12 +78,12 @@ interface BiddingDetail { biddingId: number invitationStatus: string finalQuoteAmount: number | null - finalQuoteSubmittedAt: string | null + finalQuoteSubmittedAt: Date | null isWinner: boolean isAttendingMeeting: boolean | null isBiddingParticipated: boolean | null additionalProposals: string | null - responseSubmittedAt: string | null + responseSubmittedAt: Date | null } interface PrItem { @@ -101,11 +101,11 @@ interface PrItem { hasSpecDocument: boolean | null } -interface PrItemQuotation { +interface BiddingPrItemQuotation { prItemId: number bidUnitPrice: number bidAmount: number - proposedDeliveryDate?: string | null + proposedDeliveryDate?: string technicalSpecification?: string } @@ -122,7 +122,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD // 품목별 견적 관련 상태 const [prItems, setPrItems] = React.useState<PrItem[]>([]) - const [prItemQuotations, setPrItemQuotations] = React.useState<PrItemQuotation[]>([]) + const [prItemQuotations, setPrItemQuotations] = React.useState<BiddingPrItemQuotation[]>([]) const [totalQuotationAmount, setTotalQuotationAmount] = React.useState(0) // 응찰 폼 상태 @@ -169,7 +169,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD prItemId: item.prItemId, bidUnitPrice: item.bidUnitPrice, bidAmount: item.bidAmount, - proposedDeliveryDate: item.proposedDeliveryDate || '', + proposedDeliveryDate: item.proposedDeliveryDate || undefined, technicalSpecification: item.technicalSpecification || undefined })) @@ -219,14 +219,43 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD if (result.success) { toast({ - title: '성공', - description: result.message, + title: participated ? '참여 확정' : '미참여 확정', + description: participated + ? '입찰에 참여하셨습니다. 이제 견적을 작성할 수 있습니다.' + : '입찰 참여를 거절하셨습니다.', }) // 데이터 새로고침 const updatedDetail = await getBiddingDetailsForPartners(biddingId, companyId) if (updatedDetail) { setBiddingDetail(updatedDetail) + + // 참여 확정 시 사전견적 데이터가 있다면 로드 + if (participated && updatedDetail.biddingCompanyId) { + try { + const preQuoteData = await getSavedPrItemQuotations(updatedDetail.biddingCompanyId) + const convertedQuotations = preQuoteData.map(item => ({ + prItemId: item.prItemId, + bidUnitPrice: item.bidUnitPrice, + bidAmount: item.bidAmount, + proposedDeliveryDate: item.proposedDeliveryDate || undefined, + technicalSpecification: item.technicalSpecification || undefined + })) + + setPrItemQuotations(convertedQuotations) + const total = convertedQuotations.reduce((sum, q) => sum + q.bidAmount, 0) + setTotalQuotationAmount(total) + + if (total > 0) { + setResponseData(prev => ({ + ...prev, + finalQuoteAmount: total.toString() + })) + } + } catch (error) { + console.error('Failed to load pre-quote data after participation:', error) + } + } } } else { toast({ @@ -248,7 +277,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD } // 품목별 견적 변경 핸들러 - const handleQuotationsChange = (quotations: PrItemQuotation[]) => { + const handleQuotationsChange = (quotations: BiddingPrItemQuotation[]) => { console.log('견적 변경:', quotations) setPrItemQuotations(quotations) } @@ -282,7 +311,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD prItemId: q.prItemId, bidUnitPrice: q.bidUnitPrice, bidAmount: q.bidAmount, - proposedDeliveryDate: q.proposedDeliveryDate || undefined, + proposedDeliveryDate: q.proposedDeliveryDate, technicalSpecification: q.technicalSpecification })) @@ -367,7 +396,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD prItemId: q.prItemId, bidUnitPrice: q.bidUnitPrice, bidAmount: q.bidAmount, - proposedDeliveryDate: q.proposedDeliveryDate || undefined, + proposedDeliveryDate: q.proposedDeliveryDate, technicalSpecification: q.technicalSpecification })) : undefined, }, @@ -445,7 +474,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <div className="flex items-center gap-2 mt-1"> <Badge variant="outline" className="font-mono"> {biddingDetail.biddingNumber} - {biddingDetail.revision > 0 && ` Rev.${biddingDetail.revision}`} + {biddingDetail.revision && biddingDetail.revision > 0 && ` Rev.${biddingDetail.revision}`} </Badge> <Badge variant={ biddingDetail.status === 'bidding_disposal' ? 'destructive' : @@ -460,24 +489,13 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD {/* 입찰 참여여부 상태 표시 */} <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> - )} + <Badge variant={ + biddingDetail.isBiddingParticipated === null ? 'outline' : + biddingDetail.isBiddingParticipated === true ? 'default' : 'destructive' + }> + {biddingDetail.isBiddingParticipated === null ? '참여 결정 대기' : + biddingDetail.isBiddingParticipated === true ? '응찰' : '미응찰'} + </Badge> </div> </div> @@ -516,10 +534,10 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </div> <div> <Label className="text-sm font-medium text-muted-foreground">낙찰수</Label> - <div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : '복수'}</div> + <div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : biddingDetail.awardCount === 'multiple' ? '복수' : '미설정'}</div> </div> <div> - <Label className="text-sm font-medium text-muted-foreground">담당자</Label> + <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> @@ -527,7 +545,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </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"> @@ -535,7 +553,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <span className="font-semibold">{formatCurrency(biddingDetail.budget)}</span> </div> </div> - )} + )} */} {/* 일정 정보 */} <div className="pt-4 border-t"> @@ -560,33 +578,73 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD {/* 참여 상태에 따른 섹션 표시 */} {biddingDetail.isBiddingParticipated === false ? ( /* 미응찰 상태 표시 */ - <Card> - <CardHeader> + <Card> + <CardHeader> <CardTitle className="flex items-center gap-2"> <XCircle className="w-5 h-5 text-destructive" /> 입찰 참여 거절 </CardTitle> - </CardHeader> - <CardContent> + </CardHeader> + <CardContent> <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 ? ( + </div> + </CardContent> + </Card> + ) : biddingDetail.isBiddingParticipated === null ? ( + /* 참여 의사 확인 섹션 */ + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Users className="w-5 h-5" /> + 입찰 참여 의사 확인 + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-8"> + <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4"> + <Users className="w-8 h-8 text-primary" /> + </div> + <h3 className="text-lg font-semibold mb-2">이 입찰에 참여하시겠습니까?</h3> + <p className="text-muted-foreground mb-6"> + 참여를 선택하시면 견적 작성 및 제출이 가능합니다. + </p> + <div className="flex justify-center gap-4"> + <Button + onClick={() => handleParticipationDecision(true)} + disabled={isUpdatingParticipation} + className="min-w-[120px]" + > + <CheckCircle className="w-4 h-4 mr-2" /> + 참여하기 + </Button> + <Button + onClick={() => handleParticipationDecision(false)} + disabled={isUpdatingParticipation} + variant="destructive" + className="min-w-[120px]" + > + <XCircle className="w-4 h-4 mr-2" /> + 미참여 + </Button> + </div> + </div> + </CardContent> + </Card> + ) : biddingDetail.isBiddingParticipated === true ? ( /* 응찰 폼 섹션 */ - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Send className="w-5 h-5" /> - 응찰하기 - </CardTitle> - </CardHeader> - <CardContent className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Send className="w-5 h-5" /> + 응찰하기 + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> {/* 품목별 견적 섹션 */} {/* <div className="space-y-2"> <Label htmlFor="finalQuoteAmount">총 견적금액 *</Label> @@ -648,24 +706,22 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD rows={4} /> </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> + 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} diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx index fdd05154..29a37cae 100644 --- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx +++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx @@ -381,6 +381,41 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin return } + // 품목별 납품일 검증 + if (prItemQuotations.length > 0) { + for (const quotation of prItemQuotations) { + if (!quotation.proposedDeliveryDate?.trim()) { + const prItem = prItems.find(item => item.id === quotation.prItemId) + toast({ + title: '유효성 오류', + description: `품목 ${prItem?.itemNumber || quotation.prItemId}의 납품예정일을 입력해주세요.`, + variant: 'destructive', + }) + return + } + } + } + + const requiredFields = [ + { value: responseData.proposedContractDeliveryDate, name: '제안 납품일' }, + { value: responseData.paymentTermsResponse, name: '응답 지급조건' }, + { value: responseData.taxConditionsResponse, name: '응답 세금조건' }, + { value: responseData.incotermsResponse, name: '응답 운송조건' }, + { value: responseData.proposedShippingPort, name: '제안 선적지' }, + { value: responseData.proposedDestinationPort, name: '제안 도착지' }, + { value: responseData.sparePartResponse, name: '스페어파트 응답' }, + ] + + const missingField = requiredFields.find(field => !field.value?.trim()) + if (missingField) { + toast({ + title: '유효성 오류', + description: `${missingField.name}을(를) 입력해주세요.`, + variant: 'destructive', + }) + return + } + startTransition(async () => { const submissionData = { preQuoteAmount: totalAmount, // 품목별 계산된 총 금액 사용 @@ -873,7 +908,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin {/* 총 금액 표시 (읽기 전용) */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-2"> - <Label htmlFor="totalAmount">총 사전견적 금액 *</Label> + <Label htmlFor="totalAmount">총 사전견적 금액 <span className="text-red-500">*</span></Label> <Input id="totalAmount" type="text" @@ -887,7 +922,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="proposedContractDeliveryDate">제안 납품일</Label> + <Label htmlFor="proposedContractDeliveryDate">제안 납품일 <span className="text-red-500">*</span></Label> <Input id="proposedContractDeliveryDate" type="date" @@ -905,7 +940,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-2"> - <Label htmlFor="paymentTermsResponse">응답 지급조건</Label> + <Label htmlFor="paymentTermsResponse">응답 지급조건 <span className="text-red-500">*</span></Label> <Input id="paymentTermsResponse" value={responseData.paymentTermsResponse} @@ -915,7 +950,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="taxConditionsResponse">응답 세금조건</Label> + <Label htmlFor="taxConditionsResponse">응답 세금조건 <span className="text-red-500">*</span></Label> <Input id="taxConditionsResponse" value={responseData.taxConditionsResponse} @@ -927,7 +962,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-2"> - <Label htmlFor="incotermsResponse">응답 운송조건</Label> + <Label htmlFor="incotermsResponse">응답 운송조건 <span className="text-red-500">*</span></Label> <Input id="incotermsResponse" value={responseData.incotermsResponse} @@ -937,7 +972,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="proposedShippingPort">제안 선적지</Label> + <Label htmlFor="proposedShippingPort">제안 선적지 <span className="text-red-500">*</span></Label> <Input id="proposedShippingPort" value={responseData.proposedShippingPort} @@ -949,7 +984,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-2"> - <Label htmlFor="proposedDestinationPort">제안 도착지</Label> + <Label htmlFor="proposedDestinationPort">제안 도착지 <span className="text-red-500">*</span></Label> <Input id="proposedDestinationPort" value={responseData.proposedDestinationPort} @@ -959,7 +994,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="sparePartResponse">스페어파트 응답</Label> + <Label htmlFor="sparePartResponse">스페어파트 응답 <span className="text-red-500">*</span></Label> <Input id="sparePartResponse" value={responseData.sparePartResponse} |
