diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs
index 0f2e903a..a6d4d22b 100644
--- a/PARR.API/Contracts/V1/ApiRoutes.cs
+++ b/PARR.API/Contracts/V1/ApiRoutes.cs
@@ -1,4 +1,6 @@
-namespace PARR.API.Contracts.V1
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+
+namespace PARR.API.Contracts.V1
{
// https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/
@@ -324,6 +326,16 @@
public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}";
}
+ public static class StatRobotMetrics
+ {
+ public const string GetRobotStatusMetrics = BaseStat + "/robot-metrics/robot-status/{robotCode}/{period}";
+
+ public const string GetTaskStatusMetrics = BaseStat + "/robot-metrics/task-status/{robotCode}/{period}";
+
+ public const string GetFilteredMetrics = BaseStat + "/robot-metrics/filtered";
+
+ }
+
#endregion
#region Наряды
diff --git a/PARR.API/Contracts/V1/Requests/Queries/RobotFilteredMetricsQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/RobotFilteredMetricsQuery.cs
new file mode 100644
index 00000000..5dfc4ee0
--- /dev/null
+++ b/PARR.API/Contracts/V1/Requests/Queries/RobotFilteredMetricsQuery.cs
@@ -0,0 +1,22 @@
+using PARR.Domain.Enums;
+
+namespace PARR.API.Contracts.V1.Requests.Queries
+{
+ public record RobotFilteredMetricsQuery
+ {
+ public RobotsEnum? RobotCode { get; init; }
+
+ public RobotStatusEnum? RobotStatusCode { get; init; }
+
+ public TaskStatusEnum? TaskStatusCode { get; init; }
+
+ public DateTimeOffset? DateFrom { get; init; }
+
+ public DateTimeOffset? DateTo { get; init; }
+
+ ///
+ /// Шаг группировки в минутах (например, 2, 30, 60, 1440)
+ ///
+ public int IntervalMinutes { get; set; } = 30;
+ }
+}
diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatFilteredChartPoint.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatFilteredChartPoint.cs
new file mode 100644
index 00000000..cc2d21b2
--- /dev/null
+++ b/PARR.API/Contracts/V1/Responses/Statistics/StatFilteredChartPoint.cs
@@ -0,0 +1,8 @@
+namespace PARR.API.Contracts.V1.Responses.Statistics
+{
+ public record StatFilteredChartPoint
+ {
+ public DateTimeOffset Timestamp { get; init; }
+ public int Count { get; init; }
+ }
+}
diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatRobotStatusChartPoint.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatRobotStatusChartPoint.cs
new file mode 100644
index 00000000..435f0a22
--- /dev/null
+++ b/PARR.API/Contracts/V1/Responses/Statistics/StatRobotStatusChartPoint.cs
@@ -0,0 +1,11 @@
+namespace PARR.API.Contracts.V1.Responses.Statistics
+{
+ public record StatRobotStatusChartPoint
+ {
+ public DateTimeOffset Timestamp { get; init; }
+ public int Wait { get; init; }
+ public int InProgress { get; init; }
+ public int Error { get; init; }
+ public int Complete { get; init; }
+ }
+}
diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatTaskStatusChartPoint.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatTaskStatusChartPoint.cs
new file mode 100644
index 00000000..c2576a02
--- /dev/null
+++ b/PARR.API/Contracts/V1/Responses/Statistics/StatTaskStatusChartPoint.cs
@@ -0,0 +1,10 @@
+namespace PARR.API.Contracts.V1.Responses.Statistics
+{
+ public record StatTaskStatusChartPoint
+ {
+ public DateTimeOffset Timestamp { get; init; }
+ public int Creating { get; init; }
+ public int Updating { get; init; }
+ public int Ok { get; init; }
+ }
+}
diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotMetricsController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotMetricsController.cs
new file mode 100644
index 00000000..3d891a40
--- /dev/null
+++ b/PARR.API/Controllers/V1/Statistics/StatRobotMetricsController.cs
@@ -0,0 +1,84 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using PARR.API.Contracts.V1;
+using PARR.API.Contracts.V1.Requests.Queries;
+using PARR.API.Contracts.V1.Responses.Base;
+using PARR.API.Contracts.V1.Responses.Statistics;
+using PARR.API.Controllers.V1.Base;
+using PARR.Core.Services.RobotMetrics;
+using PARR.Domain.Common.Roles;
+using PARR.Domain.DTOs.RobotMetrics;
+using PARR.Domain.Enums;
+
+namespace PARR.API.Controllers.V1.Statistics
+{
+ ///
+ /// Метрики заданий роботам
+ ///
+ [Authorize(Roles = ParrRoles.Administrator.Role)]
+ public class StatRobotMetricsController : BaseApiController
+ {
+ private readonly IMapper _mapper;
+ private readonly IRobotMetricsService _robotMetricsService;
+
+ public StatRobotMetricsController(
+ IMapper mapper,
+ IRobotMetricsService robotMetricsService
+ )
+ {
+ _mapper = mapper;
+ _robotMetricsService = robotMetricsService;
+ }
+
+
+ ///
+ /// Статистика по Заданиям Роботу, график
+ ///
+ ///
+ [HttpGet(ApiRoutes.StatRobotMetrics.GetRobotStatusMetrics)]
+ public async Task GetRobotStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
+ {
+ var data = await _robotMetricsService.GetRobotStatusMetricsAsync(robotCode, period, cancellationToken);
+
+ var response = _mapper.Map>(data);
+
+ return Ok(new Response>(response, true));
+ }
+
+
+ ///
+ /// Статистика по Статусам Заданий, график
+ ///
+ ///
+ [HttpGet(ApiRoutes.StatRobotMetrics.GetTaskStatusMetrics)]
+ public async Task GetTaskStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
+ {
+ var data = await _robotMetricsService.GetTaskStatusMetricsAsync(robotCode, period, cancellationToken);
+
+ var response = _mapper.Map>(data);
+
+ return Ok(new Response>(response, true));
+ }
+
+
+ ///
+ /// Статистика по статусам заданий, гибкий фильтр
+ ///
+ ///
+ [HttpGet(ApiRoutes.StatRobotMetrics.GetFilteredMetrics)]
+ public async Task GetFilteredMetrics([FromQuery] RobotFilteredMetricsQuery request, CancellationToken cancellationToken)
+ {
+ // ------- Правильность расчетов этого метода доконца не проверена -------
+
+ var filter = _mapper.Map(request);
+
+ var data = await _robotMetricsService.GetFilteredMetricsAsync(filter, cancellationToken);
+
+ var response = _mapper.Map>(data);
+
+ return Ok(new Response>(response, true));
+ }
+
+ }
+}
diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs
index 457c6391..604125c4 100644
--- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs
+++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs
@@ -5,6 +5,7 @@ using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.MappingProfiles.Resolvers;
using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Domain.DTOs.Matching;
+using PARR.Domain.DTOs.RobotMetrics;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.DTOs.RobotTask;
using PARR.Domain.DTOs.Shortcode;
@@ -500,6 +501,14 @@ namespace PARR.API.MappingProfiles
#endregion
+ #region StatRobotMetrics
+
+ CreateMap();
+ CreateMap();
+ CreateMap();
+
+ #endregion
+
}
}
diff --git a/PARR.API/MappingProfiles/RequestToDomainProfile.cs b/PARR.API/MappingProfiles/RequestToDomainProfile.cs
index 2e9501e6..edd1f09a 100644
--- a/PARR.API/MappingProfiles/RequestToDomainProfile.cs
+++ b/PARR.API/MappingProfiles/RequestToDomainProfile.cs
@@ -2,6 +2,7 @@
using PARR.API.Contracts.V1.Requests;
using PARR.API.Contracts.V1.Requests.Queries;
using PARR.Domain.Common.Pagination;
+using PARR.Domain.DTOs.RobotMetrics;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.Entities.JobEntities;
@@ -52,6 +53,9 @@ namespace PARR.API.MappingProfiles
#endregion
CreateMap();
+
+
+ CreateMap();
}
}
}
diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs
index d54d6055..c1d29715 100644
--- a/PARR.Core/DependencyInjection.cs
+++ b/PARR.Core/DependencyInjection.cs
@@ -8,6 +8,7 @@ using PARR.Core.Common.Interfaces;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.NextRunServices.Subservices;
+using PARR.Core.Services.RobotMetrics;
using PARR.Core.Services.RobotSnapshotServices;
using PARR.Core.Services.RobotTask.Implementations;
using PARR.Core.Services.RobotTask.Interfaces;
@@ -28,7 +29,6 @@ using PARR.Core.Services.UnitService.Implementations;
using PARR.Core.Services.UnitService.Interfaces;
using PARR.Core.Services.Workload.Implementations;
using PARR.Core.Services.Workload.Interfaces;
-using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
@@ -115,6 +115,8 @@ namespace PARR.Core
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+
//services.AddScoped();
#endregion
diff --git a/PARR.Core/Extensions/EnumerableExtensions.cs b/PARR.Core/Extensions/EnumerableExtensions.cs
new file mode 100644
index 00000000..5d90e5d2
--- /dev/null
+++ b/PARR.Core/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,10 @@
+namespace PARR.Core.Extensions
+{
+ public static class EnumerableExtensions
+ {
+ public static int MaxOrDefault(this IEnumerable source)
+ {
+ return source.Any() ? source.Max() : 0;
+ }
+ }
+}
diff --git a/PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs b/PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
new file mode 100644
index 00000000..fbfa7c27
--- /dev/null
+++ b/PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
@@ -0,0 +1,37 @@
+using PARR.Domain.DTOs.RobotMetrics;
+using PARR.Domain.Enums;
+
+namespace PARR.Core.Services.RobotMetrics
+{
+ ///
+ /// Отчетность по метрикам заданий и работы роботов.
+ ///
+ public interface IRobotMetricsService
+ {
+ ///
+ /// Статистика по Заданиям Роботу
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
+
+ ///
+ /// Статистика по Статусам Заданий
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
+
+ ///
+ /// Статистика с применением гибких фильтров
+ ///
+ ///
+ ///
+ ///
+ Task> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken);
+ }
+}
diff --git a/PARR.Core/Services/RobotMetrics/RobotMetricsService.cs b/PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
new file mode 100644
index 00000000..e8cee692
--- /dev/null
+++ b/PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
@@ -0,0 +1,260 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using PARR.Core.Extensions;
+using PARR.Core.Repositories.Interfaces;
+using PARR.Core.Repositories.Interfaces.RobotRepositories;
+using PARR.Domain.DTOs.RobotMetrics;
+using PARR.Domain.Enums;
+using PARR.Domain.Exceptions;
+
+namespace PARR.Core.Services.RobotMetrics
+{
+ internal class RobotMetricsService : IRobotMetricsService
+ {
+ private readonly IRobotConfigurationSnapshotRepository _snapshotRepository;
+ private readonly IRobotConfigurationRepository _configurationRepository;
+ private readonly ILogger _logger;
+
+ public RobotMetricsService(
+ IRobotConfigurationSnapshotRepository snapshotRepository,
+ IRobotConfigurationRepository configurationRepository,
+ ILogger logger
+ )
+ {
+ _snapshotRepository = snapshotRepository;
+ _configurationRepository = configurationRepository;
+ _logger = logger;
+ }
+
+
+ public async Task> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken)
+ {
+ // ------- Правильность расчетов этого метода доконца не проверена -------
+
+ if (filter == null)
+ throw new AppValidationException("Фильтр не может быть пустым.");
+
+ if (filter.IntervalMinutes < 1)
+ throw new AppValidationException("Интервал группировки не может быть меньше 1 минуты.");
+
+ var query = _snapshotRepository.Get().AsNoTracking();
+
+ if (filter.RobotCode.HasValue)
+ query = query.Where(t => t.RobotCode == (int)filter.RobotCode.Value);
+
+ if (filter.RobotStatusCode.HasValue)
+ query = query.Where(t => t.RobotStatusCode == (int)filter.RobotStatusCode.Value);
+
+ if (filter.TaskStatusCode.HasValue)
+ query = query.Where(t => t.TaskStatusCode == (int)filter.TaskStatusCode);
+
+ // Если даты не переданы, берем последние 24 часа по умолчанию
+ var dateFrom = filter.DateFrom ?? DateTimeOffset.UtcNow.AddDays(-1);
+ query = query.Where(s => s.DateCreated >= dateFrom);
+
+ if (filter.DateTo.HasValue)
+ query = query.Where(s => s.DateCreated <= filter.DateTo.Value);
+
+ var dbData = await query.ToListAsync(cancellationToken);
+
+ var dbGrouped = dbData
+ .GroupBy(s => RoundToInterval(s.DateCreated, filter.IntervalMinutes))
+ .ToDictionary(t => t.Key, t => t.ToList());
+
+ var dateTo = filter.DateTo ?? DateTimeOffset.UtcNow;
+
+ var result = GenerateTimeGrid(dateFrom, dateTo, filter.IntervalMinutes)
+ .Select(time => new FilteredChartPoint(
+ Timestamp: time,
+ //Count: dbGrouped.TryGetValue(time, out var points) ? points.Max(x => x.Count) : 0
+ Count: dbGrouped.TryGetValue(time, out var points)
+ ? points.GroupBy(x => x.DateCreated) // Группируем по точной минуте снапшота
+ .Select(g => g.Sum(x => x.Count)) // Складываем всё, что подошли под фильтр в эту минуту
+ .MaxOrDefault() // Берем максимальный пик за весь интервал (например, за час)
+ : 0 // Если снапшотов не было — честный ноль
+ )).ToList();
+
+ return result;
+ }
+
+
+ public async Task> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
+ {
+ ValidateRobot(robotCode);
+ CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
+
+ // История из снапшотов
+ var snapshots = await _snapshotRepository.Get()
+ .AsNoTracking()
+ .Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
+ .ToListAsync(cancellationToken);
+
+ var dbGrouped = snapshots
+ .GroupBy(t => RoundToInterval(t.DateCreated, intervalMinutes))
+ .ToDictionary(t => t.Key, t => t.ToList());
+
+ // Генерим сетку значений, если значений нет, вставляем нули
+ var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
+ .Select(time => dbGrouped.TryGetValue(time, out var points)
+ ? new RobotStatusChartPoint(
+ Timestamp: time,
+ // группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
+ Wait: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).MaxOrDefault(x => x.Count),
+ points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait)
+ .GroupBy(x => x.DateCreated)
+ .Select(t => t.Sum(x => x.Count))
+ .MaxOrDefault(),
+ InProgress: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).MaxOrDefault(x => x.Count),
+ points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count))
+ .MaxOrDefault(),
+ Error: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).MaxOrDefault(x => x.Count)
+ points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
+ .MaxOrDefault(),
+ Complete:
+ points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
+ .MaxOrDefault()
+ )
+ : new RobotStatusChartPoint(time, Wait: 0, InProgress: 0, Error: 0, Complete: 0)
+ ).ToList();
+
+ // Последнее значение в конце графика из реальной таблицы
+ var liveRaw = await _configurationRepository
+ .Get()
+ .AsNoTracking()
+ .Where(t => t.RobotCode == (int)robotCode)
+ .GroupBy(t => t.RobotStatusCode)
+ .Select(g => new { RobotStatusCode = g.Key, Count = g.Count() })
+ .ToListAsync(cancellationToken);
+
+ history.Add(new RobotStatusChartPoint(
+ Timestamp: DateTimeOffset.UtcNow,
+ Wait: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).Sum(x => x.Count),
+ InProgress: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).Sum(x => x.Count),
+ Error: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).Sum(x => x.Count),
+ Complete: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete).Sum(x => x.Count)
+ ));
+
+ return history;
+ }
+
+
+ public async Task> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
+ {
+ ValidateRobot(robotCode);
+ CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
+
+ // История из снапшотов
+ var snapshots = await _snapshotRepository.Get()
+ .AsNoTracking()
+ .Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
+ .ToListAsync(cancellationToken);
+
+ var dbGrouped = snapshots
+ .GroupBy(s => RoundToInterval(s.DateCreated, intervalMinutes))
+ .ToDictionary(t => t.Key, t => t.ToList());
+
+ var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
+ .Select(time => dbGrouped.TryGetValue(time, out var points)
+ ? new TaskStatusChartPoint(
+ Timestamp: time,
+ // группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
+ Creating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).MaxOrDefault(x => x.Count),
+ points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count))
+ .MaxOrDefault(),
+ Updating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).MaxOrDefault(x => x.Count),
+ points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count))
+ .MaxOrDefault(),
+ Ok: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).MaxOrDefault(x => x.Count)
+ points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok)
+ .GroupBy(x => x.DateCreated)
+ .Select(g => g.Sum(x => x.Count))
+ .MaxOrDefault()
+ )
+ : new TaskStatusChartPoint(time, Creating: 0, Updating: 0, Ok: 0)
+ ).ToList();
+
+ // Живой текущий кадр в конец графика
+ var liveRaw = await _configurationRepository
+ .Get()
+ .AsNoTracking()
+ .Where(t => t.RobotCode == (int)robotCode)
+ .GroupBy(t => t.TaskStatusCode)
+ .Select(g => new { TaskStatusCode = g.Key, Count = g.Count() })
+ .ToListAsync(cancellationToken);
+
+ history.Add(new TaskStatusChartPoint(
+ Timestamp: DateTimeOffset.UtcNow,
+ Creating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).Sum(x => x.Count),
+ Updating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).Sum(x => x.Count),
+ Ok: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).Sum(x => x.Count)
+ ));
+
+ return history;
+ }
+
+
+ private void ValidateRobot(RobotsEnum robotCode)
+ {
+ if (!Enum.IsDefined(typeof(RobotsEnum), robotCode))
+ throw new NotFoundException($"Робот с кодом {robotCode} не найден в системе.");
+ }
+
+
+ private void CalculatePeriodDates(ChartPeriod period, out DateTimeOffset fromDate, out int intervalMinutes)
+ {
+ switch (period)
+ {
+ case ChartPeriod.TwoHours:
+ fromDate = DateTimeOffset.UtcNow.AddHours(-2);
+ intervalMinutes = 2;
+ break;
+ case ChartPeriod.TwentyFourHours:
+ fromDate = DateTimeOffset.UtcNow.AddDays(-1);
+ intervalMinutes = 30;
+ break;
+ case ChartPeriod.SevenDays:
+ fromDate = DateTimeOffset.UtcNow.AddDays(-7);
+ intervalMinutes = 60;
+ break;
+ default:
+ throw new AppValidationException("Указан неподдерживаемый период времени.");
+ }
+ }
+
+
+ private DateTimeOffset RoundToInterval(DateTimeOffset dt, int intervalMinutes)
+ {
+ var minutes = (dt.Minute / intervalMinutes) * intervalMinutes;
+ return new DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, minutes, 0, dt.Offset);
+ }
+
+ ///
+ /// Генератор сетки времени
+ ///
+ ///
+ ///
+ ///
+ ///
+ private IEnumerable GenerateTimeGrid(DateTimeOffset fromDate, DateTimeOffset toDate, int intervalMinutes)
+ {
+ var startTime = RoundToInterval(fromDate, intervalMinutes);
+ var endTime = RoundToInterval(toDate, intervalMinutes);
+
+ for (var time = startTime; time <= endTime; time = time.AddMinutes(intervalMinutes))
+ {
+ yield return time;
+ }
+ }
+
+ }
+}
diff --git a/PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs b/PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
new file mode 100644
index 00000000..498fa195
--- /dev/null
+++ b/PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
@@ -0,0 +1,9 @@
+namespace PARR.Domain.DTOs.RobotMetrics
+{
+ ///
+ /// Статистика по роботам, согласно гибким фильтрам.
+ ///
+ ///
+ ///
+ public record FilteredChartPoint(DateTimeOffset Timestamp, int Count);
+}
diff --git a/PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs b/PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
new file mode 100644
index 00000000..77e05c32
--- /dev/null
+++ b/PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
@@ -0,0 +1,16 @@
+using PARR.Domain.Enums;
+
+namespace PARR.Domain.DTOs.RobotMetrics
+{
+ public record MetricFilter
+ {
+ public RobotsEnum? RobotCode { get; init; }
+ public RobotStatusEnum? RobotStatusCode { get; init; }
+ public TaskStatusEnum? TaskStatusCode { get; init; }
+ public DateTimeOffset? DateFrom { get; init; }
+ public DateTimeOffset? DateTo { get; init; }
+
+ // Шаг группировки в минутах (например, 2, 30, 60, 1440)
+ public int IntervalMinutes { get; set; } = 30;
+ }
+}
diff --git a/PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs b/PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
new file mode 100644
index 00000000..1c7a8388
--- /dev/null
+++ b/PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
@@ -0,0 +1,7 @@
+namespace PARR.Domain.DTOs.RobotMetrics
+{
+ ///
+ /// Статистика по Робот Сатусам, на графике
+ ///
+ public record RobotStatusChartPoint(DateTimeOffset Timestamp, int Wait, int InProgress, int Error, int Complete);
+}
diff --git a/PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs b/PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
new file mode 100644
index 00000000..ad161a95
--- /dev/null
+++ b/PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
@@ -0,0 +1,11 @@
+namespace PARR.Domain.DTOs.RobotMetrics
+{
+ ///
+ /// Статистика по Статусам заданий роботам, на графике
+ ///
+ ///
+ ///
+ ///
+ ///
+ public record TaskStatusChartPoint(DateTimeOffset Timestamp, int Creating, int Updating, int Ok);
+}
diff --git a/PARR.Domain/Enums/ChartPeriod.cs b/PARR.Domain/Enums/ChartPeriod.cs
new file mode 100644
index 00000000..277335f7
--- /dev/null
+++ b/PARR.Domain/Enums/ChartPeriod.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace PARR.Domain.Enums
+{
+ ///
+ /// Периоды для графиков
+ ///
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum ChartPeriod
+ {
+ ///
+ /// Последние 2 часа
+ ///
+ TwoHours = 1,
+
+ ///
+ /// Последние 24 часа
+ ///
+ TwentyFourHours = 2,
+
+ ///
+ /// Последняя неделя
+ ///
+ SevenDays = 3
+ }
+}