summaryrefslogtreecommitdiff
path: root/lib/bidding/pre-quote
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-08 10:29:19 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-08 10:29:19 +0000
commitf93493f68c9f368e10f1c3379f1c1384068e3b14 (patch)
treea9dada58741750fa7ca6e04b210443ad99a6bccc /lib/bidding/pre-quote
parente832a508e1b3c531fb3e1b9761e18e1b55e3d76a (diff)
(대표님, 최겸) rfqLast, bidding, prequote
Diffstat (limited to 'lib/bidding/pre-quote')
-rw-r--r--lib/bidding/pre-quote/service.ts64
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-selection-dialog.tsx158
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx4
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx37
4 files changed, 257 insertions, 6 deletions
diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts
index 7f0a9083..b5b06769 100644
--- a/lib/bidding/pre-quote/service.ts
+++ b/lib/bidding/pre-quote/service.ts
@@ -127,6 +127,33 @@ export async function updateBiddingCompany(id: number, input: UpdateBiddingCompa
}
}
+// 본입찰 등록 상태 업데이트 (복수 업체 선택 가능)
+export async function updatePreQuoteSelection(companyIds: number[], isSelected: boolean, userId: string) {
+ try {
+ await db.update(biddingCompanies)
+ .set({
+ isPreQuoteSelected: isSelected,
+ updatedAt: new Date()
+ })
+ .where(inArray(biddingCompanies.id, companyIds))
+
+ const message = isSelected
+ ? `${companyIds.length}개 업체가 본입찰 대상으로 선정되었습니다.`
+ : `${companyIds.length}개 업체의 본입찰 선정이 취소되었습니다.`
+
+ return {
+ success: true,
+ message
+ }
+ } catch (error) {
+ console.error('Failed to update pre-quote selection:', error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '본입찰 선정 상태 업데이트에 실패했습니다.'
+ }
+ }
+}
+
// 사전견적용 업체 삭제
export async function deleteBiddingCompany(id: number) {
try {
@@ -302,6 +329,17 @@ export async function sendPreQuoteInvitations(companyIds: number[]) {
}
}
}
+ // 3. 입찰 상태를 사전견적 요청으로 변경 (bidding_generated 상태에서만)
+ await tx
+ .update(biddings)
+ .set({
+ status: 'request_for_quotation',
+ updatedAt: new Date()
+ })
+ .where(and(
+ eq(biddings.id, biddingId),
+ eq(biddings.status, 'bidding_generated')
+ ))
return {
success: true,
@@ -556,6 +594,28 @@ export async function submitPreQuoteResponse(
await tx.insert(priceAdjustmentForms).values(priceAdjustmentData)
}
}
+
+ // 5. 입찰 상태를 사전견적 접수로 변경 (request_for_quotation 상태에서만)
+ // 또한 사전견적 접수일 업데이트
+ const biddingCompany = await tx
+ .select({ biddingId: biddingCompanies.biddingId })
+ .from(biddingCompanies)
+ .where(eq(biddingCompanies.id, biddingCompanyId))
+ .limit(1)
+
+ if (biddingCompany.length > 0) {
+ await tx
+ .update(biddings)
+ .set({
+ status: 'received_quotation',
+ preQuoteReceivedAt: new Date(), // 사전견적 접수일 업데이트
+ updatedAt: new Date()
+ })
+ .where(and(
+ eq(biddings.id, biddingCompany[0].biddingId),
+ eq(biddings.status, 'request_for_quotation')
+ ))
+ }
})
return {
@@ -648,6 +708,8 @@ export async function getPrItemsForBidding(biddingId: number) {
materialDescription: prItemsForBidding.materialDescription,
quantity: prItemsForBidding.quantity,
quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
currency: prItemsForBidding.currency,
requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate,
hasSpecDocument: prItemsForBidding.hasSpecDocument
@@ -665,7 +727,6 @@ export async function getPrItemsForBidding(biddingId: number) {
// SPEC 문서 조회 (PR 아이템에 연결된 문서들)
export async function getSpecDocumentsForPrItem(prItemId: number) {
try {
- console.log('getSpecDocumentsForPrItem called with prItemId:', prItemId)
const specDocs = await db
.select({
@@ -686,7 +747,6 @@ export async function getSpecDocumentsForPrItem(prItemId: number) {
)
)
- console.log('getSpecDocumentsForPrItem result:', specDocs)
return specDocs
} catch (error) {
console.error('Failed to get spec documents for PR item:', error)
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-selection-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-selection-dialog.tsx
new file mode 100644
index 00000000..7de79771
--- /dev/null
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-selection-dialog.tsx
@@ -0,0 +1,158 @@
+'use client'
+
+import * as React from 'react'
+import { BiddingCompany } from './bidding-pre-quote-vendor-columns'
+import { updatePreQuoteSelection } from '../service'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { CheckCircle, XCircle, AlertCircle } from 'lucide-react'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
+
+interface BiddingPreQuoteSelectionDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ selectedCompanies: BiddingCompany[]
+ onSuccess: () => void
+}
+
+export function BiddingPreQuoteSelectionDialog({
+ open,
+ onOpenChange,
+ selectedCompanies,
+ onSuccess
+}: BiddingPreQuoteSelectionDialogProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+ // 선택된 업체들의 현재 상태 분석 (선정만 가능)
+ const unselectedCompanies = selectedCompanies.filter(c => !c.isPreQuoteSelected)
+ const hasQuotationCompanies = selectedCompanies.filter(c => c.preQuoteAmount && Number(c.preQuoteAmount) > 0)
+
+ const handleConfirm = () => {
+ const companyIds = selectedCompanies.map(c => c.id)
+ const isSelected = true // 항상 선정으로 고정
+
+ startTransition(async () => {
+ const result = await updatePreQuoteSelection(
+ companyIds,
+ isSelected,
+ 'current-user' // TODO: 실제 사용자 ID
+ )
+
+ if (result.success) {
+ toast({
+ title: '성공',
+ description: result.message,
+ })
+ onSuccess()
+ onOpenChange(false)
+ } else {
+ toast({
+ title: '오류',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ const getActionIcon = (isSelected: boolean) => {
+ return isSelected ?
+ <CheckCircle className="h-4 w-4 text-muted-foreground" /> :
+ <CheckCircle className="h-4 w-4 text-green-600" />
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[600px]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <AlertCircle className="h-5 w-5 text-amber-500" />
+ 본입찰 선정 상태 변경
+ </DialogTitle>
+ <DialogDescription>
+ 선택된 {selectedCompanies.length}개 업체의 본입찰 선정 상태를 변경합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* 견적 제출 여부 안내 */}
+ {hasQuotationCompanies.length !== selectedCompanies.length && (
+ <div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
+ <div className="flex items-center gap-2 text-amber-800">
+ <AlertCircle className="h-4 w-4" />
+ <span className="text-sm font-medium">알림</span>
+ </div>
+ <p className="text-sm text-amber-700 mt-1">
+ 사전견적을 제출하지 않은 업체도 포함되어 있습니다.
+ 견적 미제출 업체도 본입찰에 참여시키시겠습니까?
+ </p>
+ </div>
+ )}
+
+ {/* 업체 목록 */}
+ <div className="border rounded-lg">
+ <div className="p-3 bg-muted/50 border-b">
+ <h4 className="font-medium">대상 업체 목록</h4>
+ </div>
+ <div className="max-h-64 overflow-y-auto">
+ {selectedCompanies.map((company) => (
+ <div key={company.id} className="flex items-center justify-between p-3 border-b last:border-b-0">
+ <div className="flex items-center gap-3">
+ {getActionIcon(company.isPreQuoteSelected)}
+ <div>
+ <div className="font-medium">{company.companyName}</div>
+ <div className="text-sm text-muted-foreground">{company.companyCode}</div>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ <Badge variant={company.isPreQuoteSelected ? 'default' : 'secondary'}>
+ {company.isPreQuoteSelected ? '현재 선정' : '현재 미선정'}
+ </Badge>
+ {company.preQuoteAmount && Number(company.preQuoteAmount) > 0 ? (
+ <Badge variant="outline" className="text-green-600">
+ 견적 제출
+ </Badge>
+ ) : (
+ <Badge variant="outline" className="text-muted-foreground">
+ 견적 미제출
+ </Badge>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* 결과 요약 */}
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
+ <h5 className="font-medium text-blue-900 mb-2">변경 결과</h5>
+ <div className="text-sm text-blue-800">
+ <p>• {unselectedCompanies.length}개 업체가 본입찰 대상으로 <span className="font-medium text-green-600">선정</span>됩니다.</p>
+ {selectedCompanies.length > unselectedCompanies.length && (
+ <p>• {selectedCompanies.length - unselectedCompanies.length}개 업체는 이미 선정 상태이므로 변경되지 않습니다.</p>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ 취소
+ </Button>
+ <Button onClick={handleConfirm} disabled={isPending}>
+ {isPending ? '처리 중...' : '확인'}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
index f28f9e1f..7e84f178 100644
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
@@ -317,7 +317,7 @@ export function getBiddingPreQuoteVendorColumns({
},
{
id: 'actions',
- header: '작업',
+ header: '액션',
cell: ({ row }) => {
const company = row.original
@@ -330,7 +330,6 @@ export function getBiddingPreQuoteVendorColumns({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
- <DropdownMenuLabel>작업</DropdownMenuLabel>
{/* <DropdownMenuItem onClick={() => onEdit(company)}>
<Edit className="mr-2 h-4 w-4" />
수정
@@ -341,7 +340,6 @@ export function getBiddingPreQuoteVendorColumns({
초대 발송
</DropdownMenuItem>
)}
- <DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onDelete(company)}
className="text-destructive"
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx
index c1b1baa5..6c209e2d 100644
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx
@@ -4,10 +4,11 @@ import * as React from "react"
import { type Table } from "@tanstack/react-table"
import { useTransition } from "react"
import { Button } from "@/components/ui/button"
-import { Plus, Send, Mail } from "lucide-react"
+import { Plus, Send, Mail, CheckSquare } from "lucide-react"
import { BiddingCompany } from "./bidding-pre-quote-vendor-columns"
import { BiddingPreQuoteVendorCreateDialog } from "./bidding-pre-quote-vendor-create-dialog"
import { BiddingPreQuoteInvitationDialog } from "./bidding-pre-quote-invitation-dialog"
+import { BiddingPreQuoteSelectionDialog } from "./bidding-pre-quote-selection-dialog"
import { Bidding } from "@/db/schema"
import { useToast } from "@/hooks/use-toast"
@@ -36,6 +37,7 @@ export function BiddingPreQuoteVendorToolbarActions({
const [isPending, startTransition] = useTransition()
const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false)
const [isInvitationDialogOpen, setIsInvitationDialogOpen] = React.useState(false)
+ const [isSelectionDialogOpen, setIsSelectionDialogOpen] = React.useState(false)
const handleCreateCompany = () => {
setIsCreateDialogOpen(true)
@@ -45,6 +47,19 @@ export function BiddingPreQuoteVendorToolbarActions({
setIsInvitationDialogOpen(true)
}
+ const handleManageSelection = () => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length === 0) {
+ toast({
+ title: '선택 필요',
+ description: '본입찰 선정 상태를 변경할 업체를 선택해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+ setIsSelectionDialogOpen(true)
+ }
+
return (
@@ -69,6 +84,16 @@ export function BiddingPreQuoteVendorToolbarActions({
<Mail className="mr-2 h-4 w-4" />
초대 발송
</Button>
+
+ <Button
+ variant="secondary"
+ size="sm"
+ onClick={handleManageSelection}
+ disabled={isPending}
+ >
+ <CheckSquare className="mr-2 h-4 w-4" />
+ 본입찰 선정
+ </Button>
</div>
<BiddingPreQuoteVendorCreateDialog
@@ -87,6 +112,16 @@ export function BiddingPreQuoteVendorToolbarActions({
companies={biddingCompanies}
onSuccess={onSuccess}
/>
+
+ <BiddingPreQuoteSelectionDialog
+ open={isSelectionDialogOpen}
+ onOpenChange={setIsSelectionDialogOpen}
+ selectedCompanies={table.getFilteredSelectedRowModel().rows.map(row => row.original)}
+ onSuccess={() => {
+ onSuccess()
+ table.resetRowSelection()
+ }}
+ />
</>
)
}