import { userProfileTabs, userProfileFields, userProfileFieldValues, userProfileAuditLog, roleMembers, roles, userStatuses, type UserProfileTab, type UserProfileField, type UserProfileFieldValue, type UserProfileAuditLog, type InsertUserProfileTab, type InsertUserProfileField, type InsertUserProfileFieldValue, type InsertUserProfileAuditLog, } from "@shared/schema"; import { db } from "../db"; import { eq, and, desc, inArray } from "drizzle-orm"; import { UsersStorage } from "./users.storage"; export class UserProfileStorage extends UsersStorage { // ========== TABS ========== async getUserProfileTabs(organizationId: number): Promise { return await db .select() .from(userProfileTabs) .where(eq(userProfileTabs.organizationId, organizationId)) .orderBy(userProfileTabs.displayOrder); } async getUserProfileTabsByCreationOrder(organizationId: number): Promise { return await db .select() .from(userProfileTabs) .where(eq(userProfileTabs.organizationId, organizationId)) .orderBy(userProfileTabs.id); } async bulkSaveUserProfileTabs( organizationId: number, tabs: Array<{ id?: number; name: string; code: string; position?: number; displayOrder?: number }> ): Promise { return await db.transaction(async (tx) => { const existingTabs = await tx .select() .from(userProfileTabs) .where(eq(userProfileTabs.organizationId, organizationId)); const incomingIds = tabs.filter((t) => t.id && t.id > 0).map((t) => t.id!); const tabsToDelete = existingTabs.filter((e) => !incomingIds.includes(e.id)); for (const tab of tabsToDelete) { await tx .delete(userProfileTabs) .where( and( eq(userProfileTabs.id, tab.id), eq(userProfileTabs.organizationId, organizationId) ) ); } const savedTabs: UserProfileTab[] = []; for (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; const tabData: InsertUserProfileTab = { organizationId, name: tab.name || "", code: tab.code || `tab_${i + 1}`, position: tab.position ?? i, displayOrder: tab.displayOrder ?? i, }; if (tab.id && tab.id > 0) { const [updated] = await tx .update(userProfileTabs) .set(tabData) .where( and( eq(userProfileTabs.id, tab.id), eq(userProfileTabs.organizationId, organizationId) ) ) .returning(); if (updated) savedTabs.push(updated); } else { const [created] = await tx .insert(userProfileTabs) .values(tabData) .returning(); savedTabs.push(created); } } return savedTabs; }); } // ========== FIELDS ========== async getUserProfileFields(organizationId: number): Promise { return await db .select() .from(userProfileFields) .where(eq(userProfileFields.organizationId, organizationId)) .orderBy(userProfileFields.displayOrder); } async getUserProfileFieldsByCreationOrder(organizationId: number): Promise { return await db .select() .from(userProfileFields) .where(eq(userProfileFields.organizationId, organizationId)) .orderBy(userProfileFields.id); } async getUserProfileFieldById(id: number, organizationId: number): Promise { const [field] = await db .select() .from(userProfileFields) .where(and(eq(userProfileFields.id, id), eq(userProfileFields.organizationId, organizationId))); return field || undefined; } async bulkSaveUserProfileFields( organizationId: number, fieldIdsToDelete: number[], fields: Array<{ id?: number; name: string; code: string; type: string; isRequired?: boolean; placeholder?: string | null; defaultValue?: string | null; validationRules?: unknown; options?: unknown; position?: number | null; displayOrder?: number | null; tabId?: number | null; linkedFormId?: number | null; companyAutofill?: unknown; maxFileCount?: number | null; maxFileSizeMB?: number | null; autoProlongation?: boolean | null; autoProlongationLabel?: string | null; }> ): Promise { return await db.transaction(async (tx) => { for (const fieldId of fieldIdsToDelete) { await tx .delete(userProfileFields) .where( and( eq(userProfileFields.id, fieldId), eq(userProfileFields.organizationId, organizationId) ) ); } const resultFields: UserProfileField[] = []; for (let i = 0; i < fields.length; i++) { const field = fields[i]; const fieldData: InsertUserProfileField = { organizationId, name: field.name, code: field.code, type: field.type, isRequired: field.isRequired || false, placeholder: field.placeholder || null, defaultValue: field.defaultValue || null, validationRules: field.validationRules || null, options: field.options || null, position: field.position ?? i, displayOrder: field.displayOrder ?? i, tabId: field.tabId || null, linkedFormId: field.linkedFormId || null, companyAutofill: field.companyAutofill || null, maxFileCount: field.maxFileCount != null && field.maxFileCount > 0 ? field.maxFileCount : null, maxFileSizeMB: field.maxFileSizeMB != null && field.maxFileSizeMB > 0 ? field.maxFileSizeMB : null, autoProlongation: field.autoProlongation || false, autoProlongationLabel: field.autoProlongationLabel || null, }; if (field.id && field.id > 0) { const [updated] = await tx .update(userProfileFields) .set(fieldData) .where( and( eq(userProfileFields.id, field.id), eq(userProfileFields.organizationId, organizationId) ) ) .returning(); if (updated) resultFields.push(updated); } else { const [created] = await tx .insert(userProfileFields) .values(fieldData) .returning(); resultFields.push(created); } } return resultFields; }); } // ========== FIELD VALUES ========== async getUserProfileFieldValues(userId: number): Promise { return await db .select() .from(userProfileFieldValues) .where(eq(userProfileFieldValues.userId, userId)); } async getUserProfileFieldValue(userId: number, fieldId: number): Promise { const [value] = await db .select() .from(userProfileFieldValues) .where( and( eq(userProfileFieldValues.userId, userId), eq(userProfileFieldValues.fieldId, fieldId) ) ); return value || undefined; } async createUserProfileFieldValue(insertValue: InsertUserProfileFieldValue): Promise { const [value] = await db .insert(userProfileFieldValues) .values({ ...insertValue, createdAt: new Date(), updatedAt: new Date() }) .returning(); return value; } async createUserProfileFieldValueWithAudit( insertValue: InsertUserProfileFieldValue, organizationId: number, changedBy: number ): Promise { const value = await this.createUserProfileFieldValue(insertValue); await this.createUserProfileAuditLog({ userId: insertValue.userId, organizationId, action: 'field_value.created', fieldId: insertValue.fieldId, oldValue: null, newValue: typeof insertValue.value === 'string' ? insertValue.value : JSON.stringify(insertValue.value), changedBy, }); return value; } async updateUserProfileFieldValue( userId: number, fieldId: number, updates: Partial ): Promise { const [value] = await db .update(userProfileFieldValues) .set({ ...updates, updatedAt: new Date() }) .where( and( eq(userProfileFieldValues.userId, userId), eq(userProfileFieldValues.fieldId, fieldId) ) ) .returning(); return value; } async updateUserProfileFieldValueWithAudit( userId: number, fieldId: number, updates: Partial, organizationId: number, changedBy: number ): Promise { const existing = await this.getUserProfileFieldValue(userId, fieldId); const value = await this.updateUserProfileFieldValue(userId, fieldId, updates); await this.createUserProfileAuditLog({ userId, organizationId, action: 'field_value.updated', fieldId, oldValue: existing?.value ?? null, newValue: typeof updates.value === 'string' ? updates.value : updates.value != null ? JSON.stringify(updates.value) : null, changedBy, }); return value; } async deleteUserProfileFieldValue(userId: number, fieldId: number): Promise { await db .delete(userProfileFieldValues) .where( and( eq(userProfileFieldValues.userId, userId), eq(userProfileFieldValues.fieldId, fieldId) ) ); } async deleteUserProfileFieldValueWithAudit( userId: number, fieldId: number, organizationId: number, changedBy: number ): Promise { const existing = await this.getUserProfileFieldValue(userId, fieldId); await this.deleteUserProfileFieldValue(userId, fieldId); if (existing) { await this.createUserProfileAuditLog({ userId, organizationId, action: 'field_value.deleted', fieldId, oldValue: existing.value ?? null, newValue: null, changedBy, }); } } // ========== AUDIT LOG ========== async getUserProfileAuditLog(userId: number, organizationId: number): Promise { return await db .select() .from(userProfileAuditLog) .where( and( eq(userProfileAuditLog.userId, userId), eq(userProfileAuditLog.organizationId, organizationId) ) ) .orderBy(desc(userProfileAuditLog.createdAt)); } async createUserProfileAuditLog(insertValue: InsertUserProfileAuditLog): Promise { const [entry] = await db .insert(userProfileAuditLog) .values({ ...insertValue, createdAt: new Date() }) .returning(); return entry; } // ========== DETAIL ========== async getUserProfileDetail( userId: number, organizationId: number ): Promise<{ user: any; tabs: UserProfileTab[]; fields: UserProfileField[]; fieldValues: UserProfileFieldValue[]; organizationalRoles: { id: number; name: string }[]; }> { const user = await this.getUser(userId); const tabs = await this.getUserProfileTabs(organizationId); const fields = await this.getUserProfileFields(organizationId); const fieldValues = user ? await this.getUserProfileFieldValues(userId) : []; const userRoles = user ? await db .select({ roleId: roleMembers.roleId, roleName: roles.name }) .from(roleMembers) .innerJoin(roles, eq(roleMembers.roleId, roles.id)) .where(and(eq(roleMembers.userId, userId), eq(roles.organizationId, organizationId))) : []; const organizationalRoles = userRoles.map((r) => ({ id: r.roleId, name: r.roleName })); let status = null; if (user?.statusId) { const [s] = await db.select().from(userStatuses).where(eq(userStatuses.id, user.statusId)); status = s || null; } return { user: { ...user, status }, tabs, fields, fieldValues, organizationalRoles }; } }