summaryrefslogtreecommitdiff
path: root/lib/bidding
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-19 09:24:58 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-19 09:24:58 +0000
commit0d68dbcba27ce49c15f30126f7a5dfce974847a3 (patch)
treef00f71a2c33f0110fc2ef9e1243b47719ab5c316 /lib/bidding
parent60382940bac4ac8309be64be16f4774b6820df22 (diff)
(최겸) 구매 입찰 발주비율 취소기능 추가 등
Diffstat (limited to 'lib/bidding')
-rw-r--r--lib/bidding/actions.ts6
-rw-r--r--lib/bidding/detail/service.ts34
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx25
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx24
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx89
5 files changed, 152 insertions, 26 deletions
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts
index c4c543d9..d0017413 100644
--- a/lib/bidding/actions.ts
+++ b/lib/bidding/actions.ts
@@ -613,16 +613,16 @@ export async function cancelDisposalAction(
}
}
- // 3. 입찰 상태를 입찰생성으로 변경
+ // 3. 입찰 상태를 입찰평가중으로 변경
await tx
.update(biddings)
.set({
- status: 'bidding_generated',
+ status: 'evaluation_of_bidding',
updatedAt: new Date(),
updatedBy: userName,
})
.where(eq(biddings.id, biddingId))
-
+
return {
success: true,
message: '유찰 취소가 완료되었습니다.'
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index b5a3cce8..6ab9270e 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -610,6 +610,40 @@ export async function updateBiddingDetailVendor(
}
}
+// 발주비율 취소 (발주비율을 null로 리셋하고 낙찰 상태 해제)
+export async function cancelAwardRatio(biddingCompanyId: number) {
+ try {
+ const result = await db.update(biddingCompanies)
+ .set({
+ awardRatio: null,
+ isWinner: false,
+ updatedAt: new Date(),
+ })
+ .where(eq(biddingCompanies.id, biddingCompanyId))
+ .returning({ biddingId: biddingCompanies.biddingId })
+
+ // 캐시 무효화
+ if (result.length > 0) {
+ const biddingId = result[0].biddingId
+ revalidateTag(`bidding-${biddingId}`)
+ revalidateTag('quotation-vendors')
+ revalidateTag('quotation-details')
+ revalidatePath(`/evcp/bid/${biddingId}`)
+ }
+
+ return {
+ success: true,
+ message: '발주비율이 성공적으로 취소되었습니다.',
+ }
+ } catch (error) {
+ console.error('Failed to cancel award ratio:', error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '발주비율 취소에 실패했습니다.'
+ }
+ }
+}
+
// 본입찰용 업체 추가
export async function createBiddingDetailVendor(
biddingId: number,
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
index 1a1b331e..6e5481f4 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
@@ -24,6 +24,7 @@ interface BiddingDetailVendorEditDialogProps {
onSuccess: () => void
biddingAwardCount?: string // 낙찰수 정보 추가
biddingStatus?: string // 입찰 상태 정보 추가
+ allVendors?: QuotationVendor[] // 전체 벤더 목록 추가
}
export function BiddingDetailVendorEditDialog({
@@ -32,7 +33,8 @@ export function BiddingDetailVendorEditDialog({
onOpenChange,
onSuccess,
biddingAwardCount,
- biddingStatus
+ biddingStatus,
+ allVendors = []
}: BiddingDetailVendorEditDialogProps) {
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
@@ -42,6 +44,14 @@ export function BiddingDetailVendorEditDialog({
awardRatio: 0,
})
+ // 단수낙찰의 경우 이미 100%인 벤더가 있는지 확인
+ const hasWinnerWith100Percent = React.useMemo(() => {
+ if (biddingAwardCount === 'single') {
+ return allVendors.some(v => v.awardRatio === 100 && v.id !== vendor?.id)
+ }
+ return false
+ }, [allVendors, biddingAwardCount, vendor?.id])
+
// vendor가 변경되면 폼 데이터 업데이트
React.useEffect(() => {
if (vendor) {
@@ -135,7 +145,7 @@ export function BiddingDetailVendorEditDialog({
value={formData.awardRatio}
onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })}
placeholder="발주비율을 입력하세요"
- disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected'}
+ disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected' || hasWinnerWith100Percent}
/>
{vendor?.isBiddingParticipated !== true && (
<p className="text-sm text-muted-foreground">
@@ -152,15 +162,20 @@ export function BiddingDetailVendorEditDialog({
낙찰이 완료되어 발주비율을 수정할 수 없습니다.
</p>
)}
+ {hasWinnerWith100Percent && (
+ <p className="text-sm text-orange-600">
+ 단수 낙찰의 경우 이미 100% 발주비율이 설정된 업체가 있어 다른 업체의 발주비율을 수정할 수 없습니다.
+ </p>
+ )}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
취소
</Button>
- <Button
- onClick={handleEdit}
- disabled={isPending || vendor?.isBiddingParticipated !== true}
+ <Button
+ onClick={handleEdit}
+ disabled={isPending || vendor?.isBiddingParticipated !== true || hasWinnerWith100Percent}
>
산정
</Button>
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
index cfdab9c6..1fa116ab 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
@@ -66,16 +66,16 @@ const advancedFilterFields: DataTableAdvancedFilterField<QuotationVendor>[] = [
label: '견적금액',
type: 'number',
},
- {
- id: 'status',
- label: '상태',
- type: 'multi-select',
- options: [
- { label: '제출완료', value: 'submitted' },
- { label: '선정완료', value: 'selected' },
- { label: '미제출', value: 'pending' },
- ],
- },
+ {
+ id: 'invitationStatus',
+ label: '상태',
+ type: 'multi-select',
+ options: [
+ { label: '제출완료', value: 'bidding_submitted' },
+ { label: '선정완료', value: 'bidding_accepted' },
+ { label: '미제출', value: 'pending' },
+ ],
+ },
]
export function BiddingDetailVendorTableContent({
@@ -201,6 +201,7 @@ export function BiddingDetailVendorTableContent({
userId={userId}
onOpenAwardDialog={() => setIsAwardDialogOpen(true)}
onSuccess={onRefresh}
+ winnerVendor={vendors.find(v => v.awardRatio === 100)}
/>
</DataTableAdvancedToolbar>
</DataTable>
@@ -210,8 +211,9 @@ export function BiddingDetailVendorTableContent({
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
onSuccess={onRefresh}
- biddingAwardCount={bidding.awardCount}
+ biddingAwardCount={bidding.awardCount || undefined}
biddingStatus={bidding.status}
+ allVendors={vendors}
/>
<BiddingAwardDialog
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 f2c23de9..c1d59677 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 } from "@/lib/bidding/detail/service"
+import { registerBidding, markAsDisposal, createRebidding, cancelAwardRatio } from "@/lib/bidding/detail/service"
import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from "@/lib/bidding/pre-quote/service"
import { increaseRoundOrRebid } from "@/lib/bidding/service"
@@ -14,6 +14,7 @@ import { BiddingDetailVendorCreateDialog } from "../../../../components/bidding/
import { BiddingDocumentUploadDialog } from "./bidding-document-upload-dialog"
import { Bidding } from "@/db/schema"
import { useToast } from "@/hooks/use-toast"
+import { QuotationVendor } from "@/lib/bidding/detail/service"
interface BiddingDetailVendorToolbarActionsProps {
biddingId: number
@@ -21,6 +22,7 @@ interface BiddingDetailVendorToolbarActionsProps {
userId: string
onOpenAwardDialog: () => void
onSuccess: () => void
+ winnerVendor?: QuotationVendor | null // 100% 낙찰된 벤더
}
export function BiddingDetailVendorToolbarActions({
@@ -28,7 +30,8 @@ export function BiddingDetailVendorToolbarActions({
bidding,
userId,
onOpenAwardDialog,
- onSuccess
+ onSuccess,
+ winnerVendor
}: BiddingDetailVendorToolbarActionsProps) {
const router = useRouter()
const { toast } = useToast()
@@ -39,6 +42,7 @@ export function BiddingDetailVendorToolbarActions({
const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false)
const [selectedVendors, setSelectedVendors] = React.useState<any[]>([])
const [isRoundIncreaseDialogOpen, setIsRoundIncreaseDialogOpen] = React.useState(false)
+ const [isCancelAwardDialogOpen, setIsCancelAwardDialogOpen] = React.useState(false)
// 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회
React.useEffect(() => {
@@ -178,26 +182,51 @@ export function BiddingDetailVendorToolbarActions({
})
}
+ const handleCancelAward = () => {
+ if (!winnerVendor) return
+
+ startTransition(async () => {
+ const result = await cancelAwardRatio(winnerVendor.id)
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: result.message,
+ })
+ setIsCancelAwardDialogOpen(false)
+ onSuccess()
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "발주비율 취소 중 오류가 발생했습니다.",
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
const handleRoundIncreaseWithNavigation = () => {
startTransition(async () => {
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: result.message,
+ description: successResult.message,
})
// 새로 생성된 입찰의 상세 페이지로 이동
- if (result.biddingId) {
- router.push(`/evcp/bid/${result.biddingId}`)
+ if (successResult.biddingId) {
+ router.push(`/evcp/bid/${successResult.biddingId}/info`)
} else {
- router.push(`/evcp/bid`)
+ router.push(`/evcp/bid/${biddingId}/info`)
}
onSuccess()
} else {
+ const errorResult = result as { success: false; error: string }
toast({
title: "오류",
- description: result.error || "차수증가 중 오류가 발생했습니다.",
+ description: errorResult.error || "차수증가 중 오류가 발생했습니다.",
variant: 'destructive',
})
}
@@ -244,6 +273,19 @@ export function BiddingDetailVendorToolbarActions({
</Button>
</>
)}
+
+ {/* 발주비율 취소: 100% 낙찰된 벤더가 있는 경우 */}
+ {winnerVendor && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsCancelAwardDialogOpen(true)}
+ disabled={isPending}
+ >
+ <RotateCcw className="mr-2 h-4 w-4" />
+ 발주비율 취소
+ </Button>
+ )}
{/* 구분선 */}
{(bidding.status === 'bidding_generated' ||
bidding.status === 'bidding_disposal') && (
@@ -307,6 +349,39 @@ export function BiddingDetailVendorToolbarActions({
</DialogContent>
</Dialog>
+ {/* 발주비율 취소 확인 다이얼로그 */}
+ <Dialog open={isCancelAwardDialogOpen} onOpenChange={setIsCancelAwardDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>발주비율 취소 확인</DialogTitle>
+ <DialogDescription>
+ {winnerVendor && (
+ <>
+ <strong>{winnerVendor.vendorName}</strong> 업체의 발주비율(100%)을 취소하시겠습니까?
+ <br />
+ 취소 후 다른 업체의 발주비율을 설정할 수 있습니다.
+ </>
+ )}
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setIsCancelAwardDialogOpen(false)}
+ >
+ 아니오
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleCancelAward}
+ disabled={isPending}
+ >
+ 취소하기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
</>
)
}