'use client' import * as React from 'react' import { Calendar, Save, RefreshCw, Clock, Send } from 'lucide-react' import { updateBiddingSchedule, getBiddingById, getSpecificationMeetingDetailsAction } from '@/lib/bidding/service' import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' 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 { registerBidding } from '@/lib/bidding/detail/service' import { useToast } from '@/hooks/use-toast' interface BiddingSchedule { submissionStartDate?: string submissionEndDate?: string remarks?: string isUrgent?: boolean hasSpecificationMeeting?: boolean } interface SpecificationMeetingInfo { meetingDate: string meetingTime: string location: string address: string contactPerson: string contactPhone: string contactEmail: string agenda: string materials: string notes: string isRequired: boolean } interface BiddingScheduleEditorProps { biddingId: number readonly?: boolean } interface VendorContractRequirement { vendorId: number vendorName: string vendorCode?: string vendorCountry?: string vendorEmail?: string contactPerson?: string contactEmail?: string ndaYn?: boolean generalGtcYn?: boolean projectGtcYn?: boolean agreementYn?: boolean biddingCompanyId: number biddingId: number } interface VendorWithContactInfo extends VendorContractRequirement { contacts: Array<{ id: number contactName: string contactEmail: string contactPhone?: string | null contactPosition?: string | null contactDepartment?: string | null }> selectedMainEmail: string additionalEmails: string[] customEmails: Array<{ id: string email: string name?: string }> hasExistingContracts: boolean } interface BiddingInvitationData { vendors: VendorWithContactInfo[] generatedPdfs: Array<{ key: string buffer: number[] fileName: string }> message?: string } export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingScheduleEditorProps) { const { data: session } = useSession() const router = useRouter() const { toast } = useToast() const [schedule, setSchedule] = React.useState({}) const [specMeetingInfo, setSpecMeetingInfo] = React.useState({ meetingDate: '', meetingTime: '', location: '', address: '', contactPerson: '', contactPhone: '', contactEmail: '', agenda: '', materials: '', notes: '', isRequired: false, }) const [isLoading, setIsLoading] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) const [biddingInfo, setBiddingInfo] = React.useState<{ title: string; projectName?: string; status: string } | null>(null) const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false) const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false) const [selectedVendors, setSelectedVendors] = React.useState([]) const [approvalVariables, setApprovalVariables] = React.useState>({}) const [approvalTitle, setApprovalTitle] = React.useState('') const [invitationData, setInvitationData] = React.useState(null) // 데이터 로딩 React.useEffect(() => { const loadSchedule = async () => { setIsLoading(true) try { const bidding = await getBiddingById(biddingId) if (bidding) { // 입찰 정보 저장 setBiddingInfo({ title: bidding.title || '', projectName: bidding.projectName || undefined, status: bidding.status || '', }) // 날짜를 문자열로 변환하는 헬퍼 const formatDateTime = (date: unknown): string => { if (!date) return '' if (typeof date === 'string') { // 이미 datetime-local 형식인 경우 if (date.includes('T')) { return date.slice(0, 16) } return date } if (date instanceof Date) return date.toISOString().slice(0, 16) return '' } setSchedule({ submissionStartDate: formatDateTime(bidding.submissionStartDate), submissionEndDate: formatDateTime(bidding.submissionEndDate), remarks: bidding.remarks || '', isUrgent: bidding.isUrgent || false, hasSpecificationMeeting: bidding.hasSpecificationMeeting || false, }) // 사양설명회 정보 로드 if (bidding.hasSpecificationMeeting) { try { const meetingDetails = await getSpecificationMeetingDetailsAction(biddingId) if (meetingDetails.success && meetingDetails.data) { const meeting = meetingDetails.data setSpecMeetingInfo({ meetingDate: meeting.meetingDate ? new Date(meeting.meetingDate).toISOString().slice(0, 16) : '', meetingTime: meeting.meetingTime || '', location: meeting.location || '', address: meeting.address || '', contactPerson: meeting.contactPerson || '', contactPhone: meeting.contactPhone || '', contactEmail: meeting.contactEmail || '', agenda: meeting.agenda || '', materials: meeting.materials || '', notes: meeting.notes || '', isRequired: meeting.isRequired || false, }) } } catch (error) { console.error('Failed to load specification meeting details:', error) } } } } catch (error) { console.error('Failed to load schedule:', error) toast({ title: '오류', description: '일정 정보를 불러오는데 실패했습니다.', variant: 'destructive', }) } finally { setIsLoading(false) } } loadSchedule() }, [biddingId, toast]) // 선정된 업체들 조회 const getSelectedVendors = React.useCallback(async (): Promise => { try { const result = await getSelectedVendorsForBidding(biddingId) if (result.success) { // 타입 변환: null을 undefined로 변환 return result.vendors.map((vendor): VendorContractRequirement => ({ vendorId: vendor.vendorId, vendorName: vendor.vendorName, vendorCode: vendor.vendorCode || undefined, vendorCountry: vendor.vendorCountry, vendorEmail: vendor.vendorEmail || undefined, contactPerson: vendor.contactPerson || undefined, contactEmail: vendor.contactEmail || undefined, ndaYn: vendor.ndaYn, generalGtcYn: vendor.generalGtcYn, projectGtcYn: vendor.projectGtcYn, agreementYn: vendor.agreementYn, biddingCompanyId: vendor.biddingCompanyId, biddingId: vendor.biddingId, })) } else { console.error('선정된 업체 조회 실패:', 'error' in result ? result.error : '알 수 없는 오류') return [] } } catch (error) { console.error('선정된 업체 조회 실패:', error) return [] } }, [biddingId]) // 입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회 React.useEffect(() => { if (isBiddingInvitationDialogOpen) { getSelectedVendors().then(vendors => { setSelectedVendors(vendors) }) } }, [isBiddingInvitationDialogOpen, getSelectedVendors]) // 입찰공고 버튼 클릭 핸들러 - 입찰 초대 다이얼로그 열기 const handleBiddingInvitationClick = () => { setIsBiddingInvitationDialogOpen(true) } // 결재 상신 핸들러 - 결재 완료 시 실제 입찰 등록 실행 const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => { try { if (!session?.user?.id || !session.user.epId || !invitationData) { toast({ title: '오류', description: '필요한 정보가 없습니다.', variant: 'destructive', }) return } // 결재 상신 const result = await requestBiddingInvitationWithApproval({ biddingId, vendors: selectedVendors, message: invitationData.message || '', currentUser: { id: session.user.id, epId: session.user.epId, email: session.user.email || undefined, }, approvers, }) if (result.status === 'pending_approval') { toast({ title: '입찰초대 결재 상신 완료', description: `결재가 상신되었습니다. (ID: ${result.approvalId})`, }) setIsApprovalDialogOpen(false) setIsBiddingInvitationDialogOpen(false) setInvitationData(null) router.refresh() } } catch (error) { console.error('결재 상신 중 오류 발생:', error) toast({ title: '오류', description: '결재 상신 중 오류가 발생했습니다.', variant: 'destructive', }) } } // 입찰 초대 발송 핸들러 - 결재 준비 및 결재 다이얼로그 열기 const handleBiddingInvitationSend = async (data: BiddingInvitationData) => { try { if (!session?.user?.id || !session.user.epId) { toast({ title: '오류', description: '사용자 정보가 없습니다.', variant: 'destructive', }) return } // 선정된 업체들 조회 const vendors = await getSelectedVendors() if (vendors.length === 0) { toast({ title: '오류', description: '선정된 업체가 없습니다.', variant: 'destructive', }) return } // 결재 데이터 준비 (템플릿 변수, 제목 등) const approvalData = await prepareBiddingApprovalData({ biddingId, vendors, message: data.message || '', }) // 결재 준비 완료 - invitationData와 결재 데이터 저장 및 결재 다이얼로그 열기 setInvitationData(data) setApprovalVariables(approvalData.variables) setApprovalTitle(`입찰초대 - ${approvalData.bidding.title}`) setIsApprovalDialogOpen(true) } catch (error) { console.error('결재 준비 중 오류 발생:', error) toast({ title: '오류', description: '결재 준비 중 오류가 발생했습니다.', variant: 'destructive', }) } } const handleSave = async () => { setIsSubmitting(true) try { const userId = session?.user?.id?.toString() || '1' // 사양설명회 정보 유효성 검사 if (schedule.hasSpecificationMeeting) { if (!specMeetingInfo.meetingDate || !specMeetingInfo.location || !specMeetingInfo.contactPerson) { toast({ title: '오류', description: '사양설명회 필수 정보가 누락되었습니다. (회의일시, 장소, 담당자)', variant: 'destructive', }) setIsSubmitting(false) return } } const result = await updateBiddingSchedule( biddingId, schedule, userId, schedule.hasSpecificationMeeting ? specMeetingInfo : undefined ) if (result.success) { toast({ title: '성공', description: '일정 정보가 성공적으로 저장되었습니다.', }) } else { toast({ title: '오류', description: 'error' in result ? result.error : '일정 정보 저장에 실패했습니다.', variant: 'destructive', }) } } catch (error) { console.error('Failed to save schedule:', error) toast({ title: '오류', description: '일정 정보 저장에 실패했습니다.', variant: 'destructive', }) } finally { setIsSubmitting(false) } } const handleScheduleChange = (field: keyof BiddingSchedule, value: string | boolean) => { setSchedule(prev => ({ ...prev, [field]: value })) // 사양설명회 실시 여부가 false로 변경되면 상세 정보 초기화 if (field === 'hasSpecificationMeeting' && value === false) { setSpecMeetingInfo({ meetingDate: '', meetingTime: '', location: '', address: '', contactPerson: '', contactPhone: '', contactEmail: '', agenda: '', materials: '', notes: '', isRequired: false, }) } } if (isLoading) { return (
일정 정보를 불러오는 중...
) } return (
입찰 일정 관리

입찰의 주요 일정들을 설정하고 관리합니다.

{/* 입찰서 제출 기간 */}

입찰서 제출 기간

handleScheduleChange('submissionStartDate', e.target.value)} />
handleScheduleChange('submissionEndDate', e.target.value)} />
{/* 긴급 여부 */}

