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

Раньше шаг маршрута мог нести ровно одно условие (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
@@ -10,6 +10,7 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
public DbSet<RequestListPartition> RequestListPartitions => Set<RequestListPartition>();
public DbSet<TemplateRoute> TemplateRoutes => Set<TemplateRoute>();
public DbSet<TemplateRouteStep> TemplateRouteSteps => Set<TemplateRouteStep>();
public DbSet<TemplateRouteStepCondition> TemplateRouteStepConditions => Set<TemplateRouteStepCondition>();
public DbSet<AppSetting> Settings => Set<AppSetting>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -61,11 +62,19 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
e.Property(x => x.ProjectRole).HasConversion<string>().HasMaxLength(32);
e.Property(x => x.HeadType).HasConversion<string>().HasMaxLength(32);
e.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
e.Property(x => x.FieldName).HasMaxLength(128);
e.Property(x => x.Operation).HasConversion<string>().HasMaxLength(32);
e.Property(x => x.Value).HasMaxLength(256);
e.Property(x => x.ConditionsLogic).HasConversion<string>().HasMaxLength(16).IsRequired();
e.HasOne(x => x.Route).WithMany(x => x.Steps).HasForeignKey(x => x.RouteId);
e.HasIndex(x => new { x.RouteId, x.OrderNo });
});
modelBuilder.Entity<TemplateRouteStepCondition>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.FieldName).HasMaxLength(128).IsRequired();
e.Property(x => x.Operation).HasConversion<string>().HasMaxLength(32).IsRequired();
e.Property(x => x.Value).HasMaxLength(256).IsRequired();
e.HasOne(x => x.Step).WithMany(x => x.Conditions).HasForeignKey(x => x.StepId).OnDelete(DeleteBehavior.Cascade);
e.HasIndex(x => new { x.StepId, x.OrderNo });
});
}
}