fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
130
server/utils/field-condition-context.ts
Normal file
130
server/utils/field-condition-context.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Унифицированный 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user