summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/table/bidding-detail-header.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-header.tsx')
-rw-r--r--lib/bidding/detail/table/bidding-detail-header.tsx328
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