< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 901
Coverable lines: 901
Total lines: 1508
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 626
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: GetAlertChips()0%2040%
File 1: GetAlertPreview()0%156120%
File 1: RefreshAlerts()100%210%
File 1: AcknowledgeAlert()0%620%
File 1: MuteAlert()0%2040%
File 1: RestoreAlert()100%210%
File 1: AlertResult(...)0%620%
File 2: .ctor(...)100%210%
File 2: GetBadgeSummary()0%2040%
File 2: LegacyIndexRedirect()0%2040%
File 2: Index()0%5256720%
File 2: OrderSearchSnapshot()100%210%
File 2: OrderSearchLookup()0%110100%
File 2: EnqueueOrderSearchDriftRepair()0%2040%
File 2: EnqueueOrderSearchFullReindex()100%210%
File 2: PostingErrorDetail()0%620%
File 2: PostingOrders()0%156120%
File 2: LoadTransactionsTabAsync()0%342180%
File 2: BuildOrderExecutionLookupAsync()0%4422660%
File 2: FindTransaction()0%4260%
File 2: LoadOneCTabAsync()0%600240%
File 2: MapOneCPosting(...)100%210%
File 2: GetErrorDetails(...)0%7280%
File 2: GetCreatedNomenclature(...)0%4260%
File 2: LoadWebhookTabAsync()0%156120%
File 2: RefreshWebhookHealth()100%210%
File 2: EnableWebhookEndpoint()0%4260%
File 2: LoadEmailRoutingTabAsync()100%210%
File 2: LoadFileUploadTabAsync()0%210140%
File 2: LoadStockSyncTabAsync()0%156120%
File 2: LoadOrderSearchTabAsync()100%210%
File 2: NormalizeTab(...)0%145201200%
File 3: GridRows()0%1190340%
File 3: GridFacets()0%1332360%
File 3: CanReadGrid(...)0%7280%
File 3: ExecutionsQueryAsync()0%4260%
File 3: OneCQueryAsync()0%4260%
File 3: PageAsync()0%620%
File 4: OneCStatistics()0%2040%
File 4: ExportOneCToExcel()0%7280%
File 4: FilteredOneCPostingsAsync()100%210%
File 4: BuildOneCStatisticsAsync()0%7280%
File 4: OneCResultKey(...)0%2040%
File 4: OneCResultLabel(...)0%2040%
File 4: OneCResultOrder(...)0%2040%
File 4: OneCOperationLabel(...)0%7280%
File 4: BuildOneCExcel(...)0%342180%
File 4: AddChartImages(...)0%4260%
File 4: AddChartImage(...)0%620%
File 4: DecodePngDataUrl(...)0%4260%
File 4: WriteStatisticsSheet(...)0%620%
File 4: WriteHeader(...)0%620%
File 4: get_Id()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/JobLogsController.Alerts.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.Extensions.Options;
 4using OzonOrdersWeb.WebServices.SystemAlerts;
 5
 6namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 7
 8public partial class JobLogsController
 9{
 10    private readonly SystemAlertIncidentStore _alertIncidentStore;
 11    private readonly SystemAlertEvaluationService _alertEvaluation;
 12
 13    /// <summary>Лёгкие счётчики для чипов в шапке страниц: один сгруппированный запрос и кэш на несколько секунд.</summ
 14    [HttpGet("alert-chips")]
 15    public async Task<IActionResult> GetAlertChips(CancellationToken cancellationToken)
 16    {
 017        if (!(User.IsInRole("Admin") || User.IsInRole("SuperAdmin")))
 018            return Json(new { success = false });
 19
 020        var counts = await _alertIncidentStore.GetActiveCountsAsync(cancellationToken);
 021        var ids = await _alertIncidentStore.GetActiveIdsAsync(cancellationToken);
 022        return Json(new { success = true, danger = counts.Danger, warning = counts.Warning, info = counts.Info, ids = ne
 023    }
 24
 25    /// <summary>
 26    /// Предпросмотр активных инцидентов одной критичности для всплывающей панели над чипом: те же, что считает чип
 27    /// (подтверждённые, не принятые и не заглушённые), свежие первыми.
 28    /// </summary>
 29    [HttpGet("alert-preview")]
 30    public async Task<IActionResult> GetAlertPreview(string severity, int limit = 50, CancellationToken cancellationToke
 31    {
 032        if (!(User.IsInRole("Admin") || User.IsInRole("SuperAdmin")))
 033            return Json(new { success = false });
 034        if (severity is not ("danger" or "warning" or "info"))
 035            return BadRequest(new { success = false, message = "Неизвестная критичность." });
 36
 037        var take = Math.Clamp(limit, 1, 100);
 038        var active = (await _alertIncidentStore.GetActiveAsync(cancellationToken)).Where(x => x.Severity == severity).To
 39        var items = active.Take(take).Select(x => new
 40        {
 41            x.Id, x.Key, x.Domain, x.Severity, x.Title, x.Details, x.Url, x.OrderCount,
 42            x.FirstObservedAtUtc, x.LastObservedAtUtc
 43        });
 044        return Json(new { success = true, severity, total = active.Count, items });
 045    }
 46
 47    /// <summary>
 48    /// Пересчитать алерты прямо сейчас (кнопка обновления на вкладке): исчезнувшие проблемы закрываются, новые открываю
 49    /// Не чаще <see cref="SystemAlertsOptions.ManualRefreshMinIntervalSeconds"/>.
 50    /// </summary>
 51    [Authorize(Roles = "Admin,SuperAdmin")]
 52    [HttpPost("alerts/refresh")]
 53    public async Task<IActionResult> RefreshAlerts([FromServices] IOptions<SystemAlertsOptions> options, CancellationTok
 54    {
 055        var interval = TimeSpan.FromSeconds(Math.Max(0, options.Value.ManualRefreshMinIntervalSeconds));
 056        var evaluated = await _alertEvaluation.EvaluateAsync(interval, cancellationToken);
 057        var counts = await _alertIncidentStore.GetActiveCountsAsync(cancellationToken);
 058        var ids = await _alertIncidentStore.GetActiveIdsAsync(cancellationToken);
 059        return Json(new { success = true, evaluated, danger = counts.Danger, warning = counts.Warning, info = counts.Inf
 060    }
 61
 62    /// <summary>Принять инцидент к сведению: не шумит до восстановления или повышения severity.</summary>
 63    [Authorize(Roles = "Admin,SuperAdmin")]
 64    [HttpPost("alerts/{id:long}/acknowledge")]
 65    public async Task<IActionResult> AcknowledgeAlert(long id, CancellationToken cancellationToken) =>
 066        AlertResult(await _alertIncidentStore.AcknowledgeAsync(id, User.Identity?.Name, cancellationToken));
 67
 68    /// <summary>Заглушить инцидент на несколько часов; после истечения напоминание придёт, если проблема осталась.</sum
 69    [Authorize(Roles = "Admin,SuperAdmin")]
 70    [HttpPost("alerts/{id:long}/mute")]
 71    public async Task<IActionResult> MuteAlert(long id, int hours, CancellationToken cancellationToken) =>
 072        hours <= 0
 073            ? BadRequest(new { success = false, message = "Укажите срок заглушки в часах." })
 074            : AlertResult(await _alertIncidentStore.MuteAsync(id, hours, User.Identity?.Name, cancellationToken));
 75
 76    /// <summary>Снять подтверждение и заглушку.</summary>
 77    [Authorize(Roles = "Admin,SuperAdmin")]
 78    [HttpPost("alerts/{id:long}/restore")]
 79    public async Task<IActionResult> RestoreAlert(long id, CancellationToken cancellationToken) =>
 080        AlertResult(await _alertIncidentStore.RestoreAsync(id, cancellationToken));
 81
 082    private IActionResult AlertResult(bool updated) => updated
 083        ? Json(new { success = true })
 084        : NotFound(new { success = false, message = "Активный инцидент не найден." });
 85}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/JobLogsController.cs

#LineLine coverage
 1using Hangfire;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.EntityFrameworkCore;
 5using Newtonsoft.Json;
 6using OServices.FiltersServices.FilterModels;
 7using OzonDomains;
 8using OzonDomains.Models;
 9using OzonOrdersWeb.Areas.Studio2.ViewModels.EmailRouting;
 10using OzonOrdersWeb.Areas.Studio2.ViewModels.FileUploadRecordViewModels;
 11using OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs;
 12using OzonOrdersWeb.Areas.Studio2.ViewModels.OrderHistory;
 13using OzonOrdersWeb.Areas.Studio2.ViewModels.OzonWebhookViewModels;
 14using Services.OzonWebhookServices.Monitoring;
 15using OzonOrdersWeb.Areas.Studio2.ViewModels.TransactionJobs;
 16using OzonOrdersWeb.Areas.Studio2.ViewModels.TransactionTimeTracking;
 17using OzonOrdersWeb.Areas.Studio2.ViewModels.WarehouseStock;
 18using OzonOrdersWeb.WebServices.JobLogs;
 19using OzonOrdersWeb.WebServices.SystemAlerts;
 20using OzonOrdersWeb.WebServices.TransactionTimeTracking;
 21using OzonRepositories.Context;
 22using Services.DataServices;
 23using Services.ApiServices._1CApi.Models;
 24using Services.SearchServices.OrderSearch;
 25
 26namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 27
 28[Authorize(Roles = "User,Admin,SuperAdmin")]
 29[Area("Studio2")]
 30[Route("Studio2/JobLogs")]
 31public partial class JobLogsController : Controller
 32{
 33    private readonly ApplicationDbContext _context;
 34    private readonly IJobLogsBadgeService _badgeService;
 35    private readonly OzonPushNotificationDataService _webhookDataService;
 36    private readonly OzonClientServices _ozonClientServices;
 37    private readonly EmailAttachmentProcessingResultDataServices _emailResultDataServices;
 38    private readonly EmailRoutingRuleDataServices _emailRulesDataServices;
 39    private readonly ITransactionTimeTrackingReportQueryService _timeTrackingReportQueryService;
 40    private readonly IOrderSearchDiagnosticsService _orderSearchDiagnosticsService;
 41    private readonly IServerDiagnosticsService _serverDiagnosticsService;
 42    private readonly IBackgroundJobClient _backgroundJobClient;
 43    private readonly IOzonWebhookAvailabilityService _webhookAvailability;
 44    private readonly OzonWebhookAvailabilityStore _webhookAvailabilityStore;
 45
 046    public JobLogsController(
 047        ApplicationDbContext context,
 048        IJobLogsBadgeService badgeService,
 049        OzonPushNotificationDataService webhookDataService,
 050        OzonClientServices ozonClientServices,
 051        EmailAttachmentProcessingResultDataServices emailResultDataServices,
 052        EmailRoutingRuleDataServices emailRulesDataServices,
 053        ITransactionTimeTrackingReportQueryService timeTrackingReportQueryService,
 054        IOrderSearchDiagnosticsService orderSearchDiagnosticsService,
 055        IServerDiagnosticsService serverDiagnosticsService,
 056        IBackgroundJobClient backgroundJobClient,
 057        IOzonWebhookAvailabilityService webhookAvailability,
 058        OzonWebhookAvailabilityStore webhookAvailabilityStore,
 059        SystemAlertIncidentStore alertIncidentStore,
 060        SystemAlertEvaluationService alertEvaluation)
 61    {
 062        _alertIncidentStore = alertIncidentStore;
 063        _alertEvaluation = alertEvaluation;
 064        _webhookAvailability = webhookAvailability;
 065        _webhookAvailabilityStore = webhookAvailabilityStore;
 066        _context = context;
 067        _badgeService = badgeService;
 068        _webhookDataService = webhookDataService;
 069        _ozonClientServices = ozonClientServices;
 070        _emailResultDataServices = emailResultDataServices;
 071        _emailRulesDataServices = emailRulesDataServices;
 072        _timeTrackingReportQueryService = timeTrackingReportQueryService;
 073        _orderSearchDiagnosticsService = orderSearchDiagnosticsService;
 074        _serverDiagnosticsService = serverDiagnosticsService;
 075        _backgroundJobClient = backgroundJobClient;
 076    }
 77
 78    [HttpGet("badge-summary")]
 79    public async Task<IActionResult> GetBadgeSummary(bool fresh, CancellationToken cancellationToken)
 80    {
 081        var isAdmin = User.IsInRole("Admin") || User.IsInRole("SuperAdmin");
 82
 83        // При загрузке/перезагрузке страницы (fresh) админ сразу видит актуальное состояние пуш-адресов,
 84        // а не снимок джоба (раз в 30 минут). Не чаще раза в 20 с и не дольше 6 с ожидания.
 085        if (fresh && isAdmin)
 086            await _webhookAvailability.RefreshIfStaleAsync(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(6));
 87
 088        var summary = await _badgeService.GetSummaryAsync(isAdmin, cancellationToken);
 089        return Json(summary);
 090    }
 91
 92    /// <summary>Старые ссылки /JobLogs без префикса Studio2.</summary>
 93    [HttpGet("/JobLogs")]
 94    [HttpGet("/JobLogs/Index")]
 95    public IActionResult LegacyIndexRedirect()
 96    {
 097        var query = Request.Query.ToDictionary(q => q.Key, q => q.Value.ToString());
 098        if (query.TryGetValue("txView", out var txView) &&
 099            string.Equals(txView, "onec", StringComparison.OrdinalIgnoreCase))
 100        {
 0101            query["tab"] = "onec";
 0102            query.Remove("txView");
 103        }
 104
 0105        var path = "/Studio2/JobLogs?" + string.Join("&",
 0106            query.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
 0107        return Redirect(path);
 108    }
 109
 110    [HttpGet("")]
 111    public async Task<IActionResult> Index(
 112        string tab = "transactions",
 113        long? executionId = null,
 114        int? scheduleId = null,
 115        string? status = null,
 116        string? triggerType = null,
 117        string? orderNumber = null,
 118        DateTime? from = null,
 119        DateTime? to = null,
 120        int page = 1,
 121        int pageSize = 50,
 122        string? messageType = null,
 123        int? clientId = null,
 124        string? processingStatus = null,
 125        string? postingNumber = null,
 126        int webhookTake = 200,
 127        int emailPage = 1,
 128        int emailPageSize = 25,
 129        int? ruleId = null,
 130        string? emailStatus = null,
 131        string? sender = null,
 132        string? fileName = null,
 133        DateTime? dateFrom = null,
 134        DateTime? dateTo = null,
 135        int filePage = 1,
 136        int filePageSize = 15,
 137        int stockPage = 1,
 138        int stockPageSize = 50,
 139        string? stockSourceType = null,
 140        string? stockSyncStatus = null,
 141        DateTime? stockFrom = null,
 142        DateTime? stockTo = null,
 143        string? nameFilter = null,
 144        DateTime? fileDateFrom = null,
 145        DateTime? fileDateTo = null,
 146        int onecPage = 1,
 147        int onecPageSize = 50,
 148        DateTime? onecFrom = null,
 149        DateTime? onecTo = null,
 150        bool? onecSuccess = null,
 151        string? onecChangedBy = null,
 152        long? onecExecutionId = null,
 153        DateTime? dateFromUtc = null,
 154        DateTime? dateToUtc = null,
 155        string? userName = null,
 156        string? transactionType = null,
 157        bool? alertActive = null,
 158        string? alertDomain = null,
 159        string? alertSeverity = null,
 160        CancellationToken cancellationToken = default)
 161    {
 162        // Хвосты статусов переехали на /Studio2/OrderControl — NormalizeTab это значение не знает,
 163        // поэтому проверяем сырой параметр до нормализации.
 0164        if (string.Equals(tab, "missingstatus", StringComparison.OrdinalIgnoreCase))
 0165            return RedirectToAction("Index", "OrderControl", new { area = "Studio2", missingStatus = true });
 166
 0167        var activeTab = NormalizeTab(tab);
 168
 169        // Обнуление и накат остатков живут на странице, откуда их запускают.
 0170        if (activeTab is "clear" or "push")
 0171            return RedirectToAction("Index", "BitrixStock", new
 0172            {
 0173                area = "Studio2",
 0174                tab = activeTab == "clear" ? "clearHistory" : "pushHistory",
 0175                executionId
 0176            });
 177
 0178        var isAdmin = User.IsInRole("Admin") || User.IsInRole("SuperAdmin");
 0179        if ((activeTab is "transactions" or "onec") && !isAdmin)
 0180            activeTab = "timetracking";
 181
 0182        var vm = new JobLogsPageViewModel
 0183        {
 0184            ActiveTab = activeTab,
 0185            HighlightExecutionId = executionId,
 0186            ShowTransactionsTab = isAdmin,
 0187            AlertActive = alertActive,
 0188            AlertDomain = alertDomain ?? string.Empty,
 0189            AlertSeverity = alertSeverity ?? string.Empty
 0190        };
 191
 0192        switch (activeTab)
 193        {
 0194            case "transactions" when isAdmin:
 0195                vm.Transactions = await LoadTransactionsTabAsync(
 0196                    scheduleId, status, triggerType, orderNumber, from, to, page, pageSize, cancellationToken);
 0197                vm.Transactions.HighlightExecutionId = executionId;
 0198                break;
 0199            case "onec" when isAdmin:
 0200                vm.OneC = await LoadOneCTabAsync(
 0201                    onecPage, onecPageSize, onecFrom, onecTo, onecSuccess, onecChangedBy,
 0202                    onecExecutionId ?? executionId,
 0203                    cancellationToken);
 0204                break;
 205            case "timetracking":
 0206                vm.TimeTracking = await _timeTrackingReportQueryService.GetAsync(
 0207                    dateFromUtc, dateToUtc, userName, transactionType, cancellationToken);
 0208                break;
 209            case "webhook":
 0210                vm.Webhook = await LoadWebhookTabAsync(
 0211                    messageType, clientId, processingStatus, postingNumber, from, to, webhookTake, cancellationToken);
 0212                break;
 213            case "email":
 0214                vm.EmailRouting = await LoadEmailRoutingTabAsync();
 0215                break;
 216            case "fileupload":
 0217                vm.FileUpload = await LoadFileUploadTabAsync(
 0218                    filePage, filePageSize, nameFilter, fileDateFrom, fileDateTo, cancellationToken);
 0219                break;
 220            case "stocksync":
 0221                vm.StockSync = await LoadStockSyncTabAsync(
 0222                    stockPage, stockPageSize, stockSourceType, stockSyncStatus, stockFrom, stockTo, cancellationToken);
 0223                break;
 224            case "ordersearch":
 0225                vm.OrderSearch = await LoadOrderSearchTabAsync(cancellationToken);
 0226                break;
 227            case "server":
 0228                vm.ServerDiagnostics = new JobLogsServerDiagnosticsTabViewModel
 0229                {
 0230                    Snapshot = await _serverDiagnosticsService.GetSnapshotAsync(cancellationToken)
 0231                };
 0232                break;
 0233            case "alerts" when isAdmin:
 234                // Строки системных инцидентов подгружаются серверным AG Grid блоками.
 235                break;
 236        }
 0237        if (isAdmin)
 0238            vm.CurrentSystemAlerts = await _badgeService.GetSystemAlertsAsync(cancellationToken);
 239
 0240        return View(vm);
 0241    }
 242
 243    [HttpGet("ordersearch/snapshot")]
 244    public async Task<IActionResult> OrderSearchSnapshot(CancellationToken cancellationToken)
 245    {
 0246        var snapshot = await _orderSearchDiagnosticsService.GetSnapshotAsync(cancellationToken);
 0247        return Json(snapshot);
 0248    }
 249
 250    [HttpGet("ordersearch/lookup")]
 251    public async Task<IActionResult> OrderSearchLookup(
 252        int? orderId,
 253        string? shipmentNumber,
 254        CancellationToken cancellationToken)
 255    {
 0256        var hasId = orderId is > 0;
 0257        var hasShipment = !string.IsNullOrWhiteSpace(shipmentNumber);
 258
 0259        if (!hasId && !hasShipment)
 260        {
 0261            return BadRequest(new { error = "Укажите ID заказа или номер отправления" });
 262        }
 263
 0264        if (hasId && hasShipment)
 265        {
 0266            return BadRequest(new { error = "Укажите только ID или только номер отправления" });
 267        }
 268
 0269        var result = await _orderSearchDiagnosticsService.LookupAsync(orderId, shipmentNumber, cancellationToken);
 0270        if (result.Items.Count == 0)
 271        {
 0272            return NotFound(new { error = "Заказы не найдены", queryLabel = result.QueryLabel });
 273        }
 274
 0275        return Json(result);
 0276    }
 277
 278    [Authorize(Roles = "Admin")]
 279    [HttpPost("ordersearch/repair-drift")]
 280    public IActionResult EnqueueOrderSearchDriftRepair()
 281    {
 0282        var userName = User.Identity?.Name ?? "Admin";
 0283        var hangfireJobId = _backgroundJobClient.Enqueue<OrderSearchDriftRepairJob>(
 0284            job => job.ExecuteAsync(userName));
 285
 0286        return Json(new
 0287        {
 0288            message = "Задача исправления расхождения поставлена в очередь Hangfire. Обновите страницу через 1–2 минуты.
 0289            hangfireJobId,
 0290            jobId = OrderSearchDriftRepairJob.JobId
 0291        });
 292    }
 293
 294    [Authorize(Roles = "Admin")]
 295    [HttpPost("ordersearch/full-reindex")]
 296    public IActionResult EnqueueOrderSearchFullReindex()
 297    {
 0298        var hangfireJobId = _backgroundJobClient.Enqueue<OrderSearchFullReindexJob>(
 0299            job => job.ExecuteAsync());
 300
 0301        return Json(new
 0302        {
 0303            message = "Полная переиндексация поставлена в очередь Hangfire. Это может занять несколько минут.",
 0304            hangfireJobId,
 0305            jobId = OrderSearchFullReindexJob.JobId
 0306        });
 307    }
 308
 309    /// <summary>
 310    /// Полный ответ 1С по проведению: то, что <c>ErrorSummary</c> (≤2000 символов) не вмещает. Берётся из
 311    /// <c>DetailsJson.responses[].errorDetail</c>, отдельных колонок в БД не заводилось.
 312    /// </summary>
 313    [Authorize(Roles = "Admin")]
 314    [HttpGet("onec/postings/{postingId:long}/error-detail")]
 315    public async Task<IActionResult> PostingErrorDetail(long postingId, CancellationToken cancellationToken)
 316    {
 0317        var posting = await _context.OrderHistoryOneCPostings
 0318            .AsNoTracking()
 0319            .Where(p => p.Id == postingId)
 0320            .Select(p => new { p.Id, p.ErrorSummary, p.DetailsJson })
 0321            .FirstOrDefaultAsync(cancellationToken);
 322
 0323        if (posting == null)
 0324            return NotFound();
 325
 0326        return Json(new
 0327        {
 0328            success = true,
 0329            id = posting.Id,
 0330            errorSummary = posting.ErrorSummary,
 0331            items = GetErrorDetails(posting.DetailsJson)
 0332        });
 0333    }
 334
 335    [Authorize(Roles = "Admin")]
 336    [HttpGet("onec/postings/{postingId:long}/orders")]
 337    [HttpGet("transactions/postings/{postingId:long}/orders")]
 338    public async Task<IActionResult> PostingOrders(long postingId, CancellationToken cancellationToken)
 339    {
 0340        var posting = await _context.OrderHistoryOneCPostings
 0341            .AsNoTracking()
 0342            .FirstOrDefaultAsync(p => p.Id == postingId, cancellationToken);
 343
 0344        if (posting == null)
 0345            return NotFound();
 346
 0347        if (posting.TransactionId is > 0)
 348        {
 0349            var executionId = await _context.BackgroundTransactionResults
 0350                .AsNoTracking()
 0351                .Where(r => r.TransactionId == posting.TransactionId)
 0352                .OrderByDescending(r => r.CreatedAt)
 0353                .Select(r => (long?)r.ExecutionId)
 0354                .FirstOrDefaultAsync(cancellationToken);
 355
 0356            if (executionId is > 0)
 357            {
 0358                return RedirectToAction("ExecutionOrders", "TransactionJobs", new { area = "Studio2", id = executionId.V
 359            }
 360        }
 361
 0362        var shipmentNumber = await _context.Orders
 0363            .AsNoTracking()
 0364            .Where(o => o.Id == posting.OrderId)
 0365            .Select(o => o.ShipmentNumber)
 0366            .FirstOrDefaultAsync(cancellationToken);
 367
 0368        var filterData = new OrderFilterModel
 0369        {
 0370            ShipmentNumber = string.IsNullOrWhiteSpace(shipmentNumber) ? null : shipmentNumber.Trim(),
 0371            GroupByShipment = true,
 0372            IncludeArchived = false,
 0373            SyncClientStatusesOnLoad = false,
 0374            OnlyWithZasyl = false
 0375        };
 376
 0377        Response.Cookies.Append("FilterData", JsonConvert.SerializeObject(filterData));
 0378        return RedirectToAction("Index", "Orders", new { area = "Studio2" });
 0379    }
 380
 381    private async Task<BackgroundJobsExecutionsPageViewModel> LoadTransactionsTabAsync(
 382        int? scheduleId,
 383        string? status,
 384        string? triggerType,
 385        string? orderNumber,
 386        DateTime? from,
 387        DateTime? to,
 388        int page,
 389        int pageSize,
 390        CancellationToken cancellationToken)
 391    {
 0392        page = page < 1 ? 1 : page;
 0393        pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200);
 394
 0395        var schedules = await _context.BackgroundJobSchedules
 0396            .AsNoTracking()
 0397            .Include(s => s.OzonClient)
 0398            .OrderBy(s => s.Name)
 0399            .ToListAsync(cancellationToken);
 400
 0401        var query = _context.BackgroundJobExecutions
 0402            .AsNoTracking()
 0403            .Include(e => e.Schedule)
 0404            .AsQueryable();
 405
 0406        if (scheduleId.HasValue)
 0407            query = query.Where(e => e.ScheduleId == scheduleId.Value);
 408
 0409        if (!string.IsNullOrWhiteSpace(status))
 0410            query = query.Where(e => e.Status == status);
 411
 0412        if (!string.IsNullOrWhiteSpace(triggerType))
 0413            query = query.Where(e => e.TriggerType == triggerType);
 414
 0415        OrderExecutionLookupViewModel? orderLookup = null;
 0416        if (!string.IsNullOrWhiteSpace(orderNumber))
 417        {
 0418            orderLookup = await BuildOrderExecutionLookupAsync(orderNumber.Trim(), cancellationToken);
 0419            var executionIds = orderLookup.ExecutionIds;
 0420            query = executionIds.Count > 0
 0421                ? query.Where(e => executionIds.Contains(e.Id))
 0422                : query.Where(e => false);
 0423        }
 424
 0425        if (from.HasValue)
 0426            query = query.Where(e => e.StartedAt >= from.Value);
 427
 0428        if (to.HasValue)
 429        {
 0430            var toExclusive = to.Value.Date.AddDays(1);
 0431            query = query.Where(e => e.StartedAt < toExclusive);
 432        }
 433
 0434        var totalCount = await query.CountAsync(cancellationToken);
 0435        var executions = await query
 0436            .OrderByDescending(e => e.StartedAt)
 0437            .Skip((page - 1) * pageSize)
 0438            .Take(pageSize)
 0439            .ToListAsync(cancellationToken);
 440
 0441        return new BackgroundJobsExecutionsPageViewModel
 0442        {
 0443            Schedules = schedules,
 0444            Executions = executions,
 0445            ScheduleId = scheduleId,
 0446            Status = status,
 0447            TriggerType = triggerType,
 0448            OrderNumber = orderNumber,
 0449            OrderLookup = orderLookup,
 0450            From = from,
 0451            To = to,
 0452            Page = page,
 0453            PageSize = pageSize,
 0454            TotalCount = totalCount
 0455        };
 0456    }
 457
 458    private const int OrderLookupOrdersLimit = 50;
 459    private const int OrderLookupMatchesLimit = 200;
 460
 461    /// <summary>
 462    /// Находит заказ по номеру отправления или Id и собирает, в каких транзакциях он был проведён
 463    /// (фоновой задачей и вручную) и в какие фоновые запуски попадал при отборе.
 464    /// </summary>
 465    private async Task<OrderExecutionLookupViewModel> BuildOrderExecutionLookupAsync(
 466        string query,
 467        CancellationToken cancellationToken)
 468    {
 0469        var lookup = new OrderExecutionLookupViewModel { Query = query };
 0470        var queryAsOrderId = int.TryParse(query, out var parsedId) ? parsedId : (int?)null;
 471
 0472        var orders = await _context.Orders
 0473            .AsNoTracking()
 0474            .Include(o => o.AppStatus)
 0475            .Include(o => o.ClientStatus)
 0476            .Include(o => o.OzonClient)
 0477            .Include(o => o.ShipmentWarehouse)
 0478            .Where(o => (o.ShipmentNumber != null && o.ShipmentNumber.Contains(query)) ||
 0479                        (queryAsOrderId != null && o.Id == queryAsOrderId))
 0480            .OrderByDescending(o => o.Id)
 0481            .Take(OrderLookupOrdersLimit)
 0482            .ToListAsync(cancellationToken);
 483
 484        var orderIds = orders.Select(o => o.Id).ToList();
 485        var shipmentByOrderId = orders.ToDictionary(o => o.Id, o => o.ShipmentNumber);
 486
 0487        var results = orderIds.Count == 0
 0488            ? new List<BackgroundTransactionResult>()
 0489            : await _context.BackgroundTransactionResults
 0490                .AsNoTracking()
 0491                .Include(r => r.Execution)
 0492                .ThenInclude(e => e.Schedule)
 0493                .Where(r => orderIds.Contains(r.OrderId))
 0494                .OrderByDescending(r => r.CreatedAt)
 0495                .ToListAsync(cancellationToken);
 496
 0497        var postings = orderIds.Count == 0
 0498            ? new List<OrderHistoryOneCPosting>()
 0499            : await _context.OrderHistoryOneCPostings
 0500                .AsNoTracking()
 0501                .Where(p => orderIds.Contains(p.OrderId))
 0502                .OrderByDescending(p => p.PostedAt)
 0503                .ToListAsync(cancellationToken);
 504
 0505        var transactionIds = results
 506            .Where(r => r.TransactionId is > 0).Select(r => r.TransactionId!.Value)
 507            .Concat(postings.Where(p => p.TransactionId is > 0).Select(p => p.TransactionId!.Value))
 508            .Concat(orders.Where(o => o.TransactionId is > 0).Select(o => o.TransactionId!.Value))
 0509            .Distinct()
 0510            .ToList();
 511
 0512        var transactions = transactionIds.Count == 0
 0513            ? new Dictionary<int, Transaction>()
 0514            : await _context.Transactions
 0515                .AsNoTracking()
 0516                .Where(t => transactionIds.Contains(t.Id))
 517                .ToDictionaryAsync(t => t.Id, cancellationToken);
 518
 519        Transaction? FindTransaction(int? transactionId) =>
 0520            transactionId is > 0 && transactions.TryGetValue(transactionId.Value, out var tx) ? tx : null;
 521
 0522        lookup.Orders = orders.Select(o =>
 0523        {
 0524            var tx = FindTransaction(o.TransactionId);
 0525            return new OrderExecutionLookupOrderViewModel
 0526            {
 0527                OrderId = o.Id,
 0528                ShipmentNumber = o.ShipmentNumber,
 0529                Article = o.Article,
 0530                AppStatusName = o.AppStatus?.Name,
 0531                ClientStatusName = o.ClientStatus?.Name,
 0532                OzonClientName = o.OzonClient?.Name,
 0533                ShipmentWarehouseName = o.ShipmentWarehouse?.Name,
 0534                TransactionId = o.TransactionId,
 0535                TransactionTypeName = tx?.Type?.GetDisplayName(),
 0536                TransactionCreatedAt = tx?.CreatedDateTime,
 0537                TransactionCreateBy = tx?.CreateBy
 0538            };
 0539        }).ToList();
 540
 541        // Проведение 1С по паре «заказ + транзакция» дополняет строку фонового результата складами и документами.
 0542        var postingByOrderAndTransaction = postings
 543            .Where(p => p.TransactionId is > 0)
 544            .GroupBy(p => (p.OrderId, TransactionId: p.TransactionId!.Value))
 545            .ToDictionary(g => g.Key, g => g.OrderByDescending(p => p.PostedAt).First());
 546
 0547        foreach (var result in results)
 548        {
 0549            var posting = result.TransactionId is > 0 &&
 0550                          postingByOrderAndTransaction.TryGetValue((result.OrderId, result.TransactionId.Value), out var
 0551                ? p
 0552                : null;
 0553            var tx = FindTransaction(result.TransactionId);
 554
 0555            lookup.Postings.Add(new OrderExecutionLookupPostingViewModel
 0556            {
 0557                OrderId = result.OrderId,
 0558                ShipmentNumber = shipmentByOrderId.GetValueOrDefault(result.OrderId),
 0559                ExecutionId = result.ExecutionId,
 0560                JobType = result.Execution?.JobType,
 0561                ScheduleName = result.Execution?.Schedule?.Name,
 0562                TransactionId = result.TransactionId,
 0563                TransactionTypeName = tx?.Type?.GetDisplayName(),
 0564                PerformedBy = !string.IsNullOrWhiteSpace(tx?.CreateBy) ? tx!.CreateBy : posting?.ChangedBy,
 0565                PerformedAt = result.CreatedAt,
 0566                WarehouseName = result.WarehouseName,
 0567                WarehouseSender = posting?.WarehouseSender,
 0568                WarehouseRecipient = posting?.WarehouseRecipient,
 0569                DocumentNumbers = !string.IsNullOrWhiteSpace(result.OneCDocumentNumber)
 0570                    ? result.OneCDocumentNumber
 0571                    : posting?.DocumentNumbers,
 0572                IsPosted = result.IsPosted || (posting?.IsPosted ?? false),
 0573                Status = result.Status,
 0574                Comment = result.Comment,
 0575                ErrorMessage = !string.IsNullOrWhiteSpace(result.ErrorMessage)
 0576                    ? result.ErrorMessage
 0577                    : posting?.ErrorSummary
 0578            });
 579        }
 580
 581        // Ручные проведения — те, что не попали ни в один результат фонового запуска.
 0582        var automatedKeys = results
 583            .Where(r => r.TransactionId is > 0)
 584            .Select(r => (r.OrderId, TransactionId: r.TransactionId!.Value))
 0585            .ToHashSet();
 586
 0587        foreach (var posting in postings)
 588        {
 0589            if (posting.TransactionId is > 0 &&
 0590                automatedKeys.Contains((posting.OrderId, posting.TransactionId.Value)))
 591                continue;
 592
 0593            var tx = FindTransaction(posting.TransactionId);
 0594            lookup.Postings.Add(new OrderExecutionLookupPostingViewModel
 0595            {
 0596                OrderId = posting.OrderId,
 0597                ShipmentNumber = shipmentByOrderId.GetValueOrDefault(posting.OrderId),
 0598                ExecutionId = null,
 0599                TransactionId = posting.TransactionId,
 0600                TransactionTypeName = tx?.Type?.GetDisplayName(),
 0601                PerformedBy = !string.IsNullOrWhiteSpace(posting.ChangedBy) ? posting.ChangedBy : tx?.CreateBy,
 0602                PerformedAt = posting.PostedAt,
 0603                WarehouseSender = posting.WarehouseSender,
 0604                WarehouseRecipient = posting.WarehouseRecipient,
 0605                DocumentNumbers = posting.DocumentNumbers,
 0606                IsPosted = posting.IsPosted,
 0607                Status = posting.Success ? "PostedTo1C" : "Failed",
 0608                ErrorMessage = posting.ErrorSummary
 0609            });
 610        }
 611
 0612        lookup.Postings = lookup.Postings
 613            .OrderByDescending(p => p.PerformedAt ?? DateTime.MinValue)
 0614            .ToList();
 615
 0616        var matches = await _context.BackgroundOrderMatches
 0617            .AsNoTracking()
 0618            .Include(m => m.Execution)
 0619            .ThenInclude(e => e.Schedule)
 0620            .Where(m => (m.OrderId != null && orderIds.Contains(m.OrderId.Value)) || m.OrderNumber.Contains(query))
 0621            .OrderByDescending(m => m.CreatedAt)
 0622            .Take(OrderLookupMatchesLimit)
 0623            .ToListAsync(cancellationToken);
 624
 625        lookup.Matches = matches.Select(m => new OrderExecutionLookupMatchViewModel
 626        {
 627            ExecutionId = m.ExecutionId,
 628            JobType = m.Execution?.JobType,
 629            ScheduleName = m.Execution?.Schedule?.Name,
 630            StartedAt = m.Execution?.StartedAt,
 631            OrderId = m.OrderId,
 632            OrderNumber = m.OrderNumber,
 633            Article = m.Article,
 634            OrderStatus = m.OrderStatus,
 635            IsMatchedInDb = m.IsMatchedInDb,
 636            IsEligibleForProcessing = m.IsEligibleForProcessing,
 637            RejectionReason = m.RejectionReason
 638        }).ToList();
 639
 640        lookup.ExecutionIds = results.Select(r => r.ExecutionId)
 641            .Concat(matches.Select(m => m.ExecutionId))
 0642            .Distinct()
 0643            .ToList();
 644
 0645        return lookup;
 0646    }
 647
 648    private async Task<JobLogsOneCPostingsTabViewModel> LoadOneCTabAsync(
 649        int page,
 650        int pageSize,
 651        DateTime? from,
 652        DateTime? to,
 653        bool? success,
 654        string? changedBy,
 655        long? executionId,
 656        CancellationToken cancellationToken)
 657    {
 0658        page = page < 1 ? 1 : page;
 0659        pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200);
 660
 0661        var vm = new JobLogsOneCPostingsTabViewModel
 0662        {
 0663            Page = page,
 0664            PageSize = pageSize,
 0665            From = from,
 0666            To = to,
 0667            Success = success,
 0668            ChangedBy = changedBy,
 0669            ExecutionId = executionId
 0670        };
 671
 0672        var query = _context.OrderHistoryOneCPostings.AsNoTracking();
 673
 0674        if (executionId is > 0)
 675        {
 0676            var txIds = await _context.BackgroundTransactionResults
 0677                .AsNoTracking()
 0678                .Where(r => r.ExecutionId == executionId && r.TransactionId != null)
 0679                .Select(r => r.TransactionId!.Value)
 0680                .Distinct()
 0681                .ToListAsync(cancellationToken);
 682
 0683            if (txIds.Count == 0)
 0684                return vm;
 685
 0686            query = query.Where(p => p.TransactionId != null && txIds.Contains(p.TransactionId.Value));
 0687        }
 688
 0689        if (from.HasValue)
 0690            query = query.Where(p => p.PostedAt >= from.Value);
 691
 0692        if (to.HasValue)
 693        {
 0694            var toExclusive = to.Value.Date.AddDays(1);
 0695            query = query.Where(p => p.PostedAt < toExclusive);
 696        }
 697
 0698        if (success.HasValue)
 0699            query = query.Where(p => p.Success == success.Value);
 700
 0701        if (!string.IsNullOrWhiteSpace(changedBy))
 702        {
 0703            var by = changedBy.Trim();
 0704            query = query.Where(p =>
 0705                (p.ChangedBy != null && p.ChangedBy.Contains(by)) ||
 0706                (p.TransactionId != null && _context.Transactions
 0707                    .Any(t => t.Id == p.TransactionId && t.CreateBy != null && t.CreateBy.Contains(by))));
 708        }
 709
 0710        vm.TotalCount = await query.CountAsync(cancellationToken);
 711
 0712        var postings = await query
 0713            .OrderByDescending(p => p.PostedAt)
 0714            .Skip((page - 1) * pageSize)
 0715            .Take(pageSize)
 0716            .ToListAsync(cancellationToken);
 717
 0718        var txIdsOnPage = postings
 719            .Where(p => p.TransactionId is > 0)
 720            .Select(p => p.TransactionId!.Value)
 0721            .Distinct()
 0722            .ToList();
 723
 0724        var executionByTx = txIdsOnPage.Count == 0
 0725            ? new Dictionary<int, long>()
 0726            : await _context.BackgroundTransactionResults
 0727                .AsNoTracking()
 0728                .Where(r => r.TransactionId != null && txIdsOnPage.Contains(r.TransactionId.Value))
 0729                .GroupBy(r => r.TransactionId!.Value)
 0730                .Select(g => new { TransactionId = g.Key, ExecutionId = g.Min(x => x.ExecutionId) })
 731                .ToDictionaryAsync(x => x.TransactionId, x => x.ExecutionId, cancellationToken);
 732
 0733        var createByByTx = txIdsOnPage.Count == 0
 0734            ? new Dictionary<int, string?>()
 0735            : await _context.Transactions
 0736                .AsNoTracking()
 0737                .Where(t => txIdsOnPage.Contains(t.Id))
 0738                .Select(t => new { t.Id, t.CreateBy })
 739                .ToDictionaryAsync(t => t.Id, t => t.CreateBy, cancellationToken);
 740
 741        var orderIds = postings.Select(p => p.OrderId).Distinct().ToList();
 0742        var shipmentsByOrderId = orderIds.Count == 0
 0743            ? new Dictionary<int, string?>()
 0744            : await _context.Orders
 0745                .AsNoTracking()
 0746                .Where(o => orderIds.Contains(o.Id))
 0747                .Select(o => new { o.Id, o.ShipmentNumber })
 748                .ToDictionaryAsync(o => o.Id, o => o.ShipmentNumber, cancellationToken);
 749
 0750        vm.Postings = postings.Select(p =>
 0751        {
 0752            string? performedBy = !string.IsNullOrWhiteSpace(p.ChangedBy) ? p.ChangedBy.Trim() : null;
 0753            if (performedBy == null && p.TransactionId is > 0 &&
 0754                createByByTx.TryGetValue(p.TransactionId.Value, out var createBy) &&
 0755                !string.IsNullOrWhiteSpace(createBy))
 0756            {
 0757                performedBy = createBy.Trim();
 0758            }
 0759
 0760            return new JobLogsOneCPostingRowViewModel
 0761            {
 0762                Posting = MapOneCPosting(p),
 0763                PerformedBy = performedBy,
 0764                ExecutionId = p.TransactionId is > 0 && executionByTx.TryGetValue(p.TransactionId.Value, out var execId)
 0765                    ? execId
 0766                    : null,
 0767                ShipmentNumber = shipmentsByOrderId.GetValueOrDefault(p.OrderId)
 0768            };
 0769        }).ToList();
 770
 0771        return vm;
 0772    }
 773
 0774    private static OrderHistoryOneCPostingViewModel MapOneCPosting(OrderHistoryOneCPosting p) => new()
 0775    {
 0776        Id = p.Id,
 0777        OrderId = p.OrderId,
 0778        TransactionId = p.TransactionId,
 0779        OperationKind = p.OperationKind,
 0780        Success = p.Success,
 0781        PostedAt = p.PostedAt,
 0782        ChangedBy = p.ChangedBy,
 0783        HistoryBatchKey = p.HistoryBatchKey,
 0784        OrganizationName = p.OrganizationName,
 0785        OrganizationInn = p.OrganizationInn,
 0786        SenderName = p.SenderName,
 0787        SenderInn = p.SenderInn,
 0788        WarehouseSender = p.WarehouseSender,
 0789        WarehouseRecipient = p.WarehouseRecipient,
 0790        OrdersCount = p.OrdersCount,
 0791        ProductsCount = p.ProductsCount,
 0792        ArticlesSummary = p.ArticlesSummary,
 0793        DocumentsCount = p.DocumentsCount,
 0794        DocumentNumbers = p.DocumentNumbers,
 0795        DocumentGuids = p.DocumentGuids,
 0796        IsPosted = p.IsPosted,
 0797        ErrorSummary = p.ErrorSummary,
 0798        CreatedNomenclature = GetCreatedNomenclature(p.DetailsJson)
 0799    };
 800
 801    private static List<object> GetErrorDetails(string? detailsJson)
 802    {
 0803        if (string.IsNullOrWhiteSpace(detailsJson)) return [];
 804        try
 805        {
 0806            var details = JsonConvert.DeserializeAnonymousType(detailsJson,
 0807                new { responses = Array.Empty<OneCResponse>() });
 0808            return (details?.responses ?? [])
 0809                .Where(r => !string.IsNullOrWhiteSpace(r.ErrorDetail) || !string.IsNullOrWhiteSpace(r.PostingError))
 0810                .Select(r => (object)new
 0811                {
 0812                    documentNumber = r.DocumentNumber,
 0813                    isPosted = r.IsPosted,
 0814                    postingError = r.PostingError,
 0815                    errorDetail = r.ErrorDetail
 0816                })
 0817                .ToList();
 818        }
 0819        catch (JsonException)
 820        {
 0821            return [];
 822        }
 0823    }
 824
 825    private static string? GetCreatedNomenclature(string? detailsJson)
 826    {
 0827        if (string.IsNullOrWhiteSpace(detailsJson)) return null;
 828        try
 829        {
 0830            var details = JsonConvert.DeserializeAnonymousType(detailsJson,
 0831                new { responses = Array.Empty<OneCResponse>() });
 0832            return string.Join("; ", details?.responses
 0833                .Select(r => r.CreatedNomenclature)
 0834                .Where(x => !string.IsNullOrWhiteSpace(x))
 0835                .Distinct() ?? []);
 836        }
 0837        catch (JsonException)
 838        {
 0839            return null;
 840        }
 0841    }
 842
 843    private async Task<OzonWebhookIndexViewModel> LoadWebhookTabAsync(
 844        string? messageType,
 845        int? clientId,
 846        string? processingStatus,
 847        string? postingNumber,
 848        DateTime? from,
 849        DateTime? to,
 850        int take,
 851        CancellationToken cancellationToken)
 852    {
 0853        var clients = await _ozonClientServices.GetOzonClients();
 0854        long? sellerId = null;
 0855        if (clientId is > 0)
 856        {
 0857            var client = clients.FirstOrDefault(c => c.Id == clientId.Value);
 0858            if (client?.DecryptClientId != null && long.TryParse(client.DecryptClientId, out var sid))
 0859                sellerId = sid;
 860        }
 861
 0862        var notifications = await _webhookDataService.GetFilteredAsync(
 0863            messageType,
 0864            sellerId,
 0865            processingStatus,
 0866            string.IsNullOrWhiteSpace(postingNumber) ? null : postingNumber.Trim(),
 0867            from,
 0868            to,
 0869            Math.Clamp(take, 10, 500),
 0870            cancellationToken);
 871
 0872        var messageTypes = await _webhookDataService.GetDistinctMessageTypesAsync(cancellationToken);
 873
 0874        return new OzonWebhookIndexViewModel
 0875        {
 0876            Notifications = notifications,
 0877            AvailableMessageTypes = messageTypes,
 878            Clients = clients.Select(c => new OzonClientFilterItem
 879            {
 880                Id = c.Id,
 881                Name = c.Name,
 882                DecryptClientId = c.DecryptClientId
 883            }).ToList(),
 0884            FilterMessageType = messageType,
 0885            FilterClientId = clientId,
 0886            FilterProcessingStatus = processingStatus,
 0887            FilterPostingNumber = postingNumber,
 0888            FilterFrom = from,
 0889            FilterTo = to,
 0890            Take = take,
 0891            Availability = _webhookAvailabilityStore.Current
 0892        };
 0893    }
 894
 895    /// <summary>Проверить доступность пуш-адресов Ozon сейчас (иначе снимок обновляется джобом раз в 30 минут).</summar
 896    [Authorize(Roles = "Admin,SuperAdmin")]
 897    [HttpPost("webhook-health/refresh")]
 898    public async Task<IActionResult> RefreshWebhookHealth(CancellationToken cancellationToken)
 899    {
 0900        var snapshot = await _webhookAvailability.RefreshAsync(cancellationToken);
 0901        return Json(new { success = true, hasProblem = snapshot.HasProblem });
 0902    }
 903
 904    /// <summary>Включить отправку пушей на адрес, который Ozon отключил (после исправления причины).</summary>
 905    [Authorize(Roles = "Admin,SuperAdmin")]
 906    [HttpPost("webhook-health/enable")]
 907    public async Task<IActionResult> EnableWebhookEndpoint(int clientId, long urlId, CancellationToken cancellationToken
 908    {
 0909        if (clientId <= 0 || urlId <= 0)
 0910            return BadRequest(new { success = false, message = "Не указан кабинет или адрес." });
 911
 0912        var error = await _webhookAvailability.EnableAsync(clientId, urlId, cancellationToken);
 0913        return error == null
 0914            ? Json(new { success = true })
 0915            : Json(new { success = false, message = error });
 0916    }
 917
 918    /// <summary>Вкладка Email → Dropbox: только счётчики по статусам — строки грузит серверный грид (grid/email).</summ
 919    private async Task<EmailRoutingResultsViewModel> LoadEmailRoutingTabAsync()
 920    {
 0921        var statusCounts = await _emailResultDataServices.GetStatusCounts();
 0922        return new EmailRoutingResultsViewModel
 0923        {
 0924            CountUploaded = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Uploaded),
 0925            CountOrdersImported = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.OrdersImported),
 0926            CountRejected = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.RejectedByRule),
 0927            CountFailed = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Failed),
 0928            CountDuplicate = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Duplicate)
 0929        };
 0930    }
 931
 932    private async Task<FileUploadRecordViewModel> LoadFileUploadTabAsync(
 933        int pageNumber,
 934        int pageSize,
 935        string? nameFilter,
 936        DateTime? dateFrom,
 937        DateTime? dateTo,
 938        CancellationToken cancellationToken)
 939    {
 0940        pageNumber = pageNumber < 1 ? 1 : pageNumber;
 0941        pageSize = pageSize <= 0 ? 15 : Math.Min(pageSize, 100);
 942
 0943        var query = _context.FileUploadRecords.AsNoTracking();
 944
 0945        if (!string.IsNullOrWhiteSpace(nameFilter))
 0946            query = query.Where(x => x.FileName.Contains(nameFilter));
 947
 0948        if (dateFrom.HasValue)
 0949            query = query.Where(x => x.Date >= dateFrom.Value);
 950
 0951        if (dateTo.HasValue)
 0952            query = query.Where(x => x.Date <= dateTo.Value);
 953
 0954        var totalCount = await query.CountAsync(cancellationToken);
 0955        var totalPages = totalCount == 0 ? 1 : (int)Math.Ceiling(totalCount / (double)pageSize);
 0956        var items = await query
 0957            .OrderByDescending(x => x.Date)
 0958            .Skip((pageNumber - 1) * pageSize)
 0959            .Take(pageSize)
 0960            .ToListAsync(cancellationToken);
 961
 0962        return new FileUploadRecordViewModel
 0963        {
 0964            Items = items,
 0965            PageNumber = pageNumber,
 0966            PageSize = pageSize,
 0967            TotalPages = totalPages,
 0968            TotalCount = totalCount,
 0969            NameFilter = nameFilter ?? string.Empty,
 0970            DateFromFilter = dateFrom,
 0971            DateToFilter = dateTo
 0972        };
 0973    }
 974
 975    private async Task<WarehouseStockSyncRunsPageViewModel> LoadStockSyncTabAsync(
 976        int page,
 977        int pageSize,
 978        string? sourceType,
 979        string? status,
 980        DateTime? from,
 981        DateTime? to,
 982        CancellationToken cancellationToken)
 983    {
 0984        page = page < 1 ? 1 : page;
 0985        pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200);
 986
 0987        var query = _context.WarehouseStockSyncRuns
 0988            .AsNoTracking()
 0989            .AsQueryable();
 990
 0991        if (!string.IsNullOrWhiteSpace(sourceType))
 0992            query = query.Where(x => x.SourceType == sourceType);
 0993        if (!string.IsNullOrWhiteSpace(status))
 0994            query = query.Where(x => x.Status == status);
 0995        if (from.HasValue)
 0996            query = query.Where(x => x.StartedAtUtc >= from.Value);
 0997        if (to.HasValue)
 998        {
 0999            var toExclusive = to.Value.Date.AddDays(1);
 01000            query = query.Where(x => x.StartedAtUtc < toExclusive);
 1001        }
 1002
 01003        var totalCount = await query.CountAsync(cancellationToken);
 01004        var runs = await query
 01005            .OrderByDescending(x => x.StartedAtUtc)
 01006            .Skip((page - 1) * pageSize)
 01007            .Take(pageSize)
 01008            .ToListAsync(cancellationToken);
 1009
 01010        return new WarehouseStockSyncRunsPageViewModel
 01011        {
 01012            Runs = runs,
 01013            SourceType = sourceType,
 01014            Status = status,
 01015            From = from,
 01016            To = to,
 01017            Page = page,
 01018            PageSize = pageSize,
 01019            TotalCount = totalCount
 01020        };
 01021    }
 1022
 1023    private async Task<JobLogsOrderSearchTabViewModel> LoadOrderSearchTabAsync(CancellationToken cancellationToken)
 1024    {
 01025        var snapshot = await _orderSearchDiagnosticsService.GetSnapshotAsync(cancellationToken);
 01026        return new JobLogsOrderSearchTabViewModel { Snapshot = snapshot };
 01027    }
 1028
 01029    private static string NormalizeTab(string? tab) => tab?.Trim().ToLowerInvariant() switch
 01030    {
 01031        // clear/push оставлены только ради редиректа старых закладок на /Studio2/BitrixStock
 01032        "clear" or "clearhistory" => "clear",
 01033        "push" or "pushhistory" => "push",
 01034        "transactions" or "transaction" => "transactions",
 01035        "onec" or "onecpostings" => "onec",
 01036        "timetracking" or "transactiontimetracking" or "transactiontimetrackingreport" => "timetracking",
 01037        "webhook" or "ozonwebhooklog" => "webhook",
 01038        "email" or "emailrouting" or "emailroutingresults" => "email",
 01039        "fileupload" or "fileuploadrecords" => "fileupload",
 01040        "stocksync" or "warehousestocksync" => "stocksync",
 01041        "ordersearch" or "elasticsearch" or "orderssearch" => "ordersearch",
 01042        "server" or "serverdiagnostics" or "database" => "server",
 01043        "alerts" or "systemalerts" => "alerts",
 01044        _ => "transactions"
 01045    };
 1046}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/JobLogsController.Grid.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Mvc;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models.Authorization.Enums;
 4using OzonOrdersWeb.Authorization;
 5using OzonOrdersWeb.WebServices.Grid;
 6using OzonOrdersWeb.WebServices.JobLogs;
 7
 8namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 9
 10/// <summary>
 11/// Серверные гриды журнала задач (AG Grid Infinite Row Model поверх <see cref="ServerGridQuery"/>): блоки строк и
 12/// счётчики значений по вкладкам-журналам. Права — как у страницы (<c>Studio2.JobLogs.Index</c>, View); запуски и
 13/// проведения 1С — только администраторам, как и сами вкладки.
 14/// </summary>
 15public partial class JobLogsController
 16{
 17    private const string JobLogsPageCode = "Studio2.JobLogs.Index";
 18
 19    [HttpPost("grid/{tab}")]
 20    [ValidateAntiForgeryToken]
 21    [PageAuthorize(JobLogsPageCode, AccessType.View)]
 22    public async Task<IActionResult> GridRows(string tab, [FromBody] ServerGridRequest? request, CancellationToken cance
 23    {
 024        var error = ServerGridQuery.Validate(request);
 025        if (error != null) return BadRequest(new { success = false, error });
 026        if (!CanReadGrid(tab)) return Forbid();
 27
 028        var (rows, total) = tab switch
 029        {
 030            "transactions" => await PageAsync(await ExecutionsQueryAsync(request!, cancellationToken), JobLogsGridDefini
 031                e => new
 032                {
 033                    e.Id, e.ScheduleId, ScheduleName = e.Schedule != null ? e.Schedule.Name : null, e.JobType, e.Trigger
 034                    e.StartedAt, e.CompletedAt, e.DurationMs, e.TotalOrders, e.SuccessfulOrders, e.FailedOrders, e.Error
 035                }, cancellationToken),
 036            "onec" => await PageAsync(await OneCQueryAsync(request!, cancellationToken), JobLogsGridDefinitions.OneCPost
 037                p => new
 038                {
 039                    p.Id, p.OrderId, p.TransactionId, p.OperationKind, p.Success, p.PostedAt,
 040                    PerformedBy = p.ChangedBy ?? _context.Transactions.Where(t => t.Id == p.TransactionId).Select(t => t
 041                    p.OrganizationName, p.OrganizationInn, p.SenderName, p.WarehouseSender, p.WarehouseRecipient,
 042                    p.OrdersCount, p.ProductsCount, p.ArticlesSummary, p.DocumentsCount, p.DocumentNumbers, p.DocumentGu
 043                    p.IsPosted, p.ErrorSummary,
 044                    HasErrorDetail = p.DetailsJson != null && p.DetailsJson.Contains("\"errorDetail\":"),
 045                    HasNomenclature = p.DetailsJson != null && (p.DetailsJson.Contains("\"missingNomenclature\":\"") || 
 046                    ShipmentNumber = _context.Orders.Where(o => o.Id == p.OrderId).Select(o => o.ShipmentNumber).FirstOr
 047                    ExecutionId = _context.BackgroundTransactionResults.Where(r => r.TransactionId == p.TransactionId).M
 048                }, cancellationToken),
 049            "webhook" => await PageAsync(_context.OzonPushNotifications.AsNoTracking(), JobLogsGridDefinitions.Webhook, 
 050                n => new
 051                {
 052                    n.Id, n.ReceivedAt, n.ProcessedAt, n.MessageType, n.PostingNumber, n.SellerId, n.WarehouseId, n.Proc
 053                    Result = n.ErrorMessage ?? n.ActionDetails, IsError = n.ErrorMessage != null
 054                }, cancellationToken),
 055            "email" => await PageAsync(_context.EmailAttachmentProcessingResults.AsNoTracking(), JobLogsGridDefinitions.
 056                r => new
 057                {
 058                    r.Id, r.CreatedAt, Status = r.Status.ToString(), RuleName = r.Rule != null ? r.Rule.Name : null,
 059                    Sender = r.EmailMessage != null ? r.EmailMessage.SenderEmail : null,
 060                    Subject = r.EmailMessage != null ? r.EmailMessage.Subject : null,
 061                    FileName = r.Attachment != null ? r.Attachment.FileName : r.DropboxFileName,
 062                    r.DropboxFolderPath, Reason = r.Error ?? r.Reason, IsError = r.Error != null
 063                }, cancellationToken),
 064            "fileupload" => await PageAsync(_context.FileUploadRecords.AsNoTracking(), JobLogsGridDefinitions.FileUpload
 065                r => new { r.Id, r.FileName, r.FolderName, r.Date, r.Result }, cancellationToken),
 066            "stocksync" => await PageAsync(_context.WarehouseStockSyncRuns.AsNoTracking(), JobLogsGridDefinitions.StockS
 067                r => new
 068                {
 069                    r.Id, r.SourceType, r.Status, r.StartedAtUtc, r.CompletedAtUtc, r.RowsRead, r.RowsSaved,
 070                    r.TriggeredBy, r.SourceFileName, r.ErrorMessage
 071                }, cancellationToken),
 072            "alerts" => await PageAsync(_context.SystemAlertIncidents.AsNoTracking().Where(x => x.ConfirmedAtUtc != null
 073                JobLogsGridDefinitions.SystemAlerts, request!,
 074                x => new
 075                {
 076                    x.Id, x.IsActive, x.Domain, x.Severity, x.Title, x.Details, x.Url,
 077                    x.FirstObservedAtUtc, x.LastObservedAtUtc, x.ResolvedAtUtc, x.OccurrenceCount,
 078                    x.AcknowledgedAtUtc, x.AcknowledgedBy, x.MutedUntilUtc, x.MutedBy, x.OrderCount, x.OrdersJson
 079                }, cancellationToken),
 080            _ => (null, 0)
 081        };
 82
 083        if (rows == null) return NotFound(new { success = false, error = "Неизвестная вкладка." });
 084        var lastRow = request!.StartRow + rows.Count >= total ? total : -1;
 085        return Ok(new { success = true, rows, totalCount = total, lastRow });
 086    }
 87
 88    [HttpPost("grid/{tab}/facets")]
 89    [ValidateAntiForgeryToken]
 90    [PageAuthorize(JobLogsPageCode, AccessType.View)]
 91    public async Task<IActionResult> GridFacets(string tab, [FromBody] ServerGridRequest? request, CancellationToken can
 92    {
 093        if (request is null || string.IsNullOrWhiteSpace(request.ColId)) return BadRequest(new { success = false, error 
 094        request.StartRow = 0;
 095        request.EndRow = 1;
 096        var error = ServerGridQuery.Validate(request);
 097        if (error != null) return BadRequest(new { success = false, error });
 098        if (!CanReadGrid(tab)) return Forbid();
 99
 0100        List<ServerGridFacet>? values = tab switch
 0101        {
 0102            "transactions" => await ServerGridQuery.FacetsAsync(await ExecutionsQueryAsync(request, cancellationToken), 
 0103            "onec" => await ServerGridQuery.FacetsAsync(await OneCQueryAsync(request, cancellationToken), JobLogsGridDef
 0104            "webhook" => await ServerGridQuery.FacetsAsync(_context.OzonPushNotifications.AsNoTracking(), JobLogsGridDef
 0105            "email" => await ServerGridQuery.FacetsAsync(_context.EmailAttachmentProcessingResults.AsNoTracking(), JobLo
 0106            "fileupload" => await ServerGridQuery.FacetsAsync(_context.FileUploadRecords.AsNoTracking(), JobLogsGridDefi
 0107            "stocksync" => await ServerGridQuery.FacetsAsync(_context.WarehouseStockSyncRuns.AsNoTracking(), JobLogsGrid
 0108            "alerts" => await ServerGridQuery.FacetsAsync(_context.SystemAlertIncidents.AsNoTracking().Where(x => x.Conf
 0109                JobLogsGridDefinitions.SystemAlerts, request, cancellationToken),
 0110            _ => null
 0111        };
 112
 0113        return values == null
 0114            ? NotFound(new { success = false, error = "Неизвестная вкладка." })
 0115            : Ok(new { success = true, values });
 0116    }
 117
 118    private bool CanReadGrid(string tab)
 119    {
 0120        var isAdmin = User.IsInRole("Admin") || User.IsInRole("SuperAdmin");
 0121        return tab is not ("transactions" or "onec" or "alerts") || isAdmin;
 122    }
 123
 124    /// <summary>Запуски: параметр orderNumber — поиск заказа по отправлению/Id (как раньше в фильтре вкладки).</summary
 125    private async Task<IQueryable<OzonDomains.Models.BackgroundJobExecution>> ExecutionsQueryAsync(ServerGridRequest req
 126    {
 0127        var query = _context.BackgroundJobExecutions.AsNoTracking();
 0128        if (request.Params.TryGetValue("orderNumber", out var orderNumber) && !string.IsNullOrWhiteSpace(orderNumber))
 129        {
 0130            var lookup = await BuildOrderExecutionLookupAsync(orderNumber.Trim(), ct);
 0131            var ids = lookup.ExecutionIds;
 0132            query = ids.Count > 0 ? query.Where(e => ids.Contains(e.Id)) : query.Where(e => false);
 0133        }
 134
 0135        return query;
 0136    }
 137
 138    /// <summary>Проведения 1С: параметр executionId — только транзакции этого запуска (ссылка из «Запусков»).</summary>
 139    private async Task<IQueryable<OzonDomains.Models.OrderHistoryOneCPosting>> OneCQueryAsync(ServerGridRequest request,
 140    {
 0141        var query = _context.OrderHistoryOneCPostings.AsNoTracking();
 0142        if (request.Params.TryGetValue("executionId", out var raw) && long.TryParse(raw, out var executionId) && executi
 143        {
 0144            var txIds = await _context.BackgroundTransactionResults.AsNoTracking()
 0145                .Where(r => r.ExecutionId == executionId && r.TransactionId != null)
 0146                .Select(r => r.TransactionId!.Value)
 0147                .Distinct()
 0148                .ToListAsync(ct);
 0149            query = query.Where(p => p.TransactionId != null && txIds.Contains(p.TransactionId.Value));
 0150        }
 151
 0152        return query;
 0153    }
 154
 155    private static async Task<(List<object>? Rows, int Total)> PageAsync<T, TRow>(
 156        IQueryable<T> query, ServerGridDefinition<T> def, ServerGridRequest request,
 157        System.Linq.Expressions.Expression<Func<T, TRow>> projection, CancellationToken ct)
 158    {
 0159        var filtered = ServerGridQuery.ApplyFilters(query, def, request);
 0160        var total = await filtered.CountAsync(ct);
 0161        var start = Math.Max(0, request.StartRow);
 0162        var take = Math.Clamp(request.EndRow - start, 1, ServerGridQuery.MaxBlockSize);
 0163        if (start >= total) return (new List<object>(), total);
 0164        var rows = await ServerGridQuery.ApplySort(filtered, def, request.SortModel).Skip(start).Take(take).Select(proje
 0165        return (rows.Cast<object>().ToList(), total);
 0166    }
 167}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/JobLogsController.OneCStatistics.cs

#LineLine coverage
 1using ClosedXML.Excel;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.EntityFrameworkCore;
 4using OfficeOpenXml;
 5using OzonDomains.Models.Authorization.Enums;
 6using OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs;
 7using OzonOrdersWeb.Authorization;
 8using OzonOrdersWeb.WebServices.Grid;
 9using OzonOrdersWeb.WebServices.JobLogs;
 10
 11namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 12
 13/// <summary>Статистика и Excel-выгрузка вкладки проведений 1С.</summary>
 14public partial class JobLogsController
 15{
 16    private const int MaxOneCExportRows = 50_000;
 17    private const string ExcelContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
 18
 19    [HttpPost("onec/statistics")]
 20    [ValidateAntiForgeryToken]
 21    [PageAuthorize(JobLogsPageCode, AccessType.View)]
 22    public async Task<IActionResult> OneCStatistics([FromBody] ServerGridRequest? request, CancellationToken cancellatio
 23    {
 024        var error = ServerGridQuery.Validate(request);
 025        if (error != null) return BadRequest(new { success = false, error });
 026        if (!CanReadGrid("onec")) return Forbid();
 27
 028        return Ok(new { success = true, statistics = await BuildOneCStatisticsAsync(request!, cancellationToken) });
 029    }
 30
 31    [HttpPost("onec/export/excel")]
 32    [ValidateAntiForgeryToken]
 33    [PageAuthorize(JobLogsPageCode, AccessType.View)]
 34    public async Task<IActionResult> ExportOneCToExcel([FromBody] JobLogsOneCExcelExportRequest? exportRequest, Cancella
 35    {
 036        var error = ServerGridQuery.Validate(exportRequest?.GridRequest);
 037        if (error != null) return BadRequest(new { success = false, error });
 038        if (!CanReadGrid("onec")) return Forbid();
 039        var gridRequest = exportRequest!.GridRequest!;
 40
 041        var filtered = await FilteredOneCPostingsAsync(gridRequest, cancellationToken);
 042        var total = await filtered.CountAsync(cancellationToken);
 043        if (total > MaxOneCExportRows)
 044            return BadRequest(new { success = false, error = $"Для Excel отберите не более {MaxOneCExportRows:N0} строк.
 45
 046        var rows = await ServerGridQuery.ApplySort(filtered, JobLogsGridDefinitions.OneCPostings(_context), gridRequest.
 047            .Select(p => new OneCExcelRow(
 048                p.Id, p.PostedAt, p.OperationKind, p.Success, p.IsPosted,
 049                p.ChangedBy ?? _context.Transactions.Where(t => t.Id == p.TransactionId).Select(t => t.CreateBy).FirstOr
 050                p.OrganizationName, p.WarehouseSender, p.WarehouseRecipient, p.OrdersCount, p.ProductsCount, p.ArticlesS
 051                p.DocumentsCount, p.DocumentNumbers, p.ErrorSummary, p.OrderId))
 052            .ToListAsync(cancellationToken);
 053        var statistics = await BuildOneCStatisticsAsync(gridRequest, cancellationToken);
 54
 055        var workbook = BuildOneCExcel(rows, statistics);
 056        workbook = AddChartImages(workbook, exportRequest.ResultChartImage, exportRequest.OperationChartImage);
 057        return File(workbook, ExcelContentType, $"onec-postings-{DateTime.UtcNow:yyyyMMdd-HHmm}.xlsx");
 058    }
 59
 60    private async Task<IQueryable<OzonDomains.Models.OrderHistoryOneCPosting>> FilteredOneCPostingsAsync(ServerGridReque
 61    {
 062        var query = await OneCQueryAsync(request, cancellationToken);
 063        return ServerGridQuery.ApplyFilters(query, JobLogsGridDefinitions.OneCPostings(_context), request);
 064    }
 65
 66    private async Task<JobLogsOneCStatisticsViewModel> BuildOneCStatisticsAsync(ServerGridRequest request, CancellationT
 67    {
 068        var filtered = await FilteredOneCPostingsAsync(request, cancellationToken);
 069        var totals = await filtered.GroupBy(_ => 1)
 070            .Select(group => new { PostingCount = group.Count(), DocumentsCount = group.Sum(p => p.DocumentsCount), Orde
 071            .FirstOrDefaultAsync(cancellationToken);
 072        var byResult = await filtered.GroupBy(p => new { p.Success, p.IsPosted })
 073            .Select(group => new { group.Key.Success, group.Key.IsPosted, PostingsCount = group.Count(), DocumentsCount 
 074            .ToListAsync(cancellationToken);
 075        var byOperation = await filtered.GroupBy(p => p.OperationKind)
 076            .Select(group => new { OperationKind = group.Key, PostingsCount = group.Count(), DocumentsCount = group.Sum(
 077            .OrderByDescending(x => x.DocumentsCount).ThenBy(x => x.OperationKind)
 078            .ToListAsync(cancellationToken);
 79
 080        return new JobLogsOneCStatisticsViewModel
 081        {
 082            PostingCount = totals?.PostingCount ?? 0,
 083            DocumentsCount = totals?.DocumentsCount ?? 0,
 084            OrdersCount = totals?.OrdersCount ?? 0,
 085            ProductsCount = totals?.ProductsCount ?? 0,
 86            ByResult = byResult.Select(x => new JobLogsOneCStatisticsItemViewModel(OneCResultKey(x.Success, x.IsPosted),
 87            ByOperation = byOperation.Select(x => new JobLogsOneCStatisticsItemViewModel(x.OperationKind, OneCOperationL
 088        };
 089    }
 90
 091    private static string OneCResultKey(bool success, bool isPosted) => !success ? "failed" : isPosted ? "posted" : "dra
 092    private static string OneCResultLabel(bool success, bool isPosted) => !success ? "Ошибка" : isPosted ? "Проведено" :
 093    private static int OneCResultOrder(string key) => key switch { "posted" => 0, "draft" => 1, _ => 2 };
 094    private static string OneCOperationLabel(string? operationKind) => operationKind switch
 095    {
 096        "Movement" => "Перемещение",
 097        "Receipt" => "Приходная накладная",
 098        "Expense" => "Расходная накладная",
 099        _ when string.IsNullOrWhiteSpace(operationKind) => "Не указано",
 0100        _ => operationKind
 0101    };
 102
 103    private static byte[] BuildOneCExcel(IReadOnlyList<OneCExcelRow> rows, JobLogsOneCStatisticsViewModel statistics)
 104    {
 0105        using var workbook = new XLWorkbook();
 0106        var summary = workbook.Worksheets.Add("Статистика");
 0107        WriteHeader(summary, new[] { "Показатель", "Значение" });
 0108        var metrics = new (string Label, int Value)[] { ("Записей проведения", statistics.PostingCount), ("Документов 1С
 0109        for (var i = 0; i < metrics.Length; i++) { summary.Cell(i + 2, 1).Value = metrics[i].Label; summary.Cell(i + 2, 
 0110        summary.Columns().AdjustToContents();
 111
 0112        WriteStatisticsSheet(workbook, "По результатам", statistics.ByResult);
 0113        WriteStatisticsSheet(workbook, "По операциям", statistics.ByOperation);
 114
 0115        var sheet = workbook.Worksheets.Add("Проведения 1С");
 0116        WriteHeader(sheet, new[] { "ID", "Дата", "Операция", "Результат", "Кто провёл", "Организация", "Склад отправител
 0117        var row = 2;
 0118        foreach (var item in rows)
 119        {
 0120            sheet.Cell(row, 1).Value = item.Id;
 0121            sheet.Cell(row, 2).Value = item.PostedAt;
 0122            sheet.Cell(row, 2).Style.DateFormat.Format = "dd.MM.yyyy HH:mm";
 0123            sheet.Cell(row, 3).Value = OneCOperationLabel(item.OperationKind);
 0124            sheet.Cell(row, 4).Value = OneCResultLabel(item.Success, item.IsPosted);
 0125            sheet.Cell(row, 5).Value = item.PerformedBy ?? string.Empty;
 0126            sheet.Cell(row, 6).Value = item.OrganizationName ?? string.Empty;
 0127            sheet.Cell(row, 7).Value = item.WarehouseSender ?? string.Empty;
 0128            sheet.Cell(row, 8).Value = item.WarehouseRecipient ?? string.Empty;
 0129            sheet.Cell(row, 9).Value = item.OrdersCount;
 0130            sheet.Cell(row, 10).Value = item.ProductsCount;
 0131            sheet.Cell(row, 11).Value = item.ArticlesSummary ?? string.Empty;
 0132            sheet.Cell(row, 12).Value = item.DocumentsCount;
 0133            sheet.Cell(row, 13).Value = item.DocumentNumbers ?? string.Empty;
 0134            sheet.Cell(row, 14).Value = item.ErrorSummary ?? string.Empty;
 0135            sheet.Cell(row, 15).Value = item.OrderId;
 0136            row++;
 137        }
 0138        sheet.SheetView.FreezeRows(1);
 0139        sheet.Range(1, 1, Math.Max(1, row - 1), 15).SetAutoFilter();
 0140        sheet.Columns(1, 2).AdjustToContents();
 0141        sheet.Columns(3, 15).Width = 18;
 0142        sheet.Column(11).Width = 28;
 0143        sheet.Column(13).Width = 28;
 0144        sheet.Column(14).Width = 40;
 0145        sheet.Columns(11, 14).Style.Alignment.WrapText = true;
 146
 0147        using var stream = new MemoryStream();
 0148        workbook.SaveAs(stream);
 0149        return stream.ToArray();
 0150    }
 151
 152    private static byte[] AddChartImages(byte[] workbookBytes, string? resultChartImage, string? operationChartImage)
 153    {
 0154        var resultImage = DecodePngDataUrl(resultChartImage);
 0155        var operationImage = DecodePngDataUrl(operationChartImage);
 0156        if (resultImage == null && operationImage == null) return workbookBytes;
 157
 0158        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
 0159        using var stream = new MemoryStream(workbookBytes);
 0160        using var package = new ExcelPackage(stream);
 0161        var sheet = package.Workbook.Worksheets["Статистика"];
 0162        if (sheet == null) return workbookBytes;
 163
 0164        AddChartImage(sheet, "result-chart", "Документы по результату", resultImage, 0);
 0165        AddChartImage(sheet, "operation-chart", "Документы по операции", operationImage, 22);
 0166        return package.GetAsByteArray();
 0167    }
 168
 169    private static void AddChartImage(ExcelWorksheet sheet, string name, string title, byte[]? image, int row)
 170    {
 0171        if (image == null) return;
 0172        using var stream = new MemoryStream(image);
 0173        var picture = sheet.Drawings.AddPicture(name, stream);
 0174        picture.Name = title;
 0175        picture.SetPosition(row, 0, 3, 0);
 0176        picture.SetSize(720, 280);
 0177    }
 178
 179    private static byte[]? DecodePngDataUrl(string? dataUrl)
 180    {
 181        const string prefix = "data:image/png;base64,";
 0182        if (string.IsNullOrWhiteSpace(dataUrl) || !dataUrl.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) retur
 0183        var value = dataUrl[prefix.Length..];
 0184        if (value.Length > 4_000_000) return null;
 0185        try { return Convert.FromBase64String(value); }
 0186        catch (FormatException) { return null; }
 0187    }
 188
 189    private static void WriteStatisticsSheet(XLWorkbook workbook, string name, IReadOnlyList<JobLogsOneCStatisticsItemVi
 190    {
 0191        var sheet = workbook.Worksheets.Add(name);
 0192        WriteHeader(sheet, new[] { "Категория", "Проведений", "Документов" });
 0193        for (var i = 0; i < rows.Count; i++) { sheet.Cell(i + 2, 1).Value = rows[i].Label; sheet.Cell(i + 2, 2).Value = 
 0194        sheet.SheetView.FreezeRows(1);
 0195        sheet.Columns().AdjustToContents();
 0196    }
 197
 198    private static void WriteHeader(IXLWorksheet sheet, IReadOnlyList<string> headers)
 199    {
 0200        for (var i = 0; i < headers.Count; i++)
 201        {
 0202            var cell = sheet.Cell(1, i + 1);
 0203            cell.Value = headers[i];
 0204            cell.Style.Font.Bold = true;
 0205            cell.Style.Fill.BackgroundColor = XLColor.FromArgb(0xD0, 0xE4, 0xFF);
 206        }
 0207    }
 208
 0209    private sealed record OneCExcelRow(long Id, DateTime PostedAt, string OperationKind, bool Success, bool IsPosted, st
 210}

Methods/Properties

GetAlertChips()
GetAlertPreview()
RefreshAlerts()
AcknowledgeAlert()
MuteAlert()
RestoreAlert()
AlertResult(System.Boolean)
.ctor(OzonRepositories.Context.ApplicationDbContext,OzonOrdersWeb.WebServices.JobLogs.IJobLogsBadgeService,Services.DataServices.OzonPushNotificationDataService,Services.DataServices.OzonClientServices,Services.DataServices.EmailAttachmentProcessingResultDataServices,Services.DataServices.EmailRoutingRuleDataServices,OzonOrdersWeb.WebServices.TransactionTimeTracking.ITransactionTimeTrackingReportQueryService,Services.SearchServices.OrderSearch.IOrderSearchDiagnosticsService,OzonOrdersWeb.WebServices.JobLogs.IServerDiagnosticsService,Hangfire.IBackgroundJobClient,Services.OzonWebhookServices.Monitoring.IOzonWebhookAvailabilityService,Services.OzonWebhookServices.Monitoring.OzonWebhookAvailabilityStore,OzonOrdersWeb.WebServices.SystemAlerts.SystemAlertIncidentStore,OzonOrdersWeb.WebServices.SystemAlerts.SystemAlertEvaluationService)
GetBadgeSummary()
LegacyIndexRedirect()
Index()
OrderSearchSnapshot()
OrderSearchLookup()
EnqueueOrderSearchDriftRepair()
EnqueueOrderSearchFullReindex()
PostingErrorDetail()
PostingOrders()
LoadTransactionsTabAsync()
BuildOrderExecutionLookupAsync()
FindTransaction()
LoadOneCTabAsync()
MapOneCPosting(OzonDomains.Models.OrderHistoryOneCPosting)
GetErrorDetails(System.String)
GetCreatedNomenclature(System.String)
LoadWebhookTabAsync()
RefreshWebhookHealth()
EnableWebhookEndpoint()
LoadEmailRoutingTabAsync()
LoadFileUploadTabAsync()
LoadStockSyncTabAsync()
LoadOrderSearchTabAsync()
NormalizeTab(System.String)
GridRows()
GridFacets()
CanReadGrid(System.String)
ExecutionsQueryAsync()
OneCQueryAsync()
PageAsync()
OneCStatistics()
ExportOneCToExcel()
FilteredOneCPostingsAsync()
BuildOneCStatisticsAsync()
OneCResultKey(System.Boolean,System.Boolean)
OneCResultLabel(System.Boolean,System.Boolean)
OneCResultOrder(System.String)
OneCOperationLabel(System.String)
BuildOneCExcel(System.Collections.Generic.IReadOnlyList`1<OzonOrdersWeb.Areas.Studio2.Controllers.JobLogsController/OneCExcelRow>,OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs.JobLogsOneCStatisticsViewModel)
AddChartImages(System.Byte[],System.String,System.String)
AddChartImage(OfficeOpenXml.ExcelWorksheet,System.String,System.String,System.Byte[],System.Int32)
DecodePngDataUrl(System.String)
WriteStatisticsSheet(ClosedXML.Excel.XLWorkbook,System.String,System.Collections.Generic.IReadOnlyList`1<OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs.JobLogsOneCStatisticsItemViewModel>)
WriteHeader(ClosedXML.Excel.IXLWorksheet,System.Collections.Generic.IReadOnlyList`1<System.String>)
get_Id()