'use client' import * as React from 'react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { ArrowLeft, User, Users, Send, CheckCircle, XCircle, Save, FileText, Building2, Package } from 'lucide-react' import { formatDate } from '@/lib/utils' import { getBiddingDetailsForPartners, submitPartnerResponse, updatePartnerBiddingParticipation, saveBiddingDraft } from '../detail/service' import { getPrItemsForBidding, getSavedPrItemQuotations } from '@/lib/bidding/pre-quote/service' import { PrItemsPricingTable } from './components/pr-items-pricing-table' import { SimpleFileUpload } from './components/simple-file-upload' import { biddingStatusLabels, contractTypeLabels, biddingTypeLabels } from '@/db/schema' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' import { useSession } from 'next-auth/react' interface PartnersBiddingDetailProps { biddingId: number companyId: number } interface BiddingDetail { id: number biddingNumber: string revision: number | null projectName: string | null itemName: string | null title: string description: string | null content: string | null contractType: string biddingType: string awardCount: string | null contractStartDate: Date | null contractEndDate: Date | null preQuoteDate: Date | null biddingRegistrationDate: Date | null submissionStartDate: Date | null submissionEndDate: Date | null evaluationDate: Date | null currency: string budget: number | null targetPrice: number | null status: string managerName: string | null managerEmail: string | null managerPhone: string | null biddingCompanyId: number biddingId: number invitationStatus: string finalQuoteAmount: number | null finalQuoteSubmittedAt: Date | null isWinner: boolean isAttendingMeeting: boolean | null isBiddingParticipated: boolean | null additionalProposals: string | null responseSubmittedAt: Date | null } interface PrItem { id: number itemNumber: string | null prNumber: string | null itemInfo: string | null materialDescription: string | null quantity: string | null quantityUnit: string | null totalWeight: string | null weightUnit: string | null currency: string | null requestedDeliveryDate: string | null hasSpecDocument: boolean | null } interface BiddingPrItemQuotation { prItemId: number bidUnitPrice: number bidAmount: number proposedDeliveryDate?: string technicalSpecification?: string } export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingDetailProps) { const router = useRouter() const { toast } = useToast() const session = useSession() const [isPending, startTransition] = useTransition() const [biddingDetail, setBiddingDetail] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [isUpdatingParticipation, setIsUpdatingParticipation] = React.useState(false) const [isSavingDraft, setIsSavingDraft] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) // 품목별 견적 관련 상태 const [prItems, setPrItems] = React.useState([]) const [prItemQuotations, setPrItemQuotations] = React.useState([]) const [totalQuotationAmount, setTotalQuotationAmount] = React.useState(0) // 응찰 폼 상태 const [responseData, setResponseData] = React.useState({ finalQuoteAmount: '', proposedContractDeliveryDate: '', additionalProposals: '', }) const userId = session.data?.user?.id || '' // 데이터 로드 React.useEffect(() => { const loadData = async () => { try { setIsLoading(true) const [result, prItemsResult] = await Promise.all([ getBiddingDetailsForPartners(biddingId, companyId), getPrItemsForBidding(biddingId) ]) if (result) { setBiddingDetail(result) // 기존 응답 데이터로 폼 초기화 setResponseData({ finalQuoteAmount: result.finalQuoteAmount?.toString() || '', proposedContractDeliveryDate: result.proposedContractDeliveryDate || '', additionalProposals: result.additionalProposals || '', }) } // PR 아이템 설정 setPrItems(prItemsResult) // 사전견적 데이터를 본입찰용으로 로드 (응찰 확정 시 또는 사전견적이 있는 경우) if (result?.biddingCompanyId) { try { // 사전견적 데이터를 가져와서 본입찰용으로 변환 const preQuoteData = await getSavedPrItemQuotations(result.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) // 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정 if (totalQuotationAmount > 0 && result.isBiddingParticipated === true) { setResponseData(prev => ({ ...prev, finalQuoteAmount: totalQuotationAmount.toString() })) } } catch (error) { console.error('Failed to load pre-quote data:', error) } } } catch (error) { console.error('Failed to load bidding detail:', error) toast({ title: '오류', description: '입찰 정보를 불러오는데 실패했습니다.', variant: 'destructive', }) } finally { setIsLoading(false) } } loadData() }, [biddingId, companyId, toast]) // 입찰 참여여부 결정 핸들러 const handleParticipationDecision = async (participated: boolean) => { if (!biddingDetail) return setIsUpdatingParticipation(true) try { const result = await updatePartnerBiddingParticipation( biddingDetail.biddingCompanyId, participated, userId ) if (result.success) { toast({ 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({ title: '오류', description: result.error, variant: 'destructive', }) } } catch (error) { console.error('Failed to update participation:', error) toast({ title: '오류', description: '참여여부 업데이트에 실패했습니다.', variant: 'destructive', }) } finally { setIsUpdatingParticipation(false) } } // 품목별 견적 변경 핸들러 const handleQuotationsChange = (quotations: BiddingPrItemQuotation[]) => { console.log('견적 변경:', quotations) setPrItemQuotations(quotations) } // 총 금액 변경 핸들러 const handleTotalAmountChange = (total: number) => { setTotalQuotationAmount(total) // 자동으로 총 견적 금액도 업데이트 setResponseData(prev => ({ ...prev, finalQuoteAmount: total.toString() })) } // 임시 저장 핸들러 const handleSaveDraft = async () => { if (!biddingDetail || !userId) return // 입찰 마감 상태 체크 const biddingStatus = biddingDetail.status const isClosed = biddingStatus === 'bidding_closed' || biddingStatus === 'vendor_selected' || biddingStatus === 'bidding_disposal' if (isClosed) { toast({ title: "접근 제한", description: "입찰이 마감되어 더 이상 입찰에 참여할 수 없습니다.", variant: "destructive", }) return } if (prItemQuotations.length === 0) { toast({ title: '저장할 데이터 없음', description: '저장할 품목별 견적이 없습니다.', variant: 'destructive', }) return } setIsSavingDraft(true) try { const quotationsForSave = prItemQuotations.map(q => ({ prItemId: q.prItemId, bidUnitPrice: q.bidUnitPrice, bidAmount: q.bidAmount, proposedDeliveryDate: q.proposedDeliveryDate, technicalSpecification: q.technicalSpecification })) console.log('임시저장 - prItemQuotations:', prItemQuotations) console.log('임시저장 - quotationsForSave:', quotationsForSave) const result = await saveBiddingDraft( biddingDetail.biddingCompanyId, quotationsForSave, userId ) if (result.success) { toast({ title: '임시 저장 완료', description: '품목별 견적이 임시 저장되었습니다.', }) } else { toast({ title: '임시 저장 실패', description: result.error, variant: 'destructive', }) } } catch (error) { console.error('Failed to save draft:', error) toast({ title: '오류', description: '임시 저장에 실패했습니다.', variant: 'destructive', }) } finally { setIsSavingDraft(false) } } const handleSubmitResponse = () => { if (!biddingDetail) return // 입찰 마감 상태 체크 const biddingStatus = biddingDetail.status const isClosed = biddingStatus === 'bidding_closed' || biddingStatus === 'vendor_selected' || biddingStatus === 'bidding_disposal' if (isClosed) { toast({ title: "접근 제한", description: "입찰이 마감되어 더 이상 입찰에 참여할 수 없습니다.", variant: "destructive", }) return } // 필수값 검증 if (!responseData.finalQuoteAmount.trim()) { toast({ title: '유효성 오류', description: '견적 금액을 입력해주세요.', variant: 'destructive', }) return } // 품목별 견적이 있는지 확인 if (prItems.length > 0 && prItemQuotations.length === 0) { toast({ title: '유효성 오류', description: '품목별 견적을 작성해주세요.', variant: 'destructive', }) return } setIsSubmitting(true) startTransition(async () => { try { // 1. 입찰 참여 상태를 응찰로 변경 const participationResult = await updatePartnerBiddingParticipation( biddingDetail.biddingCompanyId, true, // 응찰 userId ) if (!participationResult.success) { throw new Error(participationResult.error) } // 2. 최종 견적 응답 제출 (PR 아이템별 견적 포함) const result = await submitPartnerResponse( biddingDetail.biddingCompanyId, { finalQuoteAmount: parseFloat(responseData.finalQuoteAmount), proposedContractDeliveryDate: responseData.proposedContractDeliveryDate, additionalProposals: responseData.additionalProposals, prItemQuotations: prItemQuotations.length > 0 ? prItemQuotations.map(q => ({ prItemId: q.prItemId, bidUnitPrice: q.bidUnitPrice, bidAmount: q.bidAmount, proposedDeliveryDate: q.proposedDeliveryDate, technicalSpecification: q.technicalSpecification })) : undefined, }, userId ) if (result.success) { toast({ title: '응찰 완료', description: '견적이 성공적으로 제출되었습니다.', }) // 데이터 새로고침 const updatedDetail = await getBiddingDetailsForPartners(biddingId, companyId) if (updatedDetail) { setBiddingDetail(updatedDetail) } } else { throw new Error(result.error) } } catch (error) { console.error('Failed to submit response:', error) toast({ title: '오류', description: error instanceof Error ? error.message : '응찰 제출에 실패했습니다.', variant: 'destructive', }) } finally { setIsSubmitting(false) } }) } if (isLoading) { return (

