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>
65 lines
4.0 KiB
C#
65 lines
4.0 KiB
C#
using FluentValidation;
|
|
using Tnsu.ApprovalRoute.Domain.Enums;
|
|
|
|
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
|
|
|
|
public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator<CreateTemplateRouteStepRequest>
|
|
{
|
|
public CreateTemplateRouteStepRequestValidator()
|
|
{
|
|
RuleFor(x => x.Status).NotEqual(RouteStepStatus.None).WithMessage("Статус обязателен: Approval или Execution.");
|
|
RuleFor(x => x)
|
|
.Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1)
|
|
.WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
|
|
RuleFor(x => x)
|
|
.Must(x => TemplateRouteStepFields.AreConditionsValid(x.Conditions))
|
|
.WithMessage("В каждом условии (field_name, operation, value) все три поля обязательны.");
|
|
// Отрицательный SLA сделал бы шаг просроченным уже в момент активации;
|
|
// ноль допустим — это осознанное «напоминать сразу».
|
|
RuleFor(x => x.SlaHours)
|
|
.GreaterThanOrEqualTo(0)
|
|
.When(x => x.SlaHours is not null)
|
|
.WithMessage("SLA в часах не может быть отрицательным.");
|
|
}
|
|
}
|
|
|
|
public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator<UpdateTemplateRouteStepRequest>
|
|
{
|
|
public UpdateTemplateRouteStepRequestValidator()
|
|
{
|
|
RuleFor(x => x.Status).NotEqual(RouteStepStatus.None).WithMessage("Статус обязателен: Approval или Execution.");
|
|
RuleFor(x => x)
|
|
.Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1)
|
|
.WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
|
|
RuleFor(x => x)
|
|
.Must(x => TemplateRouteStepFields.AreConditionsValid(x.Conditions))
|
|
.WithMessage("В каждом условии (field_name, operation, value) все три поля обязательны.");
|
|
// Отрицательный SLA сделал бы шаг просроченным уже в момент активации;
|
|
// ноль допустим — это осознанное «напоминать сразу».
|
|
RuleFor(x => x.SlaHours)
|
|
.GreaterThanOrEqualTo(0)
|
|
.When(x => x.SlaHours is not null)
|
|
.WithMessage("SLA в часах не может быть отрицательным.");
|
|
}
|
|
}
|
|
|
|
internal static class TemplateRouteStepFields
|
|
{
|
|
public static int CountFilled(string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType) =>
|
|
(string.IsNullOrWhiteSpace(executorEmail) ? 0 : 1) +
|
|
(string.IsNullOrWhiteSpace(position) ? 0 : 1) +
|
|
(projectRole is null or ProjectRole.None ? 0 : 1) +
|
|
(headType is null or HeadType.None ? 0 : 1);
|
|
|
|
/// <summary>
|
|
/// Условие с пустым field_name/value или operation=None — половинчатое, а не
|
|
/// «выключенное» (выключается вовсе не добавляя условие в список), поэтому
|
|
/// такое отдельное условие недопустимо, даже если остальные условия шага корректны.
|
|
/// </summary>
|
|
public static bool AreConditionsValid(IReadOnlyList<RouteStepConditionDto>? conditions) =>
|
|
conditions is null || conditions.All(c =>
|
|
!string.IsNullOrWhiteSpace(c.FieldName) &&
|
|
c.Operation != ComparisonOperator.None &&
|
|
!string.IsNullOrWhiteSpace(c.Value));
|
|
}
|