summaryrefslogtreecommitdiff
path: root/lib/bidding/detail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail')
-rw-r--r--lib/bidding/detail/service.ts174
-rw-r--r--lib/bidding/detail/table/bidding-detail-content.tsx37
-rw-r--r--lib/bidding/detail/table/bidding-detail-header.tsx81
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-columns.tsx39
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx46
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx152
6 files changed, 408 insertions, 121 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index 2ce17713..c811f46d 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -1,7 +1,8 @@
'use server'
import db from '@/db/db'
-import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions } from '@/db/schema'
+import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions, priceAdjustmentForms } from '@/db/schema'
+import { specificationMeetings } from '@/db/schema/bidding'
import { eq, and, sql, desc, ne } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'
@@ -224,6 +225,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV
proposedShippingPort: companyConditionResponses.proposedShippingPort,
proposedDestinationPort: companyConditionResponses.proposedDestinationPort,
priceAdjustmentResponse: companyConditionResponses.priceAdjustmentResponse,
+ isInitialResponse: companyConditionResponses.isInitialResponse,
sparePartResponse: companyConditionResponses.sparePartResponse,
additionalProposals: companyConditionResponses.additionalProposals,
})
@@ -256,6 +258,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV
proposedShippingPort: vendor.proposedShippingPort || '',
proposedDestinationPort: vendor.proposedDestinationPort || '',
priceAdjustmentResponse: vendor.priceAdjustmentResponse || false,
+ isInitialResponse: vendor.isInitialResponse || false,
sparePartResponse: vendor.sparePartResponse || '',
additionalProposals: vendor.additionalProposals || '',
documents: [] // TODO: 문서 정보 조회 로직 추가
@@ -828,6 +831,7 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId:
proposedShippingPort: companyConditionResponses.proposedShippingPort,
proposedDestinationPort: companyConditionResponses.proposedDestinationPort,
priceAdjustmentResponse: companyConditionResponses.priceAdjustmentResponse,
+ isInitialResponse: companyConditionResponses.isInitialResponse,
sparePartResponse: companyConditionResponses.sparePartResponse,
additionalProposals: companyConditionResponses.additionalProposals,
responseSubmittedAt: companyConditionResponses.submittedAt,
@@ -859,9 +863,27 @@ export async function submitPartnerResponse(
proposedShippingPort?: string
proposedDestinationPort?: string
priceAdjustmentResponse?: boolean
+ isInitialResponse?: boolean
sparePartResponse?: string
additionalProposals?: string
finalQuoteAmount?: number
+ priceAdjustmentForm?: {
+ itemName?: string
+ adjustmentReflectionPoint?: string
+ majorApplicableRawMaterial?: string
+ adjustmentFormula?: string
+ rawMaterialPriceIndex?: string
+ referenceDate?: string
+ comparisonDate?: string
+ adjustmentRatio?: number
+ notes?: string
+ adjustmentConditions?: string
+ majorNonApplicableRawMaterial?: string
+ adjustmentPeriod?: string
+ contractorWriter?: string
+ adjustmentDate?: string
+ nonApplicableReason?: string
+ }
},
userId: string
) {
@@ -876,6 +898,7 @@ export async function submitPartnerResponse(
proposedShippingPort: response.proposedShippingPort,
proposedDestinationPort: response.proposedDestinationPort,
priceAdjustmentResponse: response.priceAdjustmentResponse,
+ isInitialResponse: response.isInitialResponse,
sparePartResponse: response.sparePartResponse,
additionalProposals: response.additionalProposals,
submittedAt: new Date(),
@@ -889,20 +912,67 @@ export async function submitPartnerResponse(
.where(eq(companyConditionResponses.biddingCompanyId, biddingCompanyId))
.limit(1)
+ let companyConditionResponseId: number
+
if (existingResponse.length > 0) {
// 업데이트
await tx
.update(companyConditionResponses)
.set(responseData)
.where(eq(companyConditionResponses.biddingCompanyId, biddingCompanyId))
+
+ companyConditionResponseId = existingResponse[0].id
} else {
// 새로 생성
- await tx
+ const [newResponse] = await tx
.insert(companyConditionResponses)
.values({
biddingCompanyId,
...responseData,
})
+ .returning({ id: companyConditionResponses.id })
+
+ companyConditionResponseId = newResponse.id
+ }
+
+ // 3. 연동제 정보 저장 (연동제 적용이 true이고 연동제 정보가 있는 경우)
+ if (response.priceAdjustmentResponse && response.priceAdjustmentForm) {
+ const priceAdjustmentData = {
+ companyConditionResponsesId: companyConditionResponseId,
+ itemName: response.priceAdjustmentForm.itemName,
+ adjustmentReflectionPoint: response.priceAdjustmentForm.adjustmentReflectionPoint,
+ majorApplicableRawMaterial: response.priceAdjustmentForm.majorApplicableRawMaterial,
+ adjustmentFormula: response.priceAdjustmentForm.adjustmentFormula,
+ rawMaterialPriceIndex: response.priceAdjustmentForm.rawMaterialPriceIndex,
+ referenceDate: response.priceAdjustmentForm.referenceDate ? new Date(response.priceAdjustmentForm.referenceDate) : null,
+ comparisonDate: response.priceAdjustmentForm.comparisonDate ? new Date(response.priceAdjustmentForm.comparisonDate) : null,
+ adjustmentRatio: response.priceAdjustmentForm.adjustmentRatio,
+ notes: response.priceAdjustmentForm.notes,
+ adjustmentConditions: response.priceAdjustmentForm.adjustmentConditions,
+ majorNonApplicableRawMaterial: response.priceAdjustmentForm.majorNonApplicableRawMaterial,
+ adjustmentPeriod: response.priceAdjustmentForm.adjustmentPeriod,
+ contractorWriter: response.priceAdjustmentForm.contractorWriter,
+ adjustmentDate: response.priceAdjustmentForm.adjustmentDate ? new Date(response.priceAdjustmentForm.adjustmentDate) : null,
+ nonApplicableReason: response.priceAdjustmentForm.nonApplicableReason,
+ }
+
+ // 기존 연동제 정보가 있는지 확인
+ const existingPriceAdjustment = await tx
+ .select()
+ .from(priceAdjustmentForms)
+ .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId))
+ .limit(1)
+
+ if (existingPriceAdjustment.length > 0) {
+ // 업데이트
+ await tx
+ .update(priceAdjustmentForms)
+ .set(priceAdjustmentData)
+ .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId))
+ } else {
+ // 새로 생성
+ await tx.insert(priceAdjustmentForms).values(priceAdjustmentData)
+ }
}
// 2. biddingCompanies 테이블에 견적 금액과 상태 업데이트
@@ -940,6 +1010,26 @@ export async function submitPartnerResponse(
// 사양설명회 정보 조회 (협력업체용)
export async function getSpecificationMeetingForPartners(biddingId: number) {
try {
+ // specification_meetings 테이블에서 사양설명회 정보 조회
+ const specMeeting = await db
+ .select({
+ id: specificationMeetings.id,
+ meetingDate: specificationMeetings.meetingDate,
+ meetingTime: specificationMeetings.meetingTime,
+ location: specificationMeetings.location,
+ address: specificationMeetings.address,
+ contactPerson: specificationMeetings.contactPerson,
+ contactPhone: specificationMeetings.contactPhone,
+ contactEmail: specificationMeetings.contactEmail,
+ agenda: specificationMeetings.agenda,
+ materials: specificationMeetings.materials,
+ notes: specificationMeetings.notes,
+ isRequired: specificationMeetings.isRequired,
+ })
+ .from(specificationMeetings)
+ .where(eq(specificationMeetings.biddingId, biddingId))
+ .limit(1)
+
// bidding_documents에서 사양설명회 관련 문서 조회
const documents = await db
.select({
@@ -956,17 +1046,12 @@ export async function getSpecificationMeetingForPartners(biddingId: number) {
eq(biddingDocuments.documentType, 'specification_meeting')
))
- // biddings 테이블에서 사양설명회 기본 정보 조회
+ // 기본 입찰 정보도 가져오기 (제목, 입찰번호 등)
const bidding = await db
.select({
id: biddings.id,
title: biddings.title,
biddingNumber: biddings.biddingNumber,
- preQuoteDate: biddings.preQuoteDate,
- biddingRegistrationDate: biddings.biddingRegistrationDate,
- managerName: biddings.managerName,
- managerEmail: biddings.managerEmail,
- managerPhone: biddings.managerPhone,
})
.from(biddings)
.where(eq(biddings.id, biddingId))
@@ -976,15 +1061,44 @@ export async function getSpecificationMeetingForPartners(biddingId: number) {
return { success: false, error: '입찰 정보를 찾을 수 없습니다.' }
}
+ // 사양설명회 정보가 없는 경우
+ if (specMeeting.length === 0) {
+ return {
+ success: true,
+ data: {
+ ...bidding[0],
+ documents,
+ meetingDate: null,
+ meetingTime: null,
+ location: null,
+ address: null,
+ contactPerson: null,
+ contactPhone: null,
+ contactEmail: null,
+ agenda: null,
+ materials: null,
+ notes: null,
+ isRequired: false,
+ }
+ }
+ }
+
return {
success: true,
data: {
...bidding[0],
documents,
- meetingDate: bidding[0].preQuoteDate ? bidding[0].preQuoteDate.toISOString().split('T')[0] : null,
- contactPerson: bidding[0].managerName,
- contactEmail: bidding[0].managerEmail,
- contactPhone: bidding[0].managerPhone,
+ meetingDate: specMeeting[0].meetingDate ? specMeeting[0].meetingDate.toISOString().split('T')[0] : null,
+ meetingTime: specMeeting[0].meetingTime,
+ location: specMeeting[0].location,
+ address: specMeeting[0].address,
+ contactPerson: specMeeting[0].contactPerson,
+ contactPhone: specMeeting[0].contactPhone,
+ contactEmail: specMeeting[0].contactEmail,
+ agenda: specMeeting[0].agenda,
+ materials: specMeeting[0].materials,
+ notes: specMeeting[0].notes,
+ isRequired: specMeeting[0].isRequired,
}
}
} catch (error) {
@@ -1094,3 +1208,39 @@ export async function updatePartnerAttendance(
return { success: false, error: '참석 여부 업데이트에 실패했습니다.' }
}
}
+
+// 연동제 정보 조회
+export async function getPriceAdjustmentForm(companyConditionResponseId: number) {
+ try {
+ const priceAdjustment = await db
+ .select()
+ .from(priceAdjustmentForms)
+ .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId))
+ .limit(1)
+
+ return priceAdjustment[0] || null
+ } catch (error) {
+ console.error('Failed to get price adjustment form:', error)
+ return null
+ }
+}
+
+// 입찰업체 ID로 연동제 정보 조회
+export async function getPriceAdjustmentFormByBiddingCompanyId(biddingCompanyId: number) {
+ try {
+ const result = await db
+ .select({
+ priceAdjustmentForm: priceAdjustmentForms,
+ companyConditionResponse: companyConditionResponses,
+ })
+ .from(companyConditionResponses)
+ .leftJoin(priceAdjustmentForms, eq(companyConditionResponses.id, priceAdjustmentForms.companyConditionResponsesId))
+ .where(eq(companyConditionResponses.biddingCompanyId, biddingCompanyId))
+ .limit(1)
+
+ return result[0]?.priceAdjustmentForm || null
+ } catch (error) {
+ console.error('Failed to get price adjustment form by bidding company id:', error)
+ return null
+ }
+}
diff --git a/lib/bidding/detail/table/bidding-detail-content.tsx b/lib/bidding/detail/table/bidding-detail-content.tsx
index 090e7218..50f0941e 100644
--- a/lib/bidding/detail/table/bidding-detail-content.tsx
+++ b/lib/bidding/detail/table/bidding-detail-content.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Bidding } from '@/db/schema'
import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service'
-import { BiddingDetailHeader } from './bidding-detail-header'
+
import { BiddingDetailVendorTableContent } from './bidding-detail-vendor-table'
import { BiddingDetailItemsDialog } from './bidding-detail-items-dialog'
import { BiddingDetailTargetPriceDialog } from './bidding-detail-target-price-dialog'
@@ -45,27 +45,20 @@ export function BiddingDetailContent({
}, [])
return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="p-6">
- <BiddingDetailHeader bidding={bidding} />
-
- <div className="mt-6">
- <BiddingDetailVendorTableContent
- biddingId={bidding.id}
- vendors={quotationVendors}
- biddingCompanies={biddingCompanies}
- onRefresh={handleRefresh}
- onOpenItemsDialog={() => openDialog('items')}
- onOpenTargetPriceDialog={() => openDialog('targetPrice')}
- onOpenSelectionReasonDialog={() => openDialog('selectionReason')}
- onEdit={undefined}
- onDelete={undefined}
- onSelectWinner={undefined}
- />
- </div>
- </div>
- </section>
+ <div className="space-y-6">
+ <BiddingDetailVendorTableContent
+ biddingId={bidding.id}
+ bidding={bidding}
+ vendors={quotationVendors}
+ biddingCompanies={biddingCompanies}
+ onRefresh={handleRefresh}
+ onOpenItemsDialog={() => openDialog('items')}
+ onOpenTargetPriceDialog={() => openDialog('targetPrice')}
+ onOpenSelectionReasonDialog={() => openDialog('selectionReason')}
+ onEdit={undefined}
+ onDelete={undefined}
+ onSelectWinner={undefined}
+ />
<BiddingDetailItemsDialog
open={dialogStates.items}
diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx
index 3135f37d..fcbbeb9a 100644
--- a/lib/bidding/detail/table/bidding-detail-header.tsx
+++ b/lib/bidding/detail/table/bidding-detail-header.tsx
@@ -145,17 +145,7 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
const buttons = []
// 기본 액션 버튼들 (항상 표시)
- buttons.push(
- <Button
- key="back"
- variant="outline"
- onClick={handleGoBack}
- disabled={isPending}
- >
- <ArrowLeft className="w-4 h-4 mr-2" />
- 목록으로
- </Button>
- )
+
// 모든 액션 버튼을 항상 표시 (상태 검증은 각 핸들러에서)
buttons.push(
@@ -228,74 +218,9 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
</div>
{/* 세부 정보 영역 */}
- <div className="flex flex-wrap items-center gap-6 text-sm">
- {/* 프로젝트 정보 */}
- {bidding.projectName && (
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <Building2 className="w-4 h-4" />
- <span className="font-medium">프로젝트:</span>
- <span>{bidding.projectName}</span>
- </div>
- )}
-
- {/* 품목 정보 */}
- {bidding.itemName && (
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <Package className="w-4 h-4" />
- <span className="font-medium">품목:</span>
- <span>{bidding.itemName}</span>
- </div>
- )}
-
- {/* 담당자 정보 */}
- {bidding.managerName && (
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <User className="w-4 h-4" />
- <span className="font-medium">담당자:</span>
- <span>{bidding.managerName}</span>
- </div>
- )}
-
- {/* 계약구분 */}
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <span className="font-medium">계약:</span>
- <span>{contractTypeLabels[bidding.contractType]}</span>
- </div>
-
- {/* 입찰유형 */}
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <span className="font-medium">유형:</span>
- <span>{biddingTypeLabels[bidding.biddingType]}</span>
- </div>
-
- {/* 낙찰수 */}
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <span className="font-medium">낙찰:</span>
- <span>{bidding.awardCount === 'single' ? '단수' : '복수'}</span>
- </div>
-
- {/* 통화 */}
- <div className="flex items-center gap-1.5 text-muted-foreground">
- <DollarSign className="w-4 h-4" />
- <span className="font-mono">{bidding.currency}</span>
- </div>
-
- {/* 예산 정보 */}
- {bidding.budget && (
- <div className="flex items-center gap-1.5">
- <span className="font-medium text-muted-foreground">예산:</span>
- <span className="font-semibold">
- {new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: bidding.currency || 'KRW',
- }).format(Number(bidding.budget))}
- </span>
- </div>
- )}
- </div>
{/* 일정 정보 */}
- {(bidding.submissionStartDate || bidding.evaluationDate || bidding.preQuoteDate || bidding.biddingRegistrationDate) && (
+ {/* {(bidding.submissionStartDate || bidding.evaluationDate || bidding.preQuoteDate || bidding.biddingRegistrationDate) && (
<div className="flex flex-wrap items-center gap-4 mt-3 pt-3 border-t border-border/50">
<Calendar className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
@@ -321,7 +246,7 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
)}
</div>
</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 9e06d5d1..6f02497f 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
@@ -22,12 +22,14 @@ interface GetVendorColumnsProps {
onEdit: (vendor: QuotationVendor) => void
onDelete: (vendor: QuotationVendor) => void
onSelectWinner: (vendor: QuotationVendor) => void
+ onViewPriceAdjustment?: (vendor: QuotationVendor) => void
}
export function getBiddingDetailVendorColumns({
onEdit,
onDelete,
- onSelectWinner
+ onSelectWinner,
+ onViewPriceAdjustment
}: GetVendorColumnsProps): ColumnDef<QuotationVendor>[] {
return [
{
@@ -139,13 +141,46 @@ export function getBiddingDetailVendorColumns({
{row.original.incotermsResponse || '-'}
</div>
),
+ },
+ {
+ accessorKey: 'isInitialResponse',
+ header: '초도여부',
+ cell: ({ row }) => (
+ <Badge variant={row.original.isInitialResponse ? 'default' : 'secondary'}>
+ {row.original.isInitialResponse ? 'Y' : 'N'}
+ </Badge>
+ ),
+ },
+ {
+ accessorKey: 'priceAdjustmentResponse',
+ header: '연동제',
+ cell: ({ row }) => {
+ const hasPriceAdjustment = row.original.priceAdjustmentResponse
+ return (
+ <div className="flex items-center gap-2">
+ <Badge variant={hasPriceAdjustment ? 'default' : 'secondary'}>
+ {hasPriceAdjustment ? '적용' : '미적용'}
+ </Badge>
+ {hasPriceAdjustment && onViewPriceAdjustment && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => onViewPriceAdjustment(row.original)}
+ className="h-6 px-2 text-xs"
+ >
+ 상세
+ </Button>
+ )}
+ </div>
+ )
+ },
},
{
accessorKey: 'proposedContractDeliveryDate',
header: '제안납기일',
cell: ({ row }) => (
<div className="text-sm">
- {row.original.proposedContractDeliveryDate ?
+ {row.original.proposedContractDeliveryDate ?
new Date(row.original.proposedContractDeliveryDate).toLocaleDateString('ko-KR') : '-'}
</div>
),
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
index 7ad7056c..b1f0b08e 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
@@ -9,7 +9,9 @@ import { BiddingDetailVendorToolbarActions } from './bidding-detail-vendor-toolb
import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog'
import { BiddingDetailVendorEditDialog } from './bidding-detail-vendor-edit-dialog'
import { getBiddingDetailVendorColumns } from './bidding-detail-vendor-columns'
-import { QuotationVendor } from '@/lib/bidding/detail/service'
+import { QuotationVendor, getPriceAdjustmentFormByBiddingCompanyId } from '@/lib/bidding/detail/service'
+import { Bidding } from '@/db/schema'
+import { PriceAdjustmentDialog } from '@/components/bidding/price-adjustment-dialog'
import {
deleteQuotationVendor,
selectWinner
@@ -20,6 +22,7 @@ import { useTransition } from 'react'
interface BiddingDetailVendorTableContentProps {
biddingId: number
+ bidding: Bidding
vendors: QuotationVendor[]
onRefresh: () => void
onOpenItemsDialog: () => void
@@ -83,6 +86,7 @@ const advancedFilterFields: DataTableAdvancedFilterField<QuotationVendor>[] = [
export function BiddingDetailVendorTableContent({
biddingId,
+ bidding,
vendors,
onRefresh,
onOpenItemsDialog,
@@ -96,6 +100,8 @@ export function BiddingDetailVendorTableContent({
const [isPending, startTransition] = useTransition()
const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null)
const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false)
+ const [priceAdjustmentData, setPriceAdjustmentData] = React.useState<any>(null)
+ const [isPriceAdjustmentDialogOpen, setIsPriceAdjustmentDialogOpen] = React.useState(false)
const handleDelete = (vendor: QuotationVendor) => {
if (!confirm(`${vendor.vendorName} 업체를 삭제하시겠습니까?`)) return
@@ -170,13 +176,38 @@ export function BiddingDetailVendorTableContent({
setIsEditDialogOpen(true)
}
+ const handleViewPriceAdjustment = async (vendor: QuotationVendor) => {
+ try {
+ const priceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(vendor.id)
+ if (priceAdjustmentForm) {
+ setPriceAdjustmentData(priceAdjustmentForm)
+ setSelectedVendor(vendor)
+ setIsPriceAdjustmentDialogOpen(true)
+ } else {
+ toast({
+ title: '연동제 정보 없음',
+ description: '해당 업체의 연동제 정보가 없습니다.',
+ variant: 'default',
+ })
+ }
+ } catch (error) {
+ console.error('Failed to load price adjustment form:', error)
+ toast({
+ title: '오류',
+ description: '연동제 정보를 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ }
+ }
+
const columns = React.useMemo(
() => getBiddingDetailVendorColumns({
onEdit: onEdit || handleEdit,
onDelete: onDelete || handleDelete,
- onSelectWinner: onSelectWinner || handleSelectWinner
+ onSelectWinner: onSelectWinner || handleSelectWinner,
+ onViewPriceAdjustment: handleViewPriceAdjustment
}),
- [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner]
+ [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment]
)
const { table } = useDataTable({
@@ -205,10 +236,10 @@ export function BiddingDetailVendorTableContent({
<BiddingDetailVendorToolbarActions
table={table}
biddingId={biddingId}
+ bidding={bidding}
onOpenItemsDialog={onOpenItemsDialog}
onOpenTargetPriceDialog={onOpenTargetPriceDialog}
onOpenSelectionReasonDialog={onOpenSelectionReasonDialog}
-
onSuccess={onRefresh}
/>
</DataTableAdvancedToolbar>
@@ -220,6 +251,13 @@ export function BiddingDetailVendorTableContent({
onOpenChange={setIsEditDialogOpen}
onSuccess={onRefresh}
/>
+
+ <PriceAdjustmentDialog
+ open={isPriceAdjustmentDialogOpen}
+ onOpenChange={setIsPriceAdjustmentDialogOpen}
+ data={priceAdjustmentData}
+ vendorName={selectedVendor?.vendorName || ''}
+ />
</>
)
}
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 00daa005..ca9ffc60 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -2,38 +2,184 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
+import { useRouter } from "next/navigation"
+import { useTransition } from "react"
import { Button } from "@/components/ui/button"
-import { Plus } from "lucide-react"
-import { QuotationVendor } from "@/lib/bidding/detail/service"
+import { Plus, Send, RotateCcw, XCircle } from "lucide-react"
+import { QuotationVendor, registerBidding, markAsDisposal, createRebidding } from "@/lib/bidding/detail/service"
import { BiddingDetailVendorCreateDialog } from "./bidding-detail-vendor-create-dialog"
+import { Bidding } from "@/db/schema"
+import { useToast } from "@/hooks/use-toast"
interface BiddingDetailVendorToolbarActionsProps {
table: Table<QuotationVendor>
biddingId: number
+ bidding: Bidding
onOpenItemsDialog: () => void
onOpenTargetPriceDialog: () => void
onOpenSelectionReasonDialog: () => void
-
onSuccess: () => void
}
export function BiddingDetailVendorToolbarActions({
table,
biddingId,
+ bidding,
onOpenItemsDialog,
onOpenTargetPriceDialog,
onOpenSelectionReasonDialog,
onSuccess
}: BiddingDetailVendorToolbarActionsProps) {
+ const router = useRouter()
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false)
const handleCreateVendor = () => {
setIsCreateDialogOpen(true)
}
+ const handleRegister = () => {
+ // 상태 검증
+ if (bidding.status !== 'bidding_generated') {
+ toast({
+ title: '실행 불가',
+ description: '입찰 등록은 입찰 생성 상태에서만 가능합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!confirm('입찰을 등록하시겠습니까?')) return
+
+ startTransition(async () => {
+ const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+
+ if (result.success) {
+ toast({
+ title: '성공',
+ description: result.message,
+ })
+ router.refresh()
+ } else {
+ toast({
+ title: '오류',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ const handleMarkAsDisposal = () => {
+ // 상태 검증
+ if (bidding.status !== 'bidding_closed') {
+ toast({
+ title: '실행 불가',
+ description: '유찰 처리는 입찰 마감 상태에서만 가능합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!confirm('입찰을 유찰 처리하시겠습니까?')) return
+
+ startTransition(async () => {
+ const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+
+ if (result.success) {
+ toast({
+ title: '성공',
+ description: result.message,
+ })
+ router.refresh()
+ } else {
+ toast({
+ title: '오류',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ const handleCreateRebidding = () => {
+ // 상태 검증
+ if (bidding.status !== 'bidding_disposal') {
+ toast({
+ title: '실행 불가',
+ description: '재입찰은 유찰 상태에서만 가능합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!confirm('재입찰을 생성하시겠습니까?')) return
+
+ startTransition(async () => {
+ const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
+
+ if (result.success) {
+ toast({
+ title: '성공',
+ description: result.message,
+ })
+ if (result.data?.redirectTo) {
+ router.push(result.data.redirectTo)
+ } else {
+ router.refresh()
+ }
+ } else {
+ toast({
+ title: '오류',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
return (
<>
<div className="flex items-center gap-2">
+ {/* 상태별 액션 버튼 */}
+ {/* {bidding.status === 'bidding_generated' && (
+ <Button
+ variant="default"
+ size="sm"
+ onClick={handleRegister}
+ disabled={isPending}
+ >
+ <Send className="mr-2 h-4 w-4" />
+ 입찰 등록
+ </Button>
+ )}
+
+ {bidding.status === 'bidding_closed' && (
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={handleMarkAsDisposal}
+ disabled={isPending}
+ >
+ <XCircle className="mr-2 h-4 w-4" />
+ 유찰 처리
+ </Button>
+ )}
+
+ {bidding.status === 'bidding_disposal' && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleCreateRebidding}
+ disabled={isPending}
+ >
+ <RotateCcw className="mr-2 h-4 w-4" />
+ 재입찰 생성
+ </Button>
+ )} */}
+
+ {/* 기존 버튼들 */}
<Button
variant="outline"
size="sm"