summaryrefslogtreecommitdiff
path: root/lib/bidding/detail
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-28 07:45:32 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-28 07:45:32 +0000
commit1eb7cf92d1d7711e5d62a750e7611dc6fd1a241d (patch)
treeb278c04fa755ed6375f20b5a179c60b033dd6d20 /lib/bidding/detail
parent927b3d6cbfad6ce84ec1bff2faaace95e9586efd (diff)
(최겸) 구매 피드백 반영(입찰SAP 취소 개발 잔재)
Diffstat (limited to 'lib/bidding/detail')
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-columns.tsx112
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx36
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx27
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') && (
<>