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,157 @@
import { db } from "../db";
import { eq, and, sql } from "drizzle-orm";
import { tasks, taskFieldValues, contractNumberCounters, forms, formFields } from "@shared/schema";
export interface ContractNumberConfig {
legalEntityFormId: number;
prefixFieldCode: string;
startNumber: number;
sequencePadding: number;
}
export interface GenerateContractNumberOpts {
organizationId: number;
formId: number;
legalEntityTaskId: number;
prefixFieldCode: string;
startNumber: number;
sequencePadding: number;
}
export interface PreviewContractNumberOpts {
organizationId: number;
formId: number;
legalEntityTaskId: number;
config: ContractNumberConfig;
}
/**
* Получить значение поля задачи по коду поля
*/
async function getTaskFieldValueByCode(
taskId: number,
fieldCode: string,
organizationId: number
): Promise<string | undefined> {
// Найти задачу
const [task] = await db
.select()
.from(tasks)
.innerJoin(forms, eq(tasks.formId, forms.id))
.where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId)))
.limit(1);
if (!task) return undefined;
// Найти поле по коду в форме задачи
const [field] = await db
.select()
.from(formFields)
.where(
and(
eq(formFields.formId, task.tasks.formId),
eq(formFields.code, fieldCode)
)
)
.limit(1);
if (!field) return undefined;
// Получить значение
const [value] = await db
.select()
.from(taskFieldValues)
.where(
and(
eq(taskFieldValues.taskId, taskId),
eq(taskFieldValues.fieldId, field.id)
)
)
.limit(1);
return value ? String(value.value) : undefined;
}
/**
* Сгенерировать номер договора атомарно (с резервацией счётчика)
*/
export async function generateContractNumber(
opts: GenerateContractNumberOpts
): Promise<string> {
const { organizationId, formId, legalEntityTaskId, prefixFieldCode, startNumber, sequencePadding } = opts;
// Получить префикс из задачи-юрлица по коду поля
const prefix = await getTaskFieldValueByCode(legalEntityTaskId, prefixFieldCode, organizationId);
if (!prefix) {
throw new Error(`Префикс не найден: поле ${prefixFieldCode} в задаче ${legalEntityTaskId}`);
}
const year = new Date().getFullYear() % 100;
// Атомарно получить/создать счётчик
const result = await db.execute(sql`
INSERT INTO contract_number_counters (organization_id, form_id, prefix, year, last_number)
VALUES (${organizationId}, ${formId}, ${prefix}, ${year}, ${startNumber})
ON CONFLICT (organization_id, form_id, prefix, year)
DO UPDATE SET
last_number = contract_number_counters.last_number + 1,
updated_at = NOW()
RETURNING last_number;
`);
const nextNumber = Number((result.rows[0] as any)?.last_number ?? startNumber);
const yearStr = year.toString().padStart(2, '0');
const seqStr = nextNumber.toString().padStart(sequencePadding ?? 3, '0');
return `${yearStr}${prefix}-${seqStr}`;
}
/**
* Предпросмотр номера (без резервации счётчика)
*/
export async function previewContractNumber(
opts: PreviewContractNumberOpts
): Promise<{ preview: string; year: number; prefix: string; nextNumber: number }> {
const { organizationId, formId, legalEntityTaskId, config } = opts;
const prefix = await getTaskFieldValueByCode(
legalEntityTaskId,
config.prefixFieldCode,
organizationId
);
if (!prefix) {
throw new Error(`Префикс не найден: поле ${config.prefixFieldCode} в задаче ${legalEntityTaskId}`);
}
const year = new Date().getFullYear() % 100;
// Заглянуть в существующий счётчик (без изменения)
const [counter] = await db
.select()
.from(contractNumberCounters)
.where(
and(
eq(contractNumberCounters.organizationId, organizationId),
eq(contractNumberCounters.formId, formId),
eq(contractNumberCounters.prefix, prefix),
eq(contractNumberCounters.year, year)
)
)
.limit(1);
const startNumber = config.startNumber ?? 1;
const sequencePadding = config.sequencePadding ?? 3;
const nextNumber = counter ? counter.lastNumber + 1 : startNumber;
const yearStr = year.toString().padStart(2, '0');
const seqStr = nextNumber.toString().padStart(sequencePadding, '0');
return {
preview: `${yearStr}${prefix}-${seqStr}`,
year,
prefix,
nextNumber,
};
}