using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests.BaseRequests; using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Contracts.V1.Responses.Statistics; using PARR.API.Controllers.V1.Base; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.JobGroupRepositories; using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Core.Services.NextRunServices; using PARR.Core.Services.Shortcodes; using PARR.Domain.Common.Roles; using PARR.Domain.Entities; using PARR.Domain.Entities.JobGroupEntities; using PARR.Domain.Enums; namespace PARR.API.Controllers.V1.Statistics { /// /// Статистика распределения шаблонов /// [Authorize(Roles = ParrRoles.Administrator.Role)] public class StatTemplateDistributionController : BaseApiController { private readonly ITemplateRepository templateService; private readonly IJobGroupRepository jobGroupService; private readonly IMapper mapper; private readonly IShortcodesService shortcodesService; private readonly INextRunService nextRunService; private readonly ILogger logger; private readonly IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService; public StatTemplateDistributionController( ITemplateRepository templateService, IJobGroupRepository jobGroupService, IMapper mapper, IShortcodesService shortcodesService, INextRunService nextRunService, ILogger logger, IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService ) { this.templateService = templateService; this.jobGroupService = jobGroupService; this.mapper = mapper; this.shortcodesService = shortcodesService; this.nextRunService = nextRunService; this.logger = logger; this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; } /// /// Статистика распределения шаблонов по jobGroupId, только для Used /// /// /// [HttpGet(ApiRoutes.StatTemplateDistribution.Get)] public async Task Get([FromRoute] Guid jobGroupId, [FromQuery] TimeZoneOffsetClient timeZoneQuery) { // eadc5498-dba6-4f10-9b4b-a1653e3c3e61 var jobGroup = await jobGroupService.Get() .Include(t => t.DistributionConfig) .ThenInclude(t => t.DistributionPeriod) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == jobGroupId); if (jobGroup == null) return BadRequest(new Response(false, new List { new ErrorModel { Message = "Не найдена группа работ" } })); if (!jobGroup.IsAutoDistributionEnabled || jobGroup.DistributionConfig == null) return BadRequest(new Response(false, new List { new ErrorModel { Message = "Для группы работ не включено автораспределение" } })); var items = await templateService.Get() .Where(t => t.Job!.GroupId == jobGroupId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .Select(t => new { WorkGroupMask = t.Job!.WorkGroupMask, ResponseAreaMask = t.Job!.ResponseAreaMask, Template = t }) .AsNoTracking() .ToListAsync(); if (!items.Any()) return Ok(new Response(new StatTemplateDistributorResponse { JobGroup = mapper.Map(jobGroup), ItemsList = null }, true)); var (dateStart, dateEnd) = GetStartEndPeriod(jobGroup, items.Min(t => t.Template.NextRun), items.Max(t => t.Template.NextRun), timeZoneQuery.TimeZoneOffset); // получаем список рабочих дней // получет все дни, даже с выходными, чтоб видеть реальную картину var allDays = await nextRunService.GetWorkDaysAsync(dateStart, dateEnd, false); // получить список выходных дней (показываем только если IsExcludeWeekends = true) var weekends = jobGroup.DistributionConfig.IsExcludeWeekends ? await nextRunService.GetWeekendsAsync(dateStart, dateEnd) : new HashSet(); var responseItemsList = new List(); if (jobGroup.DistributionConfig.IsGroupingByWorkGroup) { // тут группируем по рабочим группам logger.LogDebug("Группируем по рабочим группам"); // Получаем список пар (WorkGroupName, Template) var templatesWithWorkGroupNames = new List<(string WorkGroupName, Template Template)>(); // получаем названия рабочих групп foreach (var item in items) { var workGroupName = await shortcodesService.ApplyShortcodesAsync(item.WorkGroupMask, item.Template); templatesWithWorkGroupNames.Add((workGroupName, item.Template)); } // Группируем по WorkGroupName var grouped = templatesWithWorkGroupNames .GroupBy(x => x.WorkGroupName) .ToDictionary(g => g.Key, g => g.Select(x => x.Template).ToList()); foreach (var groupedItem in grouped) { var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset); responseItemsList.Add(statResult); } } else if (jobGroup.IsResponseAreaTimezone && !jobGroup.DistributionConfig.IsGroupingByWorkGroup) { // грппируем только по ЗО logger.LogDebug("Группируем по ЗО"); // Будет группироваться по EsppValue, по MSK, MSK+1... // получаем названия ЗО, из ЗО часовой пояс, формируем список шаблонов с часовым поясом ЗО var templatesWithTimeZone = new List<(Template Template, string MskTimeZone)>(); foreach (var item in items) { var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); var mskTimeZone = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea); templatesWithTimeZone.Add((item.Template, mskTimeZone.EsppValue)); } // Группируем по часовому поясу var grouped = templatesWithTimeZone .GroupBy(x => x.MskTimeZone) .ToDictionary(g => g.Key, g => g.Select(x => x.Template).ToList()); foreach (var groupedItem in grouped) { var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset); responseItemsList.Add(statResult); } } else { // не нужно группировать logger.LogDebug("Без группировки"); // сразу формируем response responseItemsList.Add(GetResponseByWorkGroup(null, items.Select(t => t.Template).ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset)); } var response = new StatTemplateDistributorResponse { JobGroup = mapper.Map(jobGroup), ItemsList = responseItemsList.OrderBy(t => t.GroupFieldName).ToList() }; return Ok(new Response(response, true)); } /// /// Получить дату начала и конца периода /// /// /// /// /// /// private (DateOnly dateStart, DateOnly dateEnd) GetStartEndPeriod(JobGroup jobGroup, DateTimeOffset minNextRunTemplate, DateTimeOffset maxNextRunTemplate, TimeSpan clientOffset) { var durationDays = nextRunService.GetDurationDays(jobGroup.DistributionConfig!); var dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Add(clientOffset)); // последний день, не может быть меньше чем durationDays var dateEnd = dateStart.AddDays(durationDays); // последний день, не может быть меньше чем refDate+durationDays var sumRefDuration = DateOnly.FromDateTime(jobGroup.ReferenceDate.Add(clientOffset).AddDays(durationDays).Date); if (dateEnd < sumRefDuration) dateEnd = sumRefDuration; // последний день, не может быть меньше чем дата последнего шаблона maxNextRunTemplate var lastTemplateDate = DateOnly.FromDateTime(maxNextRunTemplate.Add(clientOffset).Date); if (dateEnd < lastTemplateDate) dateEnd = lastTemplateDate; // Дата начала, не может быть меньше чем minNextRunTemplate var minNextRunDate = DateOnly.FromDateTime(minNextRunTemplate.Add(clientOffset).Date); if (dateStart > minNextRunDate) dateStart = minNextRunDate; return (dateStart, dateEnd); } /// /// Заполнить респонс /// /// /// /// /// /// /// private StatDistributorItemResponse GetResponseByWorkGroup(string? groupFieldName, List