diff --git a/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs b/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs index 22d4569..7bdf15a 100644 --- a/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/Reports/RouteExportService.cs @@ -24,7 +24,6 @@ public sealed class RouteExportService( { private const string Empty = "—"; private const string Always = "Всегда"; - private const string AnyOrganization = "Любая"; private const string AnyCompany = "любая компания"; private const string AnyDepartment = "любой отдел"; private const string ByRequest = "Определяется по заявке"; @@ -112,12 +111,9 @@ public sealed class RouteExportService( var sections = routes .Select(route => { - var organization = route.CompanyId is { } companyId - ? directory.CompanyName(companyId) - : AnyOrganization; return new RouteExportSection( routes.Count < 2 ? null : RouteTitle(route, directory), - BuildGroups(route.Steps, organization, directory)); + BuildGroups(route.Steps, directory)); }) .ToList(); @@ -131,7 +127,7 @@ public sealed class RouteExportService( return $"Маршрут: {company} · {department}{(route.ByProject ? " · по проекту" : string.Empty)}"; } - private List BuildGroups(IEnumerable steps, string organization, PortalDirectory directory) + private List BuildGroups(IEnumerable steps, PortalDirectory directory) { var ordered = steps.OrderBy(s => s.OrderNo).ToList(); var approvers = ordered.Where(s => s.Status != RouteStepStatus.Execution).ToList(); @@ -139,22 +135,22 @@ public sealed class RouteExportService( return new[] { - new RouteExportGroup("Согласующие", BuildRows(approvers, organization, directory)), - new RouteExportGroup("Исполнители", BuildRows(executors, organization, directory)) + new RouteExportGroup("Согласующие", BuildRows(approvers, directory)), + new RouteExportGroup("Исполнители", BuildRows(executors, directory)) } .Where(g => g.Rows.Count > 0) .ToList(); } // Параллельные шаги (равный OrderNo) получают один и тот же номер. - private List BuildRows(IReadOnlyList steps, string organization, PortalDirectory directory) + private List BuildRows(IReadOnlyList steps, PortalDirectory directory) { var orderNos = steps.Select(s => s.OrderNo).Distinct().OrderBy(n => n).ToList(); return steps .Select(step => { - var (role, executor) = DescribeRoleAndExecutor(step, directory); + var (role, executor, organization) = DescribeRoleAndExecutor(step, directory); return new RouteExportRow( orderNos.IndexOf(step.OrderNo) + 1, role, @@ -169,19 +165,22 @@ public sealed class RouteExportService( // «Роль» — должность или тип руководителя, «исполнитель» — конкретный человек, // если он известен уже при настройке маршрута. Директор Tansu и Управляющий // директор Tansu — тоже конкретные люди (их email лежит в настройках). - private (string Role, string Executor) DescribeRoleAndExecutor(TemplateRouteStep step, PortalDirectory directory) + private (string Role, string Executor, string Organization) DescribeRoleAndExecutor(TemplateRouteStep step, PortalDirectory directory) { if (!string.IsNullOrWhiteSpace(step.ExecutorEmail)) { var employee = directory.FindByEmail(step.ExecutorEmail); - return (EmptyIfBlank(employee?.Position), employee?.Fio ?? step.ExecutorEmail); + return (EmptyIfBlank(employee?.Position), employee?.Fio ?? step.ExecutorEmail, directory.CompaniesOf(step.ExecutorEmail)); } if (!string.IsNullOrWhiteSpace(step.Position)) - return (step.Position, directory.FindByPosition(step.Position)?.Fio ?? Empty); + { + var holder = directory.FindByPosition(step.Position); + return (step.Position, holder?.Fio ?? Empty, directory.CompaniesOf(holder?.Email)); + } if (step.ProjectRole is { } projectRole && projectRole != ProjectRole.None) - return (ProjectRoleLabels[projectRole], ByRequest); + return (ProjectRoleLabels[projectRole], ByRequest, Empty); if (step.HeadType is { } headType && headType != HeadType.None) { @@ -193,12 +192,12 @@ public sealed class RouteExportService( }; if (fixedEmail is not null) // Нет в справочнике — лучше показать email, чем пустую ячейку. - return (HeadTypeLabels[headType], directory.FindByEmail(fixedEmail)?.Fio ?? fixedEmail); + return (HeadTypeLabels[headType], directory.FindByEmail(fixedEmail)?.Fio ?? fixedEmail, directory.CompaniesOf(fixedEmail)); - return (HeadTypeLabels[headType], ByRequest); + return (HeadTypeLabels[headType], ByRequest, Empty); } - return (Empty, Empty); + return (Empty, Empty, Empty); } private static string DescribeCondition(TemplateRouteStep step) @@ -239,6 +238,22 @@ public sealed class RouteExportService( public EmployeeContactInfo? FindByPosition(string position) => employees.FirstOrDefault(e => string.Equals(e.Position?.Trim(), position.Trim(), StringComparison.OrdinalIgnoreCase)); + // Организация — компания СОТРУДНИКА, а не маршрута. Справочник даёт строку на + // каждую компанию, где он числится (совместители), поэтому выводим все. + public string CompaniesOf(string? email) + { + if (string.IsNullOrWhiteSpace(email)) + return Empty; + + var names = employees + .Where(e => string.Equals(e.Email, email, StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrWhiteSpace(e.Company)) + .Select(e => e.Company) + .Distinct() + .ToList(); + return names.Count == 0 ? Empty : string.Join(", ", names); + } + // Пока справочник не отдал имя (или запись удалена) — усечённый Guid, как в админке. public string CompanyName(Guid id) => companies.GetValueOrDefault(id) ?? ShortGuid(id);