summaryrefslogtreecommitdiff
path: root/lib/bidding
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding')
-rw-r--r--lib/bidding/detail/service.ts62
-rw-r--r--lib/bidding/detail/table/bidding-detail-content.tsx42
-rw-r--r--lib/bidding/detail/table/bidding-detail-header.tsx49
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-columns.tsx43
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx27
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx20
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx29
-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
-rw-r--r--lib/bidding/vendor/components/simple-file-upload.tsx20
-rw-r--r--lib/bidding/vendor/partners-bidding-attendance-dialog.tsx31
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx61
-rw-r--r--lib/bidding/vendor/partners-bidding-list.tsx12
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx389
-rw-r--r--lib/bidding/vendor/partners-bidding-toolbar-actions.tsx31
18 files changed, 688 insertions, 284 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index 956c1798..d9bcb255 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -71,7 +71,7 @@ export interface QuotationVendor {
quotationAmount: number // 견적금액
currency: string
submissionDate: string // 제출일
- isWinner: boolean // 낙찰여부
+ isWinner: boolean | null // 낙찰여부 (null: 미정, true: 낙찰, false: 탈락)
awardRatio: number | null // 발주비율
isBiddingParticipated: boolean | null // 본입찰 참여여부
status: 'pending' | 'submitted' | 'selected' | 'rejected'
@@ -262,7 +262,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV
quotationAmount: Number(vendor.quotationAmount) || 0,
currency: vendor.currency,
submissionDate: vendor.submissionDate ? (vendor.submissionDate instanceof Date ? vendor.submissionDate.toISOString().split('T')[0] : String(vendor.submissionDate).split('T')[0]) : '',
- isWinner: vendor.isWinner || false,
+ isWinner: vendor.isWinner,
awardRatio: vendor.awardRatio ? Number(vendor.awardRatio) : null,
isBiddingParticipated: vendor.isBiddingParticipated,
status: vendor.status as 'pending' | 'submitted' | 'selected' | 'rejected',
@@ -1208,7 +1208,7 @@ export async function uploadAwardDocument(biddingId: number, file: File, userId:
originalFileName: file.name,
filePath: saveResult.filePath,
fileSize: file.size,
- documentType: 'award',
+ documentType: 'other',
title: '낙찰 관련 문서',
description: '낙찰 관련 첨부파일',
uploadedBy: userId,
@@ -1347,7 +1347,7 @@ export async function awardBidding(biddingId: number, selectionReason: string, u
eq(biddingCompanies.biddingId, biddingId),
eq(biddingCompanies.isWinner, true)
))
-
+
if (awardedCompanies.length === 0) {
return { success: false, error: '낙찰된 업체가 없습니다. 먼저 발주비율을 산정해주세요.' }
}
@@ -1373,27 +1373,40 @@ export async function awardBidding(biddingId: number, selectionReason: string, u
// 2. 선정 사유 저장 (첫 번째 낙찰 업체 기준으로 저장)
const firstAwardedCompany = awardedCompanies[0]
- await tx
- .insert(vendorSelectionResults)
- .values({
- biddingId,
- selectedCompanyId: firstAwardedCompany.companyId,
- selectionReason,
- selectedBy: userId,
- selectedAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- })
- .onConflictDoUpdate({
- target: [vendorSelectionResults.biddingId],
- set: {
+
+ // 기존 선정 결과 확인
+ const existingResult = await tx
+ .select()
+ .from(vendorSelectionResults)
+ .where(eq(vendorSelectionResults.biddingId, biddingId))
+ .limit(1)
+
+ if (existingResult.length > 0) {
+ // 업데이트
+ await tx
+ .update(vendorSelectionResults)
+ .set({
selectedCompanyId: firstAwardedCompany.companyId,
selectionReason,
selectedBy: userId,
selectedAt: new Date(),
updatedAt: new Date()
- }
- })
+ })
+ .where(eq(vendorSelectionResults.biddingId, biddingId))
+ } else {
+ // 삽입
+ await tx
+ .insert(vendorSelectionResults)
+ .values({
+ biddingId,
+ selectedCompanyId: firstAwardedCompany.companyId,
+ selectionReason,
+ selectedBy: userId,
+ selectedAt: new Date(),
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
+ }
})
@@ -1644,12 +1657,14 @@ export interface PartnersBiddingListItem {
isWinner: boolean | null
isAttendingMeeting: boolean | null
isPreQuoteSelected: boolean | null
+ isPreQuoteParticipated: boolean | null
+ preQuoteDeadline: Date | null
isBiddingInvited: boolean | null
notes: string | null
createdAt: Date
updatedAt: Date
// updatedBy: string | null
-
+ hasSpecificationMeeting: boolean | null
// biddings 정보
biddingId: number
biddingNumber: string
@@ -1688,6 +1703,8 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part
isWinner: biddingCompanies.isWinner,
isAttendingMeeting: biddingCompanies.isAttendingMeeting,
isPreQuoteSelected: biddingCompanies.isPreQuoteSelected,
+ isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated,
+ preQuoteDeadline: biddingCompanies.preQuoteDeadline,
isBiddingInvited: biddingCompanies.isBiddingInvited,
notes: biddingCompanies.notes,
createdAt: biddingCompanies.createdAt,
@@ -1712,6 +1729,7 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part
managerPhone: biddings.managerPhone,
currency: biddings.currency,
budget: biddings.budget,
+ hasSpecificationMeeting: biddings.hasSpecificationMeeting,
})
.from(biddingCompanies)
.innerJoin(biddings, eq(biddingCompanies.biddingId, biddings.id))
@@ -1791,6 +1809,8 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId:
isAttendingMeeting: biddingCompanies.isAttendingMeeting,
isPreQuoteSelected: biddingCompanies.isPreQuoteSelected,
isBiddingParticipated: biddingCompanies.isBiddingParticipated,
+ isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated,
+ hasSpecificationMeeting: biddings.hasSpecificationMeeting,
// 응답한 조건들 (company_condition_responses) - 제시된 조건과 응답 모두 여기서 관리
paymentTermsResponse: companyConditionResponses.paymentTermsResponse,
taxConditionsResponse: companyConditionResponses.taxConditionsResponse,
diff --git a/lib/bidding/detail/table/bidding-detail-content.tsx b/lib/bidding/detail/table/bidding-detail-content.tsx
index 91bea2f4..a96509a9 100644
--- a/lib/bidding/detail/table/bidding-detail-content.tsx
+++ b/lib/bidding/detail/table/bidding-detail-content.tsx
@@ -7,6 +7,10 @@ import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service'
import { BiddingDetailVendorTableContent } from './bidding-detail-vendor-table'
import { BiddingDetailItemsDialog } from './bidding-detail-items-dialog'
import { BiddingDetailTargetPriceDialog } from './bidding-detail-target-price-dialog'
+import { BiddingPreQuoteItemDetailsDialog } from '../../../bidding/pre-quote/table/bidding-pre-quote-item-details-dialog'
+import { getPrItemsForBidding } from '../../../bidding/pre-quote/service'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
interface BiddingDetailContentProps {
bidding: Bidding
@@ -21,6 +25,9 @@ export function BiddingDetailContent({
quotationVendors,
prItems
}: BiddingDetailContentProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+
const [dialogStates, setDialogStates] = React.useState({
items: false,
targetPrice: false,
@@ -29,6 +36,11 @@ export function BiddingDetailContent({
})
const [refreshTrigger, setRefreshTrigger] = React.useState(0)
+
+ // PR 아이템 다이얼로그 관련 state
+ const [isItemDetailsDialogOpen, setIsItemDetailsDialogOpen] = React.useState(false)
+ const [selectedVendorForDetails, setSelectedVendorForDetails] = React.useState<QuotationVendor | null>(null)
+ const [prItemsForDialog, setPrItemsForDialog] = React.useState<any[]>([])
const handleRefresh = React.useCallback(() => {
setRefreshTrigger(prev => prev + 1)
@@ -42,6 +54,25 @@ export function BiddingDetailContent({
setDialogStates(prev => ({ ...prev, [type]: false }))
}, [])
+ const handleViewItemDetails = React.useCallback((vendor: QuotationVendor) => {
+ startTransition(async () => {
+ try {
+ // PR 아이템 정보 로드
+ const prItemsData = await getPrItemsForBidding(bidding.id)
+ setPrItemsForDialog(prItemsData)
+ setSelectedVendorForDetails(vendor)
+ setIsItemDetailsDialogOpen(true)
+ } catch (error) {
+ console.error('Failed to load PR items:', error)
+ toast({
+ title: '오류',
+ description: '품목 정보를 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ }
+ })
+ }, [bidding.id, toast])
+
return (
<div className="space-y-6">
<BiddingDetailVendorTableContent
@@ -53,6 +84,7 @@ export function BiddingDetailContent({
onOpenTargetPriceDialog={() => openDialog('targetPrice')}
onOpenSelectionReasonDialog={() => openDialog('selectionReason')}
onOpenAwardDialog={() => openDialog('award')}
+ onViewItemDetails={handleViewItemDetails}
onEdit={undefined}
onDelete={undefined}
onSelectWinner={undefined}
@@ -72,6 +104,16 @@ export function BiddingDetailContent({
bidding={bidding}
onSuccess={handleRefresh}
/>
+
+ <BiddingPreQuoteItemDetailsDialog
+ open={isItemDetailsDialogOpen}
+ onOpenChange={setIsItemDetailsDialogOpen}
+ biddingId={bidding.id}
+ biddingCompanyId={selectedVendorForDetails?.id || 0}
+ companyName={selectedVendorForDetails?.vendorName || ''}
+ prItems={prItemsForDialog}
+ currency={bidding.currency || 'KRW'}
+ />
</div>
)
}
diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx
index fcbbeb9a..2798478c 100644
--- a/lib/bidding/detail/table/bidding-detail-header.tsx
+++ b/lib/bidding/detail/table/bidding-detail-header.tsx
@@ -141,50 +141,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
})
}
- const getActionButtons = () => {
- const buttons = []
-
- // 기본 액션 버튼들 (항상 표시)
-
-
- // 모든 액션 버튼을 항상 표시 (상태 검증은 각 핸들러에서)
- buttons.push(
- <Button
- key="register"
- onClick={handleRegister}
- disabled={isPending}
- >
- <Send className="w-4 h-4 mr-2" />
- 입찰등록
- </Button>
- )
-
- buttons.push(
- <Button
- key="disposal"
- variant="destructive"
- onClick={handleMarkAsDisposal}
- disabled={isPending}
- >
- <XCircle className="w-4 h-4 mr-2" />
- 유찰
- </Button>
- )
-
- buttons.push(
- <Button
- key="rebidding"
- onClick={handleCreateRebidding}
- disabled={isPending}
- >
- <RotateCcw className="w-4 h-4 mr-2" />
- 재입찰
- </Button>
- )
-
- return buttons
- }
-
return (
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="px-6 py-4">
@@ -209,11 +165,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
</Badge>
</div>
</div>
-
- {/* 액션 버튼들 */}
- <div className="flex items-center gap-2 flex-shrink-0">
- {getActionButtons()}
- </div>
</div>
</div>
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
index bb1d2c62..cbdf79c2 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
@@ -23,6 +23,7 @@ interface GetVendorColumnsProps {
onDelete: (vendor: QuotationVendor) => void
onSelectWinner: (vendor: QuotationVendor) => void
onViewPriceAdjustment?: (vendor: QuotationVendor) => void
+ onViewItemDetails?: (vendor: QuotationVendor) => void
onSendBidding?: (vendor: QuotationVendor) => void
onUpdateParticipation?: (vendor: QuotationVendor, participated: boolean) => void
}
@@ -32,6 +33,7 @@ export function getBiddingDetailVendorColumns({
onDelete,
onSelectWinner,
onViewPriceAdjustment,
+ onViewItemDetails,
onSendBidding,
onUpdateParticipation
}: GetVendorColumnsProps): ColumnDef<QuotationVendor>[] {
@@ -72,11 +74,24 @@ export function getBiddingDetailVendorColumns({
{
accessorKey: 'quotationAmount',
header: '견적금액',
- cell: ({ row }) => (
- <div className="text-right font-mono">
- {row.original.quotationAmount ? Number(row.original.quotationAmount).toLocaleString() : '-'} {row.original.currency}
- </div>
- ),
+ cell: ({ row }) => {
+ const hasAmount = row.original.quotationAmount && Number(row.original.quotationAmount) > 0
+ return (
+ <div className="text-right font-mono">
+ {hasAmount ? (
+ <button
+ onClick={() => onViewItemDetails?.(row.original)}
+ className="text-primary hover:text-primary/80 hover:underline cursor-pointer"
+ title="품목별 견적 상세 보기"
+ >
+ {Number(row.original.quotationAmount).toLocaleString()} {row.original.currency}
+ </button>
+ ) : (
+ <span className="text-muted-foreground">- {row.original.currency}</span>
+ )}
+ </div>
+ )
+ },
},
{
accessorKey: 'biddingResult',
@@ -84,7 +99,7 @@ export function getBiddingDetailVendorColumns({
cell: ({ row }) => {
const isWinner = row.original.isWinner
if (isWinner === null || isWinner === undefined) {
- return <div>-</div>
+ return <div>미정</div>
}
return (
<Badge variant={isWinner ? 'default' : 'secondary'} className={isWinner ? 'bg-green-600' : ''}>
@@ -158,12 +173,24 @@ export function getBiddingDetailVendorColumns({
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>작업</DropdownMenuLabel>
- <DropdownMenuItem onClick={() => onEdit(vendor)}>
+ <DropdownMenuItem
+ onClick={() => onEdit(vendor)}
+ disabled={vendor.isBiddingParticipated !== true}
+ >
발주비율 산정
+ {vendor.isBiddingParticipated !== true && (
+ <span className="text-xs text-muted-foreground ml-2">(입찰참여 필요)</span>
+ )}
</DropdownMenuItem>
{vendor.status !== 'selected' && (
- <DropdownMenuItem onClick={() => onSelectWinner(vendor)}>
+ <DropdownMenuItem
+ onClick={() => onSelectWinner(vendor)}
+ disabled={vendor.isBiddingParticipated !== true}
+ >
낙찰 선정
+ {vendor.isBiddingParticipated !== true && (
+ <span className="text-xs text-muted-foreground ml-2">(입찰참여 필요)</span>
+ )}
</DropdownMenuItem>
)}
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 b10212ab..9a5408c2 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
@@ -112,6 +112,22 @@ export function BiddingDetailVendorEditDialog({
)}
{/* 수정 가능한 필드들 */}
+ {vendor && vendor.isBiddingParticipated !== true && (
+ <div className="bg-orange-50 border border-orange-200 rounded-lg p-3 mb-4">
+ <div className="flex items-center gap-2 text-orange-800">
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
+ <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
+ </svg>
+ <span className="font-medium">입찰 참여 안내</span>
+ </div>
+ <p className="text-sm text-orange-700 mt-1">
+ {vendor.isBiddingParticipated === null
+ ? '이 업체는 아직 입찰참여 여부가 결정되지 않았습니다. 입찰에 참여한 업체만 발주비율을 설정할 수 있습니다.'
+ : '이 업체는 입찰에 참여하지 않습니다. 발주비율을 설정할 수 없습니다.'
+ }
+ </p>
+ </div>
+ )}
<div className="space-y-2">
<Label htmlFor="edit-awardRatio">발주비율 (%)</Label>
@@ -123,14 +139,23 @@ export function BiddingDetailVendorEditDialog({
value={formData.awardRatio}
onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })}
placeholder="발주비율을 입력하세요"
+ disabled={vendor?.isBiddingParticipated !== true}
/>
+ {vendor?.isBiddingParticipated !== true && (
+ <p className="text-sm text-muted-foreground">
+ 입찰에 참여한 업체만 발주비율을 설정할 수 있습니다.
+ </p>
+ )}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
취소
</Button>
- <Button onClick={handleEdit} disabled={isPending}>
+ <Button
+ onClick={handleEdit}
+ disabled={isPending || vendor?.isBiddingParticipated !== true}
+ >
산정
</Button>
</DialogFooter>
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
index dd1ae94b..95f63ce9 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
@@ -1,6 +1,7 @@
'use client'
import * as React from 'react'
+import { useSession } from 'next-auth/react'
import { type DataTableAdvancedFilterField, type DataTableFilterField } from '@/types/table'
import { useDataTable } from '@/hooks/use-data-table'
import { DataTable } from '@/components/data-table/data-table'
@@ -33,6 +34,7 @@ interface BiddingDetailVendorTableContentProps {
onEdit?: (vendor: QuotationVendor) => void
onDelete?: (vendor: QuotationVendor) => void
onSelectWinner?: (vendor: QuotationVendor) => void
+ onViewItemDetails?: (vendor: QuotationVendor) => void
}
const filterFields: DataTableFilterField<QuotationVendor>[] = [
@@ -97,10 +99,15 @@ export function BiddingDetailVendorTableContent({
onOpenAwardDialog,
onEdit,
onDelete,
- onSelectWinner
+ onSelectWinner,
+ onViewItemDetails
}: BiddingDetailVendorTableContentProps) {
+ const { data: session } = useSession()
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
+
+ // 세션에서 사용자 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)
@@ -145,7 +152,7 @@ export function BiddingDetailVendorTableContent({
const result = selectWinnerSchema.safeParse({
biddingId,
vendorId: vendor.id,
- awardRatio: vendor.awardRatio,
+ awardRatio: vendor.awardRatio || 0,
})
if (!result.success) {
@@ -157,7 +164,7 @@ export function BiddingDetailVendorTableContent({
return
}
- const response = await selectWinner(biddingId, vendor.id, vendor.awardRatio, 'current-user')
+ const response = await selectWinner(biddingId, vendor.id, vendor.awardRatio || 0, userId)
if (response.success) {
toast({
@@ -209,9 +216,10 @@ export function BiddingDetailVendorTableContent({
onEdit: onEdit || handleEdit,
onDelete: onDelete || handleDelete,
onSelectWinner: onSelectWinner || handleSelectWinner,
- onViewPriceAdjustment: handleViewPriceAdjustment
+ onViewPriceAdjustment: handleViewPriceAdjustment,
+ onViewItemDetails: onViewItemDetails
}),
- [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment]
+ [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment, onViewItemDetails]
)
const { table } = useDataTable({
@@ -241,9 +249,9 @@ export function BiddingDetailVendorTableContent({
table={table}
biddingId={biddingId}
bidding={bidding}
+ userId={userId}
onOpenItemsDialog={onOpenItemsDialog}
onOpenTargetPriceDialog={onOpenTargetPriceDialog}
- onOpenSelectionReasonDialog={onOpenSelectionReasonDialog}
onOpenAwardDialog={() => setIsAwardDialogOpen(true)}
onSuccess={onRefresh}
/>
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 8cdec191..64c31633 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -15,6 +15,7 @@ interface BiddingDetailVendorToolbarActionsProps {
table: Table<QuotationVendor>
biddingId: number
bidding: Bidding
+ userId: string
onOpenItemsDialog: () => void
onOpenTargetPriceDialog: () => void
onOpenAwardDialog: () => void
@@ -25,6 +26,7 @@ export function BiddingDetailVendorToolbarActions({
table,
biddingId,
bidding,
+ userId,
onOpenItemsDialog,
onOpenTargetPriceDialog,
onOpenAwardDialog,
@@ -41,17 +43,17 @@ export function BiddingDetailVendorToolbarActions({
const handleRegister = () => {
startTransition(async () => {
- const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+ const result = await registerBidding(bidding.id, userId)
if (result.success) {
toast({
- title: '성공',
+ title: result.message,
description: result.message,
})
router.refresh()
} else {
toast({
- title: '오류',
+ title: result.error,
description: result.error,
variant: 'destructive',
})
@@ -61,17 +63,17 @@ export function BiddingDetailVendorToolbarActions({
const handleMarkAsDisposal = () => {
startTransition(async () => {
- const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+ const result = await markAsDisposal(bidding.id, userId)
if (result.success) {
toast({
- title: '성공',
+ title: result.message,
description: result.message,
})
router.refresh()
} else {
toast({
- title: '오류',
+ title: result.error,
description: result.error,
variant: 'destructive',
})
@@ -81,18 +83,18 @@ export function BiddingDetailVendorToolbarActions({
const handleCreateRebidding = () => {
startTransition(async () => {
- const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+ const result = await createRebidding(bidding.id, userId)
if (result.success) {
toast({
- title: '성공',
+ title: result.message,
description: result.message,
})
router.refresh()
onSuccess()
} else {
toast({
- title: '오류',
+ title: result.error,
description: result.error,
variant: 'destructive',
})
@@ -104,7 +106,7 @@ export function BiddingDetailVendorToolbarActions({
<>
<div className="flex items-center gap-2">
{/* 상태별 액션 버튼 */}
- {bidding.status === 'bidding_generated' && (
+ {bidding.status === 'set_target_price' && (
<Button
variant="default"
size="sm"
@@ -115,8 +117,6 @@ export function BiddingDetailVendorToolbarActions({
입찰 등록
</Button>
)}
-
- {bidding.status === 'bidding_closed' && (
<>
<Button
variant="destructive"
@@ -137,7 +137,6 @@ export function BiddingDetailVendorToolbarActions({
낙찰
</Button>
</>
- )}
{bidding.status === 'bidding_disposal' && (
<Button
@@ -159,13 +158,13 @@ export function BiddingDetailVendorToolbarActions({
)}
{/* 공통 관리 버튼들 */}
- <Button
+ {/* <Button
variant="outline"
size="sm"
onClick={onOpenItemsDialog}
>
품목 정보
- </Button>
+ </Button> */}
<Button
variant="outline"
size="sm"
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({
diff --git a/lib/bidding/vendor/components/simple-file-upload.tsx b/lib/bidding/vendor/components/simple-file-upload.tsx
index b1eb8b8f..58b60bdf 100644
--- a/lib/bidding/vendor/components/simple-file-upload.tsx
+++ b/lib/bidding/vendor/components/simple-file-upload.tsx
@@ -65,15 +65,23 @@ export function SimpleFileUpload({
try {
setIsLoading(true)
const docs = await getPreQuoteDocuments(biddingId, companyId)
+
+ // docs가 undefined이거나 배열이 아닌 경우 빈 배열로 처리
+ if (!docs || !Array.isArray(docs)) {
+ setDocuments([])
+ return
+ }
+
// Date를 string으로 변환
const mappedDocs = docs.map(doc => ({
...doc,
- uploadedAt: doc.uploadedAt.toString(),
+ uploadedAt: doc.uploadedAt?.toString() || '',
uploadedBy: doc.uploadedBy || ''
}))
setDocuments(mappedDocs)
} catch (error) {
console.error('Failed to load documents:', error)
+ setDocuments([]) // 에러 시에도 빈 배열로 설정
toast({
title: '오류',
description: '업로드된 문서 목록을 불러오는데 실패했습니다.',
@@ -155,9 +163,13 @@ export function SimpleFileUpload({
if (result.success) {
try {
- await downloadFile(result.document?.filePath, result.document?.originalFileName, {
- showToast: true
- })
+ if (result.document?.filePath && result.document?.originalFileName) {
+ await downloadFile(result.document.filePath, result.document.originalFileName, {
+ showToast: true
+ })
+ } else {
+ throw new Error('파일 정보가 없습니다.')
+ }
} catch (error) {
toast({
title: '다운로드 실패',
diff --git a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
index 9205c46a..e93702ed 100644
--- a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
+++ b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
@@ -42,6 +42,7 @@ interface PartnersBiddingAttendanceDialogProps {
preQuoteDate: string | null
biddingRegistrationDate: string | null
evaluationDate: string | null
+ hasSpecificationMeeting?: boolean // 사양설명회 여부 추가
} | null
biddingCompanyId: number
isAttending: boolean | null
@@ -206,6 +207,36 @@ export function PartnersBiddingAttendanceDialog({
if (!biddingDetail) return null
+ // 사양설명회가 없는 경우
+ if (biddingDetail.hasSpecificationMeeting === false) {
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Users className="w-5 h-5" />
+ 사양설명회 정보
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="py-6 text-center">
+ <XCircle className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
+ <h3 className="text-lg font-medium mb-2">사양설명회 없음</h3>
+ <p className="text-muted-foreground">
+ 해당 입찰 건은 사양설명회가 없습니다.
+ </p>
+ </div>
+
+ <DialogFooter>
+ <Button onClick={() => onOpenChange(false)}>
+ 확인
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh]">
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index 0d1e3123..7058f026 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -94,7 +94,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
// 액션 (드롭다운 메뉴)
columnHelper.display({
id: 'actions',
- header: 'Actions',
+ header: '액션',
cell: ({ row }) => {
const handleView = () => {
if (setRowAction) {
@@ -122,13 +122,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
})
}
}
-
- const canManageAttendance = row.original.invitationStatus === 'sent' ||
- row.original.invitationStatus === 'accepted'
- // 사전견적이 가능한 조건: 초대 발송(sent) 상태인 경우
- const canDoPreQuote = row.original.invitationStatus === 'sent' || row.original.invitationStatus === 'pending' || row.original.invitationStatus === 'submitted';
-
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -143,20 +137,12 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem onClick={handleView}>
<FileText className="mr-2 h-4 w-4" />
- 상세보기
+ 입찰 상세보기
</DropdownMenuItem>
- {canDoPreQuote && (
<DropdownMenuItem onClick={handlePreQuote}>
<Calculator className="mr-2 h-4 w-4" />
사전견적하기
</DropdownMenuItem>
- )}
- {canManageAttendance && (
- <DropdownMenuItem onClick={handleAttendance}>
- <Users className="mr-2 h-4 w-4" />
- 참석여부
- </DropdownMenuItem>
- )}
</DropdownMenuContent>
</DropdownMenu>
)
@@ -199,6 +185,22 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
},
}),
+ // 사전견적 참여의사
+ columnHelper.accessor('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>
+ )
+ },
+ }),
+
// 입찰 참여의사
columnHelper.accessor('invitationStatus', {
header: '입찰 참여의사',
@@ -250,6 +252,33 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
},
}),
+ // 사전견적 마감일
+ columnHelper.accessor('preQuoteDeadline', {
+ header: '사전견적 마감일',
+ cell: ({ row }) => {
+ const deadline = row.original.preQuoteDeadline
+ if (!deadline) {
+ return <div className="text-muted-foreground">-</div>
+ }
+
+ const now = new Date()
+ const deadlineDate = new Date(deadline)
+ const isExpired = deadlineDate < now
+
+ return (
+ <div className={`text-sm flex items-center gap-1 ${isExpired ? 'text-red-600' : ''}`}>
+ <Calendar className="w-4 h-4" />
+ <span>{formatDate(deadline, 'KR')}</span>
+ {isExpired && (
+ <Badge variant="destructive" className="text-xs">
+ 마감
+ </Badge>
+ )}
+ </div>
+ )
+ },
+ }),
+
// 계약기간
columnHelper.accessor('contractPeriod', {
header: '계약기간',
diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx
index 2e8d4164..08489da5 100644
--- a/lib/bidding/vendor/partners-bidding-list.tsx
+++ b/lib/bidding/vendor/partners-bidding-list.tsx
@@ -93,6 +93,17 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
case 'view':
// 본입찰 초대 여부 확인
const bidding = rowAction.row.original
+
+ // 사전견적 요청 상태에서는 상세보기 제한
+ if (bidding.status === 'request_for_quotation') {
+ toast({
+ title: '접근 제한',
+ description: '사전견적 요청 상태에서는 상세보기를 이용할 수 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
if (bidding.status === 'bidding_opened' && !bidding.isBiddingInvited) {
// 본입찰이 오픈되었지만 초대받지 않은 경우
toast({
@@ -227,6 +238,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
preQuoteDate: null,
biddingRegistrationDate: rowAction.row.original.submissionStartDate?.toISOString() || null,
evaluationDate: null,
+ hasSpecificationMeeting: (rowAction.row.original as any).hasSpecificationMeeting || false, // 사양설명회 여부 추가
} : null}
biddingCompanyId={rowAction?.row.original?.biddingCompanyId || 0}
isAttending={rowAction?.row.original?.isAttendingMeeting || null}
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
index 4cd0efdb..fdd05154 100644
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
@@ -30,7 +30,8 @@ import {
submitPreQuoteResponse,
getPrItemsForBidding,
getSavedPrItemQuotations,
- savePreQuoteDraft
+ savePreQuoteDraft,
+ setPreQuoteParticipation
} from '../pre-quote/service'
import { getBiddingConditions } from '../service'
import { getPriceAdjustmentFormByBiddingCompanyId } from '../detail/service'
@@ -80,6 +81,7 @@ interface BiddingDetail {
invitationStatus: string | null
preQuoteAmount: string | null
preQuoteSubmittedAt: string | null
+ preQuoteDeadline: string | null
isPreQuoteSelected: boolean | null
isAttendingMeeting: boolean | null
// companyConditionResponses에서 가져온 조건들 (제시된 조건과 응답 모두)
@@ -126,6 +128,9 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
isAttendingMeeting: false,
})
+ // 사전견적 참여의사 상태
+ const [participationDecision, setParticipationDecision] = React.useState<boolean | null>(null)
+
// 연동제 폼 상태
const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({
itemName: '',
@@ -211,6 +216,9 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
additionalProposals: result.additionalProposals || '',
isAttendingMeeting: result.isAttendingMeeting || false,
})
+
+ // 사전견적 참여의사 초기화
+ setParticipationDecision(result.isPreQuoteParticipated)
}
if (conditions) {
@@ -238,64 +246,131 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
// 임시저장 기능
const handleTempSave = () => {
- if (!biddingDetail) return
+ if (!biddingDetail || !biddingDetail.biddingCompanyId) {
+ toast({
+ title: '임시저장 실패',
+ description: '입찰 정보가 올바르지 않습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!userId) {
+ toast({
+ title: '임시저장 실패',
+ description: '사용자 정보를 확인할 수 없습니다. 다시 로그인해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
setIsSaving(true)
startTransition(async () => {
- const result = await savePreQuoteDraft(
+ try {
+ const result = await savePreQuoteDraft(
+ biddingDetail.biddingCompanyId!,
+ {
+ prItemQuotations,
+ paymentTermsResponse: responseData.paymentTermsResponse,
+ taxConditionsResponse: responseData.taxConditionsResponse,
+ incotermsResponse: responseData.incotermsResponse,
+ proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
+ proposedShippingPort: responseData.proposedShippingPort,
+ proposedDestinationPort: responseData.proposedDestinationPort,
+ priceAdjustmentResponse: responseData.priceAdjustmentResponse,
+ isInitialResponse: responseData.isInitialResponse,
+ sparePartResponse: responseData.sparePartResponse,
+ additionalProposals: responseData.additionalProposals,
+ priceAdjustmentForm: responseData.priceAdjustmentResponse ? {
+ itemName: priceAdjustmentForm.itemName,
+ adjustmentReflectionPoint: priceAdjustmentForm.adjustmentReflectionPoint,
+ majorApplicableRawMaterial: priceAdjustmentForm.majorApplicableRawMaterial,
+ adjustmentFormula: priceAdjustmentForm.adjustmentFormula,
+ rawMaterialPriceIndex: priceAdjustmentForm.rawMaterialPriceIndex,
+ referenceDate: priceAdjustmentForm.referenceDate,
+ comparisonDate: priceAdjustmentForm.comparisonDate,
+ adjustmentRatio: priceAdjustmentForm.adjustmentRatio ? parseFloat(priceAdjustmentForm.adjustmentRatio) : undefined,
+ notes: priceAdjustmentForm.notes,
+ adjustmentConditions: priceAdjustmentForm.adjustmentConditions,
+ majorNonApplicableRawMaterial: priceAdjustmentForm.majorNonApplicableRawMaterial,
+ adjustmentPeriod: priceAdjustmentForm.adjustmentPeriod,
+ contractorWriter: priceAdjustmentForm.contractorWriter,
+ adjustmentDate: priceAdjustmentForm.adjustmentDate,
+ nonApplicableReason: priceAdjustmentForm.nonApplicableReason,
+ } : undefined
+ },
+ userId
+ )
+
+ if (result.success) {
+ toast({
+ title: '임시저장 완료',
+ description: result.message,
+ })
+ } else {
+ toast({
+ title: '임시저장 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ } catch (error) {
+ console.error('Temp save error:', error)
+ toast({
+ title: '임시저장 실패',
+ description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsSaving(false)
+ }
+ })
+ }
+
+ // 사전견적 참여의사 설정 함수
+ const handleParticipationDecision = async (participate: boolean) => {
+ if (!biddingDetail?.biddingCompanyId) return
+
+ startTransition(async () => {
+ const result = await setPreQuoteParticipation(
biddingDetail.biddingCompanyId!,
- {
- prItemQuotations,
- paymentTermsResponse: responseData.paymentTermsResponse,
- taxConditionsResponse: responseData.taxConditionsResponse,
- incotermsResponse: responseData.incotermsResponse,
- proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
- proposedShippingPort: responseData.proposedShippingPort,
- proposedDestinationPort: responseData.proposedDestinationPort,
- priceAdjustmentResponse: responseData.priceAdjustmentResponse,
- isInitialResponse: responseData.isInitialResponse,
- sparePartResponse: responseData.sparePartResponse,
- additionalProposals: responseData.additionalProposals,
- priceAdjustmentForm: responseData.priceAdjustmentResponse ? {
- itemName: priceAdjustmentForm.itemName,
- adjustmentReflectionPoint: priceAdjustmentForm.adjustmentReflectionPoint,
- majorApplicableRawMaterial: priceAdjustmentForm.majorApplicableRawMaterial,
- adjustmentFormula: priceAdjustmentForm.adjustmentFormula,
- rawMaterialPriceIndex: priceAdjustmentForm.rawMaterialPriceIndex,
- referenceDate: priceAdjustmentForm.referenceDate,
- comparisonDate: priceAdjustmentForm.comparisonDate,
- adjustmentRatio: priceAdjustmentForm.adjustmentRatio ? parseFloat(priceAdjustmentForm.adjustmentRatio) : undefined,
- notes: priceAdjustmentForm.notes,
- adjustmentConditions: priceAdjustmentForm.adjustmentConditions,
- majorNonApplicableRawMaterial: priceAdjustmentForm.majorNonApplicableRawMaterial,
- adjustmentPeriod: priceAdjustmentForm.adjustmentPeriod,
- contractorWriter: priceAdjustmentForm.contractorWriter,
- adjustmentDate: priceAdjustmentForm.adjustmentDate,
- nonApplicableReason: priceAdjustmentForm.nonApplicableReason,
- } : undefined
- },
- 'current-user' // TODO: 실제 사용자 ID
+ participate,
+ userId
)
if (result.success) {
+ setParticipationDecision(participate)
toast({
- title: '임시저장 완료',
- description: result.message,
+ title: '설정 완료',
+ description: `사전견적 ${participate ? '참여' : '미참여'}로 설정되었습니다.`,
})
} else {
toast({
- title: '임시저장 실패',
+ title: '설정 실패',
description: result.error,
variant: 'destructive',
})
}
- setIsSaving(false)
})
}
const handleSubmitResponse = () => {
if (!biddingDetail) return
+ // 견적마감일 체크
+ if (biddingDetail.preQuoteDeadline) {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ if (deadline < now) {
+ toast({
+ title: '견적 마감',
+ description: '견적 마감일이 지나 제출할 수 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+ }
+
// 필수값 검증
if (prItemQuotations.length === 0 || totalAmount === 0) {
toast({
@@ -342,7 +417,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
const result = await submitPreQuoteResponse(
biddingDetail.biddingCompanyId!,
submissionData,
- 'current-user' // TODO: 실제 사용자 ID
+ userId
)
console.log('제출 결과:', result)
@@ -493,7 +568,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<span>{biddingDetail.itemName}</span>
</div>
</div>
- <div>
+ {/* <div>
<Label className="text-sm font-medium text-muted-foreground">계약구분</Label>
<div className="mt-1">{contractTypeLabels[biddingDetail.contractType]}</div>
</div>
@@ -504,7 +579,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<div>
<Label className="text-sm font-medium text-muted-foreground">낙찰수</Label>
<div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : '복수'}</div>
- </div>
+ </div> */}
<div>
<Label className="text-sm font-medium text-muted-foreground">담당자</Label>
<div className="flex items-center gap-2 mt-1">
@@ -514,7 +589,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</div>
</div>
- {biddingDetail.budget && (
+ {/* {biddingDetail.budget && (
<div>
<Label className="text-sm font-medium text-muted-foreground">예산</Label>
<div className="flex items-center gap-2 mt-1">
@@ -522,10 +597,10 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<span className="font-semibold">{formatCurrency(biddingDetail.budget)}</span>
</div>
</div>
- )}
+ )} */}
{/* 일정 정보 */}
- <div className="pt-4 border-t">
+ {/* <div className="pt-4 border-t">
<Label className="text-sm font-medium text-muted-foreground mb-2 block">일정 정보</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
{biddingDetail.submissionStartDate && biddingDetail.submissionEndDate && (
@@ -539,7 +614,60 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</div>
)}
</div>
- </div>
+ </div> */}
+
+ {/* 견적마감일 정보 */}
+ {biddingDetail.preQuoteDeadline && (
+ <div className="pt-4 border-t">
+ <Label className="text-sm font-medium text-muted-foreground mb-2 block">견적 마감 정보</Label>
+ {(() => {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ const isExpired = deadline < now
+ const timeLeft = deadline.getTime() - now.getTime()
+ const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
+ const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+
+ return (
+ <div className={`p-3 rounded-lg border-2 ${
+ isExpired
+ ? 'border-red-200 bg-red-50'
+ : daysLeft <= 1
+ ? 'border-orange-200 bg-orange-50'
+ : 'border-green-200 bg-green-50'
+ }`}>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Calendar className="w-5 h-5" />
+ <span className="font-medium">견적 마감일:</span>
+ <span className="text-lg font-semibold">
+ {formatDate(biddingDetail.preQuoteDeadline, 'KR')}
+ </span>
+ </div>
+ {isExpired ? (
+ <Badge variant="destructive" className="ml-2">
+ 마감됨
+ </Badge>
+ ) : daysLeft <= 1 ? (
+ <Badge variant="secondary" className="ml-2 bg-orange-100 text-orange-800">
+ {daysLeft === 0 ? `${hoursLeft}시간 남음` : `${daysLeft}일 남음`}
+ </Badge>
+ ) : (
+ <Badge variant="secondary" className="ml-2 bg-green-100 text-green-800">
+ {daysLeft}일 남음
+ </Badge>
+ )}
+ </div>
+ {isExpired && (
+ <div className="mt-2 text-sm text-red-600">
+ ⚠️ 견적 마감일이 지났습니다. 견적 제출이 불가능합니다.
+ </div>
+ )}
+ </div>
+ )
+ })()}
+ </div>
+ )}
</CardContent>
</Card>
@@ -617,28 +745,124 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</Card>
)}
- {/* 품목별 견적 작성 섹션 */}
- {prItems.length > 0 && (
- <PrItemsPricingTable
- prItems={prItems}
- initialQuotations={prItemQuotations}
- currency={biddingDetail?.currency || 'KRW'}
- onQuotationsChange={setPrItemQuotations}
- onTotalAmountChange={setTotalAmount}
- readOnly={false}
- />
- )}
+ {/* 사전견적 참여의사 결정 섹션 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Users className="w-5 h-5" />
+ 사전견적 참여의사 결정
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ {participationDecision === null ? (
+ <div className="space-y-4">
+ <p className="text-muted-foreground">
+ 해당 입찰의 사전견적에 참여하시겠습니까?
+ </p>
+ <div className="flex gap-3">
+ <Button
+ onClick={() => handleParticipationDecision(true)}
+ disabled={isPending}
+ className="flex items-center gap-2"
+ >
+ <CheckCircle className="w-4 h-4" />
+ 참여
+ </Button>
+ <Button
+ variant="outline"
+ onClick={() => handleParticipationDecision(false)}
+ disabled={isPending}
+ className="flex items-center gap-2"
+ >
+ <XCircle className="w-4 h-4" />
+ 미참여
+ </Button>
+ </div>
+ </div>
+ ) : (
+ <div className="space-y-4">
+ <div className={`flex items-center gap-2 p-3 rounded-lg ${
+ participationDecision ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
+ }`}>
+ {participationDecision ? (
+ <CheckCircle className="w-5 h-5" />
+ ) : (
+ <XCircle className="w-5 h-5" />
+ )}
+ <span className="font-medium">
+ 사전견적 {participationDecision ? '참여' : '미참여'}로 설정되었습니다.
+ </span>
+ </div>
+ {participationDecision === false && (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-muted-foreground">
+ 미참여로 설정되어 견적 작성 섹션이 숨겨집니다. 참여하시려면 아래 버튼을 클릭해주세요.
+ </p>
+ </div>
+ )}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setParticipationDecision(null)}
+ disabled={isPending}
+ >
+ 결정 변경하기
+ </Button>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 참여 결정 시에만 견적 작성 섹션들 표시 (단, 견적마감일이 지나지 않은 경우에만) */}
+ {participationDecision === true && (() => {
+ // 견적마감일 체크
+ if (biddingDetail?.preQuoteDeadline) {
+ const now = new Date()
+ const deadline = new Date(biddingDetail.preQuoteDeadline)
+ const isExpired = deadline < now
+
+ if (isExpired) {
+ return (
+ <Card>
+ <CardContent className="pt-6">
+ <div className="text-center py-8">
+ <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
+ <h3 className="text-lg font-semibold text-red-700 mb-2">견적 마감</h3>
+ <p className="text-muted-foreground">
+ 견적 마감일({formatDate(biddingDetail.preQuoteDeadline, 'KR')})이 지나 견적 제출이 불가능합니다.
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+ )
+ }
+ }
+
+ return true // 견적 작성 가능
+ })() && (
+ <>
+ {/* 품목별 견적 작성 섹션 */}
+ {prItems.length > 0 && (
+ <PrItemsPricingTable
+ prItems={prItems}
+ initialQuotations={prItemQuotations}
+ currency={biddingDetail?.currency || 'KRW'}
+ onQuotationsChange={setPrItemQuotations}
+ onTotalAmountChange={setTotalAmount}
+ readOnly={false}
+ />
+ )}
- {/* 견적 문서 업로드 섹션 */}
- <SimpleFileUpload
- biddingId={biddingId}
- companyId={companyId}
- userId={userId}
- readOnly={false}
- />
+ {/* 견적 문서 업로드 섹션 */}
+ <SimpleFileUpload
+ biddingId={biddingId}
+ companyId={companyId}
+ userId={userId}
+ readOnly={false}
+ />
- {/* 사전견적 폼 섹션 */}
- <Card>
+ {/* 사전견적 폼 섹션 */}
+ <Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Send className="w-5 h-5" />
@@ -952,30 +1176,23 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
)}
<div className="flex justify-end gap-2 pt-4">
- <>
- <Button
- variant="outline"
- onClick={handleTempSave}
- disabled={isSaving || isPending}
- >
- <Save className="w-4 h-4 mr-2" />
- {isSaving ? '저장중...' : '임시저장'}
- </Button>
- <Button onClick={handleSubmitResponse} disabled={isPending || isSaving}>
- <Send className="w-4 h-4 mr-2" />
- 사전견적 제출
- </Button>
- </>
-
- {/* {biddingDetail?.invitationStatus === 'submitted' && (
- <div className="flex items-center gap-2 text-green-600">
- <CheckCircle className="w-5 h-5" />
- <span className="font-medium">사전견적이 제출되었습니다</span>
- </div>
- )} */}
+ <Button
+ variant="outline"
+ onClick={handleTempSave}
+ disabled={isSaving || isPending}
+ >
+ <Save className="w-4 h-4 mr-2" />
+ {isSaving ? '저장중...' : '임시저장'}
+ </Button>
+ <Button onClick={handleSubmitResponse} disabled={isPending || isSaving}>
+ <Send className="w-4 h-4 mr-2" />
+ 사전견적 제출
+ </Button>
</div>
</CardContent>
</Card>
+ </>
+ )}
</div>
)
}
diff --git a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
index c2fb6487..1500f6a7 100644
--- a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
+++ b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
@@ -24,16 +24,6 @@ export function PartnersBiddingToolbarActions({
const selectedRows = table.getFilteredSelectedRowModel().rows
const selectedBidding = selectedRows.length === 1 ? selectedRows[0].original : null
- // 사양설명회 참석 여부 버튼 활성화 조건
- const canManageAttendance = selectedBidding && (
- selectedBidding.invitationStatus === 'sent' ||
- selectedBidding.invitationStatus === 'accepted' ||
- selectedBidding.invitationStatus === 'submitted'
- )
-
- // 참여 의사 결정 버튼 활성화 조건 (sent 상태이고 아직 참여의사를 결정하지 않은 경우)
- const canDecideParticipation = selectedBidding
-
const handleAttendanceClick = () => {
if (selectedBidding && setRowAction) {
setRowAction({
@@ -43,33 +33,14 @@ export function PartnersBiddingToolbarActions({
}
}
- const handleParticipationClick = () => {
- if (selectedBidding && setRowAction) {
- setRowAction({
- type: 'participation',
- row: { original: selectedBidding }
- })
- }
- }
+
return (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
- onClick={handleParticipationClick}
- disabled={!canDecideParticipation}
- className="flex items-center gap-2"
- >
- <CheckCircle className="w-4 h-4" />
- 참여 의사 결정
- </Button>
-
- <Button
- variant="outline"
- size="sm"
onClick={handleAttendanceClick}
- disabled={!canManageAttendance}
className="flex items-center gap-2"
>
<Users className="w-4 h-4" />