diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-19 09:24:58 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-19 09:24:58 +0000 |
| commit | 0d68dbcba27ce49c15f30126f7a5dfce974847a3 (patch) | |
| tree | f00f71a2c33f0110fc2ef9e1243b47719ab5c316 /lib/bidding | |
| parent | 60382940bac4ac8309be64be16f4774b6820df22 (diff) | |
(최겸) 구매 입찰 발주비율 취소기능 추가 등
Diffstat (limited to 'lib/bidding')
| -rw-r--r-- | lib/bidding/actions.ts | 6 | ||||
| -rw-r--r-- | lib/bidding/detail/service.ts | 34 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx | 25 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-table.tsx | 24 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx | 89 |
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> + </> ) } |
