diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportDto.cs new file mode 100644 index 00000000..ea3b3c53 --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportDto.cs @@ -0,0 +1,28 @@ +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// DTO авто-сгенерированного суточного отчёта +/// +public class AutoGeneratedDailyReportDto : AutoGeneratedDailyReportInfoDto +{ + /// + /// Блок заголовка + /// + public HeadBlockDto Head { get; set; } = null!; + + //TODO: поля не должны быть массивами + /// + /// Блок подсистем + /// + public SubsystemRecordDto[] Subsystems { get; set; } = null!; + + /// + /// Блок ограничивающих параметров + /// + public LimitingParameterRecordDto[] LimitingParameters { get; set; } = null!; + + /// + /// Баланс времени + /// + public TimeBalanceRecordDto[] TimeBalance { get; set; } = null!; +} \ No newline at end of file diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportInfoDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportInfoDto.cs new file mode 100644 index 00000000..0034879a --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/AutoGeneratedDailyReportInfoDto.cs @@ -0,0 +1,25 @@ +using System; + +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// Базовая информация о суточном отчёте +/// +public class AutoGeneratedDailyReportInfoDto +{ + /// + /// Дата формирования отчёта + /// + public DateOnly ReportDate { get; set; } + + /// + /// Название файла + /// + public string FileName { get; set; } = null!; + + /// + /// Размер файла + /// + public int FileSize { get; set; } + +} \ No newline at end of file diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/HeadBlockDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/HeadBlockDto.cs new file mode 100644 index 00000000..43ff5246 --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/HeadBlockDto.cs @@ -0,0 +1,37 @@ +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// Блок заголовка +/// +public class HeadBlockDto +{ + /// + /// Название скважины + /// + public string Well { get; set; } = null!; + + /// + /// Название куста + /// + public string Cluster { get; set; } = null!; + + /// + /// Заказчик + /// + public string Customer { get; set; } = null!; + + /// + /// Месторождение + /// + public string Deposit { get; set; } = null!; + + /// + /// Глубина забоя на дату начала интервала + /// + public double DepthFrom { get; set; } + + /// + /// Глубина забоя на дату окончания интервала + /// + public double DepthTo { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/LimitingParameterRecordDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/LimitingParameterRecordDto.cs new file mode 100644 index 00000000..e62b32d9 --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/LimitingParameterRecordDto.cs @@ -0,0 +1,27 @@ +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// Блок ограничивающих параметров +/// +public class LimitingParameterRecordDto +{ + /// + /// Время использования, мин + /// + public double Hours { get; set; } + + /// + /// Проходка, м + /// + public double Depth { get; set; } + + /// + /// Название ограничивающего параметра + /// + public string NameFeedRegulator { get; set; } = null!; + + /// + /// Процент по проходке, % + /// + public double PercentDepth { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/SubsystemRecordDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/SubsystemRecordDto.cs new file mode 100644 index 00000000..34b2e4a7 --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/SubsystemRecordDto.cs @@ -0,0 +1,27 @@ +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// Блок подсистем +/// +public class SubsystemRecordDto +{ + /// + /// Название подсистемы + /// + public string Name { get; set; } = null!; + + /// + /// Использование, % + /// + public double KUsage { get; set; } + + /// + /// Время работы, ч + /// + public double UsedTimeHours { get; set; } + + /// + /// Проходка + /// + public double Depth { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Data/AutogeneratedDailyReport/TimeBalanceRecordDto.cs b/AsbCloudApp/Data/AutogeneratedDailyReport/TimeBalanceRecordDto.cs new file mode 100644 index 00000000..a6ac9845 --- /dev/null +++ b/AsbCloudApp/Data/AutogeneratedDailyReport/TimeBalanceRecordDto.cs @@ -0,0 +1,17 @@ +namespace AsbCloudApp.Data.AutogeneratedDailyReport; + +/// +/// Баланс времени +/// +public class TimeBalanceRecordDto +{ + /// + /// Название операции + /// + public string Name { get; set; } = null!; + + /// + /// Продолжительность, часы + /// + public double DurationHours { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Data/WellSectionTypeDto.cs b/AsbCloudApp/Data/WellSectionTypeDto.cs index 88434e20..28e9ba68 100644 --- a/AsbCloudApp/Data/WellSectionTypeDto.cs +++ b/AsbCloudApp/Data/WellSectionTypeDto.cs @@ -18,5 +18,5 @@ public class WellSectionTypeDto : IId /// /// Порядок /// - public int Order { get; set; } + public float Order { get; set; } } \ No newline at end of file diff --git a/AsbCloudApp/Repositories/IProcessMapRepository.cs b/AsbCloudApp/Repositories/IProcessMapRepository.cs index b126e18a..f6dbe6b4 100644 --- a/AsbCloudApp/Repositories/IProcessMapRepository.cs +++ b/AsbCloudApp/Repositories/IProcessMapRepository.cs @@ -9,12 +9,12 @@ using System.Threading.Tasks; namespace AsbCloudApp.Repositories { /// - /// + /// РТК-план /// public interface IProcessMapPlanRepository : IRepositoryWellRelated { /// - /// . + /// Получить РТК-план начиная с даты. /// /// /// @@ -24,11 +24,20 @@ namespace AsbCloudApp.Repositories DateTime? updateFrom, CancellationToken token = default); /// - /// + /// Получить РТК-план /// /// /// /// - Task> GetProcessMapAsync(IEnumerable requests, CancellationToken token); + Task> GetProcessMapAsync(IEnumerable requests, + CancellationToken token); + + /// + /// Удалить РТК-план по скважине + /// + /// + /// + /// + Task RemoveByWellAsync(int idWell, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/AsbCloudApp/Requests/AutoGeneratedDailyReportRequest.cs b/AsbCloudApp/Requests/AutoGeneratedDailyReportRequest.cs new file mode 100644 index 00000000..5e7dae7c --- /dev/null +++ b/AsbCloudApp/Requests/AutoGeneratedDailyReportRequest.cs @@ -0,0 +1,19 @@ +using System; + +namespace AsbCloudApp.Requests; + +/// +/// Параметры запроса для получения авто-генерируемых суточных отчётов +/// +public class AutoGeneratedDailyReportRequest : RequestBase +{ + /// + /// Дата начала периода + /// + public DateOnly? StartDate { get; set; } + + /// + /// Дата конца периода + /// + public DateOnly? FinishDate { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportMakerService.cs b/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportMakerService.cs new file mode 100644 index 00000000..8571e356 --- /dev/null +++ b/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportMakerService.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.AutogeneratedDailyReport; + +namespace AsbCloudApp.Services.AutoGeneratedDailyReports; + +/// +/// Сервис для генерации файлов авто-генерируемых суточный отчётов +/// +public interface IAutoGeneratedDailyReportMakerService +{ + /// + /// Генерация файла + /// + /// + /// + /// + Task MakeReportAsync(AutoGeneratedDailyReportDto report, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportService.cs b/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportService.cs new file mode 100644 index 00000000..16cdaed6 --- /dev/null +++ b/AsbCloudApp/Services/AutoGeneratedDailyReports/IAutoGeneratedDailyReportService.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using AsbCloudApp.Requests; + +namespace AsbCloudApp.Services.AutoGeneratedDailyReports; + +/// +/// Сервис для работы с авто-генерируемыми суточными отчётами +/// +public interface IAutoGeneratedDailyReportService +{ + /// + /// Список файлов суточных отчётов + /// + /// + /// + /// + /// + Task> GetListAsync(int idWell, + AutoGeneratedDailyReportRequest request, + CancellationToken cancellationToken); + + /// + /// Генерация файла с отчётом + /// + /// + /// + /// + /// + Task<(string fileName, Stream stream)> GenerateReportAsync(int idWell, DateOnly reportDate, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/IProcessMapPlanImportService.cs b/AsbCloudApp/Services/IProcessMapPlanImportService.cs index 84442df1..c5daa587 100644 --- a/AsbCloudApp/Services/IProcessMapPlanImportService.cs +++ b/AsbCloudApp/Services/IProcessMapPlanImportService.cs @@ -14,10 +14,12 @@ public interface IProcessMapPlanImportService /// /// /// + /// /// /// /// - Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken); + Task ImportAsync(int idWell, int idUser, bool deleteProcessMapPlansBeforeImport, Stream stream, + CancellationToken cancellationToken); /// /// Сформировать файл с данными diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj index e6a5edcb..d807a6c8 100644 --- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj +++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj @@ -36,6 +36,7 @@ + diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 338982aa..64a2fa49 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -23,7 +23,9 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; +using AsbCloudApp.Services.AutoGeneratedDailyReports; using AsbCloudApp.Services.Notifications; +using AsbCloudInfrastructure.Services.AutoGeneratedDailyReports; namespace AsbCloudInfrastructure { @@ -214,7 +216,10 @@ namespace AsbCloudInfrastructure services.AddTransient, WitsRecordRepository>(); services.AddTransient, WitsRecordRepository>(); services.AddTransient, WitsRecordRepository>(); - services.AddTransient, WitsRecordRepository>(); + services.AddTransient, WitsRecordRepository>(); + + services.AddTransient(); + services.AddTransient(); return services; } diff --git a/AsbCloudInfrastructure/Repository/ProcessMapRepository.cs b/AsbCloudInfrastructure/Repository/ProcessMapRepository.cs index b80fe6d9..6597210b 100644 --- a/AsbCloudInfrastructure/Repository/ProcessMapRepository.cs +++ b/AsbCloudInfrastructure/Repository/ProcessMapRepository.cs @@ -60,6 +60,15 @@ namespace AsbCloudInfrastructure.Repository return dtos; } + public Task RemoveByWellAsync(int idWell, CancellationToken cancellationToken) + { + var query = dbContext.ProcessMap.Where(x => x.IdWell == idWell); + + dbContext.ProcessMap.RemoveRange(query); + + return dbContext.SaveChangesAsync(cancellationToken); + } + public override async Task InsertAsync(ProcessMapPlanDto dto, CancellationToken token) { diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportMakerService.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportMakerService.cs new file mode 100644 index 00000000..0f1c7076 --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportMakerService.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using AsbCloudApp.Services.AutoGeneratedDailyReports; +using AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports; + +public class AutoGeneratedDailyReportMakerService : IAutoGeneratedDailyReportMakerService +{ + private readonly IEnumerable blockWriters = new List() + { + new HeadExcelBlockWriter(), + new SubsystemExcelBlockWriter(), + new LimitingParameterExcelBlockWriter(), + new TimeBalanceExcelBlockWriter() + }; + + public async Task MakeReportAsync(AutoGeneratedDailyReportDto report, CancellationToken cancellationToken) + { + using var excelTemplateStream = await GetExcelTemplateStreamAsync(cancellationToken); + + using var workbook = new XLWorkbook(excelTemplateStream, XLEventTracking.Disabled); + + AddToWorkbook(workbook, report); + + MemoryStream memoryStream = new MemoryStream(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + + return memoryStream; + } + + private async Task GetExcelTemplateStreamAsync(CancellationToken cancellationToken) + { + var resourceName = Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("AutogeneratedDailyReportTemplate.xlsx"))!; + + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName)!; + + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + return memoryStream; + } + + private void AddToWorkbook(XLWorkbook workbook, AutoGeneratedDailyReportDto report) + { + const string sheetName = "Рапорт"; + + var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetName) + ?? throw new FileFormatException($"Книга excel не содержит листа {sheetName}."); + + foreach (var blockBuilder in blockWriters) + { + blockBuilder.Write(sheet, report); + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs new file mode 100644 index 00000000..4012ebdf --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using AsbCloudApp.Data.SAUB; +using AsbCloudApp.Data.Subsystems; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudApp.Services; +using AsbCloudApp.Services.AutoGeneratedDailyReports; +using AsbCloudApp.Services.Subsystems; +using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.SAUB; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports; + +public class AutoGeneratedDailyReportService : IAutoGeneratedDailyReportService +{ + private const string fileNameTemplate = "Суточный_отчёт_по_скважине_{0}_куст_{1}_от_{2}.xlsx"; + + private readonly IWellService wellService; + private readonly IWellOperationRepository wellOperationRepository; + private readonly TelemetryDataCache telemetryDataCache; + private readonly ISubsystemOperationTimeService subsystemOperationTimeService; + private readonly ICrudRepository subsystemRepository; + private readonly ILimitingParameterService limitingParameterService; + private readonly IAutoGeneratedDailyReportMakerService autoGeneratedDailyReportMakerService; + + public AutoGeneratedDailyReportService(IWellService wellService, + IWellOperationRepository wellOperationRepository, + TelemetryDataCache telemetryDataCache, + ISubsystemOperationTimeService subsystemOperationTimeService, + ICrudRepository subsystemRepository, + ILimitingParameterService limitingParameterService, + IAutoGeneratedDailyReportMakerService autoGeneratedDailyReportMakerService) + { + this.wellOperationRepository = wellOperationRepository; + this.wellService = wellService; + this.telemetryDataCache = telemetryDataCache; + this.subsystemOperationTimeService = subsystemOperationTimeService; + this.subsystemRepository = subsystemRepository; + this.limitingParameterService = limitingParameterService; + this.autoGeneratedDailyReportMakerService = autoGeneratedDailyReportMakerService; + } + + public async Task> GetListAsync(int idWell, + AutoGeneratedDailyReportRequest request, + CancellationToken cancellationToken) + { + var result = new PaginationContainer + { + Skip = request.Skip ?? 0, + Take = request.Take ?? 10, + Items = Enumerable.Empty() + }; + + var reports = new List(); + + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) + ?? throw new ArgumentInvalidException("Скважина не найдена", nameof(idWell)); + + if (!well.IdTelemetry.HasValue) + throw new ArgumentInvalidException("Телеметрия для скважины отсутствует", nameof(idWell)); + + var datesRange = telemetryDataCache.GetOrDefaultDataDateRange(well.IdTelemetry.Value); + + if (datesRange is null) + return result; + + result.Count = (int)(Math.Ceiling((datesRange.To - DateTime.UnixEpoch).TotalDays) - Math.Floor((datesRange.From - DateTime.UnixEpoch).TotalDays)); + + if (request.StartDate.HasValue) + { + var startDate = new DateTime(request.StartDate.Value.Year, request.StartDate.Value.Month, + request.StartDate.Value.Day); + + if(startDate.Date >= datesRange.From.Date) + datesRange.From = startDate; + } + + if (request.FinishDate.HasValue) + { + var finishDate = new DateTime(request.FinishDate.Value.Year, request.FinishDate.Value.Month, + request.FinishDate.Value.Day); + + if (finishDate.Date <= datesRange.To.Date) + datesRange.To = finishDate; + } + + for (int day = result.Skip; (day - result.Skip) < result.Take && (datesRange.From.AddDays(day)) <= datesRange.To; day++) + { + var dateFrom = datesRange.From.AddDays(day); + + reports.Add(new AutoGeneratedDailyReportDto + { + FileName = string.Format(fileNameTemplate, well.Caption, well.Cluster, DateOnly.FromDateTime(dateFrom)), + ReportDate = DateOnly.FromDateTime(dateFrom), + FileSize = GetFileSize() / 1024, + }); + } + + result.Items = reports; + + return result; + } + + public async Task<(string fileName, Stream stream)> GenerateReportAsync(int idWell, DateOnly reportDate, + CancellationToken cancellationToken) + { + var startDate = new DateTime(reportDate.Year, reportDate.Month, reportDate.Day); + var finishDate = startDate.AddDays(1); + + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) + ?? throw new ArgumentInvalidException("Скважина не найдена", nameof(idWell)); + + if (!well.IdTelemetry.HasValue) + throw new ArgumentInvalidException("Телеметрия для скважины отсутствует", nameof(idWell)); + + var factOperations = (await GetFactOperationsAsync(well.Id, startDate, finishDate, + cancellationToken)).ToArray(); + + var report = new AutoGeneratedDailyReportDto + { + FileName = string.Format(fileNameTemplate, well.Caption, well.Cluster, reportDate), + FileSize = GetFileSize() / 1024, + ReportDate = reportDate, + Head = CreateHeadBlock(well, reportDate, factOperations), + Subsystems = (await CreateSubsystemBlockAsync(idWell, startDate, finishDate, cancellationToken)).ToArray(), + LimitingParameters = (await CreateLimitingParameterBlockAsync(idWell, startDate, finishDate, cancellationToken)).ToArray(), + TimeBalance = factOperations.GroupBy(w => w.CategoryName).Select(x => new TimeBalanceRecordDto + { + Name = x.Key ?? "Название операции отсутствует", + DurationHours = x.Sum(o => o.DurationHours) + }).ToArray(), + }; + + var stream = await autoGeneratedDailyReportMakerService.MakeReportAsync(report, cancellationToken); + + return (report.FileName, stream); + } + + private HeadBlockDto CreateHeadBlock(WellDto well, DateOnly reportDate, WellOperationDto[] factOperations) + { + var customer = well.Companies.FirstOrDefault(company => company.IdCompanyType == 1); + + return new HeadBlockDto + { + Customer = customer?.Caption ?? string.Empty, + Deposit = well.Deposit ?? string.Empty, + Cluster = well.Cluster ?? string.Empty, + Well = well.Caption, + DepthFrom = factOperations.FirstOrDefault()?.DepthStart ?? 0.00, + DepthTo = factOperations.LastOrDefault()?.DepthEnd ?? 0.00 + }; + } + + private async Task> CreateSubsystemBlockAsync(int idWell, DateTime startDate, + DateTime finishDate, + CancellationToken cancellationToken) + { + var subsystems = await subsystemRepository.GetAllAsync(cancellationToken); + var subsystemStats = await GetSubsystemStatsAsync(idWell, startDate, finishDate, + cancellationToken); + + return subsystems.Select(subsystem => + { + var subsytemStat = subsystemStats?.FirstOrDefault(s => s.IdSubsystem == subsystem.Id); + + return new SubsystemRecordDto + { + Name = subsystem.Name, + KUsage = subsytemStat?.KUsage ?? 0.00, + UsedTimeHours = subsytemStat?.UsedTimeHours ?? 0.00, + Depth = subsytemStat?.SumDepthInterval ?? 0.00, + }; + }); + } + + private async Task> CreateLimitingParameterBlockAsync(int idWell, + DateTime startDate, DateTime finishDate, CancellationToken cancellationToken) + { + var limitingParameterStats = (await GetLimitingParameterStatsAsync(idWell, + startDate, finishDate, cancellationToken)).ToArray(); + + var sumDepths = limitingParameterStats.Sum(x => x.Depth); + + return limitingParameterStats.Select(l => new LimitingParameterRecordDto + { + NameFeedRegulator = l.NameFeedRegulator, + Hours = l.TotalMinutes, + PercentDepth = l.Depth / sumDepths, + Depth = l.Depth, + }); + } + + private Task> GetFactOperationsAsync(int idWell, DateTime startDate, + DateTime finishDate, CancellationToken cancellationToken) + { + var request = new WellOperationRequest + { + IdWell = idWell, + OperationType = WellOperation.IdOperationTypeFact, + GeDate = startDate, + LtDate = finishDate, + SortFields = new[] { "DateStart asc" }, + }; + + return wellOperationRepository.GetAsync(request, cancellationToken); + } + + private Task?> GetSubsystemStatsAsync(int idWell, DateTime startDate, + DateTime finishDate, CancellationToken cancellationToken) + { + var request = new SubsystemOperationTimeRequest + { + IdWell = idWell, + GtDate = startDate, + LtDate = finishDate, + }; + + return subsystemOperationTimeService.GetStatAsync(request, cancellationToken); + } + + private Task> GetLimitingParameterStatsAsync(int idWell, + DateTime startDate, DateTime finishDate, CancellationToken cancellationToken) + { + var request = new LimitingParameterRequest + { + IdWell = idWell, + GtDate = startDate, + LtDate = finishDate, + }; + + return limitingParameterService.GetStatAsync(request, cancellationToken); + } + + private int GetFileSize() + { + const int fileSizeTemplate = 10240; + // TODO: Добавку размера сделать более предсказуемой на основе даты рапорта. что то типа `(Date.Ticks * idWell) % (fileSizeTemplate / 10)` + return new Random().Next(1, 8193) + fileSizeTemplate; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/HeadExcelBlockWriter.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/HeadExcelBlockWriter.cs new file mode 100644 index 00000000..dad6bf35 --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/HeadExcelBlockWriter.cs @@ -0,0 +1,34 @@ +using AsbCloudApp.Data.AutogeneratedDailyReport; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; + +public class HeadExcelBlockWriter : IExcelBlockWriter +{ + private static readonly (int, int) customerCell = (2, 2); + private static readonly (int, int) depositCell = (4, 2); + private static readonly (int, int) clusterCell = (5, 2); + private static readonly (int, int) wellCell = (6, 2); + + private const int dateRow = 9; + private const int dateFromColumn = 2; + private const int dateFromToColumn = 3; + + private const int depthRow = 10; + private const int depthFromColumn = 2; + private const int depthToColumn = 3; + + public void Write(IXLWorksheet sheet, AutoGeneratedDailyReportDto report) + { + sheet.Cell(customerCell.Item1, customerCell.Item2).Value = report.Head.Customer; + sheet.Cell(depositCell.Item1, depositCell.Item2).Value = report.Head.Deposit; + sheet.Cell(clusterCell.Item1, clusterCell.Item2).Value = report.Head.Cluster; + sheet.Cell(wellCell.Item1, wellCell.Item2).Value = report.Head.Well; + + sheet.Cell(dateRow, dateFromColumn).Value = report.ReportDate; + sheet.Cell(dateRow, dateFromToColumn).Value = report.ReportDate.AddDays(1); + + sheet.Cell(depthRow, depthFromColumn).Value = report.Head.DepthFrom; + sheet.Cell(depthRow, depthToColumn).Value = report.Head.DepthTo; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/IExcelBlockWriter.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/IExcelBlockWriter.cs new file mode 100644 index 00000000..0f83fe05 --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/IExcelBlockWriter.cs @@ -0,0 +1,9 @@ +using AsbCloudApp.Data.AutogeneratedDailyReport; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; + +public interface IExcelBlockWriter +{ + void Write(IXLWorksheet sheet, AutoGeneratedDailyReportDto report); +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/LimitingParameterExcelBlockWriter.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/LimitingParameterExcelBlockWriter.cs new file mode 100644 index 00000000..8e99e755 --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/LimitingParameterExcelBlockWriter.cs @@ -0,0 +1,31 @@ +using System.Linq; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; + +public class LimitingParameterExcelBlockWriter : IExcelBlockWriter +{ + private const int rowHeaderBlock = 20; + + private const int columnNameFeedRegulator = 1; + private const int columnDepth = 2; + private const int columnTotalHours = 3; + private const int columnPercentDepth = 4; + + public void Write(IXLWorksheet sheet, AutoGeneratedDailyReportDto report) + { + if(!report.LimitingParameters.Any()) + return; + + for (int i = 0; i < report.LimitingParameters.Length; i++) + { + var row = sheet.Row(1 + i + rowHeaderBlock); + + row.Cell(columnNameFeedRegulator).Value = report.LimitingParameters[i].NameFeedRegulator; + row.Cell(columnDepth).Value = report.LimitingParameters[i].Depth; + row.Cell(columnTotalHours).Value = report.LimitingParameters[i].Hours; + row.Cell(columnPercentDepth).Value = report.LimitingParameters[i].PercentDepth; + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/SubsystemExcelBlockWriter.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/SubsystemExcelBlockWriter.cs new file mode 100644 index 00000000..ec32c60e --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/SubsystemExcelBlockWriter.cs @@ -0,0 +1,31 @@ +using System.Linq; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; + +public class SubsystemExcelBlockWriter : IExcelBlockWriter +{ + private const int rowHeaderBlock = 13; + + private const int columnName = 1; + private const int columnKUsage = 2; + private const int columnDepth = 3; + private const int columnUsedTimeHours = 4; + + public void Write(IXLWorksheet sheet, AutoGeneratedDailyReportDto report) + { + if(!report.Subsystems.Any()) + return; + + for (int i = 0; i < report.Subsystems.Length; i++) + { + var row = sheet.Row(1 + i + rowHeaderBlock); + + row.Cell(columnName).Value = report.Subsystems[i].Name; + row.Cell(columnKUsage).Value = report.Subsystems[i].KUsage; + row.Cell(columnDepth).Value = report.Subsystems[i].Depth; + row.Cell(columnUsedTimeHours).Value = report.Subsystems[i].UsedTimeHours; + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/TimeBalanceExcelBlockWriter.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/TimeBalanceExcelBlockWriter.cs new file mode 100644 index 00000000..c61fed8f --- /dev/null +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportBlocks/TimeBalanceExcelBlockWriter.cs @@ -0,0 +1,38 @@ +using System.Linq; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.AutoGeneratedDailyReports.AutogeneratedDailyReportBlocks; + +public class TimeBalanceExcelBlockWriter : IExcelBlockWriter +{ + private const int rowHeaderBlock = 27; + + private const int columnName = 1; + private const int columnDurationHours = 2; + + public void Write(IXLWorksheet sheet, AutoGeneratedDailyReportDto report) + { + if(!report.TimeBalance.Any()) + return; + + for (int i = 0; i < report.TimeBalance.Length; i++) + { + var row = sheet.Row(1 + i + rowHeaderBlock); + + row.Cell(columnName).Value = report.TimeBalance[i].Name; + row.Cell(columnDurationHours).Value = report.TimeBalance[i].DurationHours; + + AddBorderToCell(row.Cell(columnName)); + AddBorderToCell(row.Cell(columnDurationHours)); + } + } + + private void AddBorderToCell(IXLCell cell) + { + cell.Style.Border.TopBorder = XLBorderStyleValues.Thin; + cell.Style.Border.BottomBorder = XLBorderStyleValues.Thin; + cell.Style.Border.LeftBorder = XLBorderStyleValues.Thin; + cell.Style.Border.RightBorder = XLBorderStyleValues.Thin; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportTemplate.xlsx b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportTemplate.xlsx new file mode 100644 index 00000000..0a887a4e Binary files /dev/null and b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutogeneratedDailyReportTemplate.xlsx differ diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs index 60fbc9bd..2b4467af 100644 --- a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs +++ b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs @@ -52,7 +52,8 @@ public class ProcessMapPlanImportService : IProcessMapPlanImportService this.wellSectionTypeRepository = wellSectionTypeRepository; } - public async Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken) + public async Task ImportAsync(int idWell, int idUser, bool deleteProcessMapPlansBeforeImport, Stream stream, + CancellationToken cancellationToken) { sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray(); @@ -60,6 +61,9 @@ public class ProcessMapPlanImportService : IProcessMapPlanImportService var processPlanMaps = ParseWorkBook(workBook); + if (deleteProcessMapPlansBeforeImport) + await processMapPlanRepository.RemoveByWellAsync(idWell, cancellationToken); + foreach (var processPlanMap in processPlanMaps) { processPlanMap.IdWell = idWell; diff --git a/AsbCloudWebApi/Controllers/AutoGeneratedDailyReportController.cs b/AsbCloudWebApi/Controllers/AutoGeneratedDailyReportController.cs new file mode 100644 index 00000000..0ef6c6da --- /dev/null +++ b/AsbCloudWebApi/Controllers/AutoGeneratedDailyReportController.cs @@ -0,0 +1,89 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.AutogeneratedDailyReport; +using AsbCloudApp.Requests; +using AsbCloudApp.Services; +using AsbCloudApp.Services.AutoGeneratedDailyReports; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace AsbCloudWebApi.Controllers; + +/// +/// Контроллер для авто-генерируемых суточных отчётов +/// +[ApiController] +[Route("api/well/{idWell}/[controller]")] +[Authorize] +public class AutoGeneratedDailyReportController : ControllerBase +{ + private readonly IAutoGeneratedDailyReportService autoGeneratedDailyReportService; + private readonly IWellService wellService; + + public AutoGeneratedDailyReportController(IAutoGeneratedDailyReportService autoGeneratedDailyReportService, + IWellService wellService) + { + this.autoGeneratedDailyReportService = autoGeneratedDailyReportService; + this.wellService = wellService; + } + + /// + /// Формирование отчёта + /// + /// Id скважины + /// Дата отчёта + /// + /// + [HttpGet] + [Route("generate")] + [ProducesResponseType(typeof(PhysicalFileResult), (int)HttpStatusCode.OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task GenerateAsync([FromRoute] int idWell, + [Required] DateOnly reportDate, + CancellationToken cancellationToken) + { + if (!await CanUserAccessToWellAsync(idWell, cancellationToken)) + return Forbid(); + + var reportFile = await autoGeneratedDailyReportService.GenerateReportAsync(idWell, + reportDate, + cancellationToken); + + return File(reportFile.stream, "application/octet-stream", reportFile.fileName); + } + + /// + /// Список файлов суточных отчётов + /// + /// Id скважины + /// Параметры запроса + /// + /// + [HttpGet] + [ProducesResponseType(typeof(PaginationContainer), (int)HttpStatusCode.OK)] + public async Task GetListAsync([FromRoute][Required] int idWell, + [FromQuery] AutoGeneratedDailyReportRequest request, + CancellationToken cancellationToken) + { + if (!await CanUserAccessToWellAsync(idWell, cancellationToken)) + return Forbid(); + + var reports = await autoGeneratedDailyReportService.GetListAsync(idWell, + request, + cancellationToken); + + return Ok(reports); + } + + private async Task CanUserAccessToWellAsync(int idWell, CancellationToken cancellationToken) + { + int? idCompany = User.GetCompanyId(); + return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + idWell, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/ProcessMapController.cs b/AsbCloudWebApi/Controllers/ProcessMapController.cs index 474c260c..95f41405 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapController.cs @@ -183,12 +183,14 @@ namespace AsbCloudWebApi.Controllers /// Импортирует плановой РТК из excel (xlsx) файла /// /// Id скважины + /// Удалить РТК перед импортом = 1, если файл валидный /// Загружаемый файл /// /// [HttpPost] - [Route("import")] + [Route("import/{idWell}/{options}")] public async Task ImportAsync(int idWell, + int options, [Required] IFormFile file, CancellationToken cancellationToken) { @@ -204,6 +206,7 @@ namespace AsbCloudWebApi.Controllers await processMapPlanImportService.ImportAsync(idWell, idUser.Value, + (options & 1) > 0, stream, cancellationToken); @@ -217,9 +220,9 @@ namespace AsbCloudWebApi.Controllers /// /// [HttpGet] - [Route("export")] + [Route("export/{idWell}")] [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] - public async Task ExportAsync([FromQuery] int idWell, CancellationToken cancellationToken) + public async Task ExportAsync(int idWell, CancellationToken cancellationToken) { int? idUser = User.GetUserId();