diff options
Diffstat (limited to 'lib/rfq-last/contract-actions.ts')
| -rw-r--r-- | lib/rfq-last/contract-actions.ts | 329 |
1 files changed, 261 insertions, 68 deletions
diff --git a/lib/rfq-last/contract-actions.ts b/lib/rfq-last/contract-actions.ts index 1144cf4f..082716a0 100644 --- a/lib/rfq-last/contract-actions.ts +++ b/lib/rfq-last/contract-actions.ts @@ -1,9 +1,15 @@ "use server"; import db from "@/db/db"; -import { rfqsLast, rfqLastDetails } from "@/db/schema"; +import { rfqsLast, rfqLastDetails,rfqPrItems, + prItemsForBidding,biddingConditions,biddingCompanies, projects, + biddings,generalContracts ,generalContractItems} from "@/db/schema"; import { eq, and } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { generateContractNumber } from "../general-contracts/service"; +import { generateBiddingNumber } from "../bidding/service"; // ===== PO (SAP) 생성 ===== interface CreatePOParams { @@ -63,13 +69,13 @@ export async function createPO(params: CreatePOParams) { ); // RFQ 상태 업데이트 - await tx - .update(rfqsLast) - .set({ - status: "PO 생성 완료", - updatedAt: new Date(), - }) - .where(eq(rfqsLast.id, params.rfqId)); + // await tx + // .update(rfqsLast) + // .set({ + // status: "PO 생성 완료", + // updatedAt: new Date(), + // }) + // .where(eq(rfqsLast.id, params.rfqId)); }); revalidatePath(`/rfq/${params.rfqId}`); @@ -89,14 +95,13 @@ export async function createPO(params: CreatePOParams) { } } -// ===== 일반계약 생성 ===== interface CreateGeneralContractParams { rfqId: number; vendorId: number; vendorName: string; totalAmount: number; currency: string; - contractType?: string; + contractType: string; // 계약종류 추가 (UP, LE, IL 등) contractStartDate?: Date; contractEndDate?: Date; contractTerms?: string; @@ -104,40 +109,126 @@ interface CreateGeneralContractParams { export async function createGeneralContract(params: CreateGeneralContractParams) { try { - const userId = 1; // TODO: 실제 사용자 ID 가져오기 + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } - // 1. 선정된 업체 확인 - const [selectedVendor] = await db - .select() - .from(rfqLastDetails) + const userId = session.user.id; + + // 1. RFQ 정보 및 선정된 업체 확인 + const [rfqData] = await db + .select({ + rfq: rfqsLast, + vendor: rfqLastDetails, + project: projects, + }) + .from(rfqsLast) + .leftJoin(rfqLastDetails, eq(rfqLastDetails.rfqsLastId, rfqsLast.id)) + .leftJoin(projects, eq(projects.id, rfqsLast.projectId)) .where( and( - eq(rfqLastDetails.rfqsLastId, params.rfqId), + eq(rfqsLast.id, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); - if (!selectedVendor) { - throw new Error("선정된 업체 정보를 찾을 수 없습니다."); + if (!rfqData || !rfqData.vendor) { + throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다."); } - // 2. 계약 생성 로직 (TODO: 실제 구현 필요) - // - 계약서 템플릿 선택 - // - 계약 조건 설정 - // - 계약서 문서 생성 - // - 전자서명 프로세스 시작 + // 2. PR 아이템 정보 조회 (계약 아이템으로 변환용) + const prItems = await db + .select() + .from(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, params.rfqId)); - // 3. 계약 상태 업데이트 - await db.transaction(async (tx) => { - // rfqLastDetails에 계약 정보 업데이트 + // 3. 계약번호 생성 - generateContractNumber 함수 사용 + const contractNumber = await generateContractNumber( + params.contractType, // 계약종류 (UP, LE, IL 등) + rfqData.rfq.picCode || undefined // 발주담당자 코드 + ); + + // 4. 트랜잭션으로 계약 생성 + const result = await db.transaction(async (tx) => { + // 4-1. generalContracts 테이블에 계약 생성 + const [newContract] = await tx + .insert(generalContracts) + .values({ + contractNumber, + contractSourceType: 'estimate', // 견적에서 넘어온 계약 + status: 'Draft', // 초안 상태로 시작 + category: '일반계약', + type: params.contractType, // 계약종류 저장 (UP, LE, IL 등) + executionMethod: '일반계약', + name: `${rfqData.project?.name || rfqData.rfq.itemName || ''} 일반계약`, + selectionMethod: '견적', + + // 업체 정보 + vendorId: params.vendorId, + + // 계약 기간 + startDate: params.contractStartDate || new Date(), + endDate: params.contractEndDate || null, + + // 연계 정보 + linkedRfqOrItb: rfqData.rfq.rfqCode || undefined, + + // 금액 정보 + contractAmount: params.totalAmount, + currency: params.currency || 'KRW', + totalAmount: params.totalAmount, + + // 조건 정보 (RFQ에서 가져옴) + contractCurrency: rfqData.vendor.currency || 'KRW', + paymentTerm: rfqData.vendor.paymentTermsCode || undefined, + taxType: rfqData.vendor.taxCode || undefined, + deliveryTerm: rfqData.vendor.incotermsCode || undefined, + shippingLocation: rfqData.vendor.placeOfShipping || undefined, + dischargeLocation: rfqData.vendor.placeOfDestination || undefined, + contractDeliveryDate: rfqData.vendor.deliveryDate || undefined, + + // 연동제 적용 여부 + interlockingSystem: rfqData.vendor.materialPriceRelatedYn ? 'Y' : 'N', + + // 시스템 정보 + registeredById: userId, + registeredAt: new Date(), + lastUpdatedById: userId, + lastUpdatedAt: new Date(), + notes: params.contractTerms || rfqData.vendor.remark || undefined, + }) + .returning(); + + // 4-2. generalContractItems 테이블에 아이템 생성 + if (prItems.length > 0) { + const contractItems = prItems.map(item => ({ + contractId: newContract.id, + project: rfqData.project?.name || undefined, + itemCode: item.materialCode || undefined, + itemInfo: `${item.materialCategory || ''} / ${item.materialCode || ''}`, + specification: item.materialDescription || undefined, + quantity: item.quantity, + quantityUnit: item.uom || undefined, + contractDeliveryDate: item.deliveryDate || undefined, + contractCurrency: params.currency || 'KRW', + // 단가와 금액은 견적 데이터에서 가져와야 함 (현재는 총액을 아이템 수로 나눔) + contractUnitPrice: params.totalAmount / prItems.length, + contractAmount: params.totalAmount / prItems.length, + })); + + await tx.insert(generalContractItems).values(contractItems); + } + + // 4-3. rfqLastDetails 상태 업데이트 await tx .update(rfqLastDetails) .set({ - contractStatus: "진행중", + contractStatus: "일반계약 진행중", contractCreatedAt: new Date(), - contractNo: `CONTRACT-${Date.now()}`, // TODO: 실제 계약번호로 변경 + contractNo: contractNumber, updatedAt: new Date(), updatedBy: userId, }) @@ -149,23 +240,18 @@ export async function createGeneralContract(params: CreateGeneralContractParams) ) ); - // RFQ 상태 업데이트 - await tx - .update(rfqsLast) - .set({ - status: "일반계약 진행중", - updatedAt: new Date(), - }) - .where(eq(rfqsLast.id, params.rfqId)); + return newContract; }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); + revalidatePath("/contracts"); return { success: true, message: "일반계약이 성공적으로 생성되었습니다.", - contractNumber: `CONTRACT-${Date.now()}`, // TODO: 실제 계약번호 반환 + contractNumber: result.contractNumber, + contractId: result.id, }; } catch (error) { console.error("일반계약 생성 오류:", error); @@ -183,49 +269,162 @@ interface CreateBiddingParams { vendorName: string; totalAmount: number; currency: string; - biddingType?: string; // 공개입찰, 제한입찰 등 - biddingStartDate?: Date; - biddingEndDate?: Date; + contractType: "unit_price" | "general" | "sale"; // 계약구분 + biddingType: string; // 입찰유형 (equipment, construction 등) + awardCount: "single" | "multiple"; // 낙찰수 + biddingStartDate: Date; + biddingEndDate: Date; biddingRequirements?: string; } export async function createBidding(params: CreateBiddingParams) { try { - const userId = 1; // TODO: 실제 사용자 ID 가져오기 + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } - // 1. 선정된 업체 확인 - const [selectedVendor] = await db - .select() - .from(rfqLastDetails) + const userId = session.user.id; + + // 1. RFQ 정보 및 선정된 업체 확인 + const [rfqData] = await db + .select({ + rfq: rfqsLast, + vendor: rfqLastDetails, + project: projects, + }) + .from(rfqsLast) + .leftJoin(rfqLastDetails, eq(rfqLastDetails.rfqsLastId, rfqsLast.id)) + .leftJoin(projects, eq(projects.id, rfqsLast.projectId)) .where( and( - eq(rfqLastDetails.rfqsLastId, params.rfqId), + eq(rfqsLast.id, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); - if (!selectedVendor) { - throw new Error("선정된 업체 정보를 찾을 수 없습니다."); + if (!rfqData || !rfqData.vendor) { + throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다."); } - // 2. 입찰 생성 로직 (TODO: 실제 구현 필요) - // - 입찰 공고 생성 - // - 입찰 참가자격 설정 - // - 입찰 일정 등록 - // - 평가 기준 설정 - // - 입찰 시스템 등록 + // 2. PR 아이템 정보 조회 + const prItems = await db + .select() + .from(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, params.rfqId)); - // 3. 입찰 상태 업데이트 - await db.transaction(async (tx) => { - // rfqLastDetails에 입찰 정보 업데이트 + // 3. 트랜잭션으로 입찰 생성 + const result = await db.transaction(async (tx) => { + // 3-1. 입찰번호 생성 - generateBiddingNumber 함수 사용 + const biddingNumber = await generateBiddingNumber( + rfqData.rfq.picCode || undefined, // 발주담당자 코드 + tx // 트랜잭션 컨텍스트 전달 + ); + + // 3-2. biddings 테이블에 입찰 생성 + const [newBidding] = await tx + .insert(biddings) + .values({ + biddingNumber, + revision: 0, + biddingSourceType: 'estimate', + projectId: rfqData.rfq.projectId || undefined, + + // 기본 정보 + projectName: rfqData.project?.name || undefined, + itemName: rfqData.rfq.itemName || undefined, + title: `${rfqData.project?.name || rfqData.rfq.itemName || ''} 입찰`, + description: params.biddingRequirements || rfqData.rfq.remark || undefined, + + // 계약 정보 - 파라미터에서 받은 값 사용 + contractType: params.contractType, + biddingType: params.biddingType, + awardCount: params.awardCount, + + // 일정 관리 - 파라미터에서 받은 날짜 사용 + biddingRegistrationDate: new Date(), + submissionStartDate: params.biddingStartDate, + submissionEndDate: params.biddingEndDate, + + // 예산 및 가격 정보 + currency: params.currency || 'KRW', + budget: params.totalAmount, + targetPrice: params.totalAmount, + + // PR 정보 + prNumber: rfqData.rfq.prNumber || undefined, + hasPrDocument: false, + + // 상태 + status: 'bidding_generated', + isPublic: false, // 다이얼로그에서 체크박스로 받을 수 있음 + isUrgent: false, // 다이얼로그에서 체크박스로 받을 수 있음 + + // 담당자 정보 + managerName: rfqData.rfq.picName || undefined, + + // 메타 정보 + createdBy: String(userId), + createdAt: new Date(), + updatedAt: new Date(), + updatedBy: String(userId), + ANFNR: rfqData.rfq.ANFNR || undefined, + }) + .returning(); + + // 3-3. PR 아이템을 입찰 아이템으로 변환 + if (prItems.length > 0) { + const biddingPrItems = prItems.map(item => ({ + biddingId: newBidding.id, + itemNumber: item.rfqItem || undefined, + projectInfo: rfqData.project?.name || undefined, // 프로젝트 이름 사용 + itemInfo: item.materialDescription || undefined, + requestedDeliveryDate: item.deliveryDate || undefined, + currency: params.currency || 'KRW', + quantity: item.quantity, + quantityUnit: item.uom || undefined, + totalWeight: item.grossWeight || undefined, + weightUnit: item.gwUom || undefined, + materialDescription: item.materialDescription || undefined, + prNumber: item.prNo || undefined, + hasSpecDocument: !!item.specUrl, + })); + + await tx.insert(prItemsForBidding).values(biddingPrItems); + } + + // 3-4. 선정된 업체를 입찰 참여 업체로 등록 + await tx.insert(biddingCompanies).values({ + biddingId: newBidding.id, + companyId: params.vendorId, + invitationStatus: 'pending', + preQuoteAmount: params.totalAmount, // 견적 금액을 사전견적으로 사용 + isPreQuoteSelected: true, // 본입찰 대상으로 자동 선정 + isBiddingInvited: true, + notes: '견적에서 선정된 업체', + }); + + // 3-5. 입찰 조건 생성 (RFQ 조건 활용) + await tx.insert(biddingConditions).values({ + biddingId: newBidding.id, + paymentTerms: JSON.stringify([rfqData.vendor.paymentTermsCode]), + taxConditions: JSON.stringify([rfqData.vendor.taxCode]), + contractDeliveryDate: rfqData.vendor.deliveryDate || undefined, + isPriceAdjustmentApplicable: rfqData.vendor.materialPriceRelatedYn || false, + incoterms: JSON.stringify([rfqData.vendor.incotermsCode]), + shippingPort: rfqData.vendor.placeOfShipping || undefined, + destinationPort: rfqData.vendor.placeOfDestination || undefined, + }); + + // 3-6. rfqLastDetails 상태 업데이트 await tx .update(rfqLastDetails) .set({ contractStatus: "입찰진행중", contractCreatedAt: new Date(), - contractNo: `BID-${Date.now()}`, // TODO: 실제 입찰번호로 변경 + contractNo: biddingNumber, updatedAt: new Date(), updatedBy: userId, }) @@ -237,23 +436,18 @@ export async function createBidding(params: CreateBiddingParams) { ) ); - // RFQ 상태 업데이트 - await tx - .update(rfqsLast) - .set({ - status: "입찰 진행중", - updatedAt: new Date(), - }) - .where(eq(rfqsLast.id, params.rfqId)); + return newBidding; }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); + revalidatePath("/biddings"); return { success: true, message: "입찰이 성공적으로 생성되었습니다.", - biddingNumber: `BID-${Date.now()}`, // TODO: 실제 입찰번호 반환 + biddingNumber: result.biddingNumber, + biddingId: result.id, }; } catch (error) { console.error("입찰 생성 오류:", error); @@ -263,7 +457,6 @@ export async function createBidding(params: CreateBiddingParams) { }; } } - // ===== 계약 타입 확인 ===== export async function checkContractStatus(rfqId: number) { try { |
