diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-28 07:45:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-28 07:45:32 +0000 |
| commit | 1eb7cf92d1d7711e5d62a750e7611dc6fd1a241d (patch) | |
| tree | b278c04fa755ed6375f20b5a179c60b033dd6d20 /lib/bidding/detail/table | |
| parent | 927b3d6cbfad6ce84ec1bff2faaace95e9586efd (diff) | |
(최겸) 구매 피드백 반영(입찰SAP 취소 개발 잔재)
Diffstat (limited to 'lib/bidding/detail/table')
3 files changed, 118 insertions, 57 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index a0b69020..5368b287 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -19,22 +19,26 @@ import { import { QuotationVendor } from "@/lib/bidding/detail/service" interface GetVendorColumnsProps { - onEdit: (vendor: QuotationVendor) => void onViewPriceAdjustment?: (vendor: QuotationVendor) => void onViewItemDetails?: (vendor: QuotationVendor) => void onSendBidding?: (vendor: QuotationVendor) => void onUpdateParticipation?: (vendor: QuotationVendor, participated: boolean) => void onViewQuotationHistory?: (vendor: QuotationVendor) => void biddingStatus?: string // 입찰 상태 정보 추가 + biddingTargetPrice?: number | string | null // 입찰 내정가 + biddingFinalBidPrice?: number | string | null // 최종 확정금액 + biddingCurrency?: string // 입찰 통화 } export function getBiddingDetailVendorColumns({ - onEdit, onViewItemDetails, onSendBidding, onUpdateParticipation, onViewQuotationHistory, - biddingStatus + biddingStatus, + biddingTargetPrice, + biddingFinalBidPrice, + biddingCurrency }: GetVendorColumnsProps): ColumnDef<QuotationVendor>[] { return [ { @@ -97,6 +101,54 @@ export function getBiddingDetailVendorColumns({ }, }, { + accessorKey: 'targetPrice', + header: '내정가', + cell: ({ row }) => { + const hasTargetPrice = biddingTargetPrice && Number(biddingTargetPrice) > 0 + return ( + <div className="text-right font-mono text-sm text-muted-foreground"> + {hasTargetPrice ? ( + <> + {Number(biddingTargetPrice).toLocaleString()} {row.original.currency} + </> + ) : ( + <span>-</span> + )} + </div> + ) + }, + }, + { + accessorKey: 'priceRatio', + header: '내정가 대비', + cell: ({ row }) => { + const hasAmount = row.original.quotationAmount && Number(row.original.quotationAmount) > 0 + const hasTargetPrice = biddingTargetPrice && Number(biddingTargetPrice) > 0 + + if (!hasAmount || !hasTargetPrice) { + return <div className="text-right text-muted-foreground">-</div> + } + + const quotationAmount = Number(row.original.quotationAmount) + const targetPrice = Number(biddingTargetPrice) + const ratio = (quotationAmount / targetPrice) * 100 + + // 비율에 따른 색상 결정 + const getColorClass = (ratio: number) => { + if (ratio < 100) return 'text-blue-600 font-bold' // 내정가보다 낮음 + if (ratio === 100) return 'text-green-600 font-bold' // 내정가와 같음 + if (ratio <= 110) return 'text-orange-600 font-bold' // 10% 이내 초과 + return 'text-red-600 font-bold' // 10% 이상 초과 + } + + return ( + <div className={`text-right font-mono ${getColorClass(ratio)}`}> + {ratio.toFixed(1)}% + </div> + ) + }, + }, + { accessorKey: 'biddingResult', header: '입찰결과', cell: ({ row }) => { @@ -121,6 +173,25 @@ export function getBiddingDetailVendorColumns({ ), }, { + accessorKey: 'finalBidPrice', + header: '확정금액', + cell: ({ row }) => { + const hasFinalPrice = biddingFinalBidPrice && Number(biddingFinalBidPrice) > 0 + const currency = biddingCurrency || row.original.currency + return ( + <div className="text-right font-mono font-bold text-green-700"> + {hasFinalPrice ? ( + <> + {Number(biddingFinalBidPrice).toLocaleString()} {currency} + </> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + ) + }, + }, + { accessorKey: 'isBiddingParticipated', header: '입찰참여', cell: ({ row }) => { @@ -183,41 +254,6 @@ export function getBiddingDetailVendorColumns({ </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>작업</DropdownMenuLabel> - <DropdownMenuItem - onClick={() => onEdit(vendor)} - disabled={vendor.isBiddingParticipated !== true || biddingStatus === 'vendor_selected'} - > - 발주비율 산정 - {vendor.isBiddingParticipated !== true && ( - <span className="text-xs text-muted-foreground ml-2">(입찰참여 필요)</span> - )} - {biddingStatus === 'vendor_selected' && ( - <span className="text-xs text-muted-foreground ml-2">(낙찰 완료)</span> - )} - </DropdownMenuItem> - - {/* 입찰 참여여부 관리 */} - {vendor.isBiddingParticipated === null && onUpdateParticipation && ( - <> - <DropdownMenuSeparator /> - <DropdownMenuItem onClick={() => onUpdateParticipation(vendor, true)}> - 응찰 설정 - </DropdownMenuItem> - <DropdownMenuItem onClick={() => onUpdateParticipation(vendor, false)}> - 응찰포기 설정 - </DropdownMenuItem> - </> - )} - - {/* 입찰 보내기 (응찰한 업체만) */} - {vendor.isBiddingParticipated === true && onSendBidding && ( - <> - <DropdownMenuSeparator /> - <DropdownMenuItem onClick={() => onSendBidding(vendor)}> - 입찰 보내기 - </DropdownMenuItem> - </> - )} {/* 입찰 히스토리 (응찰한 업체만) */} {vendor.isBiddingParticipated === true && onViewQuotationHistory && ( diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index edb72aca..fffac0c1 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -25,7 +25,6 @@ interface BiddingDetailVendorTableContentProps { vendors: QuotationVendor[] onRefresh: () => void onOpenSelectionReasonDialog: () => void - onEdit?: (vendor: QuotationVendor) => void onViewItemDetails?: (vendor: QuotationVendor) => void onViewQuotationHistory?: (vendor: QuotationVendor) => void } @@ -86,7 +85,6 @@ export function BiddingDetailVendorTableContent({ bidding, vendors, onRefresh, - onEdit, onViewItemDetails, onViewQuotationHistory }: BiddingDetailVendorTableContentProps) { @@ -96,8 +94,8 @@ export function BiddingDetailVendorTableContent({ // 세션에서 사용자 ID 가져오기 const userId = session?.user?.id || '' const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null) - const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false) const [isAwardDialogOpen, setIsAwardDialogOpen] = React.useState(false) + const [isAwardRatioDialogOpen, setIsAwardRatioDialogOpen] = React.useState(false) const [priceAdjustmentData, setPriceAdjustmentData] = React.useState<any>(null) const [isPriceAdjustmentDialogOpen, setIsPriceAdjustmentDialogOpen] = React.useState(false) const [quotationHistoryData, setQuotationHistoryData] = React.useState<any>(null) @@ -116,11 +114,6 @@ export function BiddingDetailVendorTableContent({ } | null>(null) const [isApprovalPreviewDialogOpen, setIsApprovalPreviewDialogOpen] = React.useState(false) - const handleEdit = (vendor: QuotationVendor) => { - setSelectedVendor(vendor) - setIsEditDialogOpen(true) - } - const handleViewPriceAdjustment = async (vendor: QuotationVendor) => { try { const priceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(vendor.id) @@ -179,13 +172,15 @@ export function BiddingDetailVendorTableContent({ const columns = React.useMemo( () => getBiddingDetailVendorColumns({ - onEdit: onEdit || handleEdit, onViewPriceAdjustment: handleViewPriceAdjustment, onViewItemDetails: onViewItemDetails, onViewQuotationHistory: onViewQuotationHistory || handleViewQuotationHistory, - biddingStatus: bidding.status + biddingStatus: bidding.status, + biddingTargetPrice: bidding.targetPrice, + biddingFinalBidPrice: bidding.finalBidPrice, + biddingCurrency: bidding.currency || undefined }), - [onEdit, handleEdit, handleViewPriceAdjustment, onViewItemDetails, onViewQuotationHistory, handleViewQuotationHistory, bidding.status] + [handleViewPriceAdjustment, onViewItemDetails, onViewQuotationHistory, handleViewQuotationHistory, bidding.status, bidding.targetPrice, bidding.finalBidPrice, bidding.currency] ) const { table } = useDataTable({ @@ -203,6 +198,18 @@ export function BiddingDetailVendorTableContent({ clearOnDefault: true, }) + // single select된 vendor 가져오기 + const selectedRows = table.getSelectedRowModel().rows + const singleSelectedVendor = selectedRows.length === 1 ? selectedRows[0].original : null + + // 발주비율 산정 버튼 핸들러 + const handleOpenAwardRatioDialog = () => { + if (singleSelectedVendor) { + setSelectedVendor(singleSelectedVendor) + setIsAwardRatioDialogOpen(true) + } + } + // 낙찰 결재 상신 핸들러 const handleAwardApprovalConfirm = async (data: { approvers: string[]; title: string; attachments?: File[] }) => { if (!session?.user?.id || !approvalPreviewData) return @@ -258,16 +265,19 @@ export function BiddingDetailVendorTableContent({ bidding={bidding} userId={userId} onOpenAwardDialog={() => setIsAwardDialogOpen(true)} + onOpenAwardRatioDialog={handleOpenAwardRatioDialog} onSuccess={onRefresh} winnerVendor={vendors.find(v => v.awardRatio === 100)} + singleSelectedVendor={singleSelectedVendor} /> </DataTableAdvancedToolbar> </DataTable> + {/* 발주비율 산정 Dialog */} <BiddingDetailVendorEditDialog vendor={selectedVendor} - open={isEditDialogOpen} - onOpenChange={setIsEditDialogOpen} + open={isAwardRatioDialogOpen} + onOpenChange={setIsAwardRatioDialogOpen} onSuccess={onRefresh} biddingAwardCount={bidding.awardCount || undefined} biddingStatus={bidding.status} 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 53fe05f9..8df29289 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -6,7 +6,7 @@ import { useTransition } from "react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Plus, Send, RotateCcw, XCircle, Trophy, FileText, DollarSign, RotateCw } from "lucide-react" -import { registerBidding, markAsDisposal, createRebidding, cancelAwardRatio } from "@/lib/bidding/detail/service" +import { registerBidding, markAsDisposal, cancelAwardRatio } from "@/lib/bidding/detail/service" import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from "@/lib/bidding/pre-quote/service" import { increaseRoundOrRebid } from "@/lib/bidding/service" @@ -21,8 +21,10 @@ interface BiddingDetailVendorToolbarActionsProps { bidding: Bidding userId: string onOpenAwardDialog: () => void + onOpenAwardRatioDialog: () => void onSuccess: () => void winnerVendor?: QuotationVendor | null // 100% 낙찰된 벤더 + singleSelectedVendor?: QuotationVendor | null // single select된 벤더 } export function BiddingDetailVendorToolbarActions({ @@ -30,8 +32,10 @@ export function BiddingDetailVendorToolbarActions({ bidding, userId, onOpenAwardDialog, + onOpenAwardRatioDialog, onSuccess, - winnerVendor + winnerVendor, + singleSelectedVendor }: BiddingDetailVendorToolbarActionsProps) { const router = useRouter() const { toast } = useToast() @@ -210,18 +214,16 @@ export function BiddingDetailVendorToolbarActions({ const result = await increaseRoundOrRebid(bidding.id, userId, 'round_increase') if (result.success) { - const successResult = result as { success: true; message: string; biddingId: number; biddingNumber: string } toast({ title: "성공", - description: successResult.message, + description: '차수증가가 완료되었습니다.', }) router.push(`/evcp/bid`) onSuccess() } else { - const errorResult = result as { success: false; error: string } toast({ title: "오류", - description: errorResult.error || "차수증가 중 오류가 발생했습니다.", + description: result.error || "차수증가 중 오류가 발생했습니다.", variant: 'destructive', }) } @@ -245,6 +247,19 @@ export function BiddingDetailVendorToolbarActions({ </Button> )} + {/* 발주비율 산정: single select 시에만 활성화 */} + {(bidding.status === 'evaluation_of_bidding') && ( + <Button + variant="outline" + size="sm" + onClick={onOpenAwardRatioDialog} + disabled={!singleSelectedVendor || isPending || singleSelectedVendor.isBiddingParticipated !== true} + > + <DollarSign className="mr-2 h-4 w-4" /> + 발주비율 산정 + </Button> + )} + {/* 유찰/낙찰: 입찰공고 또는 입찰평가중 상태에서만 */} {(bidding.status === 'bidding_opened' || bidding.status === 'evaluation_of_bidding') && ( <> |
