summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor')
-rw-r--r--lib/bidding/vendor/components/pr-items-pricing-table.tsx27
-rw-r--r--lib/bidding/vendor/partners-bidding-detail.tsx904
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx104
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx1413
4 files changed, 879 insertions, 1569 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
index 483bce5c..efa10af2 100644
--- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -32,16 +32,27 @@ import {
interface PrItem {
id: number
+ biddingId: number
itemNumber: string | null
- prNumber: string | null
+ projectId: number | null
+ projectInfo: string | null
itemInfo: string | null
- materialDescription: string | null
+ shi: string | null
+ materialGroupNumber: string | null
+ materialGroupInfo: string | null
+ materialNumber: string | null
+ materialInfo: string | null
+ requestedDeliveryDate: Date | null
+ annualUnitPrice: string | null
+ currency: string | null
quantity: string | null
quantityUnit: string | null
totalWeight: string | null
weightUnit: string | null
- currency: string | null
- requestedDeliveryDate: string | null
+ priceUnit: string | null
+ purchaseUnit: string | null
+ materialWeight: string | null
+ prNumber: string | null
hasSpecDocument: boolean | null
}
@@ -283,8 +294,10 @@ export function PrItemsPricingTable({
<TableHead>자재내역</TableHead>
<TableHead>수량</TableHead>
<TableHead>단위</TableHead>
+ <TableHead>구매단위</TableHead>
<TableHead>중량</TableHead>
<TableHead>중량단위</TableHead>
+ <TableHead>가격단위</TableHead>
<TableHead>SHI 납품요청일</TableHead>
<TableHead>견적단가</TableHead>
<TableHead>견적금액</TableHead>
@@ -315,18 +328,20 @@ export function PrItemsPricingTable({
</div>
</TableCell>
<TableCell>
- <div className="max-w-32 truncate" title={item.materialDescription || ''}>
- {item.materialDescription || '-'}
+ <div className="max-w-32 truncate" title={item.materialInfo || ''}>
+ {item.materialInfo || '-'}
</div>
</TableCell>
<TableCell className="text-right">
{item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'}
</TableCell>
<TableCell>{item.quantityUnit || '-'}</TableCell>
+ <TableCell>{item.purchaseUnit || '-'}</TableCell>
<TableCell className="text-right">
{item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'}
</TableCell>
<TableCell>{item.weightUnit || '-'}</TableCell>
+ <TableCell>{item.priceUnit || '-'}</TableCell>
<TableCell>
{item.requestedDeliveryDate ?
formatDate(item.requestedDeliveryDate, 'KR') : '-'
diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx
index f9241f7b..273c0667 100644
--- a/lib/bidding/vendor/partners-bidding-detail.tsx
+++ b/lib/bidding/vendor/partners-bidding-detail.tsx
@@ -1,11 +1,16 @@
'use client'
import * as React from 'react'
+
+// 브라우저 환경 체크
+const isBrowser = typeof window !== 'undefined'
import { useRouter } from 'next/navigation'
+import { useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
ArrowLeft,
User,
@@ -15,8 +20,10 @@ import {
XCircle,
Save,
FileText,
- Building2,
- Package
+ Package,
+ Trash2,
+ Calendar,
+ ChevronDown
} from 'lucide-react'
import { formatDate } from '@/lib/utils'
@@ -24,19 +31,26 @@ import {
getBiddingDetailsForPartners,
submitPartnerResponse,
updatePartnerBiddingParticipation,
- saveBiddingDraft
+ saveBiddingDraft,
+ getPriceAdjustmentFormByBiddingCompanyId
} from '../detail/service'
+import { cancelBiddingResponse } from '../detail/bidding-actions'
import { getPrItemsForBidding, getSavedPrItemQuotations } from '@/lib/bidding/pre-quote/service'
+import { getBiddingConditions } from '@/lib/bidding/service'
import { PrItemsPricingTable } from './components/pr-items-pricing-table'
import { SimpleFileUpload } from './components/simple-file-upload'
+import { getTaxConditionName } from "@/lib/tax-conditions/types"
import {
biddingStatusLabels,
contractTypeLabels,
biddingTypeLabels
} from '@/db/schema'
import { useToast } from '@/hooks/use-toast'
-import { useTransition } from 'react'
import { useSession } from 'next-auth/react'
+import { getBiddingNotice } from '../service'
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
+import { Input } from '@/components/ui/input'
+import { Textarea } from '@/components/ui/textarea'
interface PartnersBiddingDetailProps {
biddingId: number
@@ -51,7 +65,6 @@ interface BiddingDetail {
itemName: string | null
title: string
description: string | null
- content: string | null
contractType: string
biddingType: string
awardCount: string | null
@@ -66,33 +79,46 @@ interface BiddingDetail {
budget: number | null
targetPrice: number | null
status: string
- managerName: string | null
- managerEmail: string | null
- managerPhone: string | null
+ bidPicName: string | null // 입찰담당자
+ supplyPicName: string | null // 조달담당자
biddingCompanyId: number
biddingId: number
invitationStatus: string
finalQuoteAmount: number | null
finalQuoteSubmittedAt: Date | null
+ isFinalSubmission: boolean
isWinner: boolean
isAttendingMeeting: boolean | null
isBiddingParticipated: boolean | null
additionalProposals: string | null
responseSubmittedAt: Date | null
+ priceAdjustmentResponse: boolean | null // 연동제 적용 여부
+ isPreQuoteParticipated: boolean | null // 사전견적 참여 여부
}
interface PrItem {
id: number
+ biddingId: number
itemNumber: string | null
- prNumber: string | null
+ projectId: number | null
+ projectInfo: string | null
itemInfo: string | null
- materialDescription: string | null
+ shi: string | null
+ materialGroupNumber: string | null
+ materialGroupInfo: string | null
+ materialNumber: string | null
+ materialInfo: string | null
+ requestedDeliveryDate: Date | null
+ annualUnitPrice: string | null
+ currency: string | null
quantity: string | null
quantityUnit: string | null
totalWeight: string | null
weightUnit: string | null
- currency: string | null
- requestedDeliveryDate: string | null
+ priceUnit: string | null
+ purchaseUnit: string | null
+ materialWeight: string | null
+ prNumber: string | null
hasSpecDocument: boolean | null
}
@@ -104,6 +130,22 @@ interface BiddingPrItemQuotation {
technicalSpecification?: string
}
+interface BiddingConditions {
+ id?: number
+ biddingId?: number
+ paymentTerms?: string
+ taxConditions?: string
+ incoterms?: string
+ incotermsOption?: string
+ contractDeliveryDate?: string
+ shippingPort?: string
+ destinationPort?: string
+ isPriceAdjustmentApplicable?: boolean
+ sparePartOptions?: string
+ createdAt?: string
+ updatedAt?: string
+}
+
export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingDetailProps) {
const router = useRouter()
const { toast } = useToast()
@@ -114,7 +156,23 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
const [isUpdatingParticipation, setIsUpdatingParticipation] = React.useState(false)
const [isSavingDraft, setIsSavingDraft] = React.useState(false)
const [isSubmitting, setIsSubmitting] = React.useState(false)
-
+ const [isCancelling, setIsCancelling] = React.useState(false)
+ const [isFinalSubmission, setIsFinalSubmission] = React.useState(false)
+ const [isBiddingNoticeLoading, setIsBiddingNoticeLoading] = React.useState(false)
+
+ // 입찰공고 관련 상태
+ const [biddingNotice, setBiddingNotice] = React.useState<{
+ id?: number
+ biddingId?: number
+ title?: string
+ content?: string
+ isTemplate?: boolean
+ createdAt?: string
+ updatedAt?: string
+ } | null>(null)
+ const [biddingConditions, setBiddingConditions] = React.useState<BiddingConditions | null>(null)
+ const [isNoticeOpen, setIsNoticeOpen] = React.useState(false)
+
// 품목별 견적 관련 상태
const [prItems, setPrItems] = React.useState<PrItem[]>([])
const [prItemQuotations, setPrItemQuotations] = React.useState<BiddingPrItemQuotation[]>([])
@@ -125,21 +183,95 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
finalQuoteAmount: '',
proposedContractDeliveryDate: '',
additionalProposals: '',
+ priceAdjustmentResponse: null as boolean | null, // null: 미선택, true: 적용, false: 미적용
+ })
+
+ // 연동제 폼 상태
+ const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({
+ itemName: '',
+ adjustmentReflectionPoint: '',
+ majorApplicableRawMaterial: '',
+ adjustmentFormula: '',
+ rawMaterialPriceIndex: '',
+ referenceDate: '',
+ comparisonDate: '',
+ adjustmentRatio: '',
+ notes: '',
+ adjustmentConditions: '',
+ majorNonApplicableRawMaterial: '',
+ adjustmentPeriod: '',
+ contractorWriter: '',
+ adjustmentDate: '',
+ nonApplicableReason: '', // 연동제 미희망 사유
})
const userId = session.data?.user?.id || ''
// 데이터 로드
+ // 입찰공고 로드 함수
+ const loadBiddingNotice = React.useCallback(async () => {
+ setIsBiddingNoticeLoading(true)
+ try {
+ const notice = await getBiddingNotice(biddingId)
+ console.log('입찰공고 로드 성공:', notice)
+ setBiddingNotice(notice)
+ } catch (error) {
+ console.error('Failed to load bidding notice:', error)
+ } finally {
+ setIsBiddingNoticeLoading(false)
+ }
+ }, [biddingId])
+
React.useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
- const [result, prItemsResult] = await Promise.all([
- getBiddingDetailsForPartners(biddingId, companyId),
- getPrItemsForBidding(biddingId)
+ // 데이터 로드 시작 로그
+ console.log('입찰 상세 데이터 로드 시작:', { biddingId, companyId })
+
+ console.log('데이터베이스 쿼리 시작...')
+
+ const [result, prItemsResult, noticeResult, conditionsResult] = await Promise.all([
+ getBiddingDetailsForPartners(biddingId, companyId).catch(error => {
+ console.error('Failed to get bidding details:', error)
+ return null
+ }),
+ getPrItemsForBidding(biddingId).catch(error => {
+ console.error('Failed to get PR items:', error)
+ return []
+ }),
+ loadBiddingNotice().catch(error => {
+ console.error('Failed to load bidding notice:', error)
+ return null
+ }),
+ getBiddingConditions(biddingId).catch(error => {
+ console.error('Failed to load bidding conditions:', error)
+ return null
+ })
])
-
+
+ console.log('데이터베이스 쿼리 완료:', {
+ resultExists: !!result,
+ prItemsExists: !!prItemsResult,
+ noticeExists: !!noticeResult,
+ conditionsExists: !!conditionsResult
+ })
+
+ console.log('데이터 로드 완료:', {
+ result: !!result,
+ prItemsCount: Array.isArray(prItemsResult) ? prItemsResult.length : 0,
+ noticeResult: !!noticeResult,
+ conditionsResult: !!conditionsResult
+ })
+
if (result) {
+ console.log('입찰 상세 데이터 로드 성공:', {
+ biddingId: result.biddingId,
+ isBiddingParticipated: result.isBiddingParticipated,
+ invitationStatus: result.invitationStatus,
+ finalQuoteAmount: result.finalQuoteAmount
+ })
+
setBiddingDetail(result)
// 기존 응답 데이터로 폼 초기화
@@ -147,7 +279,14 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
finalQuoteAmount: result.finalQuoteAmount?.toString() || '',
proposedContractDeliveryDate: result.proposedContractDeliveryDate || '',
additionalProposals: result.additionalProposals || '',
+ priceAdjustmentResponse: result.priceAdjustmentResponse || null,
})
+
+ // 입찰 조건 로드
+ if (conditionsResult) {
+ console.log('입찰 조건 로드:', conditionsResult)
+ setBiddingConditions(conditionsResult)
+ }
}
// PR 아이템 설정
@@ -158,29 +297,70 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
try {
// 사전견적 데이터를 가져와서 본입찰용으로 변환
const preQuoteData = await getSavedPrItemQuotations(result.biddingCompanyId)
-
- // 사전견적 데이터를 본입찰 포맷으로 변환
- const convertedQuotations = preQuoteData.map(item => ({
- prItemId: item.prItemId,
- bidUnitPrice: item.bidUnitPrice,
- bidAmount: item.bidAmount,
- proposedDeliveryDate: item.proposedDeliveryDate || undefined,
- technicalSpecification: item.technicalSpecification || undefined
- }))
-
- setPrItemQuotations(convertedQuotations)
-
- // 총 금액 계산
- const total = convertedQuotations.reduce((sum, q) => sum + q.bidAmount, 0)
- setTotalQuotationAmount(total)
+
+ if (preQuoteData && Array.isArray(preQuoteData) && preQuoteData.length > 0) {
+ console.log('사전견적 데이터:', preQuoteData)
+
+ // 사전견적 데이터를 본입찰 포맷으로 변환
+ const convertedQuotations = preQuoteData
+ .filter(item => item && typeof item === 'object' && item.prItemId)
+ .map(item => ({
+ prItemId: item.prItemId,
+ bidUnitPrice: item.bidUnitPrice,
+ bidAmount: item.bidAmount,
+ proposedDeliveryDate: item.proposedDeliveryDate || undefined,
+ technicalSpecification: item.technicalSpecification || undefined
+ }))
+
+ console.log('변환된 견적 데이터:', convertedQuotations)
+
+ if (Array.isArray(convertedQuotations) && convertedQuotations.length > 0) {
+ setPrItemQuotations(convertedQuotations)
+
+ // 총 금액 계산
+ const total = convertedQuotations.reduce((sum, q) => {
+ const amount = Number(q.bidAmount) || 0
+ return sum + amount
+ }, 0)
+ setTotalQuotationAmount(total)
+ console.log('계산된 총 금액:', total)
+ }
+ }
// 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정
- if (totalQuotationAmount > 0 && result.isBiddingParticipated === true) {
+ if (totalQuotationAmount > 0 && result?.isBiddingParticipated === true) {
+ console.log('응찰 확정됨, 사전견적 금액 설정:', totalQuotationAmount)
+ console.log('사전견적 금액을 finalQuoteAmount로 설정:', totalQuotationAmount)
setResponseData(prev => ({
...prev,
finalQuoteAmount: totalQuotationAmount.toString()
}))
}
+
+ // 연동제 데이터 로드 (사전견적에서 답변했으면 로드, 아니면 입찰 조건 확인)
+ if (result.priceAdjustmentResponse !== null) {
+ // 사전견적에서 이미 답변한 경우 - 연동제 폼 로드
+ const savedPriceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(result.biddingCompanyId)
+ if (savedPriceAdjustmentForm) {
+ setPriceAdjustmentForm({
+ itemName: savedPriceAdjustmentForm.itemName || '',
+ adjustmentReflectionPoint: savedPriceAdjustmentForm.adjustmentReflectionPoint || '',
+ majorApplicableRawMaterial: savedPriceAdjustmentForm.majorApplicableRawMaterial || '',
+ adjustmentFormula: savedPriceAdjustmentForm.adjustmentFormula || '',
+ rawMaterialPriceIndex: savedPriceAdjustmentForm.rawMaterialPriceIndex || '',
+ referenceDate: savedPriceAdjustmentForm.referenceDate || '',
+ comparisonDate: savedPriceAdjustmentForm.comparisonDate || '',
+ adjustmentRatio: savedPriceAdjustmentForm.adjustmentRatio || '',
+ notes: savedPriceAdjustmentForm.notes || '',
+ adjustmentConditions: savedPriceAdjustmentForm.adjustmentConditions || '',
+ majorNonApplicableRawMaterial: savedPriceAdjustmentForm.majorNonApplicableRawMaterial || '',
+ adjustmentPeriod: savedPriceAdjustmentForm.adjustmentPeriod || '',
+ contractorWriter: savedPriceAdjustmentForm.contractorWriter || '',
+ adjustmentDate: savedPriceAdjustmentForm.adjustmentDate || '',
+ nonApplicableReason: savedPriceAdjustmentForm.nonApplicableReason || '',
+ })
+ }
+ }
} catch (error) {
console.error('Failed to load pre-quote data:', error)
}
@@ -229,23 +409,38 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
if (participated && updatedDetail.biddingCompanyId) {
try {
const preQuoteData = await getSavedPrItemQuotations(updatedDetail.biddingCompanyId)
- const convertedQuotations = preQuoteData.map(item => ({
- prItemId: item.prItemId,
- bidUnitPrice: item.bidUnitPrice,
- bidAmount: item.bidAmount,
- proposedDeliveryDate: item.proposedDeliveryDate || undefined,
- technicalSpecification: item.technicalSpecification || undefined
- }))
-
- setPrItemQuotations(convertedQuotations)
- const total = convertedQuotations.reduce((sum, q) => sum + q.bidAmount, 0)
- setTotalQuotationAmount(total)
-
- if (total > 0) {
- setResponseData(prev => ({
- ...prev,
- finalQuoteAmount: total.toString()
- }))
+
+ if (preQuoteData && Array.isArray(preQuoteData) && preQuoteData.length > 0) {
+ console.log('참여확정 후 사전견적 데이터:', preQuoteData)
+
+ const convertedQuotations = preQuoteData
+ .filter(item => item && typeof item === 'object' && item.prItemId)
+ .map(item => ({
+ prItemId: item.prItemId,
+ bidUnitPrice: item.bidUnitPrice,
+ bidAmount: item.bidAmount,
+ proposedDeliveryDate: item.proposedDeliveryDate || undefined,
+ technicalSpecification: item.technicalSpecification || undefined
+ }))
+
+ console.log('참여확정 후 변환된 견적 데이터:', convertedQuotations)
+
+ if (Array.isArray(convertedQuotations) && convertedQuotations.length > 0) {
+ setPrItemQuotations(convertedQuotations)
+ const total = convertedQuotations.reduce((sum, q) => {
+ const amount = Number(q.bidAmount) || 0
+ return sum + amount
+ }, 0)
+ setTotalQuotationAmount(total)
+ console.log('참여확정 후 계산된 총 금액:', total)
+
+ if (total > 0) {
+ setResponseData(prev => ({
+ ...prev,
+ finalQuoteAmount: total.toString()
+ }))
+ }
+ }
}
} catch (error) {
console.error('Failed to load pre-quote data after participation:', error)
@@ -356,6 +551,59 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
}
}
+ // 응찰 취소 핸들러
+ const handleCancelResponse = async () => {
+ if (!biddingDetail || !userId) return
+
+ // 최종제출한 경우 취소 불가
+ if (biddingDetail.isFinalSubmission) {
+ toast({
+ title: '취소 불가',
+ description: '최종 제출된 응찰은 취소할 수 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (isBrowser && !window.confirm('응찰을 취소하시겠습니까? 작성한 견적 내용이 모두 삭제됩니다.')) {
+ return
+ }
+
+ setIsCancelling(true)
+ try {
+ const result = await cancelBiddingResponse(biddingDetail.biddingCompanyId, userId)
+
+ if (result.success) {
+ toast({
+ title: '응찰 취소 완료',
+ description: '응찰이 취소되었습니다.',
+ })
+ // 페이지 새로고침
+ if (isBrowser) {
+ window.location.reload()
+ } else {
+ // 서버사이드에서는 라우터로 이동
+ router.push(`/partners/bid/${biddingId}`)
+ }
+ } else {
+ toast({
+ title: '응찰 취소 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ } catch (error) {
+ console.error('Failed to cancel bidding response:', error)
+ toast({
+ title: '오류',
+ description: '응찰 취소에 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsCancelling(false)
+ }
+ }
+
const handleSubmitResponse = () => {
if (!biddingDetail) return
// 입찰 마감 상태 체크
@@ -412,6 +660,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
finalQuoteAmount: parseFloat(responseData.finalQuoteAmount),
proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
additionalProposals: responseData.additionalProposals,
+ isFinalSubmission, // 최종제출 여부 추가
prItemQuotations: prItemQuotations.length > 0 ? prItemQuotations.map(q => ({
prItemId: q.prItemId,
bidUnitPrice: q.bidUnitPrice,
@@ -425,8 +674,8 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
if (result.success) {
toast({
- title: '응찰 완료',
- description: '견적이 성공적으로 제출되었습니다.',
+ title: isFinalSubmission ? '응찰 완료' : '임시 저장 완료',
+ description: isFinalSubmission ? '견적이 최종 제출되었습니다.' : '견적이 임시 저장되었습니다.',
})
// 데이터 새로고침
@@ -488,9 +737,6 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<Badge variant="outline" className="font-mono text-xs">
{biddingDetail.biddingNumber}
</Badge>
- <Badge variant="outline" className="font-mono">
- Rev. {biddingDetail.revision ?? 0}
- </Badge>
<Badge variant={
biddingDetail.status === 'bidding_disposal' ? 'destructive' :
biddingDetail.status === 'vendor_selected' ? 'default' :
@@ -525,20 +771,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <Label className="text-sm font-medium text-muted-foreground">프로젝트</Label>
- <div className="flex items-center gap-2 mt-1">
- <Building2 className="w-4 h-4" />
- <span>{biddingDetail.projectName || '미설정'}</span>
- </div>
- </div>
- <div>
- <Label className="text-sm font-medium text-muted-foreground">품목</Label>
- <div className="flex items-center gap-2 mt-1">
- <Package className="w-4 h-4" />
- <span>{biddingDetail.itemName || '미설정'}</span>
- </div>
- </div>
+
<div>
<Label className="text-sm font-medium text-muted-foreground">계약구분</Label>
<div className="mt-1">{contractTypeLabels[biddingDetail.contractType]}</div>
@@ -552,22 +785,87 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : biddingDetail.awardCount === 'multiple' ? '복수' : '미설정'}</div>
</div>
<div>
- <Label className="text-sm font-medium text-muted-foreground">입찰 담당자</Label>
+ <Label className="text-sm font-medium text-muted-foreground">입찰담당자</Label>
<div className="flex items-center gap-2 mt-1">
<User className="w-4 h-4" />
- <span>{biddingDetail.managerName || '미설정'}</span>
+ <span>{biddingDetail.bidPicName || '미설정'}</span>
</div>
</div>
- </div>
-
- {/* {biddingDetail.budget && (
<div>
- <Label className="text-sm font-medium text-muted-foreground">예산</Label>
+ <Label className="text-sm font-medium text-muted-foreground">조달담당자</Label>
<div className="flex items-center gap-2 mt-1">
- <DollarSign className="w-4 h-4" />
- <span className="font-semibold">{formatCurrency(biddingDetail.budget)}</span>
+ <User className="w-4 h-4" />
+ <span>{biddingDetail.supplyPicName || '미설정'}</span>
+ </div>
+ </div>
+ </div>
+
+ {/* 계약기간 */}
+ {biddingDetail.contractStartDate && biddingDetail.contractEndDate && (
+ <div className="pt-4 border-t">
+ <Label className="text-sm font-medium text-muted-foreground mb-2 block">계약기간</Label>
+ <div className="p-3 bg-muted/50 rounded-lg">
+ <div className="flex items-center gap-2 text-sm">
+ <span className="font-medium">{formatDate(biddingDetail.contractStartDate, 'KR')}</span>
+ <span className="text-muted-foreground">~</span>
+ <span className="font-medium">{formatDate(biddingDetail.contractEndDate, 'KR')}</span>
+ </div>
</div>
</div>
+ )}
+
+
+ {/* 제출 마감일 D-day */}
+ {/* {biddingDetail.submissionEndDate && (
+ <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.submissionEndDate)
+ 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.submissionEndDate, '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>
)} */}
{/* 일정 정보 */}
@@ -576,7 +874,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
{biddingDetail.submissionStartDate && biddingDetail.submissionEndDate && (
<div>
- <span className="font-medium">제출기간:</span> {formatDate(biddingDetail.submissionStartDate, 'KR')} ~ {formatDate(biddingDetail.submissionEndDate, 'KR')}
+ <span className="font-medium">응찰기간:</span> {formatDate(biddingDetail.submissionStartDate, 'KR')} ~ {formatDate(biddingDetail.submissionEndDate, 'KR')}
</div>
)}
{biddingDetail.evaluationDate && (
@@ -589,6 +887,130 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</CardContent>
</Card>
+ {/* 입찰공고 토글 섹션 */}
+ {biddingNotice && (
+ <Card>
+ <Collapsible open={isNoticeOpen} onOpenChange={setIsNoticeOpen}>
+ <CollapsibleTrigger asChild>
+ <CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
+ <CardTitle className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 입찰공고 내용
+ </div>
+ <ChevronDown className={`w-5 h-5 transition-transform ${isNoticeOpen ? 'rotate-180' : ''}`} />
+ </CardTitle>
+ </CardHeader>
+ </CollapsibleTrigger>
+ <CollapsibleContent>
+ <CardContent className="pt-0">
+ <div className="p-4 bg-muted/50 rounded-lg">
+ {biddingNotice.title && (
+ <h3 className="font-semibold text-lg mb-3">{biddingNotice.title}</h3>
+ )}
+ {biddingNotice.content ? (
+ <div
+ className="prose prose-sm max-w-none"
+ dangerouslySetInnerHTML={{
+ __html: biddingNotice.content
+ }}
+ />
+ ) : (
+ <p className="text-muted-foreground">입찰공고 내용이 없습니다.</p>
+ )}
+ </div>
+ </CardContent>
+ </CollapsibleContent>
+ </Collapsible>
+ </Card>
+ )}
+
+ {/* 현재 설정된 조건 섹션 */}
+ {biddingConditions && (
+ <Card>
+ <CardHeader>
+ <CardTitle>현재 설정된 입찰 조건</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
+ <div>
+ <Label className="text-muted-foreground">지급조건</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.paymentTerms || "미설정"}</p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">부가세구분</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">
+ {biddingConditions.taxConditions
+ ? getTaxConditionName(biddingConditions.taxConditions)
+ : "미설정"
+ }
+ </p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">인도조건</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.incoterms || "미설정"}</p>
+ </div>
+ </div>
+ <div>
+ <Label className="text-muted-foreground">인도조건2</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.incotermsOption || "미설정"}</p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">계약 납기일</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">
+ {biddingConditions.contractDeliveryDate
+ ? formatDate(biddingConditions.contractDeliveryDate, 'KR')
+ : "미설정"
+ }
+ </p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">선적지</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.shippingPort || "미설정"}</p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">하역지</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.destinationPort || "미설정"}</p>
+ </div>
+ </div>
+
+ <div>
+ <Label className="text-muted-foreground">연동제 적용</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.isPriceAdjustmentApplicable ? "적용 가능" : "적용 불가"}</p>
+ </div>
+ </div>
+
+
+ <div >
+ <Label className="text-muted-foreground">스페어파트 옵션</Label>
+ <div className="mt-1 p-3 bg-muted rounded-md">
+ <p className="font-medium">{biddingConditions.sparePartOptions}</p>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+
{/* 참여 상태에 따른 섹션 표시 */}
{biddingDetail.isBiddingParticipated === false ? (
@@ -687,25 +1109,315 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
readOnly={false}
/>
)}
+
+ {/* 연동제 적용 여부 - SHI가 연동제를 요구하고, 사전견적에서 답변하지 않은 경우만 표시 */}
+ {biddingConditions?.isPriceAdjustmentApplicable && biddingDetail.priceAdjustmentResponse === null && (
+ <>
+ <div className="space-y-3 p-4 border rounded-lg bg-muted/30">
+ <Label className="font-semibold text-base">연동제 적용 여부 *</Label>
+ <RadioGroup
+ value={responseData.priceAdjustmentResponse === null ? 'none' : responseData.priceAdjustmentResponse ? 'apply' : 'not-apply'}
+ onValueChange={(value) => {
+ const newValue = value === 'apply' ? true : value === 'not-apply' ? false : null
+ setResponseData({...responseData, priceAdjustmentResponse: newValue})
+ }}
+ >
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="apply" id="price-adjustment-apply" />
+ <Label htmlFor="price-adjustment-apply" className="font-normal cursor-pointer">
+ 연동제 적용
+ </Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="not-apply" id="price-adjustment-not-apply" />
+ <Label htmlFor="price-adjustment-not-apply" className="font-normal cursor-pointer">
+ 연동제 미적용
+ </Label>
+ </div>
+ </RadioGroup>
+ </div>
+
+ {/* 연동제 상세 정보 */}
+ {responseData.priceAdjustmentResponse !== null && (
+ <Card className="mt-6">
+ <CardHeader>
+ <CardTitle className="text-lg">하도급대금등 연동표</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 공통 필드 - 품목등의 명칭 */}
+ <div className="space-y-2">
+ <Label htmlFor="itemName">품목등의 명칭 *</Label>
+ <Input
+ id="itemName"
+ value={priceAdjustmentForm.itemName}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, itemName: e.target.value})}
+ placeholder="품목명을 입력하세요"
+ required
+ />
+ </div>
+
+ {/* 연동제 적용 시 - 모든 필드 표시 */}
+ {responseData.priceAdjustmentResponse === true && (
+ <>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentReflectionPoint">조정대금 반영시점 *</Label>
+ <Input
+ id="adjustmentReflectionPoint"
+ value={priceAdjustmentForm.adjustmentReflectionPoint}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentReflectionPoint: e.target.value})}
+ placeholder="반영시점을 입력하세요"
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentRatio">연동 비율 (%) *</Label>
+ <Input
+ id="adjustmentRatio"
+ type="number"
+ step="0.01"
+ value={priceAdjustmentForm.adjustmentRatio}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentRatio: e.target.value})}
+ placeholder="비율을 입력하세요"
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentPeriod">조정주기 *</Label>
+ <Input
+ id="adjustmentPeriod"
+ value={priceAdjustmentForm.adjustmentPeriod}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentPeriod: e.target.value})}
+ placeholder="조정주기를 입력하세요"
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="referenceDate">기준시점 *</Label>
+ <Input
+ id="referenceDate"
+ type="date"
+ value={priceAdjustmentForm.referenceDate}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, referenceDate: e.target.value})}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="comparisonDate">비교시점 *</Label>
+ <Input
+ id="comparisonDate"
+ type="date"
+ value={priceAdjustmentForm.comparisonDate}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, comparisonDate: e.target.value})}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="contractorWriter">수탁기업(협력사) 작성자 *</Label>
+ <Input
+ id="contractorWriter"
+ value={priceAdjustmentForm.contractorWriter}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, contractorWriter: e.target.value})}
+ placeholder="작성자명을 입력하세요"
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentDate">조정일 *</Label>
+ <Input
+ id="adjustmentDate"
+ type="date"
+ value={priceAdjustmentForm.adjustmentDate}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentDate: e.target.value})}
+ required
+ />
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="majorApplicableRawMaterial">연동대상 주요 원재료 *</Label>
+ <Textarea
+ id="majorApplicableRawMaterial"
+ value={priceAdjustmentForm.majorApplicableRawMaterial}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorApplicableRawMaterial: e.target.value})}
+ placeholder="연동 대상 원재료를 입력하세요"
+ rows={3}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentFormula">하도급대금등 연동 산식 *</Label>
+ <Textarea
+ id="adjustmentFormula"
+ value={priceAdjustmentForm.adjustmentFormula}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentFormula: e.target.value})}
+ placeholder="연동 산식을 입력하세요"
+ rows={3}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="rawMaterialPriceIndex">원재료 가격 기준지표 *</Label>
+ <Textarea
+ id="rawMaterialPriceIndex"
+ value={priceAdjustmentForm.rawMaterialPriceIndex}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, rawMaterialPriceIndex: e.target.value})}
+ placeholder="가격 기준지표를 입력하세요"
+ rows={2}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="adjustmentConditions">조정요건 *</Label>
+ <Textarea
+ id="adjustmentConditions"
+ value={priceAdjustmentForm.adjustmentConditions}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentConditions: e.target.value})}
+ placeholder="조정요건을 입력하세요"
+ rows={2}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="priceAdjustmentNotes">기타 사항</Label>
+ <Textarea
+ id="priceAdjustmentNotes"
+ value={priceAdjustmentForm.notes}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, notes: e.target.value})}
+ placeholder="기타 사항을 입력하세요"
+ rows={2}
+ />
+ </div>
+ </>
+ )}
+
+ {/* 연동제 미적용 시 - 제한된 필드만 표시 */}
+ {responseData.priceAdjustmentResponse === false && (
+ <>
+ <div className="space-y-2">
+ <Label htmlFor="majorNonApplicableRawMaterial">연동제 미적용 주요 원재료 *</Label>
+ <Textarea
+ id="majorNonApplicableRawMaterial"
+ value={priceAdjustmentForm.majorNonApplicableRawMaterial}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorNonApplicableRawMaterial: e.target.value})}
+ placeholder="연동 미적용 원재료를 입력하세요"
+ rows={2}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="contractorWriter">수탁기업(협력사) 작성자 *</Label>
+ <Input
+ id="contractorWriter"
+ value={priceAdjustmentForm.contractorWriter}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, contractorWriter: e.target.value})}
+ placeholder="작성자명을 입력하세요"
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="nonApplicableReason">연동제 미희망 사유 *</Label>
+ <Textarea
+ id="nonApplicableReason"
+ value={priceAdjustmentForm.nonApplicableReason}
+ onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, nonApplicableReason: e.target.value})}
+ placeholder="미희망 사유를 입력하세요"
+ rows={3}
+ required
+ />
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+ )}
+ </>
+ )}
+
+ {/* 사전견적에서 이미 답변한 경우 - 읽기 전용으로 표시 */}
+ {biddingDetail.priceAdjustmentResponse !== null && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">연동제 적용 정보 (사전견적 제출 완료)</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="p-4 bg-muted/30 rounded-lg">
+ <div className="flex items-center gap-2 mb-2">
+ <CheckCircle className="w-5 h-5 text-green-600" />
+ <span className="font-semibold">
+ {biddingDetail.priceAdjustmentResponse ? '연동제 적용' : '연동제 미적용'}
+ </span>
+ </div>
+ <p className="text-sm text-muted-foreground">
+ 사전견적에서 이미 연동제 관련 정보를 제출하였습니다. 본입찰에서는 별도의 연동제 정보 입력이 필요하지 않습니다.
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 최종제출 체크박스 */}
+ {!biddingDetail.isFinalSubmission && (
+ <div className="flex items-center space-x-2 p-4 border rounded-lg bg-muted/30">
+ <input
+ type="checkbox"
+ id="finalSubmission"
+ checked={isFinalSubmission}
+ onChange={(e) => setIsFinalSubmission(e.target.checked)}
+ disabled={isSubmitting || isSavingDraft}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+ <label htmlFor="finalSubmission" className="text-sm font-medium cursor-pointer">
+ 최종 제출 (체크 시 제출 후 수정 및 취소 불가)
+ </label>
+ </div>
+ )}
+
{/* 응찰 제출 버튼 - 참여 확정 상태일 때만 표시 */}
- <div className="flex justify-end pt-4 gap-2">
- <Button
- variant="outline"
- onClick={handleSaveDraft}
- disabled={isSavingDraft || isSubmitting}
- className="min-w-[100px]"
- >
- <Save className="w-4 h-4 mr-2" />
- {isSavingDraft ? '저장 중...' : '임시 저장'}
- </Button>
- <Button
- onClick={handleSubmitResponse}
- disabled={isSubmitting || isSavingDraft || !!biddingDetail.responseSubmittedAt}
- className="min-w-[100px]"
- >
- <Send className="w-4 h-4 mr-2" />
- {isSubmitting ? '제출 중...' : biddingDetail.responseSubmittedAt ? '응찰 완료' : '응찰 제출'}
- </Button>
+ <div className="flex justify-between pt-4 gap-2">
+ {/* 응찰 취소 버튼 (최종제출 아닌 경우만) */}
+ {biddingDetail.finalQuoteSubmittedAt && !biddingDetail.isFinalSubmission && (
+ <Button
+ variant="destructive"
+ onClick={handleCancelResponse}
+ disabled={isCancelling || isSubmitting}
+ className="min-w-[100px]"
+ >
+ <Trash2 className="w-4 h-4 mr-2" />
+ {isCancelling ? '취소 중...' : '응찰 취소'}
+ </Button>
+ )}
+ <div className="flex gap-2 ml-auto">
+ <Button
+ variant="outline"
+ onClick={handleSaveDraft}
+ disabled={isSavingDraft || isSubmitting || biddingDetail.isFinalSubmission}
+ className="min-w-[100px]"
+ >
+ <Save className="w-4 h-4 mr-2" />
+ {isSavingDraft ? '저장 중...' : '임시 저장'}
+ </Button>
+ <Button
+ onClick={handleSubmitResponse}
+ disabled={isSubmitting || isSavingDraft || biddingDetail.isFinalSubmission}
+ className="min-w-[100px]"
+ >
+ <Send className="w-4 h-4 mr-2" />
+ {isSubmitting ? '제출 중...' : biddingDetail.isFinalSubmission ? '최종 제출 완료' : '응찰 제출'}
+ </Button>
+ </div>
</div>
</CardContent>
</Card>
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index 7fb62122..5870067a 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -17,7 +17,6 @@ import {
MoreHorizontal,
Calendar,
User,
- Calculator,
Paperclip,
AlertTriangle
} from 'lucide-react'
@@ -25,6 +24,7 @@ import { formatDate } from '@/lib/utils'
import { biddingStatusLabels, contractTypeLabels } from '@/db/schema'
import { PartnersBiddingListItem } from '../detail/service'
import { Checkbox } from '@/components/ui/checkbox'
+import { toast } from 'sonner'
const columnHelper = createColumnHelper<PartnersBiddingListItem>()
@@ -62,11 +62,15 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
header: '입찰 No.',
cell: ({ row }) => {
const biddingNumber = row.original.biddingNumber
+ const originalBiddingNumber = row.original.originalBiddingNumber
const revision = row.original.revision
return (
<div className="font-mono text-sm">
<div>{biddingNumber}</div>
- <div className="text-muted-foreground">Rev. {revision ?? 0}</div>
+ <div className="text-muted-foreground text-xs">Rev. {revision ?? 0}</div>
+ {originalBiddingNumber && (
+ <div className="text-xs text-muted-foreground">원: {originalBiddingNumber}</div>
+ )}
</div>
)
},
@@ -148,27 +152,36 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
id: 'actions',
header: '액션',
cell: ({ row }) => {
- const handleView = () => {
- if (setRowAction) {
- setRowAction({
- type: 'view',
- row: { original: row.original }
+ // 사양설명회 참석여부 체크 함수
+ const checkSpecificationMeeting = () => {
+ const hasSpecMeeting = row.original.hasSpecificationMeeting
+ const isAttending = row.original.isAttendingMeeting
+
+ // 사양설명회가 있고, 참석여부가 아직 설정되지 않은 경우
+ if (hasSpecMeeting && isAttending === null) {
+ toast.warning('사양설명회 참석여부 필요', {
+ description: '사전견적 또는 입찰을 진행하기 전에 사양설명회 참석여부를 먼저 설정해주세요.',
+ duration: 5000,
})
+ return false
}
+ return true
}
- const handlePreQuote = () => {
+ const handleView = () => {
+ // 사양설명회 체크
+ if (!checkSpecificationMeeting()) {
+ return
+ }
+
if (setRowAction) {
setRowAction({
- type: 'pre-quote',
+ type: 'view',
row: { original: row.original }
})
}
}
- const biddingStatus = row.original.status
- const isClosed = biddingStatus === 'bidding_closed' || biddingStatus === 'vendor_selected' || biddingStatus === 'bidding_disposal'
-
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -185,12 +198,6 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
<FileText className="mr-2 h-4 w-4" />
입찰 상세보기
</DropdownMenuItem>
- {!isClosed && (
- <DropdownMenuItem onClick={handlePreQuote}>
- <Calculator className="mr-2 h-4 w-4" />
- 사전견적하기
- </DropdownMenuItem>
- )}
</DropdownMenuContent>
</DropdownMenu>
)
@@ -327,61 +334,50 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
const endDate = row.original.contractEndDate
if (!startDate || !endDate) {
- return <div className="max-w-24 truncate">-</div>
+ return <div className="text-muted-foreground text-center">-</div>
}
return (
- <div className="max-w-24 truncate" title={`${formatDate(startDate, 'KR')} ~ ${formatDate(endDate, 'KR')}`}>
- {formatDate(startDate, 'KR')} ~ {formatDate(endDate, 'KR')}
+ <div className="text-sm">
+ <div>{formatDate(startDate, 'KR')}</div>
+ <div className="text-muted-foreground">~</div>
+ <div>{formatDate(endDate, 'KR')}</div>
</div>
)
},
}),
- // 참여회신 마감일
- columnHelper.accessor('responseDeadline', {
- header: '참여회신 마감일',
- cell: ({ row }) => {
- const deadline = row.original.responseDeadline
- if (!deadline) {
- return <div className="text-muted-foreground">-</div>
- }
- return <div className="text-sm">{formatDate(deadline, 'KR')}</div>
- },
- }),
-
- // 입찰제출일
- columnHelper.accessor('submissionDate', {
- header: '입찰제출일',
+ // 입찰담당자
+ columnHelper.display({
+ id: 'bidPicName',
+ header: '입찰담당자',
cell: ({ row }) => {
- const date = row.original.submissionDate
- if (!date) {
- return <div className="text-muted-foreground">-</div>
+ const name = row.original.bidPicName
+ if (!name) {
+ return <div className="text-muted-foreground text-center">-</div>
}
- return <div className="text-sm">{formatDate(date, 'KR')}</div>
+ return (
+ <div className="flex items-center gap-1">
+ <User className="h-4 w-4" />
+ <div className="text-sm">{name}</div>
+ </div>
+ )
},
}),
- // 입찰담당자
- columnHelper.accessor('managerName', {
- header: '입찰담당자',
+ // 조달담당자
+ columnHelper.display({
+ id: 'supplyPicName',
+ header: '조달담당자',
cell: ({ row }) => {
- const name = row.original.managerName
- const email = row.original.managerEmail
+ const name = row.original.supplyPicName
if (!name) {
- return <div className="text-muted-foreground">-</div>
+ return <div className="text-muted-foreground text-center">-</div>
}
return (
<div className="flex items-center gap-1">
<User className="h-4 w-4" />
- <div>
- <div className="text-sm">{name}</div>
- {email && (
- <div className="text-xs text-muted-foreground truncate max-w-32" title={email}>
- {email}
- </div>
- )}
- </div>
+ <div className="text-sm">{name}</div>
</div>
)
},
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
deleted file mode 100644
index 8a157c5f..00000000
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ /dev/null
@@ -1,1413 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { useRouter } from 'next/navigation'
-import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { Textarea } from '@/components/ui/textarea'
-import { Checkbox } from '@/components/ui/checkbox'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import {
- ArrowLeft,
- Calendar,
- Building2,
- Package,
- User,
- FileText,
- Users,
- Send,
- CheckCircle,
- XCircle,
- Save
-} from 'lucide-react'
-
-import { formatDate } from '@/lib/utils'
-import {
- getBiddingCompaniesForPartners,
- submitPreQuoteResponse,
- getPrItemsForBidding,
- getSavedPrItemQuotations,
- savePreQuoteDraft,
- setPreQuoteParticipation
-} from '../pre-quote/service'
-import { getBiddingConditions } from '../service'
-import { getPriceAdjustmentFormByBiddingCompanyId } from '../detail/service'
-import { getIncotermsForSelection, getPaymentTermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from '@/lib/procurement-select/service'
-import { TAX_CONDITIONS, getTaxConditionName } from '@/lib/tax-conditions/types'
-import { PrItemsPricingTable } from './components/pr-items-pricing-table'
-import { SimpleFileUpload } from './components/simple-file-upload'
-import {
- biddingStatusLabels,
-} from '@/db/schema'
-import { useToast } from '@/hooks/use-toast'
-import { useTransition } from 'react'
-import { useSession } from 'next-auth/react'
-
-interface PartnersBiddingPreQuoteProps {
- biddingId: number
- companyId: number
-}
-
-interface BiddingDetail {
- id: number
- biddingNumber: string
- revision: number | null
- projectName: string | null
- itemName: string | null
- title: string
- description: string | null
- content: string | null
- contractType: string
- biddingType: string
- awardCount: string
- contractStartDate: Date | null
- contractEndDate: Date | null
- preQuoteDate: string | null
- biddingRegistrationDate: string | null
- submissionStartDate: string | null
- submissionEndDate: string | null
- evaluationDate: string | null
- currency: string
- budget: number | null
- targetPrice: number | null
- status: string
- managerName: string | null
- managerEmail: string | null
- managerPhone: string | null
- biddingCompanyId: number | null
- biddingId: number // bidding의 ID 추가
- invitationStatus: string | null
- preQuoteAmount: string | null
- preQuoteSubmittedAt: string | null
- preQuoteDeadline: string | null
- isPreQuoteSelected: boolean | null
- isAttendingMeeting: boolean | null
- // companyConditionResponses에서 가져온 조건들 (제시된 조건과 응답 모두)
- paymentTermsResponse: string | null
- taxConditionsResponse: string | null
- incotermsResponse: string | null
- proposedContractDeliveryDate: string | null
- proposedShippingPort: string | null
- proposedDestinationPort: string | null
- priceAdjustmentResponse: boolean | null
- sparePartResponse: string | null
- isInitialResponse: boolean | null
- additionalProposals: string | null
-}
-
-export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddingPreQuoteProps) {
- const router = useRouter()
- const { toast } = useToast()
- const [isPending, startTransition] = useTransition()
- const session = useSession()
- const [biddingDetail, setBiddingDetail] = React.useState<BiddingDetail | null>(null)
- const [isLoading, setIsLoading] = React.useState(true)
- const [biddingConditions, setBiddingConditions] = React.useState<any | null>(null)
-
- // Procurement 데이터 상태들
- const [paymentTermsOptions, setPaymentTermsOptions] = React.useState<Array<{code: string, description: string}>>([])
- const [incotermsOptions, setIncotermsOptions] = React.useState<Array<{code: string, description: string}>>([])
- const [shippingPlaces, setShippingPlaces] = React.useState<Array<{code: string, description: string}>>([])
- const [destinationPlaces, setDestinationPlaces] = React.useState<Array<{code: string, description: string}>>([])
-
- // 품목별 견적 관련 상태
- const [prItems, setPrItems] = React.useState<any[]>([])
- const [prItemQuotations, setPrItemQuotations] = React.useState<any[]>([])
- const [totalAmount, setTotalAmount] = React.useState(0)
- const [isSaving, setIsSaving] = React.useState(false)
-
- // 사전견적 폼 상태
- const [responseData, setResponseData] = React.useState({
- preQuoteAmount: '',
- paymentTermsResponse: '',
- taxConditionsResponse: '',
- incotermsResponse: '',
- proposedContractDeliveryDate: '',
- proposedShippingPort: '',
- proposedDestinationPort: '',
- priceAdjustmentResponse: false,
- isInitialResponse: false,
- sparePartResponse: '',
- additionalProposals: '',
- isAttendingMeeting: false,
- })
-
- // 사전견적 참여의사 상태
- const [participationDecision, setParticipationDecision] = React.useState<boolean | null>(null)
-
- // 연동제 폼 상태
- const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({
- itemName: '',
- adjustmentReflectionPoint: '',
- majorApplicableRawMaterial: '',
- adjustmentFormula: '',
- rawMaterialPriceIndex: '',
- referenceDate: '',
- comparisonDate: '',
- adjustmentRatio: '',
- notes: '',
- adjustmentConditions: '',
- majorNonApplicableRawMaterial: '',
- adjustmentPeriod: '',
- contractorWriter: '',
- adjustmentDate: '',
- nonApplicableReason: '',
- })
- const userId = session.data?.user?.id || ''
-
- // Procurement 데이터 로드 함수들
- const loadPaymentTerms = React.useCallback(async () => {
- try {
- const data = await getPaymentTermsForSelection();
- setPaymentTermsOptions(data);
- } catch (error) {
- console.error("Failed to load payment terms:", error);
- }
- }, []);
-
- const loadIncoterms = React.useCallback(async () => {
- try {
- const data = await getIncotermsForSelection();
- setIncotermsOptions(data);
- } catch (error) {
- console.error("Failed to load incoterms:", error);
- }
- }, []);
-
- const loadShippingPlaces = React.useCallback(async () => {
- try {
- const data = await getPlaceOfShippingForSelection();
- setShippingPlaces(data);
- } catch (error) {
- console.error("Failed to load shipping places:", error);
- }
- }, []);
-
- const loadDestinationPlaces = React.useCallback(async () => {
- try {
- const data = await getPlaceOfDestinationForSelection();
- setDestinationPlaces(data);
- } catch (error) {
- console.error("Failed to load destination places:", error);
- }
- }, []);
-
- // 데이터 로드
- React.useEffect(() => {
- const loadData = async () => {
- try {
- setIsLoading(true)
-
- // 모든 필요한 데이터를 병렬로 로드
- const [result, conditions, prItemsData] = await Promise.all([
- getBiddingCompaniesForPartners(biddingId, companyId),
- getBiddingConditions(biddingId),
- getPrItemsForBidding(biddingId)
- ])
-
- if (result) {
- setBiddingDetail(result as BiddingDetail)
-
- // 저장된 품목별 견적 정보가 있으면 로드
- if (result.biddingCompanyId) {
- const savedQuotations = await getSavedPrItemQuotations(result.biddingCompanyId)
- setPrItemQuotations(savedQuotations)
-
- // 총 금액 계산
- const calculatedTotal = savedQuotations.reduce((sum: number, item: any) => sum + item.bidAmount, 0)
- setTotalAmount(calculatedTotal)
-
- // 저장된 연동제 정보가 있으면 로드
- if (result.priceAdjustmentResponse) {
- const savedPriceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(result.biddingCompanyId)
- if (savedPriceAdjustmentForm) {
- setPriceAdjustmentForm({
- itemName: savedPriceAdjustmentForm.itemName || '',
- adjustmentReflectionPoint: savedPriceAdjustmentForm.adjustmentReflectionPoint || '',
- majorApplicableRawMaterial: savedPriceAdjustmentForm.majorApplicableRawMaterial || '',
- adjustmentFormula: savedPriceAdjustmentForm.adjustmentFormula || '',
- rawMaterialPriceIndex: savedPriceAdjustmentForm.rawMaterialPriceIndex || '',
- referenceDate: savedPriceAdjustmentForm.referenceDate ? new Date(savedPriceAdjustmentForm.referenceDate).toISOString().split('T')[0] : '',
- comparisonDate: savedPriceAdjustmentForm.comparisonDate ? new Date(savedPriceAdjustmentForm.comparisonDate).toISOString().split('T')[0] : '',
- adjustmentRatio: savedPriceAdjustmentForm.adjustmentRatio?.toString() || '',
- notes: savedPriceAdjustmentForm.notes || '',
- adjustmentConditions: savedPriceAdjustmentForm.adjustmentConditions || '',
- majorNonApplicableRawMaterial: savedPriceAdjustmentForm.majorNonApplicableRawMaterial || '',
- adjustmentPeriod: savedPriceAdjustmentForm.adjustmentPeriod || '',
- contractorWriter: savedPriceAdjustmentForm.contractorWriter || '',
- adjustmentDate: savedPriceAdjustmentForm.adjustmentDate ? new Date(savedPriceAdjustmentForm.adjustmentDate).toISOString().split('T')[0] : '',
- nonApplicableReason: savedPriceAdjustmentForm.nonApplicableReason || '',
- })
- }
- }
- }
-
- // 기존 응답 데이터로 폼 초기화
- setResponseData({
- preQuoteAmount: result.preQuoteAmount?.toString() || '',
- paymentTermsResponse: result.paymentTermsResponse || '',
- taxConditionsResponse: result.taxConditionsResponse || '',
- incotermsResponse: result.incotermsResponse || '',
- proposedContractDeliveryDate: result.proposedContractDeliveryDate || '',
- proposedShippingPort: result.proposedShippingPort || '',
- proposedDestinationPort: result.proposedDestinationPort || '',
- priceAdjustmentResponse: result.priceAdjustmentResponse || false,
- isInitialResponse: result.isInitialResponse || false,
- sparePartResponse: result.sparePartResponse || '',
- additionalProposals: result.additionalProposals || '',
- isAttendingMeeting: result.isAttendingMeeting || false,
- })
-
- // 사전견적 참여의사 초기화
- setParticipationDecision(result.isPreQuoteParticipated)
- }
-
- if (conditions) {
- // BiddingConditionsEdit와 같은 방식으로 raw 데이터 사용
- setBiddingConditions(conditions)
- }
-
- if (prItemsData) {
- setPrItems(prItemsData)
- }
-
- // Procurement 데이터 로드
- await Promise.all([
- loadPaymentTerms(),
- loadIncoterms(),
- loadShippingPlaces(),
- loadDestinationPlaces()
- ])
- } catch (error) {
- console.error('Failed to load bidding company:', error)
- toast({
- title: '오류',
- description: '입찰 정보를 불러오는데 실패했습니다.',
- variant: 'destructive',
- })
- } finally {
- setIsLoading(false)
- }
- }
-
- loadData()
- }, [biddingId, companyId, toast, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces])
-
- // 임시저장 기능
- const handleTempSave = () => {
- if (!biddingDetail || !biddingDetail.biddingCompanyId) {
- toast({
- title: '임시저장 실패',
- description: '입찰 정보가 올바르지 않습니다.',
- variant: 'destructive',
- })
- return
- }
- // 입찰 마감 상태 체크
- const biddingStatus = biddingDetail.status
- const isClosed = biddingStatus === 'bidding_closed' || biddingStatus === 'vendor_selected' || biddingStatus === 'bidding_disposal'
-
- if (isClosed) {
- toast({
- title: "접근 제한",
- description: "입찰이 마감되어 더 이상 사전견적을 제출할 수 없습니다.",
- variant: "destructive",
- })
- router.back()
- return
- }
-
- // 사전견적 상태 체크
- const isPreQuoteStatus = biddingStatus === 'request_for_quotation' || biddingStatus === 'received_quotation'
- if (!isPreQuoteStatus) {
- toast({
- title: "접근 제한",
- description: "사전견적 단계가 아니므로 임시저장이 불가능합니다.",
- variant: "destructive",
- })
- return
- }
-
- if (!userId) {
- toast({
- title: '임시저장 실패',
- description: '사용자 정보를 확인할 수 없습니다. 다시 로그인해주세요.',
- variant: 'destructive',
- })
- return
- }
-
- setIsSaving(true)
- startTransition(async () => {
- 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 || false, // 체크 안하면 false로 설정
- isInitialResponse: responseData.isInitialResponse || false, // 체크 안하면 false로 설정
- sparePartResponse: responseData.sparePartResponse,
- additionalProposals: responseData.additionalProposals,
- priceAdjustmentForm: (responseData.priceAdjustmentResponse || false) ? {
- 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!,
- participate
- )
-
- if (result.success) {
- setParticipationDecision(participate)
- toast({
- title: '설정 완료',
- description: `사전견적 ${participate ? '참여' : '미참여'}로 설정되었습니다.`,
- })
- } else {
- toast({
- title: '설정 실패',
- description: result.error,
- variant: 'destructive',
- })
- }
- })
- }
-
- const handleSubmitResponse = () => {
- if (!biddingDetail) return
-
- // 입찰 마감 상태 체크
- const biddingStatus = biddingDetail.status
- const isClosed = biddingStatus === 'bidding_closed' || biddingStatus === 'vendor_selected' || biddingStatus === 'bidding_disposal'
-
- if (isClosed) {
- toast({
- title: "접근 제한",
- description: "입찰이 마감되어 더 이상 사전견적을 제출할 수 없습니다.",
- variant: "destructive",
- })
- router.back()
- return
- }
-
- // 사전견적 상태 체크
- const isPreQuoteStatus = biddingStatus === 'request_for_quotation' || biddingStatus === 'received_quotation'
- if (!isPreQuoteStatus) {
- toast({
- title: "접근 제한",
- description: "사전견적 단계가 아니므로 견적 제출이 불가능합니다.",
- variant: "destructive",
- })
- 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({
- title: '유효성 오류',
- description: '품목별 견적을 입력해주세요.',
- variant: 'destructive',
- })
- return
- }
-
- // 품목별 납품일 검증
- if (prItemQuotations.length > 0) {
- for (const quotation of prItemQuotations) {
- if (!quotation.proposedDeliveryDate?.trim()) {
- const prItem = prItems.find(item => item.id === quotation.prItemId)
- toast({
- title: '유효성 오류',
- description: `품목 ${prItem?.itemNumber || quotation.prItemId}의 납품예정일을 입력해주세요.`,
- variant: 'destructive',
- })
- return
- }
- }
- }
-
- const requiredFields = [
- { value: responseData.proposedContractDeliveryDate, name: '제안 납품일' },
- { value: responseData.paymentTermsResponse, name: '응답 지급조건' },
- { value: responseData.taxConditionsResponse, name: '응답 세금조건' },
- { value: responseData.incotermsResponse, name: '응답 운송조건' },
- { value: responseData.proposedShippingPort, name: '제안 선적지' },
- { value: responseData.proposedDestinationPort, name: '제안 하역지' },
- { value: responseData.sparePartResponse, name: '스페어파트 응답' },
- ]
-
- const missingField = requiredFields.find(field => !field.value?.trim())
- if (missingField) {
- toast({
- title: '유효성 오류',
- description: `${missingField.name}을(를) 입력해주세요.`,
- variant: 'destructive',
- })
- return
- }
-
- startTransition(async () => {
- const submissionData = {
- preQuoteAmount: totalAmount, // 품목별 계산된 총 금액 사용
- prItemQuotations, // 품목별 견적 데이터 추가
- paymentTermsResponse: responseData.paymentTermsResponse,
- taxConditionsResponse: responseData.taxConditionsResponse,
- incotermsResponse: responseData.incotermsResponse,
- proposedContractDeliveryDate: responseData.proposedContractDeliveryDate,
- proposedShippingPort: responseData.proposedShippingPort,
- proposedDestinationPort: responseData.proposedDestinationPort,
- priceAdjustmentResponse: responseData.priceAdjustmentResponse || false, // 체크 안하면 false로 설정
- isInitialResponse: responseData.isInitialResponse || false, // 체크 안하면 false로 설정
- sparePartResponse: responseData.sparePartResponse,
- additionalProposals: responseData.additionalProposals,
- priceAdjustmentForm: (responseData.priceAdjustmentResponse || false) ? {
- 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
- }
-
- const result = await submitPreQuoteResponse(
- biddingDetail.biddingCompanyId!,
- submissionData,
- userId
- )
-
- console.log('제출 결과:', result)
-
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
-
- // 데이터 새로고침 및 폼 상태 업데이트
- const updatedDetail = await getBiddingCompaniesForPartners(biddingId, companyId)
- console.log('업데이트된 데이터:', updatedDetail)
-
- if (updatedDetail) {
- setBiddingDetail(updatedDetail as BiddingDetail)
-
- // 폼 상태도 업데이트된 데이터로 다시 설정
- setResponseData({
- preQuoteAmount: updatedDetail.preQuoteAmount?.toString() || '',
- paymentTermsResponse: updatedDetail.paymentTermsResponse || '',
- taxConditionsResponse: updatedDetail.taxConditionsResponse || '',
- incotermsResponse: updatedDetail.incotermsResponse || '',
- proposedContractDeliveryDate: updatedDetail.proposedContractDeliveryDate || '',
- proposedShippingPort: updatedDetail.proposedShippingPort || '',
- proposedDestinationPort: updatedDetail.proposedDestinationPort || '',
- priceAdjustmentResponse: updatedDetail.priceAdjustmentResponse || false,
- isInitialResponse: updatedDetail.isInitialResponse || false,
- sparePartResponse: updatedDetail.sparePartResponse || '',
- additionalProposals: updatedDetail.additionalProposals || '',
- isAttendingMeeting: updatedDetail.isAttendingMeeting || false,
- })
-
- // 연동제 데이터도 다시 로드
- if (updatedDetail.biddingCompanyId && updatedDetail.priceAdjustmentResponse) {
- const savedPriceAdjustmentForm = await getPriceAdjustmentFormByBiddingCompanyId(updatedDetail.biddingCompanyId)
- if (savedPriceAdjustmentForm) {
- setPriceAdjustmentForm({
- itemName: savedPriceAdjustmentForm.itemName || '',
- adjustmentReflectionPoint: savedPriceAdjustmentForm.adjustmentReflectionPoint || '',
- majorApplicableRawMaterial: savedPriceAdjustmentForm.majorApplicableRawMaterial || '',
- adjustmentFormula: savedPriceAdjustmentForm.adjustmentFormula || '',
- rawMaterialPriceIndex: savedPriceAdjustmentForm.rawMaterialPriceIndex || '',
- referenceDate: savedPriceAdjustmentForm.referenceDate ? new Date(savedPriceAdjustmentForm.referenceDate).toISOString().split('T')[0] : '',
- comparisonDate: savedPriceAdjustmentForm.comparisonDate ? new Date(savedPriceAdjustmentForm.comparisonDate).toISOString().split('T')[0] : '',
- adjustmentRatio: savedPriceAdjustmentForm.adjustmentRatio?.toString() || '',
- notes: savedPriceAdjustmentForm.notes || '',
- adjustmentConditions: savedPriceAdjustmentForm.adjustmentConditions || '',
- majorNonApplicableRawMaterial: savedPriceAdjustmentForm.majorNonApplicableRawMaterial || '',
- adjustmentPeriod: savedPriceAdjustmentForm.adjustmentPeriod || '',
- contractorWriter: savedPriceAdjustmentForm.contractorWriter || '',
- adjustmentDate: savedPriceAdjustmentForm.adjustmentDate ? new Date(savedPriceAdjustmentForm.adjustmentDate).toISOString().split('T')[0] : '',
- nonApplicableReason: savedPriceAdjustmentForm.nonApplicableReason || '',
- })
- }
- }
- }
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
- })
- }
- })
- }
-
-
- if (isLoading) {
- return (
- <div className="flex items-center justify-center py-12">
- <div className="text-center">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
- <p className="text-muted-foreground">입찰 정보를 불러오는 중...</p>
- </div>
- </div>
- )
- }
-
- if (!biddingDetail) {
- return (
- <div className="text-center py-12">
- <p className="text-muted-foreground">입찰 정보를 찾을 수 없습니다.</p>
- <Button onClick={() => router.back()} className="mt-4">
- <ArrowLeft className="w-4 h-4 mr-2" />
- 돌아가기
- </Button>
- </div>
- )
- }
-
- return (
- <div className="space-y-6">
- {/* 헤더 */}
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-4">
- <Button variant="outline" onClick={() => router.back()}>
- <ArrowLeft className="w-4 h-4 mr-2" />
- 목록으로
- </Button>
- <div>
- <h1 className="text-2xl font-semibold">{biddingDetail.title}</h1>
- <div className="flex items-center gap-2 mt-1">
- <Badge variant="outline" className="font-mono">
- {biddingDetail.biddingNumber}
- {biddingDetail.revision && biddingDetail.revision > 0 && ` Rev.${biddingDetail.revision}`}
- </Badge>
- <Badge variant={
- biddingDetail.status === 'bidding_disposal' ? 'destructive' :
- biddingDetail.status === 'vendor_selected' ? 'default' :
- 'secondary'
- }>
- {biddingStatusLabels[biddingDetail.status]}
- </Badge>
- </div>
- </div>
- </div>
-
- </div>
-
- {/* 입찰 공고 섹션 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="w-5 h-5" />
- 입찰 공고
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <Label className="text-sm font-medium text-muted-foreground">프로젝트</Label>
- <div className="flex items-center gap-2 mt-1">
- <Building2 className="w-4 h-4" />
- <span>{biddingDetail.projectName}</span>
- </div>
- </div>
- <div>
- <Label className="text-sm font-medium text-muted-foreground">품목</Label>
- <div className="flex items-center gap-2 mt-1">
- <Package className="w-4 h-4" />
- <span>{biddingDetail.itemName}</span>
- </div>
- </div>
- {/* <div>
- <Label className="text-sm font-medium text-muted-foreground">계약구분</Label>
- <div className="mt-1">{contractTypeLabels[biddingDetail.contractType]}</div>
- </div>
- <div>
- <Label className="text-sm font-medium text-muted-foreground">입찰유형</Label>
- <div className="mt-1">{biddingTypeLabels[biddingDetail.biddingType]}</div>
- </div>
- <div>
- <Label className="text-sm font-medium text-muted-foreground">낙찰수</Label>
- <div className="mt-1">{biddingDetail.awardCount === 'single' ? '단수' : '복수'}</div>
- </div> */}
- <div>
- <Label className="text-sm font-medium text-muted-foreground">담당자</Label>
- <div className="flex items-center gap-2 mt-1">
- <User className="w-4 h-4" />
- <span>{biddingDetail.managerName}</span>
- </div>
- </div>
- </div>
-
- {/* {biddingDetail.budget && (
- <div>
- <Label className="text-sm font-medium text-muted-foreground">예산</Label>
- <div className="flex items-center gap-2 mt-1">
- <DollarSign className="w-4 h-4" />
- <span className="font-semibold">{formatCurrency(biddingDetail.budget)}</span>
- </div>
- </div>
- )} */}
-
- {/* 일정 정보 */}
- {/* <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 && (
- <div>
- <span className="font-medium">제출기간:</span> {formatDate(biddingDetail.submissionStartDate, 'KR')} ~ {formatDate(biddingDetail.submissionEndDate, 'KR')}
- </div>
- )}
- {biddingDetail.evaluationDate && (
- <div>
- <span className="font-medium">평가일:</span> {formatDate(biddingDetail.evaluationDate, 'KR')}
- </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>
-
- {/* 현재 설정된 조건 섹션 */}
- {biddingConditions && (
- <Card>
- <CardHeader>
- <CardTitle>현재 설정된 입찰 조건</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
- <div>
- <Label className="text-muted-foreground">지급조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.paymentTerms || "미설정"}</p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">세금조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">
- {biddingConditions.taxConditions
- ? getTaxConditionName(biddingConditions.taxConditions)
- : "미설정"
- }
- </p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">운송조건</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.incoterms || "미설정"}</p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">계약 납기일</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">
- {biddingConditions.contractDeliveryDate
- ? formatDate(biddingConditions.contractDeliveryDate, 'KR')
- : "미설정"
- }
- </p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">선적지</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.shippingPort || "미설정"}</p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">하역지</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.destinationPort || "미설정"}</p>
- </div>
- </div>
-
- <div>
- <Label className="text-muted-foreground">연동제 적용</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.isPriceAdjustmentApplicable ? "적용 가능" : "적용 불가"}</p>
- </div>
- </div>
-
-
- <div >
- <Label className="text-muted-foreground">스페어파트 옵션</Label>
- <div className="mt-1 p-3 bg-muted rounded-md">
- <p className="font-medium">{biddingConditions.sparePartOptions}</p>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- )}
-
- {/* 사전견적 참여의사 결정 섹션 */}
- <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}
- />
-
- {/* 사전견적 폼 섹션 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Send className="w-5 h-5" />
- 사전견적 제출하기
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
- {/* 총 금액 표시 (읽기 전용) */}
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="totalAmount">총 사전견적 금액 <span className="text-red-500">*</span></Label>
- <Input
- id="totalAmount"
- type="text"
- value={new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: biddingDetail?.currency || 'KRW',
- }).format(totalAmount)}
- readOnly
- className="bg-gray-50 font-semibold text-primary"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="proposedContractDeliveryDate">제안 납품일 <span className="text-red-500">*</span></Label>
- <Input
- id="proposedContractDeliveryDate"
- type="date"
- value={responseData.proposedContractDeliveryDate}
- onChange={(e) => setResponseData({...responseData, proposedContractDeliveryDate: e.target.value})}
- title={biddingConditions?.contractDeliveryDate ? `참고 납기일: ${formatDate(biddingConditions.contractDeliveryDate, 'KR')}` : "납품일을 선택하세요"}
- />
- {biddingConditions?.contractDeliveryDate && (
- <p className="text-xs text-muted-foreground">
- 참고 납기일: {formatDate(biddingConditions.contractDeliveryDate, 'KR')}
- </p>
- )}
- </div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="paymentTermsResponse">응답 지급조건 <span className="text-red-500">*</span></Label>
- <Select
- value={responseData.paymentTermsResponse}
- onValueChange={(value) => setResponseData({...responseData, paymentTermsResponse: value})}
- >
- <SelectTrigger>
- <SelectValue placeholder={biddingConditions?.paymentTerms ? `참고: ${biddingConditions.paymentTerms}` : "지급조건 선택"} />
- </SelectTrigger>
- <SelectContent>
- {paymentTermsOptions.length > 0 ? (
- paymentTermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="taxConditionsResponse">응답 세금조건 <span className="text-red-500">*</span></Label>
- <Select
- value={responseData.taxConditionsResponse}
- onValueChange={(value) => setResponseData({...responseData, taxConditionsResponse: value})}
- >
- <SelectTrigger>
- <SelectValue placeholder={biddingConditions?.taxConditions ? `참고: ${getTaxConditionName(biddingConditions.taxConditions)}` : "세금조건 선택"} />
- </SelectTrigger>
- <SelectContent>
- {TAX_CONDITIONS.map((condition) => (
- <SelectItem key={condition.code} value={condition.code}>
- {condition.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="incotermsResponse">응답 운송조건 <span className="text-red-500">*</span></Label>
- <Select
- value={responseData.incotermsResponse}
- onValueChange={(value) => setResponseData({...responseData, incotermsResponse: value})}
- >
- <SelectTrigger>
- <SelectValue placeholder={biddingConditions?.incoterms ? `참고: ${biddingConditions.incoterms}` : "운송조건 선택"} />
- </SelectTrigger>
- <SelectContent>
- {incotermsOptions.length > 0 ? (
- incotermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="proposedShippingPort">제안 선적지 <span className="text-red-500">*</span></Label>
- <Select
- value={responseData.proposedShippingPort}
- onValueChange={(value) => setResponseData({...responseData, proposedShippingPort: value})}
- >
- <SelectTrigger>
- <SelectValue placeholder={biddingConditions?.shippingPort ? `참고: ${biddingConditions.shippingPort}` : "선적지 선택"} />
- </SelectTrigger>
- <SelectContent>
- {shippingPlaces.length > 0 ? (
- shippingPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-2">
- <Label htmlFor="proposedDestinationPort">제안 하역지 <span className="text-red-500">*</span></Label>
- <Select
- value={responseData.proposedDestinationPort}
- onValueChange={(value) => setResponseData({...responseData, proposedDestinationPort: value})}
- >
- <SelectTrigger>
- <SelectValue placeholder={biddingConditions?.destinationPort ? `참고: ${biddingConditions.destinationPort}` : "하역지 선택"} />
- </SelectTrigger>
- <SelectContent>
- {destinationPlaces.length > 0 ? (
- destinationPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="sparePartResponse">스페어파트 응답 <span className="text-red-500">*</span></Label>
- <Input
- id="sparePartResponse"
- value={responseData.sparePartResponse}
- onChange={(e) => setResponseData({...responseData, sparePartResponse: e.target.value})}
- placeholder={biddingConditions?.sparePartOptions ? `참고: ${biddingConditions.sparePartOptions}` : "스페어파트 관련 응답을 입력하세요"}
- />
- </div>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="additionalProposals">변경사유</Label>
- <Textarea
- id="additionalProposals"
- value={responseData.additionalProposals}
- onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})}
- placeholder="변경사유를 입력하세요"
- rows={4}
- />
- </div>
-
- <div className="space-y-4">
- <div className="flex items-center space-x-2">
- <Checkbox
- id="isInitialResponse"
- checked={responseData.isInitialResponse}
- onCheckedChange={(checked) =>
- setResponseData({...responseData, isInitialResponse: !!checked})
- }
- />
- <Label htmlFor="isInitialResponse">초도 공급입니다</Label>
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id="priceAdjustmentResponse"
- checked={responseData.priceAdjustmentResponse}
- onCheckedChange={(checked) =>
- setResponseData({...responseData, priceAdjustmentResponse: !!checked})
- }
- />
- <Label htmlFor="priceAdjustmentResponse">연동제 적용에 동의합니다</Label>
- </div>
- </div>
-
- {/* 연동제 상세 정보 (연동제 적용 시에만 표시) */}
- {responseData.priceAdjustmentResponse && (
- <Card className="mt-6">
- <CardHeader>
- <CardTitle className="text-lg">하도급대금등 연동표</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-2">
- <Label htmlFor="itemName">품목등의 명칭</Label>
- <Input
- id="itemName"
- value={priceAdjustmentForm.itemName}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, itemName: e.target.value})}
- placeholder="품목명을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentReflectionPoint">조정대금 반영시점</Label>
- <Input
- id="adjustmentReflectionPoint"
- value={priceAdjustmentForm.adjustmentReflectionPoint}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentReflectionPoint: e.target.value})}
- placeholder="반영시점을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentRatio">연동 비율 (%)</Label>
- <Input
- id="adjustmentRatio"
- type="number"
- step="0.01"
- value={priceAdjustmentForm.adjustmentRatio}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentRatio: e.target.value})}
- placeholder="비율을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentPeriod">조정주기</Label>
- <Input
- id="adjustmentPeriod"
- value={priceAdjustmentForm.adjustmentPeriod}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentPeriod: e.target.value})}
- placeholder="조정주기를 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="referenceDate">기준시점</Label>
- <Input
- id="referenceDate"
- type="date"
- value={priceAdjustmentForm.referenceDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, referenceDate: e.target.value})}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="comparisonDate">비교시점</Label>
- <Input
- id="comparisonDate"
- type="date"
- value={priceAdjustmentForm.comparisonDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, comparisonDate: e.target.value})}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="contractorWriter">수탁기업(협력사) 작성자</Label>
- <Input
- id="contractorWriter"
- value={priceAdjustmentForm.contractorWriter}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, contractorWriter: e.target.value})}
- placeholder="작성자명을 입력하세요"
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentDate">조정일</Label>
- <Input
- id="adjustmentDate"
- type="date"
- value={priceAdjustmentForm.adjustmentDate}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentDate: e.target.value})}
- />
- </div>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="majorApplicableRawMaterial">연동대상 주요 원재료</Label>
- <Textarea
- id="majorApplicableRawMaterial"
- value={priceAdjustmentForm.majorApplicableRawMaterial}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorApplicableRawMaterial: e.target.value})}
- placeholder="연동 대상 원재료를 입력하세요"
- rows={3}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentFormula">하도급대금등 연동 산식</Label>
- <Textarea
- id="adjustmentFormula"
- value={priceAdjustmentForm.adjustmentFormula}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentFormula: e.target.value})}
- placeholder="연동 산식을 입력하세요"
- rows={3}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="rawMaterialPriceIndex">원재료 가격 기준지표</Label>
- <Textarea
- id="rawMaterialPriceIndex"
- value={priceAdjustmentForm.rawMaterialPriceIndex}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, rawMaterialPriceIndex: e.target.value})}
- placeholder="가격 기준지표를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="adjustmentConditions">조정요건</Label>
- <Textarea
- id="adjustmentConditions"
- value={priceAdjustmentForm.adjustmentConditions}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, adjustmentConditions: e.target.value})}
- placeholder="조정요건을 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="majorNonApplicableRawMaterial">연동 미적용 주요 원재료</Label>
- <Textarea
- id="majorNonApplicableRawMaterial"
- value={priceAdjustmentForm.majorNonApplicableRawMaterial}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, majorNonApplicableRawMaterial: e.target.value})}
- placeholder="연동 미적용 원재료를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="nonApplicableReason">연동 미적용 사유</Label>
- <Textarea
- id="nonApplicableReason"
- value={priceAdjustmentForm.nonApplicableReason}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, nonApplicableReason: e.target.value})}
- placeholder="미적용 사유를 입력하세요"
- rows={2}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="priceAdjustmentNotes">기타 사항</Label>
- <Textarea
- id="priceAdjustmentNotes"
- value={priceAdjustmentForm.notes}
- onChange={(e) => setPriceAdjustmentForm({...priceAdjustmentForm, notes: e.target.value})}
- placeholder="기타 사항을 입력하세요"
- rows={2}
- />
- </div>
- </CardContent>
- </Card>
- )}
-
- <div className="flex justify-end gap-2 pt-4">
- <Button
- variant="outline"
- onClick={handleTempSave}
- disabled={isSaving || isPending || (biddingDetail && !['request_for_quotation', 'received_quotation'].includes(biddingDetail.status))}
- >
- <Save className="w-4 h-4 mr-2" />
- {isSaving ? '저장중...' : '임시저장'}
- </Button>
- <Button
- onClick={handleSubmitResponse}
- disabled={isPending || isSaving || (biddingDetail && !['request_for_quotation', 'received_quotation'].includes(biddingDetail.status))}
- >
- <Send className="w-4 h-4 mr-2" />
- 사전견적 제출
- </Button>
- </div>
- </CardContent>
- </Card>
- </>
- )}
- </div>
- )
-}