diff options
Diffstat (limited to 'lib/bidding/detail')
4 files changed, 109 insertions, 68 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index b00a4f4f..a603834c 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -969,7 +969,7 @@ export async function markAsDisposal(biddingId: number, userId: string) { // 입찰 등록 (사전견적에서 선정된 업체들에게 본입찰 초대 발송) export async function registerBidding(biddingId: number, userId: string) { try { - // 사전견적에서 선정된 업체들 조회 + // 사전견적에서 선정된 업체들 + 본입찰에서 개별적으로 추가한 업체들 조회 const selectedCompanies = await db .select({ companyId: biddingCompanies.companyId, @@ -1118,18 +1118,18 @@ export async function createRebidding(biddingId: number, userId: string) { return { success: false, error: '재입찰 업데이트에 실패했습니다.' } } - // 참여 업체들의 상태를 대기로 변경 - await db - .update(biddingCompanies) - .set({ - isBiddingParticipated: null, // 대기 상태로 변경 - invitationStatus: 'sent', - updatedAt: new Date() - }) - .where(and( - eq(biddingCompanies.biddingId, biddingId), - eq(biddingCompanies.isBiddingParticipated, true) - )) + // // 참여 업체들의 상태를 대기로 변경 + // await db + // .update(biddingCompanies) + // .set({ + // isBiddingParticipated: null, // 대기 상태로 변경 + // invitationStatus: 'sent', + // updatedAt: new Date() + // }) + // .where(and( + // eq(biddingCompanies.biddingId, biddingId), + // eq(biddingCompanies.isBiddingParticipated, true) + // )) // 재입찰 안내 메일 발송 for (const company of participantCompanies) { @@ -1686,6 +1686,7 @@ export interface PartnersBiddingListItem { isAttendingMeeting: boolean | null isPreQuoteSelected: boolean | null isPreQuoteParticipated: boolean | null + isBiddingParticipated: boolean | null preQuoteDeadline: Date | null isBiddingInvited: boolean | null notes: string | null @@ -1702,7 +1703,9 @@ export interface PartnersBiddingListItem { title: string contractType: string biddingType: string - contractPeriod: string | null + preQuoteDate: Date | null + contractStartDate: Date | null + contractEndDate: Date | null submissionStartDate: Date | null submissionEndDate: Date | null status: string @@ -1733,6 +1736,7 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part isAttendingMeeting: biddingCompanies.isAttendingMeeting, isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, + isBiddingParticipated: biddingCompanies.isBiddingParticipated, preQuoteDeadline: biddingCompanies.preQuoteDeadline, isBiddingInvited: biddingCompanies.isBiddingInvited, notes: biddingCompanies.notes, @@ -1749,7 +1753,9 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part title: biddings.title, contractType: biddings.contractType, biddingType: biddings.biddingType, - contractPeriod: biddings.contractPeriod, + preQuoteDate: biddings.preQuoteDate, + contractStartDate: biddings.contractStartDate, + contractEndDate: biddings.contractEndDate, submissionStartDate: biddings.submissionStartDate, submissionEndDate: biddings.submissionEndDate, status: biddings.status, @@ -1810,7 +1816,9 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: contractType: biddings.contractType, biddingType: biddings.biddingType, awardCount: biddings.awardCount, - contractPeriod: biddings.contractPeriod, + preQuoteDate: biddings.preQuoteDate, + contractStartDate: biddings.contractStartDate, + contractEndDate: biddings.contractEndDate, // 일정 정보 preQuoteDate: biddings.preQuoteDate, @@ -1841,6 +1849,7 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, isBiddingParticipated: biddingCompanies.isBiddingParticipated, isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, + isBiddingParticipated: biddingCompanies.isBiddingParticipated, hasSpecificationMeeting: biddings.hasSpecificationMeeting, // 응답한 조건들 (company_condition_responses) - 제시된 조건과 응답 모두 여기서 관리 paymentTermsResponse: companyConditionResponses.paymentTermsResponse, @@ -2640,3 +2649,31 @@ export async function getVendorPricesForBidding(biddingId: number) { } )() } + +// 사양설명회 참여 여부 업데이트 +export async function setSpecificationMeetingParticipation(biddingCompanyId: number, participated: boolean) { + try { + const result = await db.update(biddingCompanies) + .set({ + isAttendingMeeting: participated, + updatedAt: new Date(), + }) + .where(eq(biddingCompanies.id, biddingCompanyId)) + .returning({ biddingId: biddingCompanies.biddingId }) + + if (result.length > 0) { + const biddingId = result[0].biddingId + revalidateTag(`bidding-${biddingId}`) + revalidateTag('quotation-vendors') + revalidatePath(`/partners/bid/${biddingId}`) + } + + return { + success: true, + message: `사양설명회 참여상태가 ${participated ? '참여' : '불참'}로 업데이트되었습니다.`, + } + } catch (error) { + console.error('Failed to update specification meeting participation:', error) + return { success: false, error: '사양설명회 참여상태 업데이트에 실패했습니다.' } + } +}
\ No newline at end of file diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index cbdf79c2..3b42cc88 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -142,7 +142,7 @@ export function getBiddingDetailVendorColumns({ status === 'rejected' ? 'destructive' : 'outline' const label = status === 'selected' ? '선정' : - status === 'submitted' ? '제출' : + status === 'submitted' ? '견적 제출' : status === 'rejected' ? '거절' : '대기' return <Badge variant={variant}>{label}</Badge> diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 893fb185..eec44bb1 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -68,6 +68,14 @@ export function BiddingDetailVendorToolbarActions({ } const handleRegister = () => { + if (bidding.status !== 'set_target_price') { + toast({ + title: '오류', + description: '내정가 산정이 완료되어야 입찰 등록을 할 수 있습니다.', + variant: 'destructive', + }) + return + } // 본입찰 초대 다이얼로그 열기 setIsBiddingInvitationDialogOpen(true) } diff --git a/lib/bidding/detail/table/bidding-invitation-dialog.tsx b/lib/bidding/detail/table/bidding-invitation-dialog.tsx index 031231a1..48b235f9 100644 --- a/lib/bidding/detail/table/bidding-invitation-dialog.tsx +++ b/lib/bidding/detail/table/bidding-invitation-dialog.tsx @@ -159,7 +159,7 @@ export function BiddingInvitationDialog({ try { const [contractsResult, templatesData] = await Promise.all([ getSelectedVendorsForBidding(biddingId), - getActiveContractTemplates() + getActiveContractTemplates(), ]); // 기존 계약 조회 (사전견적에서 보낸 기본계약 확인) - 서버 액션 사용 @@ -184,7 +184,6 @@ export function BiddingInvitationDialog({ checked: false })); setSelectedContracts(initialSelected); - } catch (error) { console.error('초기 데이터 로드 실패:', error); toast({ @@ -318,36 +317,34 @@ export function BiddingInvitationDialog({ const handleSendInvitation = () => { const selectedContractTemplates = selectedContracts.filter(c => c.checked); - if (selectedContractTemplates.length === 0) { - toast({ - title: '알림', - description: '발송할 기본계약서를 선택해주세요.', - variant: 'default', - }) - return - } - startTransition(async () => { try { - // 선택된 템플릿에 따라 PDF 생성 - setIsGeneratingPdfs(true) - setPdfGenerationProgress(0) + let generatedPdfs: Array<{ + key: string + buffer: number[] + fileName: string + }> = [] const generatedPdfsMap = new Map<string, { buffer: number[], fileName: string }>() - let generatedCount = 0; - for (const vendor of selectedVendors) { - // 사전견적에서 이미 기본계약을 보낸 벤더인지 확인 - const hasExistingContract = existingContracts.some((ec: any) => - ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId - ); - - if (hasExistingContract) { - console.log(`벤더 ${vendor.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`); - generatedCount++; - setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); - continue; - } + // 선택된 템플릿이 있는 경우에만 PDF 생성 + if (selectedContractTemplates.length > 0) { + setIsGeneratingPdfs(true) + setPdfGenerationProgress(0) + + let generatedCount = 0; + for (const vendor of selectedVendors) { + // 사전견적에서 이미 기본계약을 보낸 벤더인지 확인 + const hasExistingContract = existingContracts.some((ec: any) => + ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId + ); + + if (hasExistingContract) { + console.log(`벤더 ${vendor.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`); + generatedCount++; + setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); + continue; + } for (const contract of selectedContractTemplates) { setCurrentGeneratingContract(`${vendor.vendorName} - ${contract.templateName}`); @@ -374,7 +371,16 @@ export function BiddingInvitationDialog({ setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); } - setIsGeneratingPdfs(false); + setIsGeneratingPdfs(false); + + const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ + key, + buffer: data.buffer, + fileName: data.fileName, + })); + + generatedPdfs = pdfsArray; + } const vendorData = selectedVendors.map(vendor => { const hasExistingContract = existingContracts.some((ec: any) => @@ -400,15 +406,9 @@ export function BiddingInvitationDialog({ }; }); - const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ - key, - buffer: data.buffer, - fileName: data.fileName, - })); - await onSend({ vendors: vendorData, - generatedPdfs: pdfsArray, + generatedPdfs: generatedPdfs, message: additionalMessage }); @@ -428,7 +428,7 @@ export function BiddingInvitationDialog({ return ( <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col"> + <DialogContent className="sm:max-w-[800px] max-h-[90vh] flex flex-col" style={{width:900, maxWidth:900}}> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Mail className="w-5 h-5" /> @@ -448,7 +448,7 @@ export function BiddingInvitationDialog({ <AlertTitle className="text-orange-800">기존 계약 정보</AlertTitle> <AlertDescription className="text-orange-700"> 사전견적에서 이미 기본계약을 받은 업체가 있습니다. - 해당 업체들은 계약서 재생성을 건너뜁니다. + 해당 업체들은 계약서 재생성을 건너뜁니다. (본입찰 초대는 정상 진행됩니다) </AlertDescription> </Alert> )} @@ -494,18 +494,21 @@ export function BiddingInvitationDialog({ <div> <h4 className="text-sm font-medium text-orange-700 mb-2 flex items-center gap-2"> <X className="h-4 w-4 text-orange-600" /> - 기존 계약 존재 (건너뜀) ({vendorsWithExistingContracts.length}개) + 기존 계약 존재 (계약서 재생성 건너뜀) ({vendorsWithExistingContracts.length}개) </h4> <div className="space-y-2 max-h-32 overflow-y-auto"> {vendorsWithExistingContracts.map((vendor) => ( - <div key={vendor.vendorId} className="flex items-center gap-2 text-sm p-2 bg-orange-50 rounded border border-orange-200 opacity-75"> + <div key={vendor.vendorId} className="flex items-center gap-2 text-sm p-2 bg-orange-50 rounded border border-orange-200"> <X className="h-4 w-4 text-orange-600" /> - <span className="text-muted-foreground">{vendor.vendorName}</span> + <span className="font-medium">{vendor.vendorName}</span> <Badge variant="outline" className="text-xs"> {vendor.vendorCode} </Badge> - <Badge variant="secondary" className="text-xs"> - 계약 존재 + <Badge variant="secondary" className="text-xs bg-orange-100 text-orange-800"> + 계약 존재 (재생성 건너뜀) + </Badge> + <Badge variant="outline" className="text-xs border-green-500 text-green-700"> + 본입찰 초대 </Badge> </div> ))} @@ -522,7 +525,7 @@ export function BiddingInvitationDialog({ <CardHeader className="pb-3"> <CardTitle className="flex items-center gap-2 text-base"> <FileText className="h-5 w-5 text-blue-600" /> - 기본계약 선택 + 기본계약 선택 (선택사항) </CardTitle> </CardHeader> <CardContent className="space-y-4"> @@ -657,7 +660,7 @@ export function BiddingInvitationDialog({ </Button> <Button onClick={handleSendInvitation} - disabled={isPending || selectedContractCount === 0 || isGeneratingPdfs} + disabled={isPending || selectedVendors.length === 0 || isGeneratingPdfs} className="w-full sm:w-auto" > {isGeneratingPdfs ? ( @@ -678,13 +681,6 @@ export function BiddingInvitationDialog({ )} </Button> </div> - {/* {(selectedContractCount > 0) && ( - <div className="mt-4 sm:mt-0 text-sm text-muted-foreground"> - <p> - {selectedVendors.length}개 업체에 <strong>{selectedContractCount}개</strong>의 기본계약서를 발송합니다. - </p> - </div> - )} */} </DialogFooter> </DialogContent> </Dialog> |
