diff options
Diffstat (limited to 'lib/bidding/detail/table')
5 files changed, 246 insertions, 109 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-content.tsx b/lib/bidding/detail/table/bidding-detail-content.tsx index 090e7218..50f0941e 100644 --- a/lib/bidding/detail/table/bidding-detail-content.tsx +++ b/lib/bidding/detail/table/bidding-detail-content.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Bidding } from '@/db/schema' import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service' -import { BiddingDetailHeader } from './bidding-detail-header' + import { BiddingDetailVendorTableContent } from './bidding-detail-vendor-table' import { BiddingDetailItemsDialog } from './bidding-detail-items-dialog' import { BiddingDetailTargetPriceDialog } from './bidding-detail-target-price-dialog' @@ -45,27 +45,20 @@ export function BiddingDetailContent({ }, []) return ( - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="p-6"> - <BiddingDetailHeader bidding={bidding} /> - - <div className="mt-6"> - <BiddingDetailVendorTableContent - biddingId={bidding.id} - vendors={quotationVendors} - biddingCompanies={biddingCompanies} - onRefresh={handleRefresh} - onOpenItemsDialog={() => openDialog('items')} - onOpenTargetPriceDialog={() => openDialog('targetPrice')} - onOpenSelectionReasonDialog={() => openDialog('selectionReason')} - onEdit={undefined} - onDelete={undefined} - onSelectWinner={undefined} - /> - </div> - </div> - </section> + <div className="space-y-6"> + <BiddingDetailVendorTableContent + biddingId={bidding.id} + bidding={bidding} + vendors={quotationVendors} + biddingCompanies={biddingCompanies} + onRefresh={handleRefresh} + onOpenItemsDialog={() => openDialog('items')} + onOpenTargetPriceDialog={() => openDialog('targetPrice')} + onOpenSelectionReasonDialog={() => openDialog('selectionReason')} + onEdit={undefined} + onDelete={undefined} + onSelectWinner={undefined} + /> <BiddingDetailItemsDialog open={dialogStates.items} diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx index 3135f37d..fcbbeb9a 100644 --- a/lib/bidding/detail/table/bidding-detail-header.tsx +++ b/lib/bidding/detail/table/bidding-detail-header.tsx @@ -145,17 +145,7 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { const buttons = [] // 기본 액션 버튼들 (항상 표시) - buttons.push( - <Button - key="back" - variant="outline" - onClick={handleGoBack} - disabled={isPending} - > - <ArrowLeft className="w-4 h-4 mr-2" /> - 목록으로 - </Button> - ) + // 모든 액션 버튼을 항상 표시 (상태 검증은 각 핸들러에서) buttons.push( @@ -228,74 +218,9 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { </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) && ( + {/* {(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"> @@ -321,7 +246,7 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { )} </div> </div> - )} + )} */} </div> </div> ) diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index 9e06d5d1..6f02497f 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -22,12 +22,14 @@ interface GetVendorColumnsProps { onEdit: (vendor: QuotationVendor) => void onDelete: (vendor: QuotationVendor) => void onSelectWinner: (vendor: QuotationVendor) => void + onViewPriceAdjustment?: (vendor: QuotationVendor) => void } export function getBiddingDetailVendorColumns({ onEdit, onDelete, - onSelectWinner + onSelectWinner, + onViewPriceAdjustment }: GetVendorColumnsProps): ColumnDef<QuotationVendor>[] { return [ { @@ -139,13 +141,46 @@ export function getBiddingDetailVendorColumns({ {row.original.incotermsResponse || '-'} </div> ), + }, + { + accessorKey: 'isInitialResponse', + header: '초도여부', + cell: ({ row }) => ( + <Badge variant={row.original.isInitialResponse ? 'default' : 'secondary'}> + {row.original.isInitialResponse ? 'Y' : 'N'} + </Badge> + ), + }, + { + accessorKey: 'priceAdjustmentResponse', + header: '연동제', + cell: ({ row }) => { + const hasPriceAdjustment = row.original.priceAdjustmentResponse + return ( + <div className="flex items-center gap-2"> + <Badge variant={hasPriceAdjustment ? 'default' : 'secondary'}> + {hasPriceAdjustment ? '적용' : '미적용'} + </Badge> + {hasPriceAdjustment && onViewPriceAdjustment && ( + <Button + variant="ghost" + size="sm" + onClick={() => onViewPriceAdjustment(row.original)} + className="h-6 px-2 text-xs" + > + 상세 + </Button> + )} + </div> + ) + }, }, { accessorKey: 'proposedContractDeliveryDate', header: '제안납기일', cell: ({ row }) => ( <div className="text-sm"> - {row.original.proposedContractDeliveryDate ? + {row.original.proposedContractDeliveryDate ? new Date(row.original.proposedContractDeliveryDate).toLocaleDateString('ko-KR') : '-'} </div> ), diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index 7ad7056c..b1f0b08e 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -9,7 +9,9 @@ import { BiddingDetailVendorToolbarActions } from './bidding-detail-vendor-toolb import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog' import { BiddingDetailVendorEditDialog } from './bidding-detail-vendor-edit-dialog' import { getBiddingDetailVendorColumns } from './bidding-detail-vendor-columns' -import { QuotationVendor } from '@/lib/bidding/detail/service' +import { QuotationVendor, getPriceAdjustmentFormByBiddingCompanyId } from '@/lib/bidding/detail/service' +import { Bidding } from '@/db/schema' +import { PriceAdjustmentDialog } from '@/components/bidding/price-adjustment-dialog' import { deleteQuotationVendor, selectWinner @@ -20,6 +22,7 @@ import { useTransition } from 'react' interface BiddingDetailVendorTableContentProps { biddingId: number + bidding: Bidding vendors: QuotationVendor[] onRefresh: () => void onOpenItemsDialog: () => void @@ -83,6 +86,7 @@ const advancedFilterFields: DataTableAdvancedFilterField<QuotationVendor>[] = [ export function BiddingDetailVendorTableContent({ biddingId, + bidding, vendors, onRefresh, onOpenItemsDialog, @@ -96,6 +100,8 @@ export function BiddingDetailVendorTableContent({ const [isPending, startTransition] = useTransition() const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null) const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false) + const [priceAdjustmentData, setPriceAdjustmentData] = React.useState<any>(null) + const [isPriceAdjustmentDialogOpen, setIsPriceAdjustmentDialogOpen] = React.useState(false) const handleDelete = (vendor: QuotationVendor) => { if (!confirm(`${vendor.vendorName} 업체를 삭제하시겠습니까?`)) return @@ -170,13 +176,38 @@ export function BiddingDetailVendorTableContent({ setIsEditDialogOpen(true) } + const handleViewPriceAdjustment = async (vendor: QuotationVendor) => { + try { + const priceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(vendor.id) + if (priceAdjustmentForm) { + setPriceAdjustmentData(priceAdjustmentForm) + setSelectedVendor(vendor) + setIsPriceAdjustmentDialogOpen(true) + } else { + toast({ + title: '연동제 정보 없음', + description: '해당 업체의 연동제 정보가 없습니다.', + variant: 'default', + }) + } + } catch (error) { + console.error('Failed to load price adjustment form:', error) + toast({ + title: '오류', + description: '연동제 정보를 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } + } + const columns = React.useMemo( () => getBiddingDetailVendorColumns({ onEdit: onEdit || handleEdit, onDelete: onDelete || handleDelete, - onSelectWinner: onSelectWinner || handleSelectWinner + onSelectWinner: onSelectWinner || handleSelectWinner, + onViewPriceAdjustment: handleViewPriceAdjustment }), - [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner] + [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment] ) const { table } = useDataTable({ @@ -205,10 +236,10 @@ export function BiddingDetailVendorTableContent({ <BiddingDetailVendorToolbarActions table={table} biddingId={biddingId} + bidding={bidding} onOpenItemsDialog={onOpenItemsDialog} onOpenTargetPriceDialog={onOpenTargetPriceDialog} onOpenSelectionReasonDialog={onOpenSelectionReasonDialog} - onSuccess={onRefresh} /> </DataTableAdvancedToolbar> @@ -220,6 +251,13 @@ export function BiddingDetailVendorTableContent({ onOpenChange={setIsEditDialogOpen} onSuccess={onRefresh} /> + + <PriceAdjustmentDialog + open={isPriceAdjustmentDialogOpen} + onOpenChange={setIsPriceAdjustmentDialogOpen} + data={priceAdjustmentData} + vendorName={selectedVendor?.vendorName || ''} + /> </> ) } diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 00daa005..ca9ffc60 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -2,38 +2,184 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" +import { useRouter } from "next/navigation" +import { useTransition } from "react" import { Button } from "@/components/ui/button" -import { Plus } from "lucide-react" -import { QuotationVendor } from "@/lib/bidding/detail/service" +import { Plus, Send, RotateCcw, XCircle } from "lucide-react" +import { QuotationVendor, registerBidding, markAsDisposal, createRebidding } from "@/lib/bidding/detail/service" import { BiddingDetailVendorCreateDialog } from "./bidding-detail-vendor-create-dialog" +import { Bidding } from "@/db/schema" +import { useToast } from "@/hooks/use-toast" interface BiddingDetailVendorToolbarActionsProps { table: Table<QuotationVendor> biddingId: number + bidding: Bidding onOpenItemsDialog: () => void onOpenTargetPriceDialog: () => void onOpenSelectionReasonDialog: () => void - onSuccess: () => void } export function BiddingDetailVendorToolbarActions({ table, biddingId, + bidding, onOpenItemsDialog, onOpenTargetPriceDialog, onOpenSelectionReasonDialog, onSuccess }: BiddingDetailVendorToolbarActionsProps) { + const router = useRouter() + const { toast } = useToast() + const [isPending, startTransition] = useTransition() const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false) const handleCreateVendor = () => { setIsCreateDialogOpen(true) } + 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?.redirectTo) { + router.push(result.data.redirectTo) + } else { + router.refresh() + } + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + }) + } + return ( <> <div className="flex items-center gap-2"> + {/* 상태별 액션 버튼 */} + {/* {bidding.status === 'bidding_generated' && ( + <Button + variant="default" + size="sm" + onClick={handleRegister} + disabled={isPending} + > + <Send className="mr-2 h-4 w-4" /> + 입찰 등록 + </Button> + )} + + {bidding.status === 'bidding_closed' && ( + <Button + variant="destructive" + size="sm" + onClick={handleMarkAsDisposal} + disabled={isPending} + > + <XCircle className="mr-2 h-4 w-4" /> + 유찰 처리 + </Button> + )} + + {bidding.status === 'bidding_disposal' && ( + <Button + variant="outline" + size="sm" + onClick={handleCreateRebidding} + disabled={isPending} + > + <RotateCcw className="mr-2 h-4 w-4" /> + 재입찰 생성 + </Button> + )} */} + + {/* 기존 버튼들 */} <Button variant="outline" size="sm" |
