summaryrefslogtreecommitdiff
path: root/lib/gtc-contract/gtc-clauses
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gtc-contract/gtc-clauses')
-rw-r--r--lib/gtc-contract/gtc-clauses/gtc-clauses-page-header.tsx16
-rw-r--r--lib/gtc-contract/gtc-clauses/service.ts423
-rw-r--r--lib/gtc-contract/gtc-clauses/table/clause-table.tsx2
-rw-r--r--lib/gtc-contract/gtc-clauses/validations.ts98
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