Предупреждения резолва маршрута — на русском вместо имён enum
В сообщениях «не найден руководитель типа DirectionHead» и в конфликте ролей шага пользователь видел имена enum'ов. Теперь везде русские названия из общего справочника StepLabels (его же использует выгрузка маршрутов): «Не найден руководитель «Руководитель направления»: не указан у инициатора в справочнике сотрудников». Для директора Tansu и управляющего директора, которые берутся из настроек сервиса, — «Не проставлен «…»: email не задан в настройках сервиса». Так же исправлены ошибки о дубле шага с типом руководителя. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.Application.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Русские названия типов руководителя и ролей в проекте — для всего, что видит
|
||||||
|
/// человек: предупреждения резолва, ошибки валидации, выгрузка маршрутов. Имя
|
||||||
|
/// enum'а («DirectionHead») в тексте для пользователя недопустимо.
|
||||||
|
/// </summary>
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentResults;
|
using FluentResults;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Tnsu.ApprovalRoute.Application.Common;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||||
@@ -44,25 +45,6 @@ public sealed class RouteExportService(
|
|||||||
["CompaniesJson"] = "организация"
|
["CompaniesJson"] = "организация"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<ProjectRole, string> ProjectRoleLabels = new()
|
|
||||||
{
|
|
||||||
[ProjectRole.ProjectManager] = "Руководитель проекта",
|
|
||||||
[ProjectRole.Planner] = "Планировщик",
|
|
||||||
[ProjectRole.ManagerHSE] = "Менеджер HSE",
|
|
||||||
[ProjectRole.Security] = "Безопасность",
|
|
||||||
[ProjectRole.Keeper] = "Заведующий склада"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly Dictionary<HeadType, string> HeadTypeLabels = new()
|
|
||||||
{
|
|
||||||
[HeadType.Chief] = "Непосредственный руководитель",
|
|
||||||
[HeadType.DirectionHead] = "Руководитель направления",
|
|
||||||
[HeadType.DirectorTansu] = "Директор Tansu",
|
|
||||||
[HeadType.ManagingDirectorTansu] = "Управляющий директор Tansu",
|
|
||||||
[HeadType.DepartmentHead] = "Руководитель указанного отдела",
|
|
||||||
[HeadType.CompanyDirectors] = "Директора компаний инициатора"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly Dictionary<ComparisonOperator, string> OperatorSymbols = new()
|
private static readonly Dictionary<ComparisonOperator, string> OperatorSymbols = new()
|
||||||
{
|
{
|
||||||
[ComparisonOperator.GreaterThan] = ">",
|
[ComparisonOperator.GreaterThan] = ">",
|
||||||
@@ -228,7 +210,7 @@ public sealed class RouteExportService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (step.ProjectRole is { } projectRole && projectRole != ProjectRole.None)
|
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)
|
if (step.HeadType is { } headType && headType != HeadType.None)
|
||||||
{
|
{
|
||||||
@@ -240,9 +222,9 @@ public sealed class RouteExportService(
|
|||||||
};
|
};
|
||||||
if (fixedEmail is not null)
|
if (fixedEmail is not null)
|
||||||
// Нет в справочнике — лучше показать email, чем пустую ячейку.
|
// Нет в справочнике — лучше показать 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);
|
return (Empty, Empty, Empty);
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
using FluentResults;
|
using FluentResults;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Tnsu.ApprovalRoute.Application.Common;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
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))
|
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: null, cancellationToken))
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(r.HeadType!.Value)}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
var entity = new TemplateRouteStep
|
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))
|
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: entity.Id, cancellationToken))
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(r.HeadType!.Value)}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
entity.OrderNo = r.OrderNo;
|
entity.OrderNo = r.OrderNo;
|
||||||
entity.ExecutorEmail = r.ExecutorEmail;
|
entity.ExecutorEmail = r.ExecutorEmail;
|
||||||
@@ -210,7 +211,7 @@ public sealed class TemplateRouteStepService(
|
|||||||
|
|
||||||
if (await HasDuplicateHeadTypeAsync(route.Id, headType, conditions, conditionsLogic, excludeId: entity.Id, cancellationToken))
|
if (await HasDuplicateHeadTypeAsync(route.Id, headType, conditions, conditionsLogic, excludeId: entity.Id, cancellationToken))
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{headType}» и таким же условием появления — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{StepLabels.Of(headType!.Value)}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
||||||
entity.ExecutorEmail = executorEmail;
|
entity.ExecutorEmail = executorEmail;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using FluentResults;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Tnsu.ApprovalRoute.Application.Common;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||||
@@ -437,7 +438,7 @@ public sealed class TemplateRouteService(
|
|||||||
{
|
{
|
||||||
resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email;
|
resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email;
|
||||||
if (string.IsNullOrWhiteSpace(resolvedEmail))
|
if (string.IsNullOrWhiteSpace(resolvedEmail))
|
||||||
warning = $"Не найден исполнитель с ролью в проекте «{step.ProjectRole}».";
|
warning = $"Не найден исполнитель с ролью в проекте «{StepLabels.Of(step.ProjectRole.Value)}».";
|
||||||
}
|
}
|
||||||
// DepartmentHead — единственный HeadType, который резолвится НЕ по цепочке
|
// DepartmentHead — единственный HeadType, который резолвится НЕ по цепочке
|
||||||
// подчинённости инициатора, а по отделу, переданному явно в запросе (см.
|
// подчинённости инициатора, а по отделу, переданному явно в запросе (см.
|
||||||
@@ -469,7 +470,14 @@ public sealed class TemplateRouteService(
|
|||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
if (string.IsNullOrWhiteSpace(resolvedEmail))
|
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
|
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)
|
else if (existing.PositionCode is not null && step.PositionCode is not null && existing.PositionCode != step.PositionCode)
|
||||||
{
|
{
|
||||||
var conflictWarning =
|
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 }
|
var combinedWarning = string.Join(" ", new[] { existing.Warning, conflictWarning }
|
||||||
.Where(w => !string.IsNullOrWhiteSpace(w)));
|
.Where(w => !string.IsNullOrWhiteSpace(w)));
|
||||||
result[index] = existing with { Warning = combinedWarning };
|
result[index] = existing with { Warning = combinedWarning };
|
||||||
|
|||||||
+3
-2
@@ -361,8 +361,9 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
|||||||
resolved.Value.Steps[0].PositionCode.ShouldBe(HeadType.Chief);
|
resolved.Value.Steps[0].PositionCode.ShouldBe(HeadType.Chief);
|
||||||
var warning = resolved.Value.Steps[0].Warning;
|
var warning = resolved.Value.Steps[0].Warning;
|
||||||
warning.ShouldNotBeNull();
|
warning.ShouldNotBeNull();
|
||||||
warning.ShouldContain("Chief");
|
warning.ShouldContain("Непосредственный руководитель");
|
||||||
warning.ShouldContain("DirectionHead");
|
warning.ShouldContain("Руководитель направления");
|
||||||
|
warning.ShouldNotContain("DirectionHead");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(DisplayName = "Резолв маршрута с шагом «Руководитель указанного отдела» без departmentId в запросе — маршрут формируется, шаг с предупреждением")]
|
[Fact(DisplayName = "Резолв маршрута с шагом «Руководитель указанного отдела» без departmentId в запросе — маршрут формируется, шаг с предупреждением")]
|
||||||
|
|||||||
Reference in New Issue
Block a user