diff options
Diffstat (limited to 'lib/gtc-contract/gtc-clauses/service.ts')
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/service.ts | 423 |
1 files changed, 416 insertions, 7 deletions
diff --git a/lib/gtc-contract/gtc-clauses/service.ts b/lib/gtc-contract/gtc-clauses/service.ts index b6f620bc..2660dcdd 100644 --- a/lib/gtc-contract/gtc-clauses/service.ts +++ b/lib/gtc-contract/gtc-clauses/service.ts @@ -4,9 +4,15 @@ import db from "@/db/db" import { gtcClauses, gtcClausesTreeView, + gtcVendorClausesView, + gtcClausesWithVendorView, type GtcClause, type GtcClauseTreeView, - type NewGtcClause + type NewGtcClause, + gtcNegotiationHistory, + gtcVendorClauses, + gtcVendorDocuments, + GtcVendorClause } from "@/db/schema/gtc" import { users } from "@/db/schema/users" import { and, asc, count, desc, eq, ilike, or, sql, gt, lt, inArray, like } from "drizzle-orm" @@ -19,9 +25,12 @@ import type { ReorderGtcClausesSchema, BulkUpdateGtcClausesSchema, GenerateVariableNamesSchema, + GetGtcVendorClausesSchema, + UpdateVendorGtcClauseSchema, } from "@/lib/gtc-contract/gtc-clauses/validations" import { decryptWithServerAction } from "@/components/drm/drmUtils" import { saveDRMFile } from "@/lib/file-stroage" +import { vendors } from "@/db/schema" interface ClauseImage { id: string @@ -97,12 +106,14 @@ export async function getGtcClauses(input: GetGtcClausesSchema & { documentId: n // 정렬 const orderBy = - input.sort.length > 0 - ? input.sort.map((item) => { - const column = gtcClausesTreeView[item.id as keyof typeof gtcClausesTreeView] - return item.desc ? desc(column) : asc(column) - }) - : [asc(gtcClausesTreeView.sortOrder), asc(gtcClausesTreeView.depth)] + input.sort.length > 0 + ? input.sort.map((item) => { + const column = gtcClausesTreeView[item.id as keyof typeof gtcClausesTreeView] + const result = item.desc ? desc(column) : asc(column) + return result + }) + : [asc(gtcClausesTreeView.itemNumber)] + // 데이터 조회 const { data, total } = await db.transaction(async (tx) => { @@ -442,6 +453,81 @@ export async function createGtcClause( } } +export async function createVendorGtcClause( + input: CreateVendorGtcClauseSchema & { createdById: number } +) { + try { + // Calculate depth if parent is specified + let depth = 0 + + if (input.parentId) { + const parent = await db.query.gtcVendorClauses.findFirst({ + where: eq(gtcVendorClauses.id, input.parentId), + }) + if (parent) { + depth = parent.depth + 1 + } + } + + // Create the new vendor clause + const newVendorClause = { + vendorDocumentId: input.vendorDocumentId, + baseClauseId: input.baseClauseId, + parentId: input.parentId, + + // Modified fields + modifiedItemNumber: input.isNumberModified ? input.modifiedItemNumber : null, + modifiedCategory: input.isCategoryModified ? input.modifiedCategory : null, + modifiedSubtitle: input.isSubtitleModified ? input.modifiedSubtitle : null, + modifiedContent: input.isContentModified ? input.modifiedContent?.trim() : null, + + // Modification flags + isNumberModified: input.isNumberModified, + isCategoryModified: input.isCategoryModified, + isSubtitleModified: input.isSubtitleModified, + isContentModified: input.isContentModified, + + // Additional fields + sortOrder: input.sortOrder.toString(), + depth, + reviewStatus: input.reviewStatus, + negotiationNote: input.negotiationNote || null, + isExcluded: input.isExcluded, + + // Audit fields + createdById: input.createdById, + updatedById: input.createdById, + editReason: input.editReason, + images: input.images || null, + isActive: true, + } + + const [result] = await db.insert(gtcVendorClauses).values(newVendorClause).returning() + + // Create negotiation history entry + if (input.negotiationNote) { + await db.insert(gtcNegotiationHistory).values({ + vendorClauseId: result.id, + action: "created", + comment: input.negotiationNote, + actorName: input.actorName, // You'll need to pass this from the form + actorEmail: input.actorEmail, // You'll need to pass this from the form + previousStatus: null, + newStatus: input.reviewStatus, + createdById: input.createdById, + }) + } + + // Revalidate cache + // await revalidatePath(`/evcp/gtc/${input.documentId}?vendorId=${input.vendorId}`) + + return { data: result, error: null } + } catch (error) { + console.error("Error creating vendor GTC clause:", error) + return { data: null, error: "벤더 조항 생성 중 오류가 발생했습니다." } + } +} + /** * GTC 조항 수정 */ @@ -751,6 +837,15 @@ async function countGtcClauses(tx: any, where: any) { return total } +async function countGtcVendorClauses(tx: any, where: any) { + const [{ count: total }] = await tx + .select({ count: count() }) + .from(gtcClausesWithVendorView) + .where(where) + return total +} + + function buildClausesTree(clauses: GtcClauseTreeView[]): GtcClauseTreeView[] { const clauseMap = new Map<number, GtcClauseTreeView & { children: GtcClauseTreeView[] }>() const rootClauses: (GtcClauseTreeView & { children: GtcClauseTreeView[] })[] = [] @@ -781,6 +876,8 @@ async function revalidateGtcClausesCaches(documentId: number) { const { revalidateTag } = await import("next/cache") revalidateTag(`gtc-clauses-${documentId}`) revalidateTag(`gtc-clauses-tree-${documentId}`) + revalidateTag( "basicContractView-vendor") + } /** @@ -933,4 +1030,316 @@ export async function moveGtcClauseDown(clauseId: number, updatedById: number) { console.error("Error moving clause down:", error) return { error: "조항 이동 중 오류가 발생했습니다." } } +} + + +// 벤더별 조항 정보를 조회하는 함수 추가 +export async function getVendorClausesForDocument({ + documentId, + vendorId, +}: { + documentId: number + vendorId?: number +}) { + try { + // vendorId가 없으면 빈 객체 반환 + if (!vendorId) { + return {} + } + + // 1. 해당 문서와 벤더에 대한 벤더 문서 찾기 + const vendorDocument = await db + .select() + .from(gtcVendorDocuments) + .where( + and( + eq(gtcVendorDocuments.baseDocumentId, documentId), + eq(gtcVendorDocuments.vendorId, vendorId), + eq(gtcVendorDocuments.isActive, true) + ) + ) + .limit(1) + + if (!vendorDocument[0]) { + return {} + } + + // 2. 벤더 조항들 조회 (협의 이력 포함) + const vendorClauses = await db + .select({ + // 벤더 조항 정보 + id: gtcVendorClauses.id, + baseClauseId: gtcVendorClauses.baseClauseId, + vendorDocumentId: gtcVendorClauses.vendorDocumentId, + + // 수정된 내용 + modifiedItemNumber: gtcVendorClauses.modifiedItemNumber, + modifiedCategory: gtcVendorClauses.modifiedCategory, + modifiedSubtitle: gtcVendorClauses.modifiedSubtitle, + modifiedContent: gtcVendorClauses.modifiedContent, + + // 수정 플래그 + isNumberModified: gtcVendorClauses.isNumberModified, + isCategoryModified: gtcVendorClauses.isCategoryModified, + isSubtitleModified: gtcVendorClauses.isSubtitleModified, + isContentModified: gtcVendorClauses.isContentModified, + + // 협의 상태 + reviewStatus: gtcVendorClauses.reviewStatus, + negotiationNote: gtcVendorClauses.negotiationNote, + isExcluded: gtcVendorClauses.isExcluded, + + // 날짜 + createdAt: gtcVendorClauses.createdAt, + updatedAt: gtcVendorClauses.updatedAt, + }) + .from(gtcVendorClauses) + .where( + and( + eq(gtcVendorClauses.vendorDocumentId, vendorDocument[0].id), + eq(gtcVendorClauses.isActive, true) + ) + ) + + // 3. 각 벤더 조항에 대한 협의 이력 조회 + const clauseIds = vendorClauses.map(c => c.id) + const negotiationHistories = clauseIds.length > 0 + ? await db + .select({ + vendorClauseId: gtcNegotiationHistory.vendorClauseId, + action: gtcNegotiationHistory.action, + previousStatus: gtcNegotiationHistory.previousStatus, + newStatus: gtcNegotiationHistory.newStatus, + comment: gtcNegotiationHistory.comment, + actorName: gtcNegotiationHistory.actorName, + actorEmail: gtcNegotiationHistory.actorEmail, + createdAt: gtcNegotiationHistory.createdAt, + }) + .from(gtcNegotiationHistory) + .where(inArray(gtcNegotiationHistory.vendorClauseId, clauseIds)) + .orderBy(desc(gtcNegotiationHistory.createdAt)) + : [] + + // 4. baseClauseId를 키로 하는 맵 생성 + const vendorClauseMap = new Map() + + vendorClauses.forEach(vc => { + // 해당 조항의 협의 이력 필터링 + const history = negotiationHistories + .filter(h => h.vendorClauseId === vc.id) + .map(h => ({ + action: h.action, + comment: h.comment, + actorName: h.actorName, + actorEmail: h.actorEmail, + createdAt: h.createdAt, + previousStatus: h.previousStatus, + newStatus: h.newStatus, + })) + + // 가장 최근 코멘트 찾기 + const latestComment = history.find(h => h.comment)?.comment || null + + vendorClauseMap.set(vc.baseClauseId, { + ...vc, + negotiationHistory: history, + latestComment, + hasModifications: + vc.isNumberModified || + vc.isCategoryModified || + vc.isSubtitleModified || + vc.isContentModified, + }) + }) + + return { + vendorDocument: vendorDocument[0], + vendorClauseMap, + totalModified: vendorClauses.filter(vc => + vc.isNumberModified || + vc.isCategoryModified || + vc.isSubtitleModified || + vc.isContentModified + ).length, + totalExcluded: vendorClauses.filter(vc => vc.isExcluded).length, + } + } catch (error) { + console.error("Failed to fetch vendor clauses:", error) + return {} + } +} + +// service.ts에 추가 +/** + * 벤더별 GTC 조항 수정 + */ +export async function updateVendorGtcClause( + input: UpdateVendorGtcClauseSchema & { + baseClauseId: number + documentId: number + vendorId: number + updatedById: number + images?: any[] + } +) { + try { + // 1. 먼저 벤더 문서 찾기 또는 생성 + let vendorDocument = await db + .select() + .from(gtcVendorDocuments) + .where( + and( + eq(gtcVendorDocuments.baseDocumentId, input.documentId), + eq(gtcVendorDocuments.vendorId, input.vendorId), + eq(gtcVendorDocuments.isActive, true) + ) + ) + .limit(1) + + console.log(vendorDocument,"vendorDocument", input.vendorId, input.documentId) + + // 벤더 문서가 없으면 생성 + if (!vendorDocument[0]) { + const vendor = await db + .select() + .from(vendors) + .where(eq(vendors.id, input.vendorId)) + .limit(1) + + if (!vendor[0]) { + return { data: null, error: "벤더 정보를 찾을 수 없습니다." } + } + + [vendorDocument[0]] = await db + .insert(gtcVendorDocuments) + .values({ + baseDocumentId: input.documentId, + vendorId: input.vendorId, + name: `${vendor[0].vendorName} GTC Agreement`, + description: `GTC negotiation with ${vendor[0].vendorName}`, + version: "1.0", + reviewStatus: "draft", + createdById: input.updatedById, + updatedById: input.updatedById, + }) + .returning() + } + + // 2. 벤더 조항 찾기 또는 생성 + let vendorClause = await db + .select() + .from(gtcVendorClauses) + .where( + and( + eq(gtcVendorClauses.vendorDocumentId, vendorDocument[0].id), + eq(gtcVendorClauses.baseClauseId, input.baseClauseId) + ) + ) + .limit(1) + + const updateData: Partial<GtcVendorClause> = { + updatedById: input.updatedById, + updatedAt: new Date(), + + // 수정 플래그 + isNumberModified: input.isNumberModified, + isCategoryModified: input.isCategoryModified, + isSubtitleModified: input.isSubtitleModified, + isContentModified: input.isContentModified, + + // 협의 정보 + reviewStatus: input.reviewStatus, + negotiationNote: input.negotiationNote, + isExcluded: input.isExcluded, + } + + // 수정된 내용만 업데이트 + if (input.isNumberModified) { + updateData.modifiedItemNumber = input.modifiedItemNumber + } else { + updateData.modifiedItemNumber = null + } + + if (input.isCategoryModified) { + updateData.modifiedCategory = input.modifiedCategory + } else { + updateData.modifiedCategory = null + } + + if (input.isSubtitleModified) { + updateData.modifiedSubtitle = input.modifiedSubtitle + } else { + updateData.modifiedSubtitle = null + } + + if (input.isContentModified) { + updateData.modifiedContent = input.modifiedContent + } else { + updateData.modifiedContent = null + } + + console.log("updateData",updateData) + + let result + if (vendorClause[0]) { + // 업데이트 + [result] = await db + .update(gtcVendorClauses) + .set(updateData) + .where(eq(gtcVendorClauses.id, vendorClause[0].id)) + .returning() + } else { + // 새로 생성 + const baseClause = await db + .select() + .from(gtcClauses) + .where(eq(gtcClauses.id, input.baseClauseId)) + .limit(1) + + if (!baseClause[0]) { + return { data: null, error: "기본 조항을 찾을 수 없습니다." } + } + + [result] = await db + .insert(gtcVendorClauses) + .values({ + vendorDocumentId: vendorDocument[0].id, + baseClauseId: input.baseClauseId, + parentId: baseClause[0].parentId, + sortOrder: baseClause[0].sortOrder, + depth: baseClause[0].depth, + fullPath: baseClause[0].fullPath, + createdById: input.updatedById, + ...updateData, + }) + .returning() + } + + // 3. 협의 이력 추가 + if (input.negotiationNote) { + await db.insert(gtcNegotiationHistory).values({ + vendorClauseId: result.id, + action: vendorClause[0] ? "modified" : "created", + previousStatus: vendorClause[0]?.reviewStatus || null, + newStatus: input.reviewStatus, + comment: input.negotiationNote, + actorType: "internal", + actorId: input.updatedById, + changedFields: { + isNumberModified: input.isNumberModified, + isCategoryModified: input.isCategoryModified, + isSubtitleModified: input.isSubtitleModified, + isContentModified: input.isContentModified, + }, + }) + } + + // 캐시 무효화 + await revalidateGtcClausesCaches(input.documentId) + + return { data: result, error: null } + } catch (error) { + console.error("Error updating vendor GTC clause:", error) + return { data: null, error: "벤더 조항 수정 중 오류가 발생했습니다." } + } }
\ No newline at end of file |
