diff options
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-header.tsx')
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-header.tsx | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx new file mode 100644 index 00000000..3135f37d --- /dev/null +++ b/lib/bidding/detail/table/bidding-detail-header.tsx @@ -0,0 +1,328 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { Bidding, biddingStatusLabels, contractTypeLabels, biddingTypeLabels } from '@/db/schema' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + ArrowLeft, + Send, + RotateCcw, + XCircle, + Calendar, + Building2, + User, + Package, + DollarSign, + Hash +} from 'lucide-react' + +import { formatDate } from '@/lib/utils' +import { + registerBidding, + markAsDisposal, + createRebidding +} from '@/lib/bidding/detail/service' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' + +interface BiddingDetailHeaderProps { + bidding: Bidding +} + +export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { + const router = useRouter() + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + + const handleGoBack = () => { + router.push('/evcp/bid') + } + + const handleRegister = () => { + // 상태 검증 + if (bidding.status !== 'bidding_generated') { + toast({ + title: '실행 불가', + description: '입찰 등록은 입찰 생성 상태에서만 가능합니다.', + variant: 'destructive', + }) + return + } + + if (!confirm('입찰을 등록하시겠습니까?')) return + + startTransition(async () => { + const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID + + if (result.success) { + toast({ + title: '성공', + description: result.message, + }) + router.refresh() + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + }) + } + + const handleMarkAsDisposal = () => { + // 상태 검증 + if (bidding.status !== 'bidding_closed') { + toast({ + title: '실행 불가', + description: '유찰 처리는 입찰 마감 상태에서만 가능합니다.', + variant: 'destructive', + }) + return + } + + if (!confirm('입찰을 유찰 처리하시겠습니까?')) return + + startTransition(async () => { + const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID + + if (result.success) { + toast({ + title: '성공', + description: result.message, + }) + router.refresh() + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + }) + } + + const handleCreateRebidding = () => { + // 상태 검증 + if (bidding.status !== 'bidding_disposal') { + toast({ + title: '실행 불가', + description: '재입찰은 유찰 상태에서만 가능합니다.', + variant: 'destructive', + }) + return + } + + if (!confirm('재입찰을 생성하시겠습니까?')) return + + startTransition(async () => { + const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID + + if (result.success) { + toast({ + title: '성공', + description: result.message, + }) + // 새로 생성된 입찰로 이동 + if (result.data) { + router.push(`/evcp/bid/${result.data.id}`) + } else { + router.refresh() + } + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + }) + } + + const getActionButtons = () => { + const buttons = [] + + // 기본 액션 버튼들 (항상 표시) + buttons.push( + <Button + key="back" + variant="outline" + onClick={handleGoBack} + disabled={isPending} + > + <ArrowLeft className="w-4 h-4 mr-2" /> + 목록으로 + </Button> + ) + + // 모든 액션 버튼을 항상 표시 (상태 검증은 각 핸들러에서) + buttons.push( + <Button + key="register" + onClick={handleRegister} + disabled={isPending} + > + <Send className="w-4 h-4 mr-2" /> + 입찰등록 + </Button> + ) + + buttons.push( + <Button + key="disposal" + variant="destructive" + onClick={handleMarkAsDisposal} + disabled={isPending} + > + <XCircle className="w-4 h-4 mr-2" /> + 유찰 + </Button> + ) + + buttons.push( + <Button + key="rebidding" + onClick={handleCreateRebidding} + disabled={isPending} + > + <RotateCcw className="w-4 h-4 mr-2" /> + 재입찰 + </Button> + ) + + return buttons + } + + return ( + <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> + <div className="px-6 py-4"> + {/* 헤더 메인 영역 */} + <div className="flex items-center justify-between mb-4"> + <div className="flex items-center gap-4 flex-1 min-w-0"> + {/* 제목과 배지 */} + <div className="flex items-center gap-3 flex-1 min-w-0"> + <h1 className="text-xl font-semibold truncate">{bidding.title}</h1> + <div className="flex items-center gap-2 flex-shrink-0"> + <Badge variant="outline" className="font-mono text-xs"> + <Hash className="w-3 h-3 mr-1" /> + {bidding.biddingNumber} + {bidding.revision && bidding.revision > 0 && ` Rev.${bidding.revision}`} + </Badge> + <Badge variant={ + bidding.status === 'bidding_disposal' ? 'destructive' : + bidding.status === 'vendor_selected' ? 'default' : + 'secondary' + } className="text-xs"> + {biddingStatusLabels[bidding.status]} + </Badge> + </div> + </div> + + {/* 액션 버튼들 */} + <div className="flex items-center gap-2 flex-shrink-0"> + {getActionButtons()} + </div> + </div> + </div> + + {/* 세부 정보 영역 */} + <div className="flex flex-wrap items-center gap-6 text-sm"> + {/* 프로젝트 정보 */} + {bidding.projectName && ( + <div className="flex items-center gap-1.5 text-muted-foreground"> + <Building2 className="w-4 h-4" /> + <span className="font-medium">프로젝트:</span> + <span>{bidding.projectName}</span> + </div> + )} + + {/* 품목 정보 */} + {bidding.itemName && ( + <div className="flex items-center gap-1.5 text-muted-foreground"> + <Package className="w-4 h-4" /> + <span className="font-medium">품목:</span> + <span>{bidding.itemName}</span> + </div> + )} + + {/* 담당자 정보 */} + {bidding.managerName && ( + <div className="flex items-center gap-1.5 text-muted-foreground"> + <User className="w-4 h-4" /> + <span className="font-medium">담당자:</span> + <span>{bidding.managerName}</span> + </div> + )} + + {/* 계약구분 */} + <div className="flex items-center gap-1.5 text-muted-foreground"> + <span className="font-medium">계약:</span> + <span>{contractTypeLabels[bidding.contractType]}</span> + </div> + + {/* 입찰유형 */} + <div className="flex items-center gap-1.5 text-muted-foreground"> + <span className="font-medium">유형:</span> + <span>{biddingTypeLabels[bidding.biddingType]}</span> + </div> + + {/* 낙찰수 */} + <div className="flex items-center gap-1.5 text-muted-foreground"> + <span className="font-medium">낙찰:</span> + <span>{bidding.awardCount === 'single' ? '단수' : '복수'}</span> + </div> + + {/* 통화 */} + <div className="flex items-center gap-1.5 text-muted-foreground"> + <DollarSign className="w-4 h-4" /> + <span className="font-mono">{bidding.currency}</span> + </div> + + {/* 예산 정보 */} + {bidding.budget && ( + <div className="flex items-center gap-1.5"> + <span className="font-medium text-muted-foreground">예산:</span> + <span className="font-semibold"> + {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: bidding.currency || 'KRW', + }).format(Number(bidding.budget))} + </span> + </div> + )} + </div> + + {/* 일정 정보 */} + {(bidding.submissionStartDate || bidding.evaluationDate || bidding.preQuoteDate || bidding.biddingRegistrationDate) && ( + <div className="flex flex-wrap items-center gap-4 mt-3 pt-3 border-t border-border/50"> + <Calendar className="w-4 h-4 text-muted-foreground flex-shrink-0" /> + <div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground"> + {bidding.submissionStartDate && bidding.submissionEndDate && ( + <div> + <span className="font-medium">제출기간:</span> {formatDate(bidding.submissionStartDate, 'KR')} ~ {formatDate(bidding.submissionEndDate, 'KR')} + </div> + )} + {bidding.evaluationDate && ( + <div> + <span className="font-medium">평가일:</span> {formatDate(bidding.evaluationDate, 'KR')} + </div> + )} + {bidding.preQuoteDate && ( + <div> + <span className="font-medium">사전견적일:</span> {formatDate(bidding.preQuoteDate, 'KR')} + </div> + )} + {bidding.biddingRegistrationDate && ( + <div> + <span className="font-medium">입찰등록일:</span> {formatDate(bidding.biddingRegistrationDate, 'KR')} + </div> + )} + </div> + </div> + )} + </div> + </div> + ) +}
\ No newline at end of file |
