diff options
Diffstat (limited to 'components/bidding/manage/bidding-schedule-editor.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-schedule-editor.tsx | 137 |
1 files changed, 129 insertions, 8 deletions
diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index f3260f04..b5f4aaf0 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -16,7 +16,7 @@ import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog' import { requestBiddingInvitationWithApproval } from '@/lib/bidding/approval-actions' import { prepareBiddingApprovalData } from '@/lib/bidding/approval-actions' import { BiddingInvitationDialog } from '@/lib/bidding/detail/table/bidding-invitation-dialog' -import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from '@/lib/bidding/pre-quote/service' +import { sendBiddingBasicContracts, getSelectedVendorsForBidding, getPrItemsForBidding } from '@/lib/bidding/pre-quote/service' import { registerBidding } from '@/lib/bidding/detail/service' import { useToast } from '@/hooks/use-toast' import { format } from 'date-fns' @@ -61,6 +61,13 @@ interface VendorContractRequirement { agreementYn?: boolean biddingCompanyId: number biddingId: number + isPreQuoteSelected?: boolean + contacts?: Array<{ + id: number + contactName: string + contactEmail: string + contactNumber?: string | null + }> } interface VendorWithContactInfo extends VendorContractRequirement { @@ -216,6 +223,8 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc agreementYn: vendor.agreementYn, biddingCompanyId: vendor.biddingCompanyId, biddingId: vendor.biddingId, + isPreQuoteSelected: vendor.isPreQuoteSelected, + contacts: vendor.contacts || [], })) } else { console.error('선정된 업체 조회 실패:', 'error' in result ? result.error : '알 수 없는 오류') @@ -237,8 +246,64 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc }, [isBiddingInvitationDialogOpen, getSelectedVendors]) // 입찰공고 버튼 클릭 핸들러 - 입찰 초대 다이얼로그 열기 - const handleBiddingInvitationClick = () => { - setIsBiddingInvitationDialogOpen(true) + const handleBiddingInvitationClick = async () => { + try { + // 1. 입찰서 제출기간 검증 + if (!schedule.submissionStartDate || !schedule.submissionEndDate) { + toast({ + title: '입찰서 제출기간 미설정', + description: '입찰서 제출 시작일시와 마감일시를 모두 설정해주세요.', + variant: 'destructive', + }) + return + } + + // 2. 선정된 업체들 조회 및 검증 + const vendors = await getSelectedVendors() + if (vendors.length === 0) { + toast({ + title: '선정된 업체 없음', + description: '입찰에 참여할 업체가 없습니다.', + variant: 'destructive', + }) + return + } + + // 3. 업체 담당자 검증 + const vendorsWithoutContacts = vendors.filter(vendor => + !vendor.contacts || vendor.contacts.length === 0 + ) + if (vendorsWithoutContacts.length > 0) { + toast({ + title: '업체 담당자 정보 부족', + description: `${vendorsWithoutContacts.length}개 업체의 담당자가 없습니다. 각 업체에 담당자를 추가해주세요.`, + variant: 'destructive', + }) + return + } + + // 4. 입찰 품목 검증 + const prItems = await getPrItemsForBidding(biddingId) + if (!prItems || prItems.length === 0) { + toast({ + title: '입찰 품목 없음', + description: '입찰에 포함할 품목이 없습니다.', + variant: 'destructive', + }) + return + } + + // 모든 검증 통과 시 다이얼로그 열기 + setSelectedVendors(vendors) + setIsBiddingInvitationDialogOpen(true) + } catch (error) { + console.error('입찰공고 검증 중 오류 발생:', error) + toast({ + title: '오류', + description: '입찰공고 검증 중 오류가 발생했습니다.', + variant: 'destructive', + }) + } } // 결재 상신 핸들러 - 결재 완료 시 실제 입찰 등록 실행 @@ -331,7 +396,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc // 입찰 초대 발송 핸들러 - 결재 준비 및 결재 다이얼로그 열기 const handleBiddingInvitationSend = async (data: BiddingInvitationData) => { try { - if (!session?.user?.id || !session.user.epId) { + if (!session?.user?.id) { toast({ title: '오류', description: '사용자 정보가 없습니다.', @@ -384,7 +449,18 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc setIsSubmitting(true) try { const userId = session?.user?.id?.toString() || '1' - + + // 입찰서 제출기간 필수 검증 + if (!schedule.submissionStartDate || !schedule.submissionEndDate) { + toast({ + title: '입찰서 제출기간 미설정', + description: '입찰서 제출 시작일시와 마감일시를 모두 설정해주세요.', + variant: 'destructive', + }) + setIsSubmitting(false) + return + } + // 사양설명회 정보 유효성 검사 if (schedule.hasSpecificationMeeting) { if (!specMeetingInfo.meetingDate || !specMeetingInfo.location || !specMeetingInfo.contactPerson) { @@ -430,8 +506,45 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc } const handleScheduleChange = (field: keyof BiddingSchedule, value: string | boolean) => { + // 마감일시 검증 - 현재일 이전 설정 불가 + if (field === 'submissionEndDate' && typeof value === 'string' && value) { + const selectedDate = new Date(value) + const now = new Date() + now.setHours(0, 0, 0, 0) // 시간을 00:00:00으로 설정하여 날짜만 비교 + + if (selectedDate < now) { + toast({ + title: '마감일시 오류', + description: '마감일시는 현재일 이전으로 설정할 수 없습니다.', + variant: 'destructive', + }) + return // 변경을 적용하지 않음 + } + } + + // 긴급여부 미선택 시 당일 제출시작 불가 + if (field === 'submissionStartDate' && typeof value === 'string' && value) { + const selectedDate = new Date(value) + const today = new Date() + today.setHours(0, 0, 0, 0) // 시간을 00:00:00으로 설정 + selectedDate.setHours(0, 0, 0, 0) + + // 현재 긴급 여부 확인 (field가 'isUrgent'인 경우 value 사용, 아니면 기존 schedule 값) + const isUrgent = field === 'isUrgent' ? (value as boolean) : schedule.isUrgent || false + + // 긴급이 아닌 경우 당일 시작 불가 + if (!isUrgent && selectedDate.getTime() === today.getTime()) { + toast({ + title: '제출 시작일시 오류', + description: '긴급 입찰이 아닌 경우 당일 제출 시작은 불가능합니다.', + variant: 'destructive', + }) + return // 변경을 적용하지 않음 + } + } + setSchedule(prev => ({ ...prev, [field]: value })) - + // 사양설명회 실시 여부가 false로 변경되면 상세 정보 초기화 if (field === 'hasSpecificationMeeting' && value === false) { setSpecMeetingInfo({ @@ -480,22 +593,30 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc </h3> <div className="grid grid-cols-2 gap-4"> <div className="space-y-2"> - <Label htmlFor="submission-start">제출 시작일시</Label> + <Label htmlFor="submission-start">제출 시작일시 <span className="text-red-500">*</span></Label> <Input id="submission-start" type="datetime-local" value={schedule.submissionStartDate} onChange={(e) => handleScheduleChange('submissionStartDate', e.target.value)} + className={!schedule.submissionStartDate ? 'border-red-200' : ''} /> + {!schedule.submissionStartDate && ( + <p className="text-sm text-red-500">제출 시작일시는 필수입니다</p> + )} </div> <div className="space-y-2"> - <Label htmlFor="submission-end">제출 마감일시</Label> + <Label htmlFor="submission-end">제출 마감일시 <span className="text-red-500">*</span></Label> <Input id="submission-end" type="datetime-local" value={schedule.submissionEndDate} onChange={(e) => handleScheduleChange('submissionEndDate', e.target.value)} + className={!schedule.submissionEndDate ? 'border-red-200' : ''} /> + {!schedule.submissionEndDate && ( + <p className="text-sm text-red-500">제출 마감일시는 필수입니다</p> + )} </div> </div> </div> |