입찰 정보를 불러오는 중...

) } if (!biddingDetail) { return (

입찰 정보를 찾을 수 없습니다.

) } return (
{/* 헤더 */}

{biddingDetail.title}

{biddingDetail.biddingNumber} Rev. {biddingDetail.revision ?? 0} {biddingStatusLabels[biddingDetail.status]}
{/* 입찰 참여여부 상태 표시 */}
{biddingDetail.isBiddingParticipated === null ? '참여 결정 대기' : biddingDetail.isBiddingParticipated === true ? '응찰' : '미응찰'}
{/* 입찰 공고 섹션 */} 입찰 공고
{biddingDetail.projectName || '미설정'}
{biddingDetail.itemName || '미설정'}
{contractTypeLabels[biddingDetail.contractType]}
{biddingTypeLabels[biddingDetail.biddingType]}
{biddingDetail.awardCount === 'single' ? '단수' : biddingDetail.awardCount === 'multiple' ? '복수' : '미설정'}
{biddingDetail.managerName || '미설정'}
{/* {biddingDetail.budget && (
{formatCurrency(biddingDetail.budget)}
)} */} {/* 일정 정보 */}
{biddingDetail.submissionStartDate && biddingDetail.submissionEndDate && (
제출기간: {formatDate(biddingDetail.submissionStartDate, 'KR')} ~ {formatDate(biddingDetail.submissionEndDate, 'KR')}
)} {biddingDetail.evaluationDate && (
평가일: {formatDate(biddingDetail.evaluationDate, 'KR')}
)}
{/* 참여 상태에 따른 섹션 표시 */} {biddingDetail.isBiddingParticipated === false ? ( /* 미응찰 상태 표시 */ 입찰 참여 거절

입찰에 참여하지 않기로 결정했습니다

해당 입찰에 대한 견적 제출 및 관련 기능은 이용할 수 없습니다.

) : biddingDetail.isBiddingParticipated === null ? ( /* 참여 의사 확인 섹션 */ 입찰 참여 의사 확인

이 입찰에 참여하시겠습니까?

참여를 선택하시면 견적 작성 및 제출이 가능합니다.

) : biddingDetail.isBiddingParticipated === true ? ( /* 응찰 폼 섹션 */ 응찰하기 {/* 품목별 상세 견적 테이블 */} {prItems.length > 0 ? ( ) : (

등록된 품목이 없습니다.

)} {/* 견적 첨부파일 섹션 */} {biddingDetail && userId && ( )} {/* 응찰 제출 버튼 - 참여 확정 상태일 때만 표시 */}
) : null}
) }