Feature/routes export #29

Merged
gorbenko.pavel merged 13 commits from feature/routes-export into main 2026-10-14 05:06:58 +00:00
Showing only changes of commit 7c64f3ccf4 - Show all commits
@@ -24,7 +24,6 @@ public sealed class RouteExportService(
{ {
private const string Empty = "—"; private const string Empty = "—";
private const string Always = "Всегда"; private const string Always = "Всегда";
private const string AnyOrganization = "Любая";
private const string AnyCompany = "любая компания"; private const string AnyCompany = "любая компания";
private const string AnyDepartment = "любой отдел"; private const string AnyDepartment = "любой отдел";
private const string ByRequest = "Определяется по заявке"; private const string ByRequest = "Определяется по заявке";
@@ -112,12 +111,9 @@ public sealed class RouteExportService(
var sections = routes var sections = routes
.Select(route => .Select(route =>
{ {
var organization = route.CompanyId is { } companyId
? directory.CompanyName(companyId)
: AnyOrganization;
return new RouteExportSection( return new RouteExportSection(
routes.Count < 2 ? null : RouteTitle(route, directory), routes.Count < 2 ? null : RouteTitle(route, directory),
BuildGroups(route.Steps, organization, directory)); BuildGroups(route.Steps, directory));
}) })
.ToList(); .ToList();
@@ -131,7 +127,7 @@ public sealed class RouteExportService(
return $"Маршрут: {company} · {department}{(route.ByProject ? " · по проекту" : string.Empty)}"; return $"Маршрут: {company} · {department}{(route.ByProject ? " · по проекту" : string.Empty)}";
} }
private List<RouteExportGroup> BuildGroups(IEnumerable<TemplateRouteStep> steps, string organization, PortalDirectory directory) private List<RouteExportGroup> BuildGroups(IEnumerable<TemplateRouteStep> steps, PortalDirectory directory)
{ {
var ordered = steps.OrderBy(s => s.OrderNo).ToList(); var ordered = steps.OrderBy(s => s.OrderNo).ToList();
var approvers = ordered.Where(s => s.Status != RouteStepStatus.Execution).ToList(); var approvers = ordered.Where(s => s.Status != RouteStepStatus.Execution).ToList();
@@ -139,22 +135,22 @@ public sealed class RouteExportService(
return new[] return new[]
{ {
new RouteExportGroup("Согласующие", BuildRows(approvers, organization, directory)), new RouteExportGroup("Согласующие", BuildRows(approvers, directory)),
new RouteExportGroup("Исполнители", BuildRows(executors, organization, directory)) new RouteExportGroup("Исполнители", BuildRows(executors, directory))
} }
.Where(g => g.Rows.Count > 0) .Where(g => g.Rows.Count > 0)
.ToList(); .ToList();
} }
// Параллельные шаги (равный OrderNo) получают один и тот же номер. // Параллельные шаги (равный OrderNo) получают один и тот же номер.
private List<RouteExportRow> BuildRows(IReadOnlyList<TemplateRouteStep> steps, string organization, PortalDirectory directory) private List<RouteExportRow> BuildRows(IReadOnlyList<TemplateRouteStep> steps, PortalDirectory directory)
{ {
var orderNos = steps.Select(s => s.OrderNo).Distinct().OrderBy(n => n).ToList(); var orderNos = steps.Select(s => s.OrderNo).Distinct().OrderBy(n => n).ToList();
return steps return steps
.Select(step => .Select(step =>
{ {
var (role, executor) = DescribeRoleAndExecutor(step, directory); var (role, executor, organization) = DescribeRoleAndExecutor(step, directory);
return new RouteExportRow( return new RouteExportRow(
orderNos.IndexOf(step.OrderNo) + 1, orderNos.IndexOf(step.OrderNo) + 1,
role, role,
@@ -169,19 +165,22 @@ public sealed class RouteExportService(
// «Роль» — должность или тип руководителя, «исполнитель» — конкретный человек, // «Роль» — должность или тип руководителя, «исполнитель» — конкретный человек,
// если он известен уже при настройке маршрута. Директор Tansu и Управляющий // если он известен уже при настройке маршрута. Директор Tansu и Управляющий
// директор Tansu — тоже конкретные люди (их email лежит в настройках). // директор 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)) if (!string.IsNullOrWhiteSpace(step.ExecutorEmail))
{ {
var employee = directory.FindByEmail(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)) 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) 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) if (step.HeadType is { } headType && headType != HeadType.None)
{ {
@@ -193,12 +192,12 @@ public sealed class RouteExportService(
}; };
if (fixedEmail is not null) if (fixedEmail is not null)
// Нет в справочнике — лучше показать email, чем пустую ячейку. // Нет в справочнике — лучше показать 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) private static string DescribeCondition(TemplateRouteStep step)
@@ -239,6 +238,22 @@ public sealed class RouteExportService(
public EmployeeContactInfo? FindByPosition(string position) => public EmployeeContactInfo? FindByPosition(string position) =>
employees.FirstOrDefault(e => string.Equals(e.Position?.Trim(), position.Trim(), StringComparison.OrdinalIgnoreCase)); 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, как в админке. // Пока справочник не отдал имя (или запись удалена) — усечённый Guid, как в админке.
public string CompanyName(Guid id) => companies.GetValueOrDefault(id) ?? ShortGuid(id); public string CompanyName(Guid id) => companies.GetValueOrDefault(id) ?? ShortGuid(id);