긴급 입찰로 표시할 경우 활성화하세요

handleScheduleChange('isUrgent', checked)} />
{/* 사양설명회 실시 여부 */}

사양설명회를 실시할 경우 상세 정보를 입력하세요

handleScheduleChange('hasSpecificationMeeting', checked)} />
{/* 사양설명회 상세 정보 */} {schedule.hasSpecificationMeeting && (
setSpecMeetingInfo((prev) => ({ ...prev, meetingDate: e.target.value }))} className={!specMeetingInfo.meetingDate ? 'border-red-200' : ''} /> {!specMeetingInfo.meetingDate && (

회의일시는 필수입니다

)}
setSpecMeetingInfo((prev) => ({ ...prev, meetingTime: e.target.value }))} />
setSpecMeetingInfo((prev) => ({ ...prev, location: e.target.value }))} className={!specMeetingInfo.location ? 'border-red-200' : ''} /> {!specMeetingInfo.location && (

회의 장소는 필수입니다

)}
setSpecMeetingInfo((prev) => ({ ...prev, address: e.target.value }))} />
setSpecMeetingInfo((prev) => ({ ...prev, contactPerson: e.target.value }))} className={!specMeetingInfo.contactPerson ? 'border-red-200' : ''} /> {!specMeetingInfo.contactPerson && (

담당자는 필수입니다

)}
setSpecMeetingInfo((prev) => ({ ...prev, contactPhone: e.target.value }))} />
setSpecMeetingInfo((prev) => ({ ...prev, contactEmail: e.target.value }))} />