- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
383 lines
12 KiB
TypeScript
383 lines
12 KiB
TypeScript
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<UserProfileTab[]> {
|
|
return await db
|
|
.select()
|
|
.from(userProfileTabs)
|
|
.where(eq(userProfileTabs.organizationId, organizationId))
|
|
.orderBy(userProfileTabs.displayOrder);
|
|
}
|
|
|
|
async getUserProfileTabsByCreationOrder(organizationId: number): Promise<UserProfileTab[]> {
|
|
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<UserProfileTab[]> {
|
|
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<UserProfileField[]> {
|
|
return await db
|
|
.select()
|
|
.from(userProfileFields)
|
|
.where(eq(userProfileFields.organizationId, organizationId))
|
|
.orderBy(userProfileFields.displayOrder);
|
|
}
|
|
|
|
async getUserProfileFieldsByCreationOrder(organizationId: number): Promise<UserProfileField[]> {
|
|
return await db
|
|
.select()
|
|
.from(userProfileFields)
|
|
.where(eq(userProfileFields.organizationId, organizationId))
|
|
.orderBy(userProfileFields.id);
|
|
}
|
|
|
|
async getUserProfileFieldById(id: number, organizationId: number): Promise<UserProfileField | undefined> {
|
|
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<UserProfileField[]> {
|
|
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<UserProfileFieldValue[]> {
|
|
return await db
|
|
.select()
|
|
.from(userProfileFieldValues)
|
|
.where(eq(userProfileFieldValues.userId, userId));
|
|
}
|
|
|
|
async getUserProfileFieldValue(userId: number, fieldId: number): Promise<UserProfileFieldValue | undefined> {
|
|
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<UserProfileFieldValue> {
|
|
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<UserProfileFieldValue> {
|
|
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<UserProfileFieldValue>
|
|
): Promise<UserProfileFieldValue> {
|
|
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<UserProfileFieldValue>,
|
|
organizationId: number,
|
|
changedBy: number
|
|
): Promise<UserProfileFieldValue> {
|
|
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<void> {
|
|
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<void> {
|
|
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<UserProfileAuditLog[]> {
|
|
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<UserProfileAuditLog> {
|
|
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 };
|
|
}
|
|
}
|