- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
131 lines
4.5 KiB
TypeScript
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,
|
|
};
|
|
}
|