Несколько условий шага, объединяемых через И/ИЛИ

Раньше шаг маршрута мог нести ровно одно условие (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>
This commit is contained in:
gorbenko.pavel
2026-08-25 09:56:36 +05:00
parent e4a6cb8167
commit 10c3057eff
21 changed files with 1201 additions and 346 deletions
@@ -7,6 +7,9 @@ namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
// (0 часов и false — это осознанный выбор, а не «пусто»). Добавлены в конец
// записей, чтобы не ломать существующие позиционные вызовы.
/// <summary>Одно условие появления шага — см. Domain.Entities.TemplateRouteStepCondition.</summary>
public sealed record RouteStepConditionDto(string FieldName, ComparisonOperator Operation, string Value);
public sealed record TemplateRouteStepDto(
int Id,
int OrderNo,
@@ -17,9 +20,8 @@ public sealed record TemplateRouteStepDto(
RouteStepStatus Status,
int RouteId,
bool AllowReassignToOtherApprovers,
string? FieldName,
ComparisonOperator? Operation,
string? Value,
IReadOnlyList<RouteStepConditionDto> Conditions,
ConditionsLogic ConditionsLogic,
string? Warning = null,
int? SlaHours = null,
bool? AskOverdueReason = null);
@@ -32,9 +34,8 @@ public sealed record CreateTemplateRouteStepRequest(
HeadType? HeadType,
RouteStepStatus Status,
bool AllowReassignToOtherApprovers,
string? FieldName,
ComparisonOperator? Operation,
string? Value,
IReadOnlyList<RouteStepConditionDto>? Conditions,
ConditionsLogic ConditionsLogic = ConditionsLogic.And,
int? SlaHours = null,
bool? AskOverdueReason = null);
@@ -46,12 +47,16 @@ public sealed record UpdateTemplateRouteStepRequest(
HeadType? HeadType,
RouteStepStatus Status,
bool AllowReassignToOtherApprovers,
string? FieldName,
ComparisonOperator? Operation,
string? Value,
IReadOnlyList<RouteStepConditionDto>? Conditions,
ConditionsLogic ConditionsLogic = ConditionsLogic.And,
int? SlaHours = null,
bool? AskOverdueReason = null);
/// <summary>
/// Как и остальные поля PATCH, Conditions=null означает «не менять». Отличить
/// «не менять» от «убрать все условия» здесь нельзя (тот же приём, что и раньше
/// для FieldName) — очистка условий делается через PUT с Conditions: [].
/// </summary>
public sealed record PatchTemplateRouteStepRequest(
int? OrderNo,
string? ExecutorEmail,
@@ -60,8 +65,7 @@ public sealed record PatchTemplateRouteStepRequest(
HeadType? HeadType,
RouteStepStatus? Status,
bool? AllowReassignToOtherApprovers,
string? FieldName,
ComparisonOperator? Operation,
string? Value,
IReadOnlyList<RouteStepConditionDto>? Conditions,
ConditionsLogic? ConditionsLogic = null,
int? SlaHours = null,
bool? AskOverdueReason = null);