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

View File

@@ -0,0 +1,109 @@
import { userStatuses, users, type UserStatus, type InsertUserStatus } from "@shared/schema";
import { db } from "../db";
import { eq, and, asc, sql } from "drizzle-orm";
export class UserStatusesStorage {
async getById(id: number): Promise<UserStatus | undefined> {
const [status] = await db.select().from(userStatuses).where(eq(userStatuses.id, id));
return status || undefined;
}
async listByOrganization(organizationId: number): Promise<UserStatus[]> {
return await db
.select()
.from(userStatuses)
.where(eq(userStatuses.organizationId, organizationId))
.orderBy(asc(userStatuses.position), asc(userStatuses.name));
}
async getDefaultByOrganization(organizationId: number): Promise<UserStatus | undefined> {
const [status] = await db
.select()
.from(userStatuses)
.where(and(eq(userStatuses.organizationId, organizationId), eq(userStatuses.isDefault, true)))
.limit(1);
return status || undefined;
}
async create(data: InsertUserStatus): Promise<UserStatus> {
const { organizationId, isDefault } = data;
if (isDefault) {
await db
.update(userStatuses)
.set({ isDefault: false })
.where(eq(userStatuses.organizationId, organizationId));
}
const [status] = await db.insert(userStatuses).values(data).returning();
return status;
}
async update(id: number, organizationId: number, data: Partial<InsertUserStatus>): Promise<UserStatus> {
if (data.isDefault) {
await db
.update(userStatuses)
.set({ isDefault: false })
.where(eq(userStatuses.organizationId, organizationId));
}
const [status] = await db
.update(userStatuses)
.set(data)
.where(and(eq(userStatuses.id, id), eq(userStatuses.organizationId, organizationId)))
.returning();
return status;
}
async delete(id: number, organizationId: number): Promise<void> {
const status = await this.getById(id);
if (!status || status.organizationId !== organizationId) return;
const defaultStatus = await this.getDefaultByOrganization(organizationId);
await db.transaction(async (tx) => {
if (defaultStatus && defaultStatus.id !== id) {
await tx
.update(users)
.set({ statusId: defaultStatus.id })
.where(eq(users.statusId, id));
} else {
await tx
.update(users)
.set({ statusId: null })
.where(eq(users.statusId, id));
}
await tx
.delete(userStatuses)
.where(and(eq(userStatuses.id, id), eq(userStatuses.organizationId, organizationId)));
});
}
async ensureDefaultStatus(organizationId: number): Promise<UserStatus> {
const existing = await this.getDefaultByOrganization(organizationId);
if (existing) return existing;
const [status] = await db
.insert(userStatuses)
.values({
organizationId,
name: "Зарегистрирован",
color: "#e5e7eb",
isDefault: true,
position: 0,
})
.returning();
return status;
}
async getUsageCount(statusId: number): Promise<number> {
const [result] = await db
.select({ total: sql<number>`count(*)::int` })
.from(users)
.where(eq(users.statusId, statusId));
return result?.total || 0;
}
}
export const userStatusesStorage = new UserStatusesStorage();