diff options
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-content.tsx')
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-content.tsx | 201 |
1 files changed, 200 insertions, 1 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-content.tsx b/lib/bidding/detail/table/bidding-detail-content.tsx index 895016a2..05c7d567 100644 --- a/lib/bidding/detail/table/bidding-detail-content.tsx +++ b/lib/bidding/detail/table/bidding-detail-content.tsx @@ -9,8 +9,17 @@ import { BiddingDetailItemsDialog } from './bidding-detail-items-dialog' import { BiddingDetailTargetPriceDialog } from './bidding-detail-target-price-dialog' import { BiddingPreQuoteItemDetailsDialog } from '../../../bidding/pre-quote/table/bidding-pre-quote-item-details-dialog' import { getPrItemsForBidding } from '../../../bidding/pre-quote/service' +import { checkAllVendorsFinalSubmitted, performBidOpening } from '../bidding-actions' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' +import { useSession } from 'next-auth/react' +import { BiddingNoticeEditor } from '@/lib/bidding/bidding-notice-editor' +import { getBiddingNotice } from '@/lib/bidding/service' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { FileText, Eye, CheckCircle2, AlertCircle } from 'lucide-react' interface BiddingDetailContentProps { bidding: Bidding @@ -27,12 +36,14 @@ export function BiddingDetailContent({ }: BiddingDetailContentProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() + const session = useSession() const [dialogStates, setDialogStates] = React.useState({ items: false, targetPrice: false, selectionReason: false, - award: false + award: false, + biddingNotice: false }) const [, setRefreshTrigger] = React.useState(0) @@ -42,14 +53,119 @@ export function BiddingDetailContent({ const [selectedVendorForDetails, setSelectedVendorForDetails] = React.useState<QuotationVendor | null>(null) const [prItemsForDialog, setPrItemsForDialog] = React.useState<any[]>([]) + // 입찰공고 관련 state + const [biddingNotice, setBiddingNotice] = React.useState<any>(null) + const [isBiddingNoticeLoading, setIsBiddingNoticeLoading] = React.useState(false) + + // 최종제출 현황 관련 state + const [finalSubmissionStatus, setFinalSubmissionStatus] = React.useState<{ + allSubmitted: boolean + totalCompanies: number + submittedCompanies: number + }>({ allSubmitted: false, totalCompanies: 0, submittedCompanies: 0 }) + const [isPerformingBidOpening, setIsPerformingBidOpening] = React.useState(false) + const handleRefresh = React.useCallback(() => { setRefreshTrigger(prev => prev + 1) }, []) + // 입찰공고 로드 함수 + const loadBiddingNotice = React.useCallback(async () => { + if (!bidding.id) return + + setIsBiddingNoticeLoading(true) + try { + const notice = await getBiddingNotice(bidding.id) + setBiddingNotice(notice) + } catch (error) { + console.error('Failed to load bidding notice:', error) + toast({ + title: '오류', + description: '입찰공고문을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsBiddingNoticeLoading(false) + } + }, [bidding.id, toast]) + const openDialog = React.useCallback((type: keyof typeof dialogStates) => { setDialogStates(prev => ({ ...prev, [type]: true })) }, []) + // 최종제출 현황 로드 함수 + const loadFinalSubmissionStatus = React.useCallback(async () => { + if (!bidding.id) return + + try { + const status = await checkAllVendorsFinalSubmitted(bidding.id) + setFinalSubmissionStatus(status) + } catch (error) { + console.error('Failed to load final submission status:', error) + } + }, [bidding.id]) + + // 개찰 핸들러 + const handlePerformBidOpening = async (isEarly: boolean = false) => { + if (!session.data?.user?.id) { + toast({ + title: '권한 없음', + description: '로그인이 필요합니다.', + variant: 'destructive', + }) + return + } + + if (!finalSubmissionStatus.allSubmitted) { + toast({ + title: '개찰 불가', + description: `모든 벤더가 최종 제출해야 개찰할 수 있습니다. (${finalSubmissionStatus.submittedCompanies}/${finalSubmissionStatus.totalCompanies})`, + variant: 'destructive', + }) + return + } + + const message = isEarly ? '조기개찰을 진행하시겠습니까?' : '개찰을 진행하시겠습니까?' + if (!window.confirm(message)) { + return + } + + setIsPerformingBidOpening(true) + try { + const result = await performBidOpening(bidding.id, session.data.user.id.toString(), isEarly) + + if (result.success) { + toast({ + title: '개찰 완료', + description: result.message, + }) + // 페이지 새로고침 + window.location.reload() + } else { + toast({ + title: '개찰 실패', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + console.error('Failed to perform bid opening:', error) + toast({ + title: '오류', + description: '개찰에 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsPerformingBidOpening(false) + } + } + + // 컴포넌트 마운트 시 입찰공고 및 최종제출 현황 로드 + React.useEffect(() => { + loadBiddingNotice() + loadFinalSubmissionStatus() + }, [loadBiddingNotice, loadFinalSubmissionStatus]) + const closeDialog = React.useCallback((type: keyof typeof dialogStates) => { setDialogStates(prev => ({ ...prev, [type]: false })) }, []) @@ -73,8 +189,91 @@ export function BiddingDetailContent({ }) }, [bidding.id, toast]) + // 개찰 버튼 표시 여부 (입찰평가중 상태에서만) + const showBidOpeningButtons = bidding.status === 'evaluation_of_bidding' + return ( <div className="space-y-6"> + {/* 입찰공고 편집 버튼 */} + <div className="flex justify-between items-center"> + <div> + <h2 className="text-2xl font-bold">입찰 상세</h2> + <p className="text-muted-foreground">{bidding.title}</p> + </div> + <Dialog open={dialogStates.biddingNotice} onOpenChange={(open) => setDialogStates(prev => ({ ...prev, biddingNotice: open }))}> + <DialogTrigger asChild> + <Button variant="outline" className="gap-2"> + <FileText className="h-4 w-4" /> + 입찰공고 편집 + </Button> + </DialogTrigger> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> + <DialogHeader> + <DialogTitle>입찰공고 편집</DialogTitle> + </DialogHeader> + <div className="max-h-[60vh] overflow-y-auto"> + <BiddingNoticeEditor + initialData={biddingNotice} + biddingId={bidding.id} + onSaveSuccess={() => setDialogStates(prev => ({ ...prev, biddingNotice: false }))} + /> + </div> + </DialogContent> + </Dialog> + </div> + + {/* 최종제출 현황 및 개찰 버튼 */} + {showBidOpeningButtons && ( + <Card> + <CardContent className="pt-6"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <div> + <div className="flex items-center gap-2 mb-1"> + {finalSubmissionStatus.allSubmitted ? ( + <CheckCircle2 className="h-5 w-5 text-green-600" /> + ) : ( + <AlertCircle className="h-5 w-5 text-yellow-600" /> + )} + <h3 className="text-lg font-semibold">최종제출 현황</h3> + </div> + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + 최종 제출: {finalSubmissionStatus.submittedCompanies}/{finalSubmissionStatus.totalCompanies}개 업체 + </span> + {finalSubmissionStatus.allSubmitted ? ( + <Badge variant="default">모든 업체 제출 완료</Badge> + ) : ( + <Badge variant="secondary">제출 대기 중</Badge> + )} + </div> + </div> + </div> + + {/* 개찰 버튼들 */} + <div className="flex gap-2"> + <Button + onClick={() => handlePerformBidOpening(false)} + disabled={!finalSubmissionStatus.allSubmitted || isPerformingBidOpening} + variant="default" + > + <Eye className="h-4 w-4 mr-2" /> + {isPerformingBidOpening ? '처리 중...' : '개찰'} + </Button> + <Button + onClick={() => handlePerformBidOpening(true)} + disabled={!finalSubmissionStatus.allSubmitted || isPerformingBidOpening} + variant="outline" + > + <Eye className="h-4 w-4 mr-2" /> + {isPerformingBidOpening ? '처리 중...' : '조기개찰'} + </Button> + </div> + </div> + </CardContent> + </Card> + )} + <BiddingDetailVendorTableContent biddingId={bidding.id} bidding={bidding} |
