Заглушка для департамента (#8)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/8
This commit was merged in pull request #8.
This commit is contained in:
+37
@@ -56,6 +56,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, excludeId: null, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим."));
|
||||
|
||||
var entity = new TemplateRouteStep
|
||||
{
|
||||
RouteId = route.Id,
|
||||
@@ -105,6 +109,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, excludeId: entity.Id, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим."));
|
||||
|
||||
entity.OrderNo = r.OrderNo;
|
||||
entity.ExecutorEmail = r.ExecutorEmail;
|
||||
entity.Position = r.Position;
|
||||
@@ -165,6 +173,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, headType, excludeId: entity.Id, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{headType}» — второй такой шаг недопустим."));
|
||||
|
||||
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
||||
entity.ExecutorEmail = executorEmail;
|
||||
entity.Position = position;
|
||||
@@ -208,6 +220,31 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
x.Status == status,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Любой HeadType (Chief/DirectionHead/DirectorTansu/DepartmentHead) резолвится
|
||||
/// в ОДНОГО конкретного человека — цепочка подчинённости инициатора или, для
|
||||
/// DepartmentHead, единственный departmentId запроса (см.
|
||||
/// ResolveTemplateRouteRequest.DepartmentId) не оставляют места для выбора.
|
||||
/// Поэтому два шага одного маршрута с одинаковым HeadType всегда резолвились бы
|
||||
/// в одного и того же (или неотличимо какого) руководителя — например, «два
|
||||
/// разных директора одной и той же компании» просто не существуют. В отличие
|
||||
/// от HasDuplicateAsync (точное совпадение ВСЕХ полей включая статус), здесь
|
||||
/// достаточно самого факта совпадения HeadType — вне зависимости от статуса
|
||||
/// или остальных полей шага.
|
||||
/// </summary>
|
||||
private Task<bool> HasDuplicateHeadTypeAsync(
|
||||
int routeId, HeadType? headType, int? excludeId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (headType is null or HeadType.None)
|
||||
return Task.FromResult(false);
|
||||
|
||||
return db.TemplateRouteSteps.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RouteId == routeId &&
|
||||
x.HeadType == headType,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
|
||||
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
+9
-1
@@ -18,9 +18,17 @@ public sealed record ResolveTemplateRouteResultDto(
|
||||
/// <summary>Значение поля списка SharePoint (field_name шага) для проверки условий шагов при resolve.</summary>
|
||||
public sealed record ResolveFieldValueDto(string FieldName, string Value);
|
||||
|
||||
/// <param name="DepartmentId">
|
||||
/// Отдел для шагов с HeadType.DepartmentHead ("руководитель указанного отдела") —
|
||||
/// не выводится из инициатора, поэтому должен быть передан явно. Необязателен:
|
||||
/// если в маршруте нет такого шага, поле не нужно; если есть, а поле не
|
||||
/// передано — шаг не резолвится и получает предупреждение (см.
|
||||
/// TemplateRouteService.ResolveStepAsync), весь запрос при этом не падает.
|
||||
/// </param>
|
||||
public sealed record ResolveTemplateRouteRequest(
|
||||
int RequestTypeId,
|
||||
string? UserEmail,
|
||||
Guid? ProjectId,
|
||||
RouteStepStatus Status,
|
||||
IReadOnlyList<ResolveFieldValueDto>? FieldValues = null);
|
||||
IReadOnlyList<ResolveFieldValueDto>? FieldValues = null,
|
||||
Guid? DepartmentId = null);
|
||||
|
||||
@@ -168,7 +168,7 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var steps = new List<ResolvedRouteStepDto>();
|
||||
foreach (var step in matchingSteps)
|
||||
steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, cancellationToken));
|
||||
steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, request.DepartmentId, cancellationToken));
|
||||
|
||||
var collapsedSteps = CollapseByExecutorEmail(steps);
|
||||
|
||||
@@ -212,7 +212,7 @@ public sealed class TemplateRouteService(
|
||||
|
||||
private async Task<ResolvedRouteStepDto> ResolveStepAsync(
|
||||
TemplateRouteStep step, string initiatorEmail, string initiatorCompany,
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors, CancellationToken cancellationToken)
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors, Guid? departmentId, CancellationToken cancellationToken)
|
||||
{
|
||||
string? resolvedEmail;
|
||||
string? warning = null;
|
||||
@@ -233,6 +233,25 @@ public sealed class TemplateRouteService(
|
||||
if (string.IsNullOrWhiteSpace(resolvedEmail))
|
||||
warning = $"Не найден исполнитель с ролью в проекте «{step.ProjectRole}».";
|
||||
}
|
||||
// DepartmentHead — единственный HeadType, который резолвится НЕ по цепочке
|
||||
// подчинённости инициатора, а по отделу, переданному явно в запросе (см.
|
||||
// ResolveTemplateRouteRequest.DepartmentId): отдел инициатора тут ни при
|
||||
// чём, шаг про «руководителя ТАКОГО-ТО отдела», а не «моего руководителя».
|
||||
// Поэтому у него отдельная ветка, а не место в switch чуть ниже.
|
||||
else if (step.HeadType == HeadType.DepartmentHead)
|
||||
{
|
||||
if (departmentId is null)
|
||||
{
|
||||
resolvedEmail = null;
|
||||
warning = "Шаг требует руководителя указанного отдела, но departmentId не передан в запросе.";
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedEmail = await portalClient.GetDepartmentHeadEmailAsync(departmentId.Value, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(resolvedEmail))
|
||||
warning = $"Не найден руководитель отдела с id «{departmentId}».";
|
||||
}
|
||||
}
|
||||
else if (step.HeadType is not (null or HeadType.None))
|
||||
{
|
||||
resolvedEmail = step.HeadType switch
|
||||
|
||||
@@ -5,5 +5,14 @@ public enum HeadType
|
||||
None,
|
||||
Chief,
|
||||
DirectionHead,
|
||||
DirectorTansu
|
||||
DirectorTansu,
|
||||
|
||||
/// <summary>
|
||||
/// Руководитель отдела, указанного явно в запросе на resolve (см.
|
||||
/// ResolveTemplateRouteRequest.DepartmentId), а не отдела инициатора —
|
||||
/// этим отличается от Chief/DirectionHead, которые всегда идут по цепочке
|
||||
/// подчинённости самого инициатора. В маршруте не может быть больше одного
|
||||
/// шага с этим типом (см. TemplateRouteStepService).
|
||||
/// </summary>
|
||||
DepartmentHead
|
||||
}
|
||||
|
||||
@@ -56,6 +56,16 @@ public sealed class HttpTnsuPortalClient(
|
||||
return executors;
|
||||
}
|
||||
|
||||
// STUB(department-head): в api.tnsu.kz пока нет эндпоинта «руководитель отдела
|
||||
// по Id отдела» — до его появления просто возвращаем захардкоженный email,
|
||||
// чтобы можно было отладить всю остальную логику HeadType.DepartmentHead
|
||||
// (resolve, warning-и, UI) не дожидаясь портала. Когда эндпоинт появится —
|
||||
// заменить тело этого метода на реальный вызов через GetAsync<T> (по
|
||||
// образцу остальных методов этого класса); сигнатуру и вызывающий код
|
||||
// (TemplateRouteService.ResolveStepAsync) менять не придётся.
|
||||
public Task<string?> GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<string?>("gorbenko.pavel@tnsu.kz");
|
||||
|
||||
private async Task<T?> GetAsync<T>(string requestUri, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
|
||||
@@ -9,4 +9,7 @@ public interface ITnsuPortalClient
|
||||
Task<string?> GetManagerAsync(string email, string managerType, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Email руководителя указанного отдела (для HeadType.DepartmentHead) — null, если не найден.</summary>
|
||||
Task<string?> GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user