summaryrefslogtreecommitdiff
path: root/lib/bidding/pre-quote
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-09 10:34:05 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-09 10:34:05 +0000
commit86b1fd1cc801f45642f84d24c0b5c84368454ff0 (patch)
tree63176d1feb6d3fbbb71d942343056ba6d793b586 /lib/bidding/pre-quote
parentc62ec046327fd388ebce04571b55910747e69a3b (diff)
(최겸) 구매 입찰 사전견적, 입찰, 낙찰, 유찰, 재입찰 기능 개발
Diffstat (limited to 'lib/bidding/pre-quote')
-rw-r--r--lib/bidding/pre-quote/service.ts15
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx8
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx30
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx86
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx17
5 files changed, 108 insertions, 48 deletions
diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts
index 35bc8941..3f1b916c 100644
--- a/lib/bidding/pre-quote/service.ts
+++ b/lib/bidding/pre-quote/service.ts
@@ -133,6 +133,7 @@ export async function updatePreQuoteSelection(companyIds: number[], isSelected:
await db.update(biddingCompanies)
.set({
isPreQuoteSelected: isSelected,
+ invitationStatus: 'pending', // 초기 상태: 입찰생성
updatedAt: new Date()
})
.where(inArray(biddingCompanies.id, companyIds))
@@ -194,7 +195,9 @@ export async function getBiddingCompanies(biddingId: number) {
respondedAt: biddingCompanies.respondedAt,
preQuoteAmount: biddingCompanies.preQuoteAmount,
preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt,
+ preQuoteDeadline: biddingCompanies.preQuoteDeadline,
isPreQuoteSelected: biddingCompanies.isPreQuoteSelected,
+ isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated,
isAttendingMeeting: biddingCompanies.isAttendingMeeting,
notes: biddingCompanies.notes,
contactPerson: biddingCompanies.contactPerson,
@@ -217,6 +220,7 @@ export async function getBiddingCompanies(biddingId: number) {
proposedShippingPort: companyConditionResponses.proposedShippingPort,
proposedDestinationPort: companyConditionResponses.proposedDestinationPort,
sparePartResponse: companyConditionResponses.sparePartResponse,
+ additionalProposals: companyConditionResponses.additionalProposals,
})
.from(biddingCompanies)
.leftJoin(
@@ -243,7 +247,7 @@ export async function getBiddingCompanies(biddingId: number) {
}
// 선택된 업체들에게 사전견적 초대 발송
-export async function sendPreQuoteInvitations(companyIds: number[]) {
+export async function sendPreQuoteInvitations(companyIds: number[], preQuoteDeadline?: Date | string) {
try {
if (companyIds.length === 0) {
return {
@@ -292,6 +296,7 @@ export async function sendPreQuoteInvitations(companyIds: number[]) {
.set({
invitationStatus: 'sent', // 사전견적 초대 발송 상태
invitedAt: new Date(),
+ preQuoteDeadline: preQuoteDeadline ? new Date(preQuoteDeadline) : null,
updatedAt: new Date()
})
.where(eq(biddingCompanies.id, id))
@@ -406,7 +411,9 @@ export async function getBiddingCompaniesForPartners(biddingId: number, companyI
invitationStatus: biddingCompanies.invitationStatus,
preQuoteAmount: biddingCompanies.preQuoteAmount,
preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt,
+ preQuoteDeadline: biddingCompanies.preQuoteDeadline,
isPreQuoteSelected: biddingCompanies.isPreQuoteSelected,
+ isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated,
isAttendingMeeting: biddingCompanies.isAttendingMeeting,
// company_condition_responses 정보
paymentTermsResponse: companyConditionResponses.paymentTermsResponse,
@@ -443,7 +450,9 @@ export async function getBiddingCompaniesForPartners(biddingId: number, companyI
invitationStatus: null,
preQuoteAmount: null,
preQuoteSubmittedAt: null,
+ preQuoteDeadline: null,
isPreQuoteSelected: false,
+ isPreQuoteParticipated: null,
isAttendingMeeting: null,
paymentTermsResponse: null,
taxConditionsResponse: null,
@@ -666,7 +675,7 @@ export async function respondToPreQuoteInvitation(
}
}
-// 벤더에서 사전견적 참여 여부 결정 (isPreQuoteSelected 사용)
+// 벤더에서 사전견적 참여 여부 결정 (isPreQuoteSelected, isPreQuoteParticipated 사용)
export async function setPreQuoteParticipation(
biddingCompanyId: number,
isParticipating: boolean,
@@ -675,8 +684,8 @@ export async function setPreQuoteParticipation(
try {
await db.update(biddingCompanies)
.set({
+ isPreQuoteParticipated: isParticipating,
isPreQuoteSelected: isParticipating,
- invitationStatus: isParticipating ? 'accepted' : 'declined',
respondedAt: new Date(),
updatedAt: new Date()
})
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx
index 692d12ea..91b80bd3 100644
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx
@@ -2,7 +2,7 @@
import * as React from 'react'
import { Bidding } from '@/db/schema'
-import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service'
+import { QuotationDetails } from '@/lib/bidding/detail/service'
import { getBiddingCompanies } from '../service'
import { BiddingPreQuoteVendorTableContent } from './bidding-pre-quote-vendor-table'
@@ -10,7 +10,6 @@ import { BiddingPreQuoteVendorTableContent } from './bidding-pre-quote-vendor-ta
interface BiddingPreQuoteContentProps {
bidding: Bidding
quotationDetails: QuotationDetails | null
- quotationVendors: QuotationVendor[]
biddingCompanies: any[]
prItems: any[]
}
@@ -18,7 +17,6 @@ interface BiddingPreQuoteContentProps {
export function BiddingPreQuoteContent({
bidding,
quotationDetails,
- quotationVendors,
biddingCompanies: initialBiddingCompanies,
prItems
}: BiddingPreQuoteContentProps) {
@@ -42,15 +40,11 @@ export function BiddingPreQuoteContent({
<BiddingPreQuoteVendorTableContent
biddingId={bidding.id}
bidding={bidding}
- vendors={quotationVendors}
biddingCompanies={biddingCompanies}
onRefresh={handleRefresh}
onOpenItemsDialog={() => {}}
onOpenTargetPriceDialog={() => {}}
onOpenSelectionReasonDialog={() => {}}
- onEdit={undefined}
- onDelete={undefined}
- onSelectWinner={undefined}
/>
</div>
)
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
index 84824c1e..1b0598b7 100644
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
@@ -3,6 +3,8 @@
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
@@ -16,7 +18,7 @@ import { BiddingCompany } from './bidding-pre-quote-vendor-columns'
import { sendPreQuoteInvitations } from '../service'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
-import { Mail, Building2 } from 'lucide-react'
+import { Mail, Building2, Calendar } from 'lucide-react'
interface BiddingPreQuoteInvitationDialogProps {
open: boolean
@@ -34,6 +36,7 @@ export function BiddingPreQuoteInvitationDialog({
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
const [selectedCompanyIds, setSelectedCompanyIds] = React.useState<number[]>([])
+ const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('')
// 초대 가능한 업체들 (pending 상태인 업체들)
const invitableCompanies = companies.filter(company =>
@@ -67,7 +70,10 @@ export function BiddingPreQuoteInvitationDialog({
}
startTransition(async () => {
- const response = await sendPreQuoteInvitations(selectedCompanyIds)
+ const response = await sendPreQuoteInvitations(
+ selectedCompanyIds,
+ preQuoteDeadline || undefined
+ )
if (response.success) {
toast({
@@ -75,6 +81,7 @@ export function BiddingPreQuoteInvitationDialog({
description: response.message,
})
setSelectedCompanyIds([])
+ setPreQuoteDeadline('')
onOpenChange(false)
onSuccess()
} else {
@@ -91,6 +98,7 @@ export function BiddingPreQuoteInvitationDialog({
onOpenChange(open)
if (!open) {
setSelectedCompanyIds([])
+ setPreQuoteDeadline('')
}
}
@@ -114,6 +122,24 @@ export function BiddingPreQuoteInvitationDialog({
</div>
) : (
<>
+ {/* 견적마감일 설정 */}
+ <div className="mb-6 p-4 border rounded-lg bg-muted/30">
+ <Label htmlFor="preQuoteDeadline" className="text-sm font-medium mb-2 flex items-center gap-2">
+ <Calendar className="w-4 h-4" />
+ 견적 마감일 (선택사항)
+ </Label>
+ <Input
+ id="preQuoteDeadline"
+ type="datetime-local"
+ value={preQuoteDeadline}
+ onChange={(e) => setPreQuoteDeadline(e.target.value)}
+ className="w-full"
+ />
+ <p className="text-xs text-muted-foreground mt-1">
+ 설정하지 않으면 마감일 없이 초대가 발송됩니다.
+ </p>
+ </div>
+
{/* 전체 선택 */}
<div className="flex items-center space-x-2 mb-4 pb-2 border-b">
<Checkbox
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 7e84f178..5c6f41ce 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
@@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
- MoreHorizontal, Edit, Trash2, UserPlus, Paperclip
+ MoreHorizontal, Edit, Trash2, Paperclip
} from "lucide-react"
import {
DropdownMenu,
@@ -27,7 +27,9 @@ export interface BiddingCompany {
respondedAt: Date | null
preQuoteAmount: string | null
preQuoteSubmittedAt: Date | null
+ preQuoteDeadline: Date | null
isPreQuoteSelected: boolean
+ isPreQuoteParticipated: boolean | null
isAttendingMeeting: boolean | null
notes: string | null
contactPerson: string | null
@@ -56,7 +58,6 @@ export interface BiddingCompany {
interface GetBiddingCompanyColumnsProps {
onEdit: (company: BiddingCompany) => void
onDelete: (company: BiddingCompany) => void
- onInvite: (company: BiddingCompany) => void
onViewPriceAdjustment?: (company: BiddingCompany) => void
onViewItemDetails?: (company: BiddingCompany) => void
onViewAttachments?: (company: BiddingCompany) => void
@@ -65,7 +66,6 @@ interface GetBiddingCompanyColumnsProps {
export function getBiddingPreQuoteVendorColumns({
onEdit,
onDelete,
- onInvite,
onViewPriceAdjustment,
onViewItemDetails,
onViewAttachments
@@ -109,11 +109,28 @@ export function getBiddingPreQuoteVendorColumns({
header: '초대 상태',
cell: ({ row }) => {
const status = row.original.invitationStatus
- const variant = status === 'accepted' ? 'default' :
- status === 'declined' ? 'destructive' : 'outline'
+ let variant: any
+ let label: string
- const label = status === 'accepted' ? '수락' :
- status === 'declined' ? '거절' : '대기중'
+ if (status === 'accepted') {
+ variant = 'default'
+ label = '수락'
+ } else if (status === 'declined') {
+ variant = 'destructive'
+ label = '거절'
+ } else if (status === 'pending') {
+ variant = 'outline'
+ label = '대기중'
+ } else if (status === 'sent') {
+ variant = 'outline'
+ label = '요청됨'
+ } else if (status === 'submitted') {
+ variant = 'outline'
+ label = '제출됨'
+ } else {
+ variant = 'outline'
+ label = status || '-'
+ }
return <Badge variant={variant}>{label}</Badge>
},
@@ -150,6 +167,31 @@ export function getBiddingPreQuoteVendorColumns({
),
},
{
+ accessorKey: 'preQuoteDeadline',
+ header: '사전견적 마감일',
+ cell: ({ row }) => {
+ const deadline = row.original.preQuoteDeadline
+ if (!deadline) {
+ return <div className="text-muted-foreground text-sm">-</div>
+ }
+
+ const now = new Date()
+ const deadlineDate = new Date(deadline)
+ const isExpired = deadlineDate < now
+
+ return (
+ <div className={`text-sm ${isExpired ? 'text-red-600' : ''}`}>
+ <div>{deadlineDate.toLocaleDateString('ko-KR')}</div>
+ {isExpired && (
+ <Badge variant="destructive" className="text-xs mt-1">
+ 마감
+ </Badge>
+ )}
+ </div>
+ )
+ },
+ },
+ {
accessorKey: 'attachments',
header: '첨부파일',
cell: ({ row }) => {
@@ -174,6 +216,21 @@ export function getBiddingPreQuoteVendorColumns({
},
},
{
+ accessorKey: 'isPreQuoteParticipated',
+ header: '사전견적 참여의사',
+ cell: ({ row }) => {
+ const participated = row.original.isPreQuoteParticipated
+ if (participated === null) {
+ return <Badge variant="outline">미결정</Badge>
+ }
+ return (
+ <Badge variant={participated ? 'default' : 'destructive'}>
+ {participated ? '참여' : '미참여'}
+ </Badge>
+ )
+ },
+ },
+ {
accessorKey: 'isPreQuoteSelected',
header: '본입찰 선정',
cell: ({ row }) => (
@@ -307,15 +364,6 @@ export function getBiddingPreQuoteVendorColumns({
),
},
{
- accessorKey: 'notes',
- header: '특이사항',
- cell: ({ row }) => (
- <div className="text-sm max-w-32 truncate" title={row.original.notes || ''}>
- {row.original.notes || '-'}
- </div>
- ),
- },
- {
id: 'actions',
header: '액션',
cell: ({ row }) => {
@@ -334,12 +382,6 @@ export function getBiddingPreQuoteVendorColumns({
<Edit className="mr-2 h-4 w-4" />
수정
</DropdownMenuItem> */}
- {company.invitationStatus === 'pending' && (
- <DropdownMenuItem onClick={() => onInvite(company)}>
- <UserPlus className="mr-2 h-4 w-4" />
- 초대 발송
- </DropdownMenuItem>
- )}
<DropdownMenuItem
onClick={() => onDelete(company)}
className="text-destructive"
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx
index a1319821..7ea05721 100644
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx
@@ -23,7 +23,6 @@ import { getPrItemsForBidding } from '../service'
interface BiddingPreQuoteVendorTableContentProps {
biddingId: number
bidding: Bidding
- vendors: any[] // 사용하지 않음
biddingCompanies: BiddingCompany[]
onRefresh: () => void
onOpenItemsDialog: () => void
@@ -31,7 +30,6 @@ interface BiddingPreQuoteVendorTableContentProps {
onOpenSelectionReasonDialog: () => void
onEdit?: (company: BiddingCompany) => void
onDelete?: (company: BiddingCompany) => void
- onSelectWinner?: (company: BiddingCompany) => void
}
const filterFields: DataTableFilterField<BiddingCompany>[] = [
@@ -80,6 +78,7 @@ const advancedFilterFields: DataTableAdvancedFilterField<BiddingCompany>[] = [
options: [
{ label: '수락', value: 'accepted' },
{ label: '거절', value: 'declined' },
+ { label: '요청됨', value: 'sent' },
{ label: '대기중', value: 'pending' },
],
},
@@ -88,15 +87,13 @@ const advancedFilterFields: DataTableAdvancedFilterField<BiddingCompany>[] = [
export function BiddingPreQuoteVendorTableContent({
biddingId,
bidding,
- vendors,
biddingCompanies,
onRefresh,
onOpenItemsDialog,
onOpenTargetPriceDialog,
onOpenSelectionReasonDialog,
onEdit,
- onDelete,
- onSelectWinner
+ onDelete
}: BiddingPreQuoteVendorTableContentProps) {
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
@@ -137,13 +134,6 @@ export function BiddingPreQuoteVendorTableContent({
setIsEditDialogOpen(true)
}
- const handleInvite = (company: BiddingCompany) => {
- // TODO: 초대 발송 로직 구현
- toast({
- title: '알림',
- description: `${company.companyName} 업체에 초대를 발송했습니다.`,
- })
- }
const handleViewPriceAdjustment = async (company: BiddingCompany) => {
startTransition(async () => {
@@ -190,12 +180,11 @@ export function BiddingPreQuoteVendorTableContent({
() => getBiddingPreQuoteVendorColumns({
onEdit: onEdit || handleEdit,
onDelete: onDelete || handleDelete,
- onInvite: handleInvite,
onViewPriceAdjustment: handleViewPriceAdjustment,
onViewItemDetails: handleViewItemDetails,
onViewAttachments: handleViewAttachments
}),
- [onEdit, onDelete, handleEdit, handleDelete, handleInvite, handleViewPriceAdjustment, handleViewItemDetails, handleViewAttachments]
+ [onEdit, onDelete, handleEdit, handleDelete, handleViewPriceAdjustment, handleViewItemDetails, handleViewAttachments]
)
const { table } = useDataTable({