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.Constants; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.NextRunServices; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Schedule; namespace PARR.API.Controllers.V1.Statistics { /// /// Статистика распределения шаблонов /// [Authorize(Roles = ParrRoles.Administrator.Role)] public class StatTemplateDistributionController : BaseApiController { private readonly ITemplateService templateService; private readonly IJobGroupService jobGroupService; private readonly IMapper mapper; private readonly IShortcodesService shortcodesService; private readonly INextRunService nextRunService; private readonly ILogger logger; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; public StatTemplateDistributionController( ITemplateService templateService, IJobGroupService jobGroupService, IMapper mapper, IShortcodesService shortcodesService, INextRunService nextRunService, ILogger logger, IScheduleResponseAreaTimeOffsetService 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 //TODO: похоже что timeZoneQuery лишняя 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.Max(t => t.Template.NextRun), timeZoneQuery.TimeZoneOffsetHours); // получаем список рабочих дней // получет все дни, даже с выходными, чтоб видеть реальную картину var workDays = await nextRunService.GetWorkDaysAsync(dateStart, dateEnd, false, jobGroup.ReferenceDate); // получить список выходных дней (показываем только если IsExcludeWeekends = true) var weekends = jobGroup.DistributionConfig.IsExcludeWeekends ? await nextRunService.GetWeekendsAsync(dateStart, dateEnd, jobGroup.ReferenceDate) : new HashSet(); var responseItemsList = new List(); if (jobGroup.DistributionConfig.IsGroupingByWorkGroup) { // тут группируем по рабочим группам // Получаем список пар (WorkGroupName, Template) var templatesWithWorkGroupNames = new List<(string WorkGroupName, (Template Template, string ResponseArea))>(); // получаем названия рабочих групп и ЗО foreach (var item in items) { var workGroupName = await shortcodesService.ApplyShortcodesAsync(item.WorkGroupMask, item.Template); var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); templatesWithWorkGroupNames.Add((workGroupName, (item.Template, responseArea))); } // Группируем по WorkGroupName var grouped = templatesWithWorkGroupNames .GroupBy(x => x.WorkGroupName) .ToDictionary(g => g.Key, g => g.Select(x => x.Item2).ToList()); foreach (var groupedItem in grouped) { var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), workDays, weekends, timeZoneQuery.TimeZoneOffsetHours, jobGroup.ReferenceDate); responseItemsList.Add(statResult); } } else { // не нужно группировать по рабочим группам // получаем названия ЗО var templatesWithResponseArea = new List<(Template Template, string ResponseArea)>(); foreach (var item in items) { var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); templatesWithResponseArea.Add((item.Template, responseArea)); } // сразу формируем response responseItemsList.Add(GetResponseByWorkGroup(null, templatesWithResponseArea, workDays, weekends, timeZoneQuery.TimeZoneOffsetHours, jobGroup.ReferenceDate)); } var response = new StatTemplateDistributorResponse { JobGroup = mapper.Map(jobGroup), ItemsList = responseItemsList.OrderBy(t => t.WorkGroupName).ToList() }; return Ok(new Response(response, true)); } /// /// Получить дату начала и конца периода /// /// /// /// /// private (DateOnly dateStart, DateOnly dateEnd) GetStartEndPeriod(JobGroup jobGroup, DateTimeOffset maxNextRunTemplate, int timeZoneOffsetHours) { var durationDays = nextRunService.GetDurationDays(jobGroup.DistributionConfig!); var dateStart = DateOnly.FromDateTime(DateTime.UtcNow.AddHours(timeZoneOffsetHours)); // последний день, не может быть меньше чем durationDays var dateEnd = dateStart.AddDays(durationDays); // последний день, не может быть меньше чем refDate+durationDays var sumRefDuration = DateOnly.FromDateTime(jobGroup.ReferenceDate.AddHours(timeZoneOffsetHours).AddDays(durationDays).Date); if (dateEnd < sumRefDuration) dateEnd = sumRefDuration; // с DateEnd вообще какая-то шурпатня :( // как наглядно понять ок не ок распределяется, если на каком-то графике dateEnd может ухеать за период распределения // последний день, не может быть меньше чем дата последнего шаблона maxNextRunTemplate var lastTempalteDate = DateOnly.FromDateTime(maxNextRunTemplate.AddHours(timeZoneOffsetHours).Date); if (dateEnd < lastTempalteDate) dateEnd = lastTempalteDate; return (dateStart, dateEnd); } /// /// Заполнить респонс /// /// /// /// /// /// private StatDistributorItemResponse GetResponseByWorkGroup(string? workGroupName, List<(Template Template, string ResponseArea)> templateList, List workDays, HashSet weekends, int timeZoneOffsetHours, DateTimeOffset referenceDate) { var result = new StatDistributorItemResponse { WorkGroupName = workGroupName, Statistics = workDays.OrderBy(t => t).Select(t => new StatTemplateDistributorDateStat { AllCount = 0, Date = new DateTimeOffset(t.Year, t.Month, t.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset), IsWorkDay = !weekends.Contains(t), IsActivatedCount = 0, IsDeactivatedCount = 0, Templates = new List() }).ToList() }; //foreach (var templatesFromDate in templates.GroupBy(t => DateOnly.FromDateTime(t.NextRun.Date))) // смещаем по часовой зоне и группируем по дате //foreach (var templatesFromDate in templates.GroupBy(t => DateOnly.FromDateTime(t.NextRun.AddHours(timeZoneOffsetHours).Date))) foreach (var templatesFromDate in templateList.GroupBy(t => DateOnly.FromDateTime(t.Template.NextRun.AddHours(timeZoneOffsetHours).Date))) { var statisticsForDate = result.Statistics.FirstOrDefault(t => DateOnly.FromDateTime(t.Date.AddHours(timeZoneOffsetHours).Date) == templatesFromDate.Key); if (statisticsForDate == null) { logger.LogWarning("В списке рабочих дней, нет дня для шаблонов {count} шт. выполняющихся в {date}", templatesFromDate.Count(), templatesFromDate.Key); continue; } statisticsForDate.AllCount = templatesFromDate.Count(); statisticsForDate.IsDeactivatedCount = templatesFromDate.Count(t => !t.Template.IsActiveTemplate || !t.Template.IsActiveSchedule); statisticsForDate.IsActivatedCount = templatesFromDate.Count(t => t.Template.IsActiveTemplate && t.Template.IsActiveSchedule); statisticsForDate.Templates = templatesFromDate.Select(t => new StatTempleteDistribItem { Name = t.Template.Name, IsActiveSchedular = t.Template.IsActiveSchedule, IsActiveTemplate = t.Template.IsActiveTemplate, NextRun = t.Template.NextRun, ResponseAreaOffset = mapper.Map(scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(t.ResponseArea)) }) .OrderBy(t => t.Name) .ToList(); } return result; } } }