From 547c51ca3cd94f57d494eab2e974a1cfc15f047c Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 16 Oct 2026 10:39:55 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=BE=D0=BB=D0=B2=D0=B0=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=B0=20=E2=80=94=20=D0=BD=D0=B0=20=D1=80=D1=83=D1=81?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=BC=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20=D0=B8=D0=BC=D1=91=D0=BD=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В сообщениях «не найден руководитель типа DirectionHead» и в конфликте ролей шага пользователь видел имена enum'ов. Теперь везде русские названия из общего справочника StepLabels (его же использует выгрузка маршрутов): «Не найден руководитель «Руководитель направления»: не указан у инициатора в справочнике сотрудников». Для директора Tansu и управляющего директора, которые берутся из настроек сервиса, — «Не проставлен «…»: email не задан в настройках сервиса». Так же исправлены ошибки о дубле шага с типом руководителя. Co-Authored-By: Claude Sonnet 5.5 --- .../Common/StepLabels.cs | 32 +++++++++++++++++++ .../Reports/RouteExportService.cs | 26 +++------------ .../TemplateRouteStepService.cs | 7 ++-- .../TemplateRoutes/TemplateRouteService.cs | 14 ++++++-- .../TemplateRouteServiceTests.cs | 5 +-- 5 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 backend/Tnsu.ApprovalRoute.Application/Common/StepLabels.cs diff --git a/backend/Tnsu.ApprovalRoute.Application/Common/StepLabels.cs b/backend/Tnsu.ApprovalRoute.Application/Common/StepLabels.cs new file mode 100644 index 0000000..5e92a8f --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/Common/StepLabels.cs @@ -0,0 +1,32 @@ +using Tnsu.ApprovalRoute.Domain.Enums; + +namespace Tnsu.ApprovalRoute.Application.Common; + +/// +/// Русские названия типов руководителя и ролей в проекте — для всего, что видит +/// человек: предупреждения резолва, ошибки валидации, выгрузка маршрутов. Имя +/// enum'а («DirectionHead») в тексте для пользователя недопустимо. +/// +public static class StepLabels +{ + public static string Of(HeadType headType) => headType switch + { + HeadType.Chief => "Непосредственный руководитель", + HeadType.DirectionHead => "Руководитель направления", + HeadType.DirectorTansu => "Директор Tansu", + HeadType.ManagingDirectorTansu => "Управляющий директор Tansu", + HeadType.DepartmentHead => "Руководитель указанного отдела", + HeadType.CompanyDirectors => "Директора компаний инициатора", + _ => headType.ToString() + }; + + public static string Of(ProjectRole projectRole) => projectRole switch + { + ProjectRole.ProjectManager => "Руководитель проекта", + ProjectRole.Planner => "Планировщик", + ProjectRole.ManagerHSE => "Менеджер HSE", + ProjectRole.Security => "Безопасность", + ProjectRole.Keeper => "Заведующий склада", + _ => projectRole.ToString() + }; +} diff --git a/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs b/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs index a8f04ff..785f5d7 100644 --- a/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs @@ -1,6 +1,7 @@ using FluentResults; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using Tnsu.ApprovalRoute.Application.Common; using Tnsu.ApprovalRoute.Domain.Entities; using Tnsu.ApprovalRoute.Domain.Enums; using Tnsu.ApprovalRoute.Infrastructure.Persistence; @@ -44,25 +45,6 @@ public sealed class RouteExportService( ["CompaniesJson"] = "организация" }; - private static readonly Dictionary ProjectRoleLabels = new() - { - [ProjectRole.ProjectManager] = "Руководитель проекта", - [ProjectRole.Planner] = "Планировщик", - [ProjectRole.ManagerHSE] = "Менеджер HSE", - [ProjectRole.Security] = "Безопасность", - [ProjectRole.Keeper] = "Заведующий склада" - }; - - private static readonly Dictionary HeadTypeLabels = new() - { - [HeadType.Chief] = "Непосредственный руководитель", - [HeadType.DirectionHead] = "Руководитель направления", - [HeadType.DirectorTansu] = "Директор Tansu", - [HeadType.ManagingDirectorTansu] = "Управляющий директор Tansu", - [HeadType.DepartmentHead] = "Руководитель указанного отдела", - [HeadType.CompanyDirectors] = "Директора компаний инициатора" - }; - private static readonly Dictionary OperatorSymbols = new() { [ComparisonOperator.GreaterThan] = ">", @@ -228,7 +210,7 @@ public sealed class RouteExportService( } if (step.ProjectRole is { } projectRole && projectRole != ProjectRole.None) - return (ProjectRoleLabels[projectRole], ByRequest, Empty); + return (StepLabels.Of(projectRole), ByRequest, Empty); if (step.HeadType is { } headType && headType != HeadType.None) { @@ -240,9 +222,9 @@ public sealed class RouteExportService( }; if (fixedEmail is not null) // Нет в справочнике — лучше показать email, чем пустую ячейку. - return (HeadTypeLabels[headType], directory.FindByEmail(fixedEmail)?.Fio ?? fixedEmail, directory.CompaniesOf(fixedEmail)); + return (StepLabels.Of(headType), directory.FindByEmail(fixedEmail)?.Fio ?? fixedEmail, directory.CompaniesOf(fixedEmail)); - return (HeadTypeLabels[headType], ByRequest, Empty); + return (StepLabels.Of(headType), ByRequest, Empty); } return (Empty, Empty, Empty); diff --git a/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index d1d9d29..f877a8a 100644 --- a/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -1,6 +1,7 @@ using FluentResults; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Tnsu.ApprovalRoute.Application.Common; using Tnsu.ApprovalRoute.Application.Common.Errors; using Tnsu.ApprovalRoute.Application.Common.Interfaces; using Tnsu.ApprovalRoute.Domain.Entities; @@ -82,7 +83,7 @@ public sealed class TemplateRouteStepService( if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: null, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(r.HeadType!.Value)}» и таким же условием появления — второй такой шаг недопустим.")); var entity = new TemplateRouteStep { @@ -141,7 +142,7 @@ public sealed class TemplateRouteStepService( if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: entity.Id, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(r.HeadType!.Value)}» и таким же условием появления — второй такой шаг недопустим.")); entity.OrderNo = r.OrderNo; entity.ExecutorEmail = r.ExecutorEmail; @@ -210,7 +211,7 @@ public sealed class TemplateRouteStepService( if (await HasDuplicateHeadTypeAsync(route.Id, headType, conditions, conditionsLogic, excludeId: entity.Id, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{headType}» и таким же условием появления — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(headType!.Value)}» и таким же условием появления — второй такой шаг недопустим.")); entity.OrderNo = r.OrderNo ?? entity.OrderNo; entity.ExecutorEmail = executorEmail; diff --git a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index 657f00d..16c686b 100644 --- a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -3,6 +3,7 @@ using FluentResults; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Tnsu.ApprovalRoute.Application.Common; using Tnsu.ApprovalRoute.Application.Common.Errors; using Tnsu.ApprovalRoute.Application.Common.Interfaces; using Tnsu.ApprovalRoute.Domain.Entities; @@ -437,7 +438,7 @@ public sealed class TemplateRouteService( { resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email; if (string.IsNullOrWhiteSpace(resolvedEmail)) - warning = $"Не найден исполнитель с ролью в проекте «{step.ProjectRole}»."; + warning = $"Не найден исполнитель с ролью в проекте «{StepLabels.Of(step.ProjectRole.Value)}»."; } // DepartmentHead — единственный HeadType, который резолвится НЕ по цепочке // подчинённости инициатора, а по отделу, переданному явно в запросе (см. @@ -469,7 +470,14 @@ public sealed class TemplateRouteService( _ => null }; if (string.IsNullOrWhiteSpace(resolvedEmail)) - warning = $"Не найден руководитель типа «{step.HeadType}»."; + { + var headLabel = StepLabels.Of(step.HeadType.Value); + // Директор Tansu и управляющий директор берутся из настроек сервиса, а не из + // справочника сотрудников — там email не «не найден», а «не проставлен». + warning = step.HeadType is HeadType.DirectorTansu or HeadType.ManagingDirectorTansu + ? $"Не проставлен «{headLabel}»: email не задан в настройках сервиса." + : $"Не найден руководитель «{headLabel}»: не указан у инициатора в справочнике сотрудников."; + } } else { @@ -612,7 +620,7 @@ public sealed class TemplateRouteService( else if (existing.PositionCode is not null && step.PositionCode is not null && existing.PositionCode != step.PositionCode) { var conflictWarning = - $"Исполнитель «{step.ExecutorEmail}» получен через разные роли шага ({existing.PositionCode} и {step.PositionCode}) — оставлен {existing.PositionCode}."; + $"Исполнитель «{step.ExecutorEmail}» получен через разные роли шага ({StepLabels.Of(existing.PositionCode.Value)} и {StepLabels.Of(step.PositionCode.Value)}) — оставлен «{StepLabels.Of(existing.PositionCode.Value)}»."; var combinedWarning = string.Join(" ", new[] { existing.Warning, conflictWarning } .Where(w => !string.IsNullOrWhiteSpace(w))); result[index] = existing with { Warning = combinedWarning }; diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index b3c3857..8b7c2af 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -361,8 +361,9 @@ public sealed class TemplateRouteServiceTests : IClassFixture