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

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

131 lines
4.5 KiB
TypeScript

/**
* Унифицированный builder контекста для условных правил полей.
* Используется в валидаторах required/immutable и в переходах статусов.
*/
import {
SYSTEM_TASK_FIELDS,
buildSystemFieldValuesForConditions,
type FieldConditionContext,
} from "@shared/field-conditions";
import { resolveTaskFieldTitles, buildTableRowMap } from "../routes/shared";
export interface BuildFieldConditionContextOptions {
/** Текущая задача (может отсутствовать при создании). */
task?: { currentStatusId?: number | null; formId?: number; [key: string]: any } | null;
/** Поля формы (пользовательские). */
formFields: Array<{
id: number;
code: string;
type?: string;
options?: unknown;
autoProlongationLabel?: string | null;
}>;
organizationId: number;
storage: any;
/** true — режим первичного создания задачи. */
isCreation?: boolean;
/** Статусы формы (если не переданы — будут загружены). */
statuses?: Array<{ id: number; name?: string; position: number; isFinal?: boolean | null }>;
/** Уже сохранённые значения полей (для редактирования). */
existingFieldValues?: Array<{ fieldId: number; value: unknown }>;
/** Предзагруженные пользователи (оптимизация для batch). */
users?: Array<{
id: number;
firstName?: string | null;
middleName?: string | null;
lastName?: string | null;
email?: string;
}>;
/** Предзагруженные роли (оптимизация для batch). */
roles?: Array<{ id: number; name?: string }>;
}
export interface FieldConditionContextResult {
users: FieldConditionContext["users"];
roles: FieldConditionContext["roles"];
taskTitleMap: Map<number, string>;
tableMap: FieldConditionContext["tableMap"];
isCreation?: boolean;
statuses?: Array<{ id: number; name?: string; position: number; isFinal?: boolean | null }>;
/** Объединённые пользовательские и системные поля. */
fields: Array<{ id: number; code: string; type?: string; options?: unknown; autoProlongationLabel?: string | null }>;
/** Объединённые значения пользовательских и системных полей. */
fieldValues: Array<{ fieldId: number; value: unknown }>;
}
export async function buildFieldConditionContext(
options: BuildFieldConditionContextOptions
): Promise<FieldConditionContextResult> {
const {
task,
formFields,
organizationId,
storage,
isCreation,
statuses: providedStatuses,
existingFieldValues,
users: providedUsers,
roles: providedRoles,
} = options;
const [users, roles] = await Promise.all([
providedUsers ?? storage.getUsersByOrganization(organizationId),
providedRoles ?? storage.getRoles(organizationId),
]);
const usersWithFullName = users.map((u: any) => ({
...u,
fullName:
`${u.firstName || ""} ${u.middleName || ""} ${u.lastName || ""}`.trim() || u.email,
}));
const fieldValues: Array<{ fieldId: number; value: unknown }> = existingFieldValues
? [...existingFieldValues]
: [];
let statuses = providedStatuses;
if (!statuses && task?.formId) {
try {
statuses = await storage.getFormStatuses(task.formId, organizationId);
} catch {
statuses = undefined;
}
}
const statusName = task?.currentStatusId
? statuses?.find((s) => s.id === task.currentStatusId)?.name
: undefined;
if (task) {
const systemFieldValues = buildSystemFieldValuesForConditions(task, { statusName });
fieldValues.push(...systemFieldValues);
}
const valueByFieldId = new Map<number, any>(fieldValues.map((v) => [v.fieldId, v.value]));
const taskFieldsForTitle = formFields.filter((f): f is typeof f & { type: string } => !!f.type);
const [taskTitleMap, tableMap] = await Promise.all([
resolveTaskFieldTitles(valueByFieldId, taskFieldsForTitle, organizationId),
buildTableRowMap(formFields, organizationId),
]);
const fields = [...formFields, ...SYSTEM_TASK_FIELDS] as Array<{
id: number;
code: string;
type?: string;
options?: unknown;
autoProlongationLabel?: string | null;
}>;
return {
users: usersWithFullName,
roles,
taskTitleMap,
tableMap,
isCreation,
statuses,
fields,
fieldValues,
};
}