diff options
Diffstat (limited to 'lib/gtc-contract/gtc-clauses')
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx | 16 | ||||
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/service.ts | 423 | ||||
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/table/clause-table.tsx | 2 | ||||
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/validations.ts | 98 |
4 files changed, 507 insertions, 32 deletions
diff --git a/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx b/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx index 52faea3c..4cf29362 100644 --- a/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx +++ b/lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx @@ -15,23 +15,9 @@ export function GtcClausesPageHeader({ document }: GtcClausesPageHeaderProps) { const router = useRouter() const handleBack = () => { - router.push('/evcp/basic-contract-template/gtc') + router.push('/evcp/gtc') } - const handlePreview = () => { - // PDFTron 미리보기 기능 - console.log("PDF Preview for document:", document.id) - } - - const handleDownload = () => { - // 문서 다운로드 기능 - console.log("Download document:", document.id) - } - - const handleSettings = () => { - // 문서 설정 (템플릿 관리 등) - console.log("Document settings:", document.id) - } return ( <div className="flex items-center justify-between"> 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 diff --git a/lib/gtc-contract/gtc-clauses/table/clause-table.tsx b/lib/gtc-contract/gtc-clauses/table/clause-table.tsx index 89674db9..32550299 100644 --- a/lib/gtc-contract/gtc-clauses/table/clause-table.tsx +++ b/lib/gtc-contract/gtc-clauses/table/clause-table.tsx @@ -43,8 +43,6 @@ interface GtcClausesTableProps { export function GtcClausesTable({ promises, documentId, document }: GtcClausesTableProps) { const [{ data, pageCount }, users] = React.use(promises) - console.log(data) - const [rowAction, setRowAction] = React.useState<DataTableRowAction<GtcClauseTreeView> | null>(null) diff --git a/lib/gtc-contract/gtc-clauses/validations.ts b/lib/gtc-contract/gtc-clauses/validations.ts index edbcf612..f60255ba 100644 --- a/lib/gtc-contract/gtc-clauses/validations.ts +++ b/lib/gtc-contract/gtc-clauses/validations.ts @@ -1,4 +1,4 @@ -import { type GtcClause } from "@/db/schema/gtc" +import { GtcClauseTreeView, GtcClauseWithVendorView, GtcVendorClauseView, type GtcClause } from "@/db/schema/gtc" import { createSearchParamsCache, parseAsArrayOf, @@ -15,13 +15,24 @@ export const searchParamsCache = createSearchParamsCache({ ), page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(20), - sort: getSortingStateParser<GtcClause>().withDefault([ - { id: "sortOrder", desc: false }, + sort: getSortingStateParser<GtcClauseTreeView>().withDefault([ + { id: "itemNumber", desc: false }, + ]), + // advanced filter + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) + +export const searchParamsVendorCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(20), + sort: getSortingStateParser<GtcClauseWithVendorView>().withDefault([ + { id: "effectiveItemNumber", desc: false }, ]), - // 검색 필터들 - category: parseAsString.withDefault(""), - depth: parseAsInteger.withDefault(0), - parentId: parseAsInteger.withDefault(0), // advanced filter filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), @@ -117,8 +128,79 @@ export const generateVariableNamesSchema = z.object({ }) export type GetGtcClausesSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> +export type GetGtcVendorClausesSchema = Awaited<ReturnType<typeof searchParamsVendorCache.parse>> export type CreateGtcClauseSchema = z.infer<typeof createGtcClauseSchema> export type UpdateGtcClauseSchema = z.infer<typeof updateGtcClauseSchema> export type ReorderGtcClausesSchema = z.infer<typeof reorderGtcClausesSchema> export type BulkUpdateGtcClausesSchema = z.infer<typeof bulkUpdateGtcClausesSchema> -export type GenerateVariableNamesSchema = z.infer<typeof generateVariableNamesSchema>
\ No newline at end of file +export type GenerateVariableNamesSchema = z.infer<typeof generateVariableNamesSchema> + + +// validations.ts에 추가 +export const updateVendorGtcClauseSchema = z.object({ + modifiedItemNumber: z.string().optional(), + modifiedCategory: z.string().optional(), + modifiedSubtitle: z.string().optional(), + modifiedContent: z.string().optional(), + + isNumberModified: z.boolean().default(false), + isCategoryModified: z.boolean().default(false), + isSubtitleModified: z.boolean().default(false), + isContentModified: z.boolean().default(false), + + reviewStatus: z.enum([ + "draft", + "pending", + "reviewing", + "approved", + "rejected", + "revised" + ]).default("draft"), + + negotiationNote: z.string().optional(), + isExcluded: z.boolean().default(false), +}) + +export type UpdateVendorGtcClauseSchema = z.infer<typeof updateVendorGtcClauseSchema> + +// validations.ts +export const createVendorGtcClauseSchema = z.object({ + vendorDocumentId: z.number({ + required_error: "벤더 문서 ID는 필수입니다.", + }), + baseClauseId: z.number({ + required_error: "기본 조항 ID는 필수입니다.", + }), + documentId: z.number({ + required_error: "문서 ID는 필수입니다.", + }), + parentId: z.number().nullable().optional(), + modifiedItemNumber: z.string().optional().nullable(), + modifiedCategory: z.string().optional().nullable(), + modifiedSubtitle: z.string().optional().nullable(), + modifiedContent: z.string().optional().nullable(), + sortOrder: z.number().default(0), + reviewStatus: z.enum(["draft", "pending", "reviewing", "approved", "rejected", "revised"]).default("draft"), + negotiationNote: z.string().optional().nullable(), + isExcluded: z.boolean().default(false), + isNumberModified: z.boolean().default(false), + isCategoryModified: z.boolean().default(false), + isSubtitleModified: z.boolean().default(false), + isContentModified: z.boolean().default(false), + editReason: z.string().optional().nullable(), + images: z.array( + z.object({ + id: z.string(), + url: z.string(), + fileName: z.string(), + size: z.number(), + savedName: z.string().optional(), + mimeType: z.string().optional(), + width: z.number().optional(), + height: z.number().optional(), + hash: z.string().optional(), + }) + ).optional().nullable(), +}) + +export type CreateVendorGtcClauseSchema = z.infer<typeof createVendorGtcClauseSchema>
\ No newline at end of file |
