diff options
Diffstat (limited to 'lib/bidding/vendor')
| -rw-r--r-- | lib/bidding/vendor/components/pr-items-pricing-table.tsx | 23 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-detail.tsx | 821 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-list.tsx | 13 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-pre-quote.tsx | 4 |
4 files changed, 405 insertions, 456 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index 01885f7a..1dee7adb 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -39,6 +39,8 @@ interface PrItem { materialDescription: string | null quantity: string | null quantityUnit: string | null + totalWeight: string | null + weightUnit: string | null currency: string | null requestedDeliveryDate: string | null hasSpecDocument: boolean | null @@ -221,11 +223,20 @@ export function PrItemsPricingTable({ if (q.prItemId === prItemId) { const updated = { ...q, [field]: value } - // 단가나 수량이 변경되면 금액 자동 계산 + // 단가가 변경되면 금액 자동 계산 (수량 우선, 없으면 중량 사용) if (field === 'bidUnitPrice') { const prItem = prItems.find(item => item.id === prItemId) - const quantity = parseFloat(prItem?.quantity || '1') - updated.bidAmount = updated.bidUnitPrice * quantity + let multiplier = 1 + + if (prItem?.quantity && parseFloat(prItem.quantity) > 0) { + // 수량이 있으면 수량 기준 + multiplier = parseFloat(prItem.quantity) + } else if (prItem?.totalWeight && parseFloat(prItem.totalWeight) > 0) { + // 수량이 없으면 중량 기준 + multiplier = parseFloat(prItem.totalWeight) + } + + updated.bidAmount = updated.bidUnitPrice * multiplier } return updated @@ -273,6 +284,8 @@ export function PrItemsPricingTable({ <TableHead>자재내역</TableHead> <TableHead>수량</TableHead> <TableHead>단위</TableHead> + <TableHead>중량</TableHead> + <TableHead>중량단위</TableHead> <TableHead>견적단가</TableHead> <TableHead>견적금액</TableHead> <TableHead>납품예정일</TableHead> @@ -310,6 +323,10 @@ export function PrItemsPricingTable({ {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} </TableCell> <TableCell>{item.quantityUnit || '-'}</TableCell> + <TableCell className="text-right"> + {item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'} + </TableCell> + <TableCell>{item.weightUnit || '-'}</TableCell> <TableCell> {readOnly ? ( <span className="font-medium"> diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx index 1e6ae479..8d24ca66 100644 --- a/lib/bidding/vendor/partners-bidding-detail.tsx +++ b/lib/bidding/vendor/partners-bidding-detail.tsx @@ -20,15 +20,21 @@ import { Users, Send, CheckCircle, - XCircle + XCircle, + Save } from 'lucide-react' import { formatDate } from '@/lib/utils' import { getBiddingDetailsForPartners, submitPartnerResponse, - updatePartnerAttendance + updatePartnerAttendance, + updatePartnerBiddingParticipation, + saveBiddingDraft } from '../detail/service' +import { getPrItemsForBidding, getSavedPrItemQuotations } from '@/lib/bidding/pre-quote/service' +import { PrItemsPricingTable } from './components/pr-items-pricing-table' +import { SimpleFileUpload } from './components/simple-file-upload' import { biddingStatusLabels, contractTypeLabels, @@ -36,6 +42,7 @@ import { } from '@/db/schema' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' +import { useSession } from 'next-auth/react' interface PartnersBiddingDetailProps { biddingId: number @@ -45,115 +52,144 @@ interface PartnersBiddingDetailProps { interface BiddingDetail { id: number biddingNumber: string - revision: number - projectName: string - itemName: string + revision: number | null + projectName: string | null + itemName: string | null title: string - description: string - content: string + description: string | null + content: string | null contractType: string biddingType: string awardCount: string - contractPeriod: string - preQuoteDate: string - biddingRegistrationDate: string - submissionStartDate: string - submissionEndDate: string - evaluationDate: string + contractPeriod: string | null + preQuoteDate: string | null + biddingRegistrationDate: string | null + submissionStartDate: string | null + submissionEndDate: string | null + evaluationDate: string | null currency: string - budget: number - targetPrice: number + budget: number | null + targetPrice: number | null status: string - managerName: string - managerEmail: string - managerPhone: string + managerName: string | null + managerEmail: string | null + managerPhone: string | null biddingCompanyId: number - biddingId: number // bidding의 ID 추가 + biddingId: number invitationStatus: string - finalQuoteAmount: number - finalQuoteSubmittedAt: string + finalQuoteAmount: number | null + finalQuoteSubmittedAt: string | null isWinner: boolean isAttendingMeeting: boolean | null - // companyConditionResponses에서 가져온 조건들 (제시된 조건과 응답 모두) - paymentTermsResponse: string - taxConditionsResponse: string - incotermsResponse: string - proposedContractDeliveryDate: string - proposedShippingPort: string - proposedDestinationPort: string - priceAdjustmentResponse: boolean - sparePartResponse: string - additionalProposals: string - responseSubmittedAt: string + isBiddingParticipated: boolean | null + additionalProposals: string | null + responseSubmittedAt: string | null +} + +interface PrItem { + id: number + itemNumber: string | null + prNumber: string | null + itemInfo: string | null + materialDescription: string | null + quantity: string | null + quantityUnit: string | null + totalWeight: string | null + weightUnit: string | null + currency: string | null + requestedDeliveryDate: string | null + hasSpecDocument: boolean | null +} + +interface PrItemQuotation { + prItemId: number + bidUnitPrice: number + bidAmount: number + proposedDeliveryDate?: string | null + technicalSpecification?: string } export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingDetailProps) { const router = useRouter() const { toast } = useToast() + const session = useSession() const [isPending, startTransition] = useTransition() const [biddingDetail, setBiddingDetail] = React.useState<BiddingDetail | null>(null) const [isLoading, setIsLoading] = React.useState(true) + const [isUpdatingParticipation, setIsUpdatingParticipation] = React.useState(false) + const [isSavingDraft, setIsSavingDraft] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + + // 품목별 견적 관련 상태 + const [prItems, setPrItems] = React.useState<PrItem[]>([]) + const [prItemQuotations, setPrItemQuotations] = React.useState<PrItemQuotation[]>([]) + const [totalQuotationAmount, setTotalQuotationAmount] = React.useState(0) // 응찰 폼 상태 const [responseData, setResponseData] = React.useState({ finalQuoteAmount: '', - paymentTermsResponse: '', - taxConditionsResponse: '', - incotermsResponse: '', proposedContractDeliveryDate: '', - proposedShippingPort: '', - proposedDestinationPort: '', - priceAdjustmentResponse: false, - isInitialResponse: false, - sparePartResponse: '', additionalProposals: '', - isAttendingMeeting: false, }) + const userId = session.data?.user?.id || '' - // 연동제 폼 상태 - const [priceAdjustmentForm, setPriceAdjustmentForm] = React.useState({ - itemName: '', - adjustmentReflectionPoint: '', - majorApplicableRawMaterial: '', - adjustmentFormula: '', - rawMaterialPriceIndex: '', - referenceDate: '', - comparisonDate: '', - adjustmentRatio: '', - notes: '', - adjustmentConditions: '', - majorNonApplicableRawMaterial: '', - adjustmentPeriod: '', - contractorWriter: '', - adjustmentDate: '', - nonApplicableReason: '', - }) // 데이터 로드 React.useEffect(() => { const loadData = async () => { try { setIsLoading(true) - const result = await getBiddingDetailsForPartners(biddingId, companyId) + const [result, prItemsResult] = await Promise.all([ + getBiddingDetailsForPartners(biddingId, companyId), + getPrItemsForBidding(biddingId) + ]) + if (result) { setBiddingDetail(result) // 기존 응답 데이터로 폼 초기화 setResponseData({ finalQuoteAmount: result.finalQuoteAmount?.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, }) } + + // PR 아이템 설정 + setPrItems(prItemsResult) + + // 사전견적 데이터를 본입찰용으로 로드 (응찰 확정 시 또는 사전견적이 있는 경우) + if (result?.biddingCompanyId) { + try { + // 사전견적 데이터를 가져와서 본입찰용으로 변환 + const preQuoteData = await getSavedPrItemQuotations(result.biddingCompanyId) + + // 사전견적 데이터를 본입찰 포맷으로 변환 + const convertedQuotations = preQuoteData.map(item => ({ + prItemId: item.prItemId, + bidUnitPrice: item.bidUnitPrice, + bidAmount: item.bidAmount, + proposedDeliveryDate: item.proposedDeliveryDate || '', + technicalSpecification: item.technicalSpecification || undefined + })) + + setPrItemQuotations(convertedQuotations) + + // 총 금액 계산 + const total = convertedQuotations.reduce((sum, q) => sum + q.bidAmount, 0) + setTotalQuotationAmount(total) + + // 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정 + if (total > 0 && result.isBiddingParticipated === true) { + setResponseData(prev => ({ + ...prev, + finalQuoteAmount: total.toString() + })) + } + } catch (error) { + console.error('Failed to load pre-quote data:', error) + } + } } catch (error) { console.error('Failed to load bidding detail:', error) toast({ @@ -169,53 +205,16 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD loadData() }, [biddingId, companyId, toast]) - const handleSubmitResponse = () => { + // 입찰 참여여부 결정 핸들러 + const handleParticipationDecision = async (participated: boolean) => { if (!biddingDetail) return - // 필수값 검증 - if (!responseData.finalQuoteAmount.trim()) { - toast({ - title: '유효성 오류', - description: '견적 금액을 입력해주세요.', - variant: 'destructive', - }) - return - } - - startTransition(async () => { - const result = await submitPartnerResponse( + setIsUpdatingParticipation(true) + try { + const result = await updatePartnerBiddingParticipation( biddingDetail.biddingCompanyId, - { - finalQuoteAmount: parseFloat(responseData.finalQuoteAmount), - 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 + participated, + userId ) if (result.success) { @@ -236,6 +235,169 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD variant: 'destructive', }) } + } catch (error) { + console.error('Failed to update participation:', error) + toast({ + title: '오류', + description: '참여여부 업데이트에 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsUpdatingParticipation(false) + } + } + + // 품목별 견적 변경 핸들러 + const handleQuotationsChange = (quotations: PrItemQuotation[]) => { + console.log('견적 변경:', quotations) + setPrItemQuotations(quotations) + } + + // 총 금액 변경 핸들러 + const handleTotalAmountChange = (total: number) => { + setTotalQuotationAmount(total) + // 자동으로 총 견적 금액도 업데이트 + setResponseData(prev => ({ + ...prev, + finalQuoteAmount: total.toString() + })) + } + + // 임시 저장 핸들러 + const handleSaveDraft = async () => { + if (!biddingDetail || !userId) return + + if (prItemQuotations.length === 0) { + toast({ + title: '저장할 데이터 없음', + description: '저장할 품목별 견적이 없습니다.', + variant: 'destructive', + }) + return + } + + setIsSavingDraft(true) + try { + const quotationsForSave = prItemQuotations.map(q => ({ + prItemId: q.prItemId, + bidUnitPrice: q.bidUnitPrice, + bidAmount: q.bidAmount, + proposedDeliveryDate: q.proposedDeliveryDate || undefined, + technicalSpecification: q.technicalSpecification + })) + + console.log('임시저장 - prItemQuotations:', prItemQuotations) + console.log('임시저장 - quotationsForSave:', quotationsForSave) + + const result = await saveBiddingDraft( + biddingDetail.biddingCompanyId, + quotationsForSave, + userId + ) + + if (result.success) { + toast({ + title: '임시 저장 완료', + description: '품목별 견적이 임시 저장되었습니다.', + }) + } else { + toast({ + title: '임시 저장 실패', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + console.error('Failed to save draft:', error) + toast({ + title: '오류', + description: '임시 저장에 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsSavingDraft(false) + } + } + + const handleSubmitResponse = () => { + if (!biddingDetail) return + + // 필수값 검증 + if (!responseData.finalQuoteAmount.trim()) { + toast({ + title: '유효성 오류', + description: '견적 금액을 입력해주세요.', + variant: 'destructive', + }) + return + } + + // 품목별 견적이 있는지 확인 + if (prItems.length > 0 && prItemQuotations.length === 0) { + toast({ + title: '유효성 오류', + description: '품목별 견적을 작성해주세요.', + variant: 'destructive', + }) + return + } + + setIsSubmitting(true) + startTransition(async () => { + try { + // 1. 입찰 참여 상태를 응찰로 변경 + const participationResult = await updatePartnerBiddingParticipation( + biddingDetail.biddingCompanyId, + true, // 응찰 + userId + ) + + if (!participationResult.success) { + throw new Error(participationResult.error) + } + + // 2. 최종 견적 응답 제출 (PR 아이템별 견적 포함) + const result = await submitPartnerResponse( + biddingDetail.biddingCompanyId, + { + finalQuoteAmount: parseFloat(responseData.finalQuoteAmount), + proposedContractDeliveryDate: responseData.proposedContractDeliveryDate, + additionalProposals: responseData.additionalProposals, + prItemQuotations: prItemQuotations.length > 0 ? prItemQuotations.map(q => ({ + prItemId: q.prItemId, + bidUnitPrice: q.bidUnitPrice, + bidAmount: q.bidAmount, + proposedDeliveryDate: q.proposedDeliveryDate || undefined, + technicalSpecification: q.technicalSpecification + })) : undefined, + }, + userId + ) + + if (result.success) { + toast({ + title: '응찰 완료', + description: '견적이 성공적으로 제출되었습니다.', + }) + + // 데이터 새로고침 + const updatedDetail = await getBiddingDetailsForPartners(biddingId, companyId) + if (updatedDetail) { + setBiddingDetail(updatedDetail) + } + } else { + throw new Error(result.error) + } + } catch (error) { + console.error('Failed to submit response:', error) + toast({ + title: '오류', + description: error instanceof Error ? error.message : '응찰 제출에 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsSubmitting(false) + } }) } @@ -296,6 +458,28 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </div> </div> + {/* 입찰 참여여부 상태 표시 */} + <div className="flex items-center gap-2"> + {biddingDetail.isBiddingParticipated === null ? ( + <div className="flex items-center gap-2"> + <Badge variant="outline">참여 결정 대기</Badge> + <Button + onClick={() => handleParticipationDecision(false)} + disabled={isUpdatingParticipation} + variant="destructive" + size="sm" + > + <XCircle className="w-4 h-4 mr-1" /> + 미응찰 + </Button> + </div> + ) : ( + <Badge variant={biddingDetail.isBiddingParticipated ? 'default' : 'destructive'}> + {biddingDetail.isBiddingParticipated ? '응찰' : '미응찰'} + </Badge> + )} + </div> + </div> {/* 입찰 공고 섹션 */} @@ -312,14 +496,14 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <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> + <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> + <span>{biddingDetail.itemName || '미설정'}</span> </div> </div> <div> @@ -338,7 +522,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <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.managerName || '미설정'}</span> </div> </div> </div> @@ -372,73 +556,29 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </CardContent> </Card> - {/* 현재 설정된 조건 섹션 */} + + {/* 참여 상태에 따른 섹션 표시 */} + {biddingDetail.isBiddingParticipated === false ? ( + /* 미응찰 상태 표시 */ <Card> <CardHeader> - <CardTitle>현재 설정된 입찰 조건</CardTitle> + <CardTitle className="flex items-center gap-2"> + <XCircle className="w-5 h-5 text-destructive" /> + 입찰 참여 거절 + </CardTitle> </CardHeader> <CardContent> - <div className="grid grid-cols-2 gap-4"> - <div> - <Label className="text-sm font-medium">지급조건</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.paymentTermsResponse} - </div> - </div> - - <div> - <Label className="text-sm font-medium">세금조건</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.taxConditionsResponse} - </div> - </div> - - <div> - <Label className="text-sm font-medium">운송조건</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.incotermsResponse} - </div> - </div> - - <div> - <Label className="text-sm font-medium">제안 계약납기일</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.proposedContractDeliveryDate ? formatDate(biddingDetail.proposedContractDeliveryDate, 'KR') : '미설정'} - </div> - </div> - - <div> - <Label className="text-sm font-medium">제안 선적지</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.proposedShippingPort} - </div> - </div> - - <div> - <Label className="text-sm font-medium">제안 도착지</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.proposedDestinationPort} - </div> - </div> - - <div> - <Label className="text-sm font-medium">스페어파트 응답</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.sparePartResponse} - </div> - </div> - - <div> - <Label className="text-sm font-medium">연동제 적용</Label> - <div className="mt-1 p-3 bg-muted rounded-md"> - {biddingDetail.priceAdjustmentResponse ? '적용' : '미적용'} - </div> - </div> + <div className="text-center py-8"> + <XCircle className="w-16 h-16 text-destructive mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-destructive mb-2">입찰에 참여하지 않기로 결정했습니다</h3> + <p className="text-muted-foreground"> + 해당 입찰에 대한 견적 제출 및 관련 기능은 이용할 수 없습니다. + </p> </div> </CardContent> </Card> - - {/* 응찰 폼 섹션 */} + ) : biddingDetail.isBiddingParticipated === true || biddingDetail.isBiddingParticipated === null ? ( + /* 응찰 폼 섹션 */ <Card> <CardHeader> <CardTitle className="flex items-center gap-2"> @@ -447,19 +587,19 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </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="finalQuoteAmount">견적금액 *</Label> + {/* 품목별 견적 섹션 */} + {/* <div className="space-y-2"> + <Label htmlFor="finalQuoteAmount">총 견적금액 *</Label> <Input id="finalQuoteAmount" type="number" value={responseData.finalQuoteAmount} onChange={(e) => setResponseData({...responseData, finalQuoteAmount: e.target.value})} - placeholder="견적금액을 입력하세요" + placeholder="총 견적금액을 입력하세요" /> - </div> + </div> */} - <div className="space-y-2"> + {/* <div className="space-y-2"> <Label htmlFor="proposedContractDeliveryDate">제안 납품일</Label> <Input id="proposedContractDeliveryDate" @@ -467,289 +607,68 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD value={responseData.proposedContractDeliveryDate} onChange={(e) => setResponseData({...responseData, proposedContractDeliveryDate: e.target.value})} /> + </div> */} + + {/* 품목별 상세 견적 테이블 */} + {prItems.length > 0 ? ( + <PrItemsPricingTable + prItems={prItems} + initialQuotations={prItemQuotations} + currency={biddingDetail?.currency || 'KRW'} + onQuotationsChange={handleQuotationsChange} + onTotalAmountChange={handleTotalAmountChange} + readOnly={false} + /> + ) : ( + <div className="border rounded-lg p-4 bg-muted/20"> + <p className="text-sm text-muted-foreground text-center py-4"> + 등록된 품목이 없습니다. + </p> </div> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - <div className="space-y-2"> - <Label htmlFor="paymentTermsResponse">응답 지급조건</Label> - <Input - id="paymentTermsResponse" - value={responseData.paymentTermsResponse} - onChange={(e) => setResponseData({...responseData, paymentTermsResponse: e.target.value})} - placeholder="지급조건에 대한 의견을 입력하세요" - /> - </div> - - <div className="space-y-2"> - <Label htmlFor="taxConditionsResponse">응답 세금조건</Label> - <Input - id="taxConditionsResponse" - value={responseData.taxConditionsResponse} - onChange={(e) => setResponseData({...responseData, taxConditionsResponse: e.target.value})} - placeholder="세금조건에 대한 의견을 입력하세요" - /> - </div> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - <div className="space-y-2"> - <Label htmlFor="incotermsResponse">응답 운송조건</Label> - <Input - id="incotermsResponse" - value={responseData.incotermsResponse} - onChange={(e) => setResponseData({...responseData, incotermsResponse: e.target.value})} - placeholder="운송조건에 대한 의견을 입력하세요" - /> - </div> - - <div className="space-y-2"> - <Label htmlFor="proposedShippingPort">제안 선적지</Label> - <Input - id="proposedShippingPort" - value={responseData.proposedShippingPort} - onChange={(e) => setResponseData({...responseData, proposedShippingPort: e.target.value})} - placeholder="선적지를 입력하세요" - /> - </div> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - <div className="space-y-2"> - <Label htmlFor="proposedDestinationPort">제안 도착지</Label> - <Input - id="proposedDestinationPort" - value={responseData.proposedDestinationPort} - onChange={(e) => setResponseData({...responseData, proposedDestinationPort: e.target.value})} - placeholder="도착지를 입력하세요" - /> - </div> + )} - <div className="space-y-2"> - <Label htmlFor="sparePartResponse">스페어파트 응답</Label> - <Input - id="sparePartResponse" - value={responseData.sparePartResponse} - onChange={(e) => setResponseData({...responseData, sparePartResponse: e.target.value})} - placeholder="스페어파트 관련 응답을 입력하세요" - /> - </div> - </div> + {/* 견적 첨부파일 섹션 */} + {biddingDetail && userId && ( + <SimpleFileUpload + biddingId={biddingId} + companyId={companyId} + userId={userId} + readOnly={false} + /> + )} - <div className="space-y-2"> - <Label htmlFor="additionalProposals">추가 제안사항</Label> + {/* 기타 사항 */} + {/* <div className="space-y-2"> + <Label htmlFor="additionalProposals">기타 사항</Label> <Textarea id="additionalProposals" value={responseData.additionalProposals} onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})} - placeholder="추가 제안사항을 입력하세요" + 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 pt-4"> - <Button onClick={handleSubmitResponse} disabled={isPending}> + </div> */} + {/* 응찰 제출 버튼 - 미응찰 상태가 아닐 때만 표시 */} + {(biddingDetail.isBiddingParticipated === true || biddingDetail.isBiddingParticipated === null) && ( + <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} className="min-w-[100px]"> <Send className="w-4 h-4 mr-2" /> - 응찰 제출 + {isSubmitting ? '제출 중...' : '응찰 제출'} </Button> </div> + )} </CardContent> </Card> + ) : null} </div> ) } diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx index 9f182911..2e8d4164 100644 --- a/lib/bidding/vendor/partners-bidding-list.tsx +++ b/lib/bidding/vendor/partners-bidding-list.tsx @@ -9,6 +9,7 @@ import type { } from '@/types/table' import { useDataTable } from '@/hooks/use-data-table' +import { useToast } from '@/hooks/use-toast' import { DataTable } from '@/components/data-table/data-table' import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar' import { getPartnersBiddingListColumns } from './partners-bidding-list-columns' @@ -32,6 +33,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) { const [selectedBiddingForPreQuoteParticipation, setSelectedBiddingForPreQuoteParticipation] = React.useState<any | null>(null) const router = useRouter() + const { toast } = useToast() // 데이터 새로고침 함수 const refreshData = React.useCallback(async () => { @@ -89,6 +91,17 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) { if (rowAction) { switch (rowAction.type) { case 'view': + // 본입찰 초대 여부 확인 + const bidding = rowAction.row.original + if (bidding.status === 'bidding_opened' && !bidding.isBiddingInvited) { + // 본입찰이 오픈되었지만 초대받지 않은 경우 + toast({ + title: '접근 제한', + description: '본입찰에 초대받지 않은 업체입니다.', + variant: 'destructive', + }) + return + } // 상세 페이지로 이동 (biddingId 사용) router.push(`/partners/bid/${rowAction.row.original.biddingId}`) break diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx index 94b76f58..4cd0efdb 100644 --- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx +++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx @@ -746,12 +746,12 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin </div> <div className="space-y-2"> - <Label htmlFor="additionalProposals">추가 제안사항</Label> + <Label htmlFor="additionalProposals">사유</Label> <Textarea id="additionalProposals" value={responseData.additionalProposals} onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})} - placeholder="추가 제안사항을 입력하세요" + placeholder="사유를 입력하세요" rows={4} /> </div> |
