/** * Унифицированный 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; 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 { 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(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, }; }