summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
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/vendor
parentc62ec046327fd388ebce04571b55910747e69a3b (diff)
(최겸) 구매 입찰 사전견적, 입찰, 낙찰, 유찰, 재입찰 기능 개발
Diffstat (limited to 'lib/bidding/vendor')
-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
6 files changed, 408 insertions, 136 deletions
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" />