fix(number-fields): избегаем потери точности длинных чисел

- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
2026-07-07 21:03:40 +03:00
commit 1f5ecb6da4
1089 changed files with 237546 additions and 0 deletions

100
server/utils/user-access.ts Normal file
View File

@@ -0,0 +1,100 @@
import { storage } from "../storage";
import type { User } from "@shared/schema";
interface RoleNode {
id: number;
parentId: number | null;
managerId: number | null;
children: RoleNode[];
}
function buildRoleTree(roles: { id: number; parentId: number | null; managerId: number | null }[]): RoleNode[] {
const map = new Map<number, RoleNode>();
for (const role of roles) {
map.set(role.id, { id: role.id, parentId: role.parentId, managerId: role.managerId, children: [] });
}
const roots: RoleNode[] = [];
for (const node of map.values()) {
if (node.parentId && map.has(node.parentId)) {
map.get(node.parentId)!.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
function findRoleNode(tree: RoleNode[], roleId: number): RoleNode | null {
for (const node of tree) {
if (node.id === roleId) return node;
const found = findRoleNode(node.children, roleId);
if (found) return found;
}
return null;
}
function collectSubtreeRoleIds(node: RoleNode): number[] {
const ids = [node.id];
for (const child of node.children) {
ids.push(...collectSubtreeRoleIds(child));
}
return ids;
}
export async function getManagedUserIds(
currentUserId: number,
organizationId: number
): Promise<Set<number>> {
const roles = await storage.getRoles(organizationId);
const managedRoleIds = roles
.filter(r => r.managerId === currentUserId)
.map(r => r.id);
if (managedRoleIds.length === 0) return new Set();
const tree = buildRoleTree(roles.map(r => ({ id: r.id, parentId: r.parentId, managerId: r.managerId })));
const allManagedRoleIds = new Set<number>();
for (const roleId of managedRoleIds) {
const node = findRoleNode(tree, roleId);
if (node) {
for (const id of collectSubtreeRoleIds(node)) {
allManagedRoleIds.add(id);
}
}
}
const managedUserIds = new Set<number>();
for (const role of roles) {
if (allManagedRoleIds.has(role.id)) {
for (const member of role.members) {
managedUserIds.add(member.userId);
}
}
}
return managedUserIds;
}
export async function canManageUser(
currentUser: User | undefined,
targetUserId: number,
organizationId: number
): Promise<boolean> {
if (!currentUser) return false;
if (currentUser.appRole === "admin") return true;
if (currentUser.id === targetUserId) return true;
const managedIds = await getManagedUserIds(currentUser.id, organizationId);
return managedIds.has(targetUserId);
}
export async function filterUsersByAccess(
currentUser: User | undefined,
organizationId: number,
users: { id: number }[]
): Promise<{ id: number }[]> {
if (!currentUser) return [];
if (currentUser.appRole === "admin") return users;
const managedIds = await getManagedUserIds(currentUser.id, organizationId);
return users.filter(u => managedIds.has(u.id) || u.id === currentUser.id);
}