From c8beed57d9fb10c02b8951cd4267017984ca5beb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 17 Sep 2025 10:41:29 +0000 Subject: (최겸) 구매 일반계약 프로젝트id추가, 선적지, 하역지 연동, numbering 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../general-contract-approval-request-dialog.tsx | 2 +- .../detail/general-contract-basic-info.tsx | 241 ++++++++++++++++----- .../main/create-general-contract-dialog.tsx | 156 +++++-------- .../main/general-contract-update-sheet.tsx | 98 ++++----- .../main/general-contracts-table-columns.tsx | 45 ++-- .../main/general-contracts-table.tsx | 17 +- lib/general-contracts/service.ts | 84 +++++-- lib/general-contracts/types.ts | 19 +- lib/general-contracts/validation.ts | 3 +- 9 files changed, 387 insertions(+), 278 deletions(-) (limited to 'lib/general-contracts') diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx index e4aa022a..f05fe9ef 100644 --- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx +++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx @@ -863,7 +863,7 @@ export function ContractApprovalRequestDialog({ 선적지: {String(contractSummary?.basicInfo?.shippingLocation || '')}
- 도착지: {String(contractSummary?.basicInfo?.dischargeLocation || '')} + 하역지: {String(contractSummary?.basicInfo?.dischargeLocation || '')}
계약납기: {String(contractSummary?.basicInfo?.contractDeliveryDate || '')} diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx index ac1315bb..882ed8b2 100644 --- a/lib/general-contracts/detail/general-contract-basic-info.tsx +++ b/lib/general-contracts/detail/general-contract-basic-info.tsx @@ -14,6 +14,7 @@ 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' interface ContractBasicInfoProps { contractId: number @@ -27,6 +28,13 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { // 독립적인 상태 관리 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: '', @@ -169,6 +177,67 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { 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('사용자 정보를 찾을 수 없습니다.') @@ -518,15 +587,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - paymentBeforeDelivery: { - ...prev.paymentBeforeDelivery, - apBondPercent: e.target.value - } + onChange={(e) => setFormData(prev => ({ + ...prev, + paymentBeforeDelivery: { + ...prev.paymentBeforeDelivery, + apBondPercent: e.target.value + } }))} disabled={!formData.paymentBeforeDelivery.apBond} /> @@ -548,15 +618,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - paymentBeforeDelivery: { - ...prev.paymentBeforeDelivery, - drawingSubmissionPercent: e.target.value - } + onChange={(e) => setFormData(prev => ({ + ...prev, + paymentBeforeDelivery: { + ...prev.paymentBeforeDelivery, + drawingSubmissionPercent: e.target.value + } }))} disabled={!formData.paymentBeforeDelivery.drawingSubmission} /> @@ -578,15 +649,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - paymentBeforeDelivery: { - ...prev.paymentBeforeDelivery, - materialPurchasePercent: e.target.value - } + onChange={(e) => setFormData(prev => ({ + ...prev, + paymentBeforeDelivery: { + ...prev.paymentBeforeDelivery, + materialPurchasePercent: e.target.value + } }))} disabled={!formData.paymentBeforeDelivery.materialPurchase} /> @@ -618,6 +690,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
setPaymentDeliveryPercent(e.target.value)} placeholder="퍼센트" @@ -654,15 +727,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - paymentAfterDelivery: { - ...prev.paymentAfterDelivery, - commissioningPercent: e.target.value - } + onChange={(e) => setFormData(prev => ({ + ...prev, + paymentAfterDelivery: { + ...prev.paymentAfterDelivery, + commissioningPercent: e.target.value + } }))} disabled={!formData.paymentAfterDelivery.commissioning} /> @@ -684,15 +758,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - paymentAfterDelivery: { - ...prev.paymentAfterDelivery, - finalDocumentPercent: e.target.value - } + onChange={(e) => setFormData(prev => ({ + ...prev, + paymentAfterDelivery: { + ...prev.paymentAfterDelivery, + finalDocumentPercent: e.target.value + } }))} disabled={!formData.paymentAfterDelivery.finalDocument} /> @@ -736,13 +811,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- setFormData(prev => ({ ...prev, paymentTerm: e.target.value }))} - placeholder="지불조건을 입력하세요" - className={errors.paymentTerm ? 'border-red-500' : ''} - /> + onValueChange={(value) => setFormData(prev => ({ ...prev, paymentTerm: value }))} + > + + + + + {paymentTermsOptions.length > 0 ? ( + paymentTermsOptions.map((option) => ( + + {option.code} {option.description && `(${option.description})`} + + )) + ) : ( + + 데이터를 불러오는 중... + + )} + + {errors.paymentTerm && (

지불조건은 필수값입니다.

)} @@ -781,12 +870,13 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { setFormData(prev => ({ - ...prev, - liquidatedDamagesPercent: e.target.value + onChange={(e) => setFormData(prev => ({ + ...prev, + liquidatedDamagesPercent: e.target.value }))} disabled={!formData.liquidatedDamages} /> @@ -823,12 +913,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- setFormData(prev => ({ ...prev, deliveryTerm: e.target.value }))} - placeholder="인도조건을 입력하세요" - /> + onValueChange={(value) => setFormData(prev => ({ ...prev, deliveryTerm: value }))} + > + + + + + {incotermsOptions.length > 0 ? ( + incotermsOptions.map((option) => ( + + {option.code} {option.description && `(${option.description})`} + + )) + ) : ( + + 데이터를 불러오는 중... + + )} + +
@@ -838,11 +943,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- setFormData(prev => ({ ...prev, shippingLocation: e.target.value }))} - placeholder="선적지를 입력하세요" - /> + onValueChange={(value) => setFormData(prev => ({ ...prev, shippingLocation: value }))} + > + + + + + {shippingPlaces.length > 0 ? ( + shippingPlaces.map((place) => ( + + {place.code} {place.description && `(${place.description})`} + + )) + ) : ( + + 데이터를 불러오는 중... + + )} + +
@@ -852,11 +973,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- setFormData(prev => ({ ...prev, dischargeLocation: e.target.value }))} - placeholder="하역지를 입력하세요" - /> + onValueChange={(value) => setFormData(prev => ({ ...prev, dischargeLocation: value }))} + > + + + + + {destinationPlaces.length > 0 ? ( + destinationPlaces.map((place) => ( + + {place.code} {place.description && `(${place.description})`} + + )) + ) : ( + + 데이터를 불러오는 중... + + )} + +
diff --git a/lib/general-contracts/main/create-general-contract-dialog.tsx b/lib/general-contracts/main/create-general-contract-dialog.tsx index 3eb8b11c..2c3fc8bc 100644 --- a/lib/general-contracts/main/create-general-contract-dialog.tsx +++ b/lib/general-contracts/main/create-general-contract-dialog.tsx @@ -24,7 +24,7 @@ import { CalendarIcon } from "lucide-react" import { format } from "date-fns" import { ko } from "date-fns/locale" import { cn } from "@/lib/utils" -import { createContract, getVendors } from "@/lib/general-contracts/service" +import { createContract, getVendors, getProjects } from "@/lib/general-contracts/service" import { GENERAL_CONTRACT_CATEGORIES, GENERAL_CONTRACT_TYPES, @@ -38,17 +38,12 @@ interface CreateContractForm { category: string type: string executionMethod: string - selectionMethod: string vendorId: number | null + projectId: number | null startDate: Date | undefined endDate: Date | undefined validityEndDate: Date | undefined - // contractScope: string - // specificationType: string notes: string - linkedRfqOrItb: string - linkedBidNumber: string - linkedPoNumber: string } export function CreateGeneralContractDialog() { @@ -57,24 +52,20 @@ export function CreateGeneralContractDialog() { const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) const [vendors, setVendors] = React.useState>([]) - + const [projects, setProjects] = React.useState>([]) + const [form, setForm] = React.useState({ contractNumber: '', name: '', category: '', type: '', executionMethod: '', - selectionMethod: '', vendorId: null, + projectId: null, startDate: undefined, endDate: undefined, validityEndDate: undefined, - // contractScope: '', - // specificationType: '', notes: '', - linkedRfqOrItb: '', - linkedBidNumber: '', - linkedPoNumber: '', }) // 업체 목록 조회 @@ -90,6 +81,20 @@ export function CreateGeneralContractDialog() { fetchVendors() }, []) + // 프로젝트 목록 조회 + React.useEffect(() => { + const fetchProjects = async () => { + try { + const projectList = await getProjects() + console.log(projectList) + setProjects(projectList) + } catch (error) { + console.error('Error fetching projects:', error) + } + } + fetchProjects() + }, []) + const handleSubmit = async () => { // 필수 필드 검증 if (!form.name || !form.category || !form.type || !form.executionMethod || @@ -111,23 +116,19 @@ export function CreateGeneralContractDialog() { category: form.category, type: form.type, executionMethod: form.executionMethod, - selectionMethod: form.selectionMethod, + projectId: form.projectId, + contractSourceType: 'manual', vendorId: form.vendorId!, startDate: form.startDate!.toISOString().split('T')[0], endDate: form.endDate!.toISOString().split('T')[0], validityEndDate: (form.validityEndDate || form.endDate!).toISOString().split('T')[0], - // contractScope: form.contractScope, - // specificationType: form.specificationType, status: 'Draft', registeredById: session?.user?.id || 1, lastUpdatedById: session?.user?.id || 1, notes: form.notes, - linkedRfqOrItb: form.linkedRfqOrItb, - linkedBidNumber: form.linkedBidNumber, - linkedPoNumber: form.linkedPoNumber, } - const newContract = await createContract(contractData) + await createContract(contractData) toast.success("새 계약이 생성되었습니다.") setOpen(false) @@ -150,17 +151,12 @@ export function CreateGeneralContractDialog() { category: '', type: '', executionMethod: '', - selectionMethod: '', vendorId: null, + projectId: null, startDate: undefined, endDate: undefined, validityEndDate: undefined, - // contractScope: '', - // specificationType: '', notes: '', - linkedRfqOrItb: '', - linkedBidNumber: '', - linkedPoNumber: '', }) } @@ -185,16 +181,6 @@ export function CreateGeneralContractDialog() {
- {/*
- - setForm(prev => ({ ...prev, contractNumber: e.target.value }))} - placeholder="자동 생성됩니다" - /> -
*/} -
- {GENERAL_CONTRACT_CATEGORIES.map((category) => ( + {GENERAL_CONTRACT_CATEGORIES.map((category) => { + const categoryLabels = { + 'unit_price': '단가계약', + 'general': '일반계약', + 'sale': '매각계약' + } + return ( - {category} - - ))} + {category} - {categoryLabels[category as keyof typeof categoryLabels]} + + ) + })}
@@ -275,6 +268,22 @@ export function CreateGeneralContractDialog() {
+
+ + +
+
setForm(prev => ({ ...prev, contractScope: value }))}> - - - - - {GENERAL_CONTRACT_SCOPES.map((scope) => ( - - {scope} - - ))} - - -
- -
- - -
-
*/} - -
-
- - setForm(prev => ({ ...prev, linkedRfqOrItb: e.target.value }))} - placeholder="연계 견적/입찰번호를 입력하세요" - /> -
- -
- - setForm(prev => ({ ...prev, linkedBidNumber: e.target.value }))} - placeholder="연계 BID번호를 입력하세요" - /> -
-
- -
- - setForm(prev => ({ ...prev, linkedPoNumber: e.target.value }))} - placeholder="연계PO번호를 입력하세요" - /> -
-