diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs index 6cac78c..7946471 100644 --- a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs +++ b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Tnsu.ApprovalRoute.Api.Common; using Tnsu.ApprovalRoute.Application.TemplateRoutes; +using Tnsu.ApprovalRoute.Domain.Enums; namespace Tnsu.ApprovalRoute.Api.Controllers; @@ -11,6 +12,15 @@ namespace Tnsu.ApprovalRoute.Api.Controllers; [Route("api/template-routes")] public sealed class TemplateRoutesController(ISender mediator) : ControllerBase { + [HttpGet("resolve")] + public async Task Resolve( + [FromQuery] Guid requestTypeId, + [FromQuery] string? userEmail, + [FromQuery] Guid? projectId, + [FromQuery] RouteStepStatus status, + CancellationToken ct) => + (await mediator.Send(new ResolveTemplateRouteQuery(requestTypeId, userEmail, projectId, status), ct)).ToActionResult(); + [HttpGet] public async Task List([FromQuery] Guid? requestTypeId, CancellationToken ct) => (await mediator.Send(new ListTemplateRoutesQuery(requestTypeId), ct)).ToActionResult(); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs new file mode 100644 index 0000000..7f828dd --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs @@ -0,0 +1,19 @@ +using Tnsu.ApprovalRoute.Domain.Enums; + +namespace Tnsu.ApprovalRoute.Application.TemplateRoutes; + +public sealed record ResolvedRouteStepDto( + Guid Id, + int OrderNo, + string ExecutorEmail, + string Position, + ProjectRole ProjectRole, + HeadType HeadType, + RouteStepStatus Status, + bool AllowReassignToOtherApprovers); + +public sealed record ResolveTemplateRouteResultDto( + Guid RouteId, + RouteType RouteType, + bool RequiresControl, + IReadOnlyList Steps); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs new file mode 100644 index 0000000..5e83386 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs @@ -0,0 +1,95 @@ +using FluentResults; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Tnsu.ApprovalRoute.Application.Common.Errors; +using Tnsu.ApprovalRoute.Application.Common.Interfaces; +using Tnsu.ApprovalRoute.Domain.Entities; +using Tnsu.ApprovalRoute.Domain.Enums; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; +using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; + +namespace Tnsu.ApprovalRoute.Application.TemplateRoutes; + +public sealed record ResolveTemplateRouteQuery( + Guid RequestTypeId, + string? UserEmail, + Guid? ProjectId, + RouteStepStatus Status) : IRequest>; + +public sealed class ResolveTemplateRouteHandler( + ApprovalRouteDbContext db, + HttpTnsuPortalClient portalClient, + ICurrentUser currentUser) + : IRequestHandler> +{ + public async Task> Handle(ResolveTemplateRouteQuery request, CancellationToken cancellationToken) + { + if (request.Status == RouteStepStatus.None) + return Result.Fail( + new ValidationError("Статус обязателен: Approval или Execution.")); + + var email = string.IsNullOrWhiteSpace(request.UserEmail) ? currentUser.Email : request.UserEmail; + if (string.IsNullOrWhiteSpace(email)) + return Result.Fail( + new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене.")); + + var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken); + if (!requestTypeExists) + return Result.Fail(new NotFoundError("Тип заявки", request.RequestTypeId)); + + var employee = await portalClient.GetEmployeeByEmailAsync(email, cancellationToken); + var byProject = request.ProjectId is not null; + + var route = await db.TemplateRoutes.AsNoTracking() + .Include(x => x.Steps) + .Where(x => + x.RequestTypeId == request.RequestTypeId && + x.CompanyId == employee.CompanyId && + x.ByProject == byProject && + x.DepartmentId == employee.DepartmentId) + .FirstOrDefaultAsync(cancellationToken) + ?? await db.TemplateRoutes.AsNoTracking() + .Include(x => x.Steps) + .Where(x => + x.RequestTypeId == request.RequestTypeId && + x.CompanyId == employee.CompanyId && + x.ByProject == byProject && + x.DepartmentId == null) + .FirstOrDefaultAsync(cancellationToken); + + if (route is null) + return Result.Fail(new NotFoundError( + "Маршрут для этого типа заявки, отдела/компании инициатора и признака «по проекту»", + request.RequestTypeId)); + + IReadOnlyList projectExecutors = []; + if (request.ProjectId is { } projectId) + projectExecutors = await portalClient.GetProjectExecutorsAsync(projectId, cancellationToken); + + var steps = route.Steps + .Where(s => s.Status == request.Status) + .OrderBy(s => s.OrderNo) + .Select(s => ResolveStep(s, projectExecutors)) + .ToList(); + + return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, steps)); + } + + private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList projectExecutors) + { + // Для шагов с ролью в проекте фактический исполнитель приходит из данных проекта (api.tnsu.kz), + // а не из статичного ExecutorEmail шаблона — тот используется только для шагов без привязки к проекту. + if (step.ProjectRole != ProjectRole.None) + { + var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole); + if (projectExecutor is not null) + return new ResolvedRouteStepDto( + step.Id, step.OrderNo, projectExecutor.Email, projectExecutor.Position, + step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers); + } + + return new ResolvedRouteStepDto( + step.Id, step.OrderNo, step.ExecutorEmail, step.Position, + step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers); + } +}