diff --git a/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs b/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs
index 29a2a63e..c35d4135 100644
--- a/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs
+++ b/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs
@@ -87,5 +87,10 @@ namespace AsbCloudApp.Data.ProcessMap
/// Плановый процент использования spin master
///
public double UsageSpin { get; set; }
+
+ ///
+ /// DTO типа секции
+ ///
+ public WellSectionTypeDto WellSectionType { get; set; } = null!;
}
}
diff --git a/AsbCloudApp/Data/WellSectionTypeDto.cs b/AsbCloudApp/Data/WellSectionTypeDto.cs
new file mode 100644
index 00000000..4e89779a
--- /dev/null
+++ b/AsbCloudApp/Data/WellSectionTypeDto.cs
@@ -0,0 +1,17 @@
+namespace AsbCloudApp.Data;
+
+///
+/// Тип секции
+///
+public class WellSectionTypeDto : IId
+{
+ ///
+ /// Id секции
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Название типа секции
+ ///
+ public string Caption { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Services/IProcessMapPlanImportService.cs b/AsbCloudApp/Services/IProcessMapPlanImportService.cs
new file mode 100644
index 00000000..84442df1
--- /dev/null
+++ b/AsbCloudApp/Services/IProcessMapPlanImportService.cs
@@ -0,0 +1,35 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudApp.Services;
+
+///
+/// Сервис импорта/экспорта для РТК вводимых вручную
+///
+public interface IProcessMapPlanImportService
+{
+ ///
+ /// Загрузить данные из файла
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken);
+
+ ///
+ /// Сформировать файл с данными
+ ///
+ ///
+ ///
+ ///
+ Task ExportAsync(int idWell, CancellationToken cancellationToken);
+
+ ///
+ /// Получение шаблона для заполнения
+ ///
+ ///
+ Task GetExcelTemplateStreamAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj
index 86ae7c27..e6a5edcb 100644
--- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj
+++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj
@@ -35,6 +35,7 @@
+
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index 6ddc0248..ed88ec5f 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -128,6 +128,7 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -195,6 +196,8 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient, CrudCacheRepositoryBase>();
// Subsystem service
services.AddTransient, CrudCacheRepositoryBase>();
diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs
new file mode 100644
index 00000000..60fbc9bd
--- /dev/null
+++ b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs
@@ -0,0 +1,352 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.ProcessMap;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using ClosedXML.Excel;
+using System;
+using AsbCloudApp.Data;
+
+namespace AsbCloudInfrastructure.Services.ProcessMap;
+
+/*
+* password for ProcessMapImportTemplate.xlsx is ASB2020!
+*/
+public class ProcessMapPlanImportService : IProcessMapPlanImportService
+{
+ private readonly IProcessMapPlanRepository processMapPlanRepository;
+ private readonly ICrudRepository wellSectionTypeRepository;
+
+ private const string sheetNamePlan = "План";
+
+ private const int headerRowsCount = 2;
+
+ private const int columnWellSectionType = 1;
+ private const int columnMode = 2;
+ private const int columnDepthStart = 3;
+ private const int columnDepthEnd = 4;
+ private const int columnPressurePlan = 5;
+ private const int columnPressureLimitMax = 6;
+ private const int columnAxialLoadPlan = 7;
+ private const int columnAxialLoadLimitMax = 8;
+ private const int columnTopDriveTorquePlan = 9;
+ private const int columnTopDriveTorqueLimitMax = 10;
+ private const int columnTopDriveSpeedPlan = 11;
+ private const int columnTopDriveSpeedLimitMax = 12;
+ private const int columnFlowPlan = 13;
+ private const int columnFlowLimitMax = 14;
+ private const int columnRopPlan = 15;
+ private const int columnUsageSaub = 16;
+ private const int columnUsageSpin = 17;
+
+ private WellSectionTypeDto[] sections = null!;
+
+ public ProcessMapPlanImportService(IProcessMapPlanRepository processMapPlanRepository,
+ ICrudRepository wellSectionTypeRepository)
+ {
+ this.processMapPlanRepository = processMapPlanRepository;
+ this.wellSectionTypeRepository = wellSectionTypeRepository;
+ }
+
+ public async Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken)
+ {
+ sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray();
+
+ using var workBook = new XLWorkbook(stream);
+
+ var processPlanMaps = ParseWorkBook(workBook);
+
+ foreach (var processPlanMap in processPlanMaps)
+ {
+ processPlanMap.IdWell = idWell;
+ processPlanMap.IdUser = idUser;
+ }
+
+ await processMapPlanRepository.InsertRangeAsync(processPlanMaps, cancellationToken);
+ }
+
+ public async Task ExportAsync(int idWell, CancellationToken cancellationToken)
+ {
+ var processMapPlans = (await processMapPlanRepository.GetByIdWellAsync(idWell,
+ cancellationToken)).ToArray();
+
+ return await GenerateExcelFileStreamAsync(processMapPlans,
+ cancellationToken);
+ }
+
+ public async Task GetExcelTemplateStreamAsync(CancellationToken cancellationToken)
+ {
+ var resourceName = Assembly.GetExecutingAssembly()
+ .GetManifestResourceNames()
+ .FirstOrDefault(n => n.EndsWith("ProcessMapPlanTemplate.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, ProcessMapPlanDto[] processMapPlans)
+ {
+ if (!processMapPlans.Any())
+ return;
+
+ var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan)
+ ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}.");
+
+ AddToSheet(sheet, processMapPlans);
+ }
+
+ private void AddToSheet(IXLWorksheet sheet, ProcessMapPlanDto[] processMapPlans)
+ {
+ for (int i = 0; i < processMapPlans.Length; i++)
+ {
+ var row = sheet.Row(1 + i + headerRowsCount);
+ AddToRow(row, processMapPlans[i]);
+ }
+ }
+
+ private void AddToRow(IXLRow row, ProcessMapPlanDto processMap)
+ {
+ row.Cell(columnWellSectionType).Value = processMap.WellSectionType.Caption;
+ row.Cell(columnMode).Value = GetModeCaption(processMap.IdMode);
+ row.Cell(columnDepthStart).Value = processMap.DepthStart;
+ row.Cell(columnDepthEnd).Value = processMap.DepthEnd;
+ row.Cell(columnPressurePlan).Value = processMap.Pressure.Plan;
+ row.Cell(columnPressureLimitMax).Value = processMap.Pressure.LimitMax;
+ row.Cell(columnAxialLoadPlan).Value = processMap.AxialLoad.Plan;
+ row.Cell(columnAxialLoadLimitMax).Value = processMap.AxialLoad.LimitMax;
+ row.Cell(columnTopDriveTorquePlan).Value = processMap.TopDriveTorque.Plan;
+ row.Cell(columnTopDriveTorqueLimitMax).Value = processMap.TopDriveTorque.LimitMax;
+ row.Cell(columnTopDriveSpeedPlan).Value = processMap.TopDriveSpeed.Plan;
+ row.Cell(columnTopDriveSpeedLimitMax).Value = processMap.TopDriveSpeed.LimitMax;
+ row.Cell(columnFlowPlan).Value = processMap.Flow.Plan;
+ row.Cell(columnFlowLimitMax).Value = processMap.Flow.LimitMax;
+ row.Cell(columnRopPlan).Value = processMap.RopPlan;
+ row.Cell(columnUsageSaub).Value = processMap.UsageSaub;
+ row.Cell(columnUsageSpin).Value = processMap.UsageSpin;
+ }
+
+ private ProcessMapPlanDto[] ParseWorkBook(IXLWorkbook workbook)
+ {
+ var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan)
+ ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}.");
+
+ return ParseSheet(sheet);
+ }
+
+ private ProcessMapPlanDto[] ParseSheet(IXLWorksheet sheet)
+ {
+ const int columnsCount = 17;
+
+ if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < columnsCount)
+ throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов.");
+
+ var rowsCount = sheet.RowsUsed().Count() - headerRowsCount;
+
+ if (rowsCount <= 0)
+ return Array.Empty();
+
+ var processMapPlans = new ProcessMapPlanDto[rowsCount];
+
+ var parseErrors = new List();
+
+ for (int i = 0; i < processMapPlans.Length; i++)
+ {
+ var row = sheet.Row(1 + i + headerRowsCount);
+
+ try
+ {
+ processMapPlans[i] = ParseRow(row);
+ }
+ catch (FileFormatException ex)
+ {
+ parseErrors.Add(ex.Message);
+ }
+ }
+
+ if (parseErrors.Any())
+ throw new FileFormatException(string.Join("\r\n", parseErrors));
+
+ return processMapPlans;
+ }
+
+ private ProcessMapPlanDto ParseRow(IXLRow row)
+ {
+ var wellSectionTypeCaption = GetCellValue(row, columnWellSectionType).Trim().ToLower();
+ var modeName = GetCellValue(row, columnMode).Trim().ToLower();
+ var depthStart = GetCellValue(row, columnDepthStart);
+ var depthEnd = GetCellValue(row, columnDepthEnd);
+ var pressurePlan = GetCellValue(row, columnPressurePlan);
+ var pressureLimitMax = GetCellValue(row, columnPressureLimitMax);
+ var axialLoadPlan = GetCellValue(row, columnAxialLoadPlan);
+ var axialLoadLimitMax = GetCellValue(row, columnAxialLoadLimitMax);
+ var topDriveTorquePlan = GetCellValue(row, columnTopDriveTorquePlan);
+ var topDriveTorqueLimitMax = GetCellValue(row, columnTopDriveTorqueLimitMax);
+ var topDriveSpeedPlan = GetCellValue(row, columnTopDriveSpeedPlan);
+ var topDriveSpeedLimitMax = GetCellValue(row, columnTopDriveSpeedLimitMax);
+ var flowPlan = GetCellValue(row, columnFlowPlan);
+ var flowLimitMax = GetCellValue(row, columnFlowLimitMax);
+ var ropPlan = GetCellValue(row, columnRopPlan);
+ var usageSaub = GetCellValue(row, columnUsageSaub);
+ var usageSpin = GetCellValue(row, columnUsageSpin);
+
+ var wellSection = sections.FirstOrDefault(s => s.Caption.Trim().ToLower() == wellSectionTypeCaption)
+ ?? throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная секция");
+
+ var idMode = GetIdMode(modeName)
+ ?? throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный режим");
+
+ if (depthStart is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная стартовая глубина");
+
+ if (depthEnd is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная конечная глубина");
+
+ if (pressurePlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение перепада давления");
+
+ if (pressureLimitMax is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение перепада давления");
+
+ if (axialLoadPlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение нагрузки");
+
+ if (axialLoadLimitMax is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение нагрузки");
+
+ if (topDriveTorquePlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение момента на ВСП");
+
+ if (topDriveTorqueLimitMax is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение момента на ВСП");
+
+ if (topDriveSpeedPlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение оборотов на ВСП");
+
+ if (topDriveSpeedLimitMax is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничения оборота на ВСП");
+
+ if (flowPlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение расхода");
+
+ if (flowLimitMax is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение расхода");
+
+ if (ropPlan is < 0 or > 50000)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение механической скорости");
+
+ if (usageSaub is < 0 or > 100)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный плановый процент использования АКБ");
+
+ if (usageSpin is < 0 or > 100)
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректные плановый процент использования spin master");
+
+ return new()
+ {
+ IdWellSectionType = wellSection.Id,
+ IdMode = idMode,
+ DepthStart = depthStart,
+ LastUpdate = DateTime.UtcNow,
+ DepthEnd = depthEnd,
+ Pressure = new()
+ {
+ Plan = pressurePlan,
+ LimitMax = pressureLimitMax
+ },
+ AxialLoad = new()
+ {
+ Plan = axialLoadPlan,
+ LimitMax = axialLoadLimitMax
+ },
+ TopDriveTorque = new()
+ {
+ Plan = topDriveTorquePlan,
+ LimitMax = topDriveTorqueLimitMax
+ },
+ TopDriveSpeed = new()
+ {
+ Plan = topDriveSpeedPlan,
+ LimitMax = topDriveSpeedLimitMax
+ },
+ Flow = new()
+ {
+ Plan = flowPlan,
+ LimitMax = flowLimitMax
+ },
+ RopPlan = ropPlan,
+ UsageSaub = usageSaub,
+ UsageSpin = usageSpin
+ };
+ }
+
+ private async Task GenerateExcelFileStreamAsync(ProcessMapPlanDto[] processMapPlans,
+ CancellationToken cancellationToken)
+ {
+ using var excelTemplateStream = await GetExcelTemplateStreamAsync(cancellationToken);
+
+ using var workbook = new XLWorkbook(excelTemplateStream, XLEventTracking.Disabled);
+
+ AddToWorkbook(workbook, processMapPlans);
+
+ MemoryStream memoryStream = new MemoryStream();
+ workbook.SaveAs(memoryStream, new SaveOptions { });
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ return memoryStream;
+ }
+
+ private static int? GetIdMode(string modeName) =>
+ modeName switch
+ {
+ "ручной" => 0,
+ "ротор" => 1,
+ "слайд" => 2,
+ _ => null
+ };
+
+ private static string GetModeCaption(int idMode)
+ => idMode switch
+ {
+ 1 => "Ротор",
+ 2 => "Слайд",
+ _ => "Ручной",
+ };
+
+ private static T GetCellValue(IXLRow row, int columnNumber)
+ {
+ var cell = row.Cell(columnNumber);
+
+ if (cell.Value is T cellValue)
+ {
+ return cellValue;
+ }
+
+ throw new FileFormatException(
+ $"Лист {row.Worksheet.Name}. Ячейка:{columnNumber},{row.RowNumber()} содержит некорректное значение");
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx
new file mode 100644
index 00000000..48d2708e
Binary files /dev/null and b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx differ
diff --git a/AsbCloudWebApi/Controllers/ProcessMapController.cs b/AsbCloudWebApi/Controllers/ProcessMapController.cs
index a8fbd303..474c260c 100644
--- a/AsbCloudWebApi/Controllers/ProcessMapController.cs
+++ b/AsbCloudWebApi/Controllers/ProcessMapController.cs
@@ -7,9 +7,12 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
namespace AsbCloudWebApi.Controllers
{
@@ -25,6 +28,7 @@ namespace AsbCloudWebApi.Controllers
private readonly IHubContext telemetryHubContext;
private readonly IProcessMapReportMakerService processMapReportService;
private readonly IProcessMapReportService processMapService;
+ private readonly IProcessMapPlanImportService processMapPlanImportService;
private const string SirnalRMethodGetDataName = "UpdateProcessMap";
@@ -34,13 +38,15 @@ namespace AsbCloudWebApi.Controllers
IProcessMapReportMakerService processMapReportService,
IProcessMapReportService processMapService,
ITelemetryService telemetryService,
- IHubContext telemetryHubContext)
+ IHubContext telemetryHubContext,
+ IProcessMapPlanImportService processMapPlanImportService)
: base(wellService, repository)
{
this.telemetryService = telemetryService;
this.telemetryHubContext = telemetryHubContext;
this.processMapReportService = processMapReportService;
this.processMapService = processMapService;
+ this.processMapPlanImportService = processMapPlanImportService;
}
@@ -61,7 +67,6 @@ namespace AsbCloudWebApi.Controllers
var idWell = telemetryService.GetIdWellByTelemetryUid(uid);
if (idWell is null)
return BadRequest($"Wrong uid {uid}");
-#warning implement Process map get method
return Ok(Enumerable.Empty());
}
@@ -94,7 +99,6 @@ namespace AsbCloudWebApi.Controllers
///
/// ///
///
- ///
[HttpGet]
[Route("getReportFile/{wellId}")]
[ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
@@ -132,7 +136,7 @@ namespace AsbCloudWebApi.Controllers
}
///
- /// Добавить запись
+ /// Добавить запись плановой РТК
///
///
///
@@ -147,7 +151,7 @@ namespace AsbCloudWebApi.Controllers
}
///
- /// Редактировать запись по id
+ /// Редактировать запись по id плановой РТК
///
/// запись
///
@@ -161,6 +165,77 @@ namespace AsbCloudWebApi.Controllers
return result;
}
+ ///
+ /// Возвращает шаблон файла импорта плановой РТК
+ ///
+ /// Запрашиваемый файл
+ [HttpGet]
+ [Route("template")]
+ [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
+ public async Task GetTemplateAsync(CancellationToken cancellationToken)
+ {
+ var stream = await processMapPlanImportService.GetExcelTemplateStreamAsync(cancellationToken);
+ var fileName = "ЕЦП_шаблон_файла_РТК.xlsx";
+ return File(stream, "application/octet-stream", fileName);
+ }
+
+ ///
+ /// Импортирует плановой РТК из excel (xlsx) файла
+ ///
+ /// Id скважины
+ /// Загружаемый файл
+ ///
+ ///
+ [HttpPost]
+ [Route("import")]
+ public async Task ImportAsync(int idWell,
+ [Required] IFormFile file,
+ CancellationToken cancellationToken)
+ {
+ int? idUser = User.GetUserId();
+
+ if (idUser is null)
+ return Forbid();
+
+ if (Path.GetExtension(file.FileName).ToLower() != ".xlsx")
+ return BadRequest("Требуется xlsx файл.");
+
+ using Stream stream = file.OpenReadStream();
+
+ await processMapPlanImportService.ImportAsync(idWell,
+ idUser.Value,
+ stream,
+ cancellationToken);
+
+ return Ok();
+ }
+
+ ///
+ /// Экспорт плановой РТК в excel
+ ///
+ /// Id скважины
+ ///
+ ///
+ [HttpGet]
+ [Route("export")]
+ [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
+ public async Task ExportAsync([FromQuery] int idWell, CancellationToken cancellationToken)
+ {
+ int? idUser = User.GetUserId();
+
+ if (idUser is null)
+ return Forbid();
+
+ var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken);
+
+ if (well is null)
+ return NoContent();
+
+ var stream = await processMapPlanImportService.ExportAsync(idWell, cancellationToken);
+ var fileName = $"РТК-план по скважине {well.Caption} куст {well.Cluster}.xlsx";
+ return File(stream, "application/octet-stream", fileName);
+ }
+
private async Task NotifyUsersBySignalR(int idWell, CancellationToken token)
{
var dtos = await service.GetAllAsync(idWell, null, token);