Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/utils/stepExecutor.ts
T
gorbenko.pavel 10c3057eff Несколько условий шага, объединяемых через И/ИЛИ
Раньше шаг маршрута мог нести ровно одно условие (field_name/operation/
value). Теперь условий может быть сколько угодно, а их объединение задаётся
явно — ConditionsLogic.And (все условия) или .Or (любое из условий).

Бэкенд:
- Новая сущность TemplateRouteStepCondition (дочерняя таблица
  template_route_step_conditions, FK на шаг, cascade delete) вместо трёх
  колонок на TemplateRouteStep. Миграция AddMultipleStepConditions переносит
  существующие условия шагов в новую таблицу перед удалением старых колонок,
  чтобы не потерять уже настроенные в деве/проде условия.
- TemplateRouteService.MatchesConditions: несколько условий шага объединяются
  по ConditionsLogic (Any/All); HasUnevaluatableManagerCondition проверяет
  условие на IsManager в списке условий, а не единственное поле.
- TemplateRouteStepService: дубликаты шагов теперь сравниваются по всему
  набору условий (без учёта порядка) и, если условий два и более, по логике
  их объединения — сравнение в памяти, а не в SQL, кандидатов на дубликат
  в пределах маршрута всегда мало.
- Валидация условия: раньше «все три поля или ни одного», теперь — в каждом
  элементе списка все три поля обязательны.

Фронтенд:
- RouteStepFormModal: условие — динамический список строк (добавить/удалить),
  у каждой строки свой набор допустимых операторов по типу колонки; селектор
  И/ИЛИ появляется, когда условий два и больше.
- describeStepCondition в таблице шагов склеивает все условия через « И »/« ИЛИ ».

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:56:36 +05:00

53 lines
2.9 KiB
TypeScript

import { ConditionsLogic, Employee, HeadType, ProjectRole, StepExecutorKind, TemplateRouteStep } from '../models';
import { comparisonOperatorSymbols, conditionsLogicJoiners, headTypeLabels, projectRoleLabels, texts } from '../texts';
import { findEmployeeByEmail } from './employee';
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
/* eslint-disable @rushstack/no-new-null -- форма зеркалит TemplateRouteStep (nullable JSON-контракт бэкенда) */
export function detectExecutorKind(step: {
executorEmail?: string | null;
position?: string | null;
projectRole?: ProjectRole | null;
headType?: HeadType | null;
}): StepExecutorKind {
/* eslint-enable @rushstack/no-new-null */
if (step.executorEmail) return 'email';
if (step.position) return 'position';
if (step.projectRole && step.projectRole !== ProjectRole.None) return 'projectRole';
return 'headType';
}
export interface StepExecutorDisplay {
primary: string;
/** Должность и email под ФИО — когда исполнитель задан конкретным сотрудником и тот найден в справочнике. */
secondary?: string;
}
/** Колонка «Исполнитель» в таблице шагов шаблона маршрута. */
export function describeStepExecutor(step: TemplateRouteStep, employees: Employee[]): StepExecutorDisplay {
if (step.executorEmail) {
const employee = findEmployeeByEmail(employees, step.executorEmail ?? undefined);
if (!employee) return { primary: step.executorEmail };
// Должность рядом с email: по одному ФИО не видно, тот ли это человек, что
// задумывался при настройке маршрута (в справочнике есть однофамильцы).
return { primary: employee.fio, secondary: [employee.position, employee.email].filter(Boolean).join(' · ') };
}
if (step.position) return { primary: step.position };
if (step.projectRole && step.projectRole !== ProjectRole.None) {
return { primary: texts.routeSteps.executorRole(projectRoleLabels[step.projectRole]) };
}
if (step.headType && step.headType !== HeadType.None) {
return { primary: texts.routeSteps.executorHead(headTypeLabels[step.headType]) };
}
return { primary: texts.common.empty };
}
/** Колонка «Условие» в таблице шагов — undefined, если у шага условий нет. */
export function describeStepCondition(step: TemplateRouteStep): string | undefined {
if (step.conditions.length === 0) return undefined;
const joiner = conditionsLogicJoiners[step.conditionsLogic ?? ConditionsLogic.And];
return step.conditions
.map(c => `${c.fieldName} ${comparisonOperatorSymbols[c.operation]} ${c.value}`)
.join(joiner);
}