Files
iistwin/server/storage/user-profile.storage.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

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 };
}
}