diff --git a/AsbCloudApp/Data/SlipsStatDto.cs b/AsbCloudApp/Data/SlipsStatDto.cs
new file mode 100644
index 00000000..05843a92
--- /dev/null
+++ b/AsbCloudApp/Data/SlipsStatDto.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AsbCloudApp.Data
+{
+ ///
+ /// DTO, описывающая аналитику удержания в клиньях
+ ///
+ public class SlipsStatDto
+ {
+ ///
+ /// ФИО бурильщика
+ ///
+ public string DrillerName { get; set; } = null!;
+
+ ///
+ /// Количество скважин
+ ///
+ public int WellCount { get; set; }
+
+ ///
+ /// Название секции
+ ///
+ public string SectionCaption { get; set; } = null!;
+
+ ///
+ /// Количество удержаний в клиньях, шт.
+ ///
+ public int SlipsCount { get; set; }
+
+ ///
+ /// Время удержания в клиньях, мин.
+ ///
+ public double SlipsTimeInMinutes { get; set; }
+
+ ///
+ /// Проходка, м.
+ ///
+ public double SlipsDepth { get; set; }
+ }
+}
diff --git a/AsbCloudApp/Repositories/ISlipsStatsRepository.cs b/AsbCloudApp/Repositories/ISlipsStatsRepository.cs
new file mode 100644
index 00000000..644a8a79
--- /dev/null
+++ b/AsbCloudApp/Repositories/ISlipsStatsRepository.cs
@@ -0,0 +1,23 @@
+using AsbCloudApp.Data;
+using AsbCloudApp.Requests;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudApp.Repositories
+{
+ ///
+ /// Сервис для получения аналитики удержания в клиньях
+ ///
+ public interface ISlipsStatsRepository
+ {
+ ///
+ /// Получение записей для построения аналитики удержания в клиньях
+ ///
+ /// параметры запроса
+ ///
+ ///
+ Task> GetAllAsync(OperationStatRequest request, CancellationToken token);
+ }
+}
diff --git a/AsbCloudApp/Requests/OperationStatRequest.cs b/AsbCloudApp/Requests/OperationStatRequest.cs
new file mode 100644
index 00000000..c249b86b
--- /dev/null
+++ b/AsbCloudApp/Requests/OperationStatRequest.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace AsbCloudApp.Requests
+{
+ ///
+ /// Параметры фильтра операции
+ ///
+ public class OperationStatRequest : RequestBase
+ {
+
+ ///
+ /// Дата начала периода, за который строится отчет
+ ///
+ public DateTime? DateStart { get; set; }
+
+ ///
+ /// Дата окончания периода, за который строится отчет
+ ///
+ public DateTime? DateEnd { get; set; }
+
+
+ ///
+ /// Минимальная продолжительность операции, мину
+ ///
+ public int? DurationMinutesMin { get; set; }
+
+ ///
+ /// Максимальная продолжительность операции, мин
+ ///
+ public int? DurationMinutesMax { get; set; }
+
+
+ }
+}
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index 3b480e2d..a0268bd9 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -199,6 +199,7 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient, CrudCacheRepositoryBase>();
diff --git a/AsbCloudInfrastructure/Repository/SlipsStatRepository.cs b/AsbCloudInfrastructure/Repository/SlipsStatRepository.cs
new file mode 100644
index 00000000..7e43f000
--- /dev/null
+++ b/AsbCloudInfrastructure/Repository/SlipsStatRepository.cs
@@ -0,0 +1,173 @@
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Requests;
+using AsbCloudDb.Model;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudInfrastructure.Repository
+{
+ public class SlipsStatRepository : ISlipsStatsRepository
+ {
+ private readonly IAsbCloudDbContext db;
+ public SlipsStatRepository(IAsbCloudDbContext db)
+ {
+ this.db = db;
+ }
+
+ public async Task> GetAllAsync(OperationStatRequest request, CancellationToken token)
+ {
+ if (request.DateStart.HasValue)
+ request.DateStart = DateTime.SpecifyKind(request.DateStart.Value, DateTimeKind.Utc);
+
+ if (request.DateEnd.HasValue)
+ request.DateEnd = DateTime.SpecifyKind(request.DateEnd.Value, DateTimeKind.Utc);
+
+ var schedulesQuery = db.Schedule
+ .Include(s => s.Well)
+ .Include(s => s.Driller)
+ .AsNoTracking();
+
+ if (request.DateStart.HasValue && request.DateEnd.HasValue)
+ schedulesQuery = schedulesQuery.
+ Where(s => s.DrillStart >= request.DateStart && s.DrillEnd <= request.DateEnd);
+
+ var schedules = await schedulesQuery.ToArrayAsync(token);
+
+ var wells = schedules
+ .Select(d => d.Well)
+ .Where(well => well.IdTelemetry != null)
+ .GroupBy(w => w.Id)
+ .ToDictionary(g => g.Key, g => g.First().IdTelemetry!.Value);
+
+ var idsWells = wells.Keys;
+ var idsTelemetries = wells.Values;
+ var telemetries = wells.ToDictionary(wt => wt.Value, wt => wt.Key);
+
+ var factWellOperationsQuery = db.WellOperations
+ .Where(o => idsWells.Contains(o.IdWell))
+ .Where(o => o.IdType == 1)
+ .Where(o => WellOperationCategory.MechanicalDrillingSubIds.Contains(o.IdCategory))
+ .Include(o => o.WellSectionType)
+ .AsNoTracking();
+
+ if (request.DateStart.HasValue && request.DateEnd.HasValue)
+ factWellOperationsQuery = factWellOperationsQuery
+ .Where(o => o.DateStart.AddHours(o.DurationHours) > request.DateStart && o.DateStart < request.DateEnd);
+
+ var factWellOperations = await factWellOperationsQuery.ToArrayAsync(token);
+
+ var sections = factWellOperations
+ .GroupBy(o => new { o.IdWell, o.IdWellSectionType })
+ .Select(g => new
+ {
+ g.Key.IdWell,
+ g.Key.IdWellSectionType,
+ DepthStart = g.Min(o => o.DepthStart),
+ DepthEnd = g.Max(o => o.DepthEnd),
+ g.FirstOrDefault()!.WellSectionType.Caption
+ });
+
+ var detectedOperationsQuery = db.DetectedOperations
+ .Where(o => idsTelemetries.Contains(o.IdTelemetry))
+ .Where(o => o.IdCategory == WellOperationCategory.IdSlipsTime)
+ .AsNoTracking();
+
+ if (request.DateStart.HasValue && request.DateEnd.HasValue)
+ detectedOperationsQuery = detectedOperationsQuery
+ .Where(o => o.DateStart < request.DateEnd)
+ .Where(o => o.DateEnd > request.DateStart);
+
+ TimeSpan? durationMinutesMin = request.DurationMinutesMin.HasValue
+ ? new TimeSpan(0, request.DurationMinutesMin.Value, 0)
+ : null;
+ TimeSpan? durationMinutesMax = request.DurationMinutesMax.HasValue
+ ? new TimeSpan(0, request.DurationMinutesMax.Value, 0)
+ : null;
+
+ if (durationMinutesMin.HasValue && durationMinutesMax.HasValue)
+ {
+ detectedOperationsQuery = detectedOperationsQuery
+ .Where(o => o.DateEnd - o.DateStart >= durationMinutesMin.Value
+ && o.DateEnd - o.DateStart <= durationMinutesMax.Value);
+ }
+ else if (durationMinutesMin.HasValue && !durationMinutesMax.HasValue)
+ {
+ detectedOperationsQuery = detectedOperationsQuery
+ .Where(o => o.DateEnd - o.DateStart >= durationMinutesMin.Value);
+ }
+ else if (!durationMinutesMin.HasValue && durationMinutesMax.HasValue)
+ {
+ detectedOperationsQuery = detectedOperationsQuery
+ .Where(o => o.DateEnd - o.DateStart <= durationMinutesMax.Value);
+ }
+
+ var detectedOperations = await detectedOperationsQuery
+ .ToArrayAsync(token);
+
+ var detectedOperationsGroupedByDrillerAndSection = detectedOperations.Select(o => new
+ {
+ Operation = o,
+ IdWell = telemetries[o.IdTelemetry],
+ schedules.FirstOrDefault(s =>
+ s.IdWell == telemetries[o.IdTelemetry]
+ && s.DrillStart <= o.DateStart
+ && s.DrillEnd >= o.DateStart
+ && new TimeDto(s.ShiftStart) <= new TimeDto(o.DateStart.DateTime)
+ && new TimeDto(s.ShiftEnd) >= new TimeDto(o.DateStart.DateTime))
+ ?.Driller,
+ Section = sections.FirstOrDefault(s =>
+ s.IdWell == telemetries[o.IdTelemetry]
+ && s.DepthStart <= o.DepthStart
+ && s.DepthEnd >= o.DepthStart)
+ })
+ .Where(o => o.Driller != null)
+ .Where(o => o.Section != null)
+ .Select(o => new
+ {
+ o.Operation,
+ o.IdWell,
+ Driller = o.Driller!,
+ Section = o.Section!
+ })
+ .GroupBy(o => new { o.Driller.Id, o.Section.IdWellSectionType });
+
+
+ var factWellOperationsGroupedByDrillerAndSection = factWellOperations
+ .Select(o => new
+ {
+ Operation = o,
+ schedules.FirstOrDefault(s =>
+ s.IdWell == o.IdWell
+ && s.DrillStart <= o.DateStart
+ && s.DrillEnd >= o.DateStart
+ && new TimeDto(s.ShiftStart) <= new TimeDto(o.DateStart.DateTime)
+ && new TimeDto(s.ShiftEnd) >= new TimeDto(o.DateStart.DateTime))
+ ?.Driller,
+ })
+ .Where(o => o.Driller != null)
+ .GroupBy(o => new { o.Driller!.Id, o.Operation.IdWellSectionType });
+
+
+ var stats = detectedOperationsGroupedByDrillerAndSection.Select(group => new SlipsStatDto
+ {
+ DrillerName = $"{group.First().Driller!.Name} {group.First().Driller!.Patronymic} {group.First().Driller!.Surname}",
+ SlipsCount = group.Count(),
+ SlipsTimeInMinutes = group
+ .Sum(y => (y.Operation.DateEnd - y.Operation.DateStart).TotalMinutes),
+ SlipsDepth = factWellOperationsGroupedByDrillerAndSection
+ .Where(o => o.Key.Id == group.Key.Id)
+ .Where(o => o.Key.IdWellSectionType == group.Key.IdWellSectionType)
+ .Sum(o => o.Max(op => op.Operation.DepthEnd) - o.Min(op => op.Operation.DepthStart)),
+ SectionCaption = group.First().Section!.Caption,
+ WellCount = group.GroupBy(g => g.IdWell).Count(),
+ });
+
+ return stats;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Controllers/SlipsStatController.cs b/AsbCloudWebApi/Controllers/SlipsStatController.cs
new file mode 100644
index 00000000..f5e1dec8
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/SlipsStatController.cs
@@ -0,0 +1,48 @@
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Requests;
+using AsbCloudDb.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudWebApi.Controllers
+{
+ ///
+ /// Аналитика по удержанию в клиньях
+ ///
+ [Route("api/slipsStat")]
+ [ApiController]
+ [Authorize]
+ public class SlipsStatController : ControllerBase
+ {
+ private readonly ISlipsStatsRepository slipsAnalyticsService;
+
+ public SlipsStatController(ISlipsStatsRepository slipsAnalyticsService)
+ {
+ this.slipsAnalyticsService = slipsAnalyticsService;
+ }
+
+ ///
+ /// Получить аналитику по удержанию в клиньях (по бурильщикам)
+ ///
+ /// Параметры запроса
+ /// Токен отмены задачи
+ /// Список бурильщиков
+ [HttpGet]
+ [Permission]
+ [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)]
+ public async Task GetAllAsync(
+ [FromQuery] OperationStatRequest request,
+ CancellationToken token)
+ {
+ var modal = await slipsAnalyticsService.GetAllAsync(request, token).ConfigureAwait(false);
+
+ return Ok(modal);
+ }
+ }
+}
+