From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/general-contract-basic-info.tsx | 1250 ++++++++++++++++++++ 1 file changed, 1250 insertions(+) create mode 100644 lib/general-contracts_old/detail/general-contract-basic-info.tsx (limited to 'lib/general-contracts_old/detail/general-contract-basic-info.tsx') diff --git a/lib/general-contracts_old/detail/general-contract-basic-info.tsx b/lib/general-contracts_old/detail/general-contract-basic-info.tsx new file mode 100644 index 00000000..d891fe63 --- /dev/null +++ b/lib/general-contracts_old/detail/general-contract-basic-info.tsx @@ -0,0 +1,1250 @@ +'use client' + +import React, { useState } from 'react' +import { useSession } from 'next-auth/react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { Button } from '@/components/ui/button' +import { Save, LoaderIcon } from 'lucide-react' +import { updateContractBasicInfo, getContractBasicInfo } from '../service' +import { toast } from 'sonner' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { GeneralContract } from '@/db/schema' +import { ContractDocuments } from './general-contract-documents' +import { getPaymentTermsForSelection, getIncotermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from '@/lib/procurement-select/service' +import { TAX_CONDITIONS, getTaxConditionName } from '@/lib/tax-conditions/types' + +interface ContractBasicInfoProps { + contractId: number +} + +export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { + const session = useSession() + const [isLoading, setIsLoading] = useState(false) + const [contract, setContract] = useState(null) + const userId = session.data?.user?.id ? Number(session.data.user.id) : null + + // 독립적인 상태 관리 + const [paymentDeliveryPercent, setPaymentDeliveryPercent] = useState('') + + // Procurement 데이터 상태들 + const [paymentTermsOptions, setPaymentTermsOptions] = useState>([]) + const [incotermsOptions, setIncotermsOptions] = useState>([]) + const [shippingPlaces, setShippingPlaces] = useState>([]) + const [destinationPlaces, setDestinationPlaces] = useState>([]) + const [procurementLoading, setProcurementLoading] = useState(false) + + const [formData, setFormData] = useState({ + specificationType: '', + specificationManualText: '', + unitPriceType: '', + warrantyPeriod: { + 납품후: { enabled: false, period: 0, maxPeriod: 0 }, + 인도후: { enabled: false, period: 0, maxPeriod: 0 }, + 작업후: { enabled: false, period: 0, maxPeriod: 0 }, + 기타: { enabled: false, period: 0, maxPeriod: 0 }, + }, + contractAmount: null as number | null, + currency: 'KRW', + linkedPoNumber: '', + linkedBidNumber: '', + notes: '', + // 개별 JSON 필드들 (스키마에 맞게) + paymentBeforeDelivery: {} as any, + paymentDelivery: '', // varchar 타입 + paymentAfterDelivery: {} as any, + paymentTerm: '', + taxType: '', + liquidatedDamages: false as boolean, + liquidatedDamagesPercent: '', + deliveryType: '', + deliveryTerm: '', + shippingLocation: '', + dischargeLocation: '', + contractDeliveryDate: '', + contractEstablishmentConditions: { + regularVendorRegistration: false, + projectAward: false, + ownerApproval: false, + other: false, + }, + interlockingSystem: '', + mandatoryDocuments: { + technicalDataAgreement: false, + nda: false, + basicCompliance: false, + safetyHealthAgreement: false, + }, + contractTerminationConditions: { + standardTermination: false, + projectNotAwarded: false, + other: false, + }, + }) + + const [errors] = useState>({}) + + // 계약 데이터 로드 + React.useEffect(() => { + const loadContract = async () => { + try { + console.log('Loading contract with ID:', contractId) + const contractData = await getContractBasicInfo(contractId) + console.log('Contract data received:', contractData) + setContract(contractData as GeneralContract) + + // JSON 필드들 파싱 (null 체크) - 스키마에 맞게 개별 필드로 접근 + const paymentBeforeDelivery = (contractData?.paymentBeforeDelivery && typeof contractData.paymentBeforeDelivery === 'object') ? contractData.paymentBeforeDelivery as any : {} + const paymentAfterDelivery = (contractData?.paymentAfterDelivery && typeof contractData.paymentAfterDelivery === 'object') ? contractData.paymentAfterDelivery as any : {} + const warrantyPeriod = (contractData?.warrantyPeriod && typeof contractData.warrantyPeriod === 'object') ? contractData.warrantyPeriod as any : {} + const contractEstablishmentConditions = (contractData?.contractEstablishmentConditions && typeof contractData.contractEstablishmentConditions === 'object') ? contractData.contractEstablishmentConditions as any : {} + const mandatoryDocuments = (contractData?.mandatoryDocuments && typeof contractData.mandatoryDocuments === 'object') ? contractData.mandatoryDocuments as any : {} + const contractTerminationConditions = (contractData?.contractTerminationConditions && typeof contractData.contractTerminationConditions === 'object') ? contractData.contractTerminationConditions as any : {} + + // paymentDelivery에서 퍼센트와 타입 분리 + const paymentDeliveryValue = contractData?.paymentDelivery || '' + let paymentDeliveryType = '' + let paymentDeliveryPercentValue = '' + + if (paymentDeliveryValue.includes('%')) { + const match = paymentDeliveryValue.match(/(\d+)%\s*(.+)/) + if (match) { + paymentDeliveryPercentValue = match[1] + paymentDeliveryType = match[2] + } + } else { + paymentDeliveryType = paymentDeliveryValue + } + + setPaymentDeliveryPercent(paymentDeliveryPercentValue) + + setFormData({ + specificationType: contractData?.specificationType || '', + specificationManualText: contractData?.specificationManualText || '', + unitPriceType: contractData?.unitPriceType || '', + warrantyPeriod: warrantyPeriod || { + 납품후: { enabled: false, period: 0, maxPeriod: 0 }, + 인도후: { enabled: false, period: 0, maxPeriod: 0 }, + 작업후: { enabled: false, period: 0, maxPeriod: 0 }, + 기타: { enabled: false, period: 0, maxPeriod: 0 }, + }, + contractAmount: contractData?.contractAmount || null, + currency: contractData?.currency || 'KRW', + linkedPoNumber: contractData?.linkedPoNumber || '', + linkedBidNumber: contractData?.linkedBidNumber || '', + notes: contractData?.notes || '', + // 개별 JSON 필드들 + paymentBeforeDelivery: paymentBeforeDelivery || {} as any, + paymentDelivery: paymentDeliveryType, // 분리된 타입만 저장 + paymentAfterDelivery: paymentAfterDelivery || {} as any, + paymentTerm: contractData?.paymentTerm || '', + taxType: contractData?.taxType || '', + liquidatedDamages: Boolean(contractData?.liquidatedDamages), + liquidatedDamagesPercent: contractData?.liquidatedDamagesPercent || '', + deliveryType: contractData?.deliveryType || '', + deliveryTerm: contractData?.deliveryTerm || '', + shippingLocation: contractData?.shippingLocation || '', + dischargeLocation: contractData?.dischargeLocation || '', + contractDeliveryDate: contractData?.contractDeliveryDate || '', + contractEstablishmentConditions: contractEstablishmentConditions || { + regularVendorRegistration: false, + projectAward: false, + ownerApproval: false, + other: false, + }, + interlockingSystem: contractData?.interlockingSystem || '', + mandatoryDocuments: mandatoryDocuments || { + technicalDataAgreement: false, + nda: false, + basicCompliance: false, + safetyHealthAgreement: false, + }, + contractTerminationConditions: contractTerminationConditions || { + standardTermination: false, + projectNotAwarded: false, + other: false, + }, + }) + } catch (error) { + console.error('Error loading contract:', error) + toast.error('계약 정보를 불러오는 중 오류가 발생했습니다.') + } + } + + if (contractId) { + loadContract() + } + }, [contractId]) + + // Procurement 데이터 로드 함수들 + const loadPaymentTerms = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPaymentTermsForSelection(); + setPaymentTermsOptions(data); + } catch (error) { + console.error("Failed to load payment terms:", error); + toast.error("결제조건 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadIncoterms = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getIncotermsForSelection(); + setIncotermsOptions(data); + } catch (error) { + console.error("Failed to load incoterms:", error); + toast.error("운송조건 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadShippingPlaces = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPlaceOfShippingForSelection(); + setShippingPlaces(data); + } catch (error) { + console.error("Failed to load shipping places:", error); + toast.error("선적지 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadDestinationPlaces = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPlaceOfDestinationForSelection(); + setDestinationPlaces(data); + } catch (error) { + console.error("Failed to load destination places:", error); + toast.error("하역지 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + // 컴포넌트 마운트 시 procurement 데이터 로드 + React.useEffect(() => { + loadPaymentTerms(); + loadIncoterms(); + loadShippingPlaces(); + loadDestinationPlaces(); + }, [loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]); + const handleSaveContractInfo = async () => { + if (!userId) { + toast.error('사용자 정보를 찾을 수 없습니다.') + return + } + try { + setIsLoading(true) + + // 필수값 validation 체크 + const validationErrors: string[] = [] + if (!formData.specificationType) validationErrors.push('사양') + if (!formData.paymentDelivery) validationErrors.push('납품 지급조건') + if (!formData.currency) validationErrors.push('계약통화') + if (!formData.paymentTerm) validationErrors.push('지불조건') + if (!formData.taxType) validationErrors.push('세금조건') + + if (validationErrors.length > 0) { + toast.error(`다음 필수 항목을 입력해주세요: ${validationErrors.join(', ')}`) + return + } + + // paymentDelivery와 paymentDeliveryPercent 합쳐서 저장 + const dataToSave = { + ...formData, + paymentDelivery: (formData.paymentDelivery === 'L/C' || formData.paymentDelivery === 'T/T') && paymentDeliveryPercent + ? `${paymentDeliveryPercent}% ${formData.paymentDelivery}` + : formData.paymentDelivery + } + + await updateContractBasicInfo(contractId, dataToSave, userId as number) + toast.success('계약 정보가 저장되었습니다.') + } catch (error) { + console.error('Error saving contract info:', error) + toast.error('계약 정보 저장 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + } + + return ( + + + 계약 기본 정보 + + + + + 기본 정보 + 지급/인도 조건 + 추가 조건 + 계약첨부문서 + + + {/* 기본 정보 탭 */} + + + {/* 보증기간 및 단가유형 */} + + 보증기간 및 단가유형 + + + {/* 3그리드: 보증기간, 사양, 단가 */} +
+ {/* 보증기간 */} +
+ +
+
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.납품후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.인도후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.작업후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 기타: { + ...prev.warrantyPeriod.기타, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+
+
+ {/* 사양 */} +
+ + + {errors.specificationType && ( +

사양은 필수값입니다.

+ )} +
+ {/* 단가 */} +
+ + +
+ {/* 선택에 따른 폼: vertical로 출력 */} + + + {/* 사양이 수기사양일 때 매뉴얼 텍스트 */} + {formData.specificationType === '수기사양' && ( +
+ +