< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_EmailRoutingResults_Index
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/EmailRoutingResults/Index.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 42
Coverable lines: 42
Total lines: 468
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 68
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%4160640%
<ExecuteAsync()100%210%
<ExecuteAsync()100%210%
<ExecuteAsync()0%2040%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/EmailRoutingResults/Index.cshtml

#LineLine coverage
 1@using OzonOrdersWeb.Areas.Studio2.ViewModels.Grid
 2@model OzonOrdersWeb.Areas.Studio2.ViewModels.EmailRouting.EmailRoutingResultsViewModel
 3
 4@{
 05    ViewData["Title"] = "Результаты обработки почты";
 06    Layout = "~/Views/Shared/_Layout.cshtml";
 7}
 8
 9<link rel="stylesheet" href="~/css/Studio2/EmailRouting/email-routing-results-index.css" asp-append-version="true"/>
 10
 011@await Html.PartialAsync("_BsxGridShell")
 12
 13<div class="container-fluid py-3 bsx-surface err-page">
 14
 15    <div class="app-section-hero app-section-hero--import d-flex flex-wrap justify-content-between align-items-start gap
 16        <div>
 17            <h1><i class="fas fa-envelope-open-text me-2 text-primary"></i>Импорт заказов из почты</h1>
 18            <div class="subtitle">Результаты обработки писем и импорта вложений в систему</div>
 19            <div class="subtitle-help">
 20                Система читает входящую почту по правилам импорта и загружает данные из вложений прямо в заказы.
 21                Здесь — статус IMAP-подключения, журнал последних запусков и кнопки ручного прогона.
 22            </div>
 23            <div class="app-section-hero__quick-links">
 24                <a asp-area="Studio2" asp-controller="EmailRoutingRules" asp-action="Index">
 25                    <i class="fas fa-route"></i>Правила импорта
 26                </a>
 27                <a asp-area="Studio2" asp-controller="EmailRoutingScheduler" asp-action="Index">
 28                    <i class="fas fa-clock"></i>Планировщик
 29                </a>
 30                <a asp-area="Studio2" asp-controller="JobLogs" asp-action="Index">
 31                    <i class="fas fa-clipboard-list"></i>Журнал заданий
 32                </a>
 33            </div>
 34        </div>
 35        <div class="err-hero-actions">
 36            <form asp-action="DryRun" method="post" class="d-inline">
 037                @Html.AntiForgeryToken()
 38                <button type="submit" class="btn btn-outline-warning btn-sm">
 39                    <i class="fas fa-play-circle me-1"></i>Тестовый прогон
 40                </button>
 41            </form>
 42            <form asp-action="RunNow" method="post" class="d-inline">
 043                @Html.AntiForgeryToken()
 44                <button type="submit" class="btn btn-primary btn-sm">
 45                    <i class="fas fa-bolt me-1"></i>Запустить сейчас
 46                </button>
 47            </form>
 48        </div>
 49    </div>
 50
 051    @if (TempData["TransactionResult"] != null)
 52    {
 53        <div class="alert alert-info alert-dismissible fade show" role="alert">
 054            <i class="bi bi-info-circle-fill"></i> @TempData["TransactionResult"]
 55            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 56        </div>
 57    }
 58
 59    @{
 060        var imap = Model.ImapConnection;
 061        var initialStatusClass = imap == null ? "secondary"
 062            : !imap.IsConfigured ? "secondary"
 063            : imap.IsConnected ? "success"
 064            : "danger";
 65    }
 66    <div id="imapConnectionCard" class="card mb-4 border-@initialStatusClass">
 67        <div class="card-body py-3">
 68            <div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
 69                <div>
 70                    <div class="fw-semibold mb-1">
 71                        <i class="bi bi-mailbox text-primary me-1"></i> Подключённый почтовый ящик
 72                        <span id="imapLiveIndicator" class="badge bg-light text-muted border ms-1 d-none">
 73                            <span class="spinner-border spinner-border-sm me-1" role="status"></span> обновление…
 74                        </span>
 75                    </div>
 076                    <div id="imapUserName" class="fs-5 fw-bold">@(imap?.IsConfigured == true ? imap.UserName : "—")</div
 77                    <div id="imapServerInfo" class="text-muted small">
 078                        @if (imap?.IsConfigured == true)
 79                        {
 080                            @($"{imap.Host}:{imap.Port} · папка {imap.Folder} · лимит {imap.FetchLimit} писем/запуск")
 81                        }
 82                        else
 83                        {
 84                            @:IMAP не настроен в appsettings (секция ImapSettings).
 85                        }
 86                    </div>
 87                </div>
 88                <div class="text-end">
 89                    <div class="d-flex align-items-center justify-content-end gap-2 mb-2">
 090                        @if (User.IsInRole("Admin"))
 91                        {
 92                            <button type="button" id="imapSettingsToggleBtn" class="btn btn-sm btn-outline-primary" titl
 93                                <i class="bi bi-gear"></i>
 94                            </button>
 95                        }
 96                        <button type="button" id="imapRefreshBtn" class="btn btn-sm btn-outline-secondary" title="Обнови
 97                            <i class="bi bi-arrow-clockwise"></i>
 98                        </button>
 99                        <span id="imapStatusBadge" class="badge bg-@initialStatusClass fs-6">
 0100                            @if (imap == null || !imap.IsConfigured)
 101                            {
 102                                <i class="bi bi-gear"></i> @:Не настроено
 103                            }
 0104                            else if (imap.IsConnected)
 105                            {
 106                                <i class="bi bi-check-circle-fill"></i> @:Подключено
 107                            }
 108                            else
 109                            {
 110                                <i class="bi bi-x-circle-fill"></i> @:Ошибка подключения
 111                            }
 112                        </span>
 113                    </div>
 114                    <div id="imapCounts" class="small text-muted mt-2">
 0115                        @if (imap?.IsConnected == true)
 116                        {
 0117                            <span>Непрочитанных: <strong>@imap.UnseenCount</strong></span>
 0118                            @if (imap.UnseenReadyCount.HasValue)
 119                            {
 0120                                <span> · в окне выборки: <strong>@imap.UnseenReadyCount</strong></span>
 121                            }
 122                        }
 123                    </div>
 124                    <div id="imapError" class="small text-danger mt-2" style="@(string.IsNullOrWhiteSpace(imap?.ErrorMes
 125                         title="@imap?.ErrorMessage">
 0126                        @if (!string.IsNullOrWhiteSpace(imap?.ErrorMessage))
 127                        {
 0128                            @(imap.ErrorMessage.Length > 120 ? imap.ErrorMessage[..120] + "…" : imap.ErrorMessage)
 129                        }
 130                    </div>
 131                    <div id="imapCheckedAt" class="small text-muted mt-1">
 0132                        @if (imap != null)
 133                        {
 0134                            @($"Проверено: {imap.CheckedAtUtc.ToLocalTime():dd.MM.yyyy HH:mm:ss} · автообновление каждые
 135                        }
 136                    </div>
 137                </div>
 138            </div>
 139        </div>
 140    </div>
 141
 0142    @if (User.IsInRole("Admin") && Model.ImapSettingsEdit != null)
 143    {
 0144        var edit = Model.ImapSettingsEdit;
 145        <div id="imapSettingsPanel" class="card mb-4 d-none">
 146            <div class="card-header d-flex justify-content-between align-items-center">
 147                <span class="fw-semibold"><i class="bi bi-gear text-primary me-1"></i> Настройки IMAP-почты</span>
 148                <span class="small text-muted">Сохраняется в data/appsettings.runtime.json</span>
 149            </div>
 150            <div class="card-body">
 0151                @Html.AntiForgeryToken()
 152                <div class="row g-3">
 153                    <div class="col-md-6">
 154                        <label class="form-label" for="imapEditHost">IMAP-сервер</label>
 155                        <input type="text" class="form-control" id="imapEditHost" value="@edit.Host" placeholder="imap.y
 156                    </div>
 157                    <div class="col-md-3">
 158                        <label class="form-label" for="imapEditPort">Порт</label>
 159                        <input type="number" class="form-control" id="imapEditPort" value="@edit.Port" min="1" max="6553
 160                    </div>
 161                    <div class="col-md-3 d-flex align-items-end">
 162                        <div class="form-check mb-2">
 0163                            <input class="form-check-input" type="checkbox" id="imapEditUseSsl" @(edit.UseSsl ? "checked
 164                            <label class="form-check-label" for="imapEditUseSsl">SSL/TLS</label>
 165                        </div>
 166                    </div>
 167                    <div class="col-md-6">
 168                        <label class="form-label" for="imapEditUserName">Email (логин)</label>
 169                        <input type="email" class="form-control" id="imapEditUserName" value="@edit.UserName" placeholde
 170                    </div>
 171                    <div class="col-md-6">
 172                        <label class="form-label" for="imapEditPassword">Пароль</label>
 173                        <input type="password" class="form-control" id="imapEditPassword" autocomplete="new-password"
 174                               placeholder="@(edit.HasSavedPassword ? "Оставьте пустым, чтобы не менять" : "Введите паро
 175                    </div>
 176                    <div class="col-md-4">
 177                        <label class="form-label" for="imapEditFolder">Папка</label>
 178                        <input type="text" class="form-control" id="imapEditFolder" value="@edit.Folder" />
 179                    </div>
 180                    <div class="col-md-4">
 181                        <label class="form-label" for="imapEditFetchLimit">Лимит писем/запуск</label>
 182                        <input type="number" class="form-control" id="imapEditFetchLimit" value="@edit.FetchLimit" min="
 183                    </div>
 184                </div>
 185                <div id="imapSettingsFeedback" class="alert d-none mt-3 mb-0" role="alert"></div>
 186                <div class="d-flex flex-wrap gap-2 mt-3">
 187                    <button type="button" id="imapTestSettingsBtn" class="btn btn-outline-primary">
 188                        <i class="bi bi-plug"></i> Проверить подключение
 189                    </button>
 190                    <button type="button" id="imapSaveSettingsBtn" class="btn btn-success">
 191                        <i class="bi bi-save"></i> Сохранить
 192                    </button>
 193                </div>
 194            </div>
 195        </div>
 196    }
 197
 198    <!-- Статистика: клик — фильтр грида по статусу -->
 199    <div class="bsx-dir-stats err-stats mb-3">
 0200        <button type="button" class="bsx-dir-stat" data-err-status="Uploaded"><i class="bi bi-check2-circle text-success
 0201        <button type="button" class="bsx-dir-stat" data-err-status="OrdersImported"><i class="bi bi-database-add text-pr
 0202        <button type="button" class="bsx-dir-stat" data-err-status="RejectedByRule"><i class="bi bi-slash-circle"></i> О
 0203        <button type="button" class="bsx-dir-stat" data-err-status="Duplicate"><i class="bi bi-files text-info"></i> Дуб
 0204        <button type="button" class="bsx-dir-stat bsx-dir-stat--warn" data-err-status="Failed"><i class="bi bi-exclamati
 205    </div>
 206
 0207    @await Html.PartialAsync("_BsxDirectoryGrid", new BsxDirectoryGridViewModel("errResults", "Результаты обработки влож
 0208        "Фильтры и сортировка — по всему журналу на сервере. Дата — письма; серым — дата обработки, если IMAP не отдал д
 209
 210    <div class="bsx-dir-card mt-3">
 211        <div class="bsx-grid-toolbar">
 212            <span class="bsx-dir-title"><i class="fas fa-inbox"></i>Последние письма из почтового ящика</span>
 213            <div class="bsx-grid-toolbar-end">
 0214                <span class="bsx-grid-count">@Model.LastMessagesLimit последних из базы</span>
 215            </div>
 216        </div>
 217        <div class="bsx-dir-body">
 218            <div id="errMessagesGrid" class="bsx-grid"></div>
 219        </div>
 220    </div>
 221</div>
 222
 0223<script type="application/json" id="errMessagesData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MessageRo
 224
 225@section Scripts {
 226<script src="~/js/Studio2/Grid/bsx-directory.js" asp-append-version="true"></script>
 227<script src="~/js/Studio2/EmailRouting/email-routing-results-grids.js" asp-append-version="true"></script>
 228<script>
 229(function () {
 0230    const statusUrl = '@Url.Action("GetImapStatus", "EmailRoutingResults", new { area = "Studio2" })';
 231    const pollMs = 30000;
 232    let pollTimer = null;
 233    let inFlight = false;
 234
 235    const card = document.getElementById('imapConnectionCard');
 236    const liveIndicator = document.getElementById('imapLiveIndicator');
 237    const refreshBtn = document.getElementById('imapRefreshBtn');
 238    if (!card || !statusUrl) return;
 239
 240    function statusMeta(data) {
 241        if (!data || !data.isConfigured) {
 242            return { cls: 'secondary', icon: 'bi-gear', text: 'Не настроено' };
 243        }
 244        if (data.isConnected) {
 245            return { cls: 'success', icon: 'bi-check-circle-fill', text: 'Подключено' };
 246        }
 247        return { cls: 'danger', icon: 'bi-x-circle-fill', text: 'Ошибка подключения' };
 248    }
 249
 250    function formatCheckedAt(utcIso) {
 251        if (!utcIso) return '';
 252        const d = new Date(utcIso);
 253        if (Number.isNaN(d.getTime())) return '';
 254        const pad = n => String(n).padStart(2, '0');
 255        return `Проверено: ${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.
 256    }
 257
 258    function renderImapStatus(data) {
 259        const meta = statusMeta(data);
 260        card.className = `card mb-4 border-${meta.cls}`;
 261
 262        const userNameEl = document.getElementById('imapUserName');
 263        const serverInfoEl = document.getElementById('imapServerInfo');
 264        const badgeEl = document.getElementById('imapStatusBadge');
 265        const countsEl = document.getElementById('imapCounts');
 266        const errorEl = document.getElementById('imapError');
 267        const checkedAtEl = document.getElementById('imapCheckedAt');
 268
 269        if (userNameEl) {
 270            userNameEl.textContent = data?.isConfigured ? (data.userName || '—') : '—';
 271        }
 272
 273        if (serverInfoEl) {
 274            serverInfoEl.textContent = data?.isConfigured
 275                ? `${data.host}:${data.port} · папка ${data.folder} · лимит ${data.fetchLimit} писем/запуск`
 276                : 'IMAP не настроен в appsettings (секция ImapSettings).';
 277        }
 278
 279        if (badgeEl) {
 280            badgeEl.className = `badge bg-${meta.cls} fs-6`;
 281            badgeEl.innerHTML = `<i class="bi ${meta.icon}"></i> ${meta.text}`;
 282        }
 283
 284        if (countsEl) {
 285            if (data?.isConnected) {
 286                let html = `Непрочитанных: <strong>${data.unseenCount ?? 0}</strong>`;
 287                if (data.unseenReadyCount != null) {
 288                    html += ` · в окне выборки: <strong>${data.unseenReadyCount}</strong>`;
 289                }
 290                countsEl.innerHTML = html;
 291                countsEl.style.display = '';
 292            } else {
 293                countsEl.innerHTML = '';
 294                countsEl.style.display = 'none';
 295            }
 296        }
 297
 298        if (errorEl) {
 299            const err = data?.errorMessage || '';
 300            if (err && !data?.isConnected) {
 301                errorEl.textContent = err.length > 120 ? err.slice(0, 120) + '…' : err;
 302                errorEl.title = err;
 303                errorEl.style.display = '';
 304            } else {
 305                errorEl.textContent = '';
 306                errorEl.style.display = 'none';
 307            }
 308        }
 309
 310        if (checkedAtEl) {
 311            checkedAtEl.textContent = formatCheckedAt(data?.checkedAtUtc);
 312        }
 313    }
 314
 315    async function refreshImapStatus() {
 316        if (inFlight) return;
 317        inFlight = true;
 318        if (liveIndicator) liveIndicator.classList.remove('d-none');
 319        if (refreshBtn) refreshBtn.disabled = true;
 320
 321        try {
 322            const resp = await fetch(statusUrl, {
 323                headers: { 'Accept': 'application/json' },
 324                cache: 'no-store'
 325            });
 326            if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
 327            const data = await resp.json();
 328            renderImapStatus(data);
 329        } catch (e) {
 330            console.warn('IMAP status refresh failed', e);
 331        } finally {
 332            inFlight = false;
 333            if (liveIndicator) liveIndicator.classList.add('d-none');
 334            if (refreshBtn) refreshBtn.disabled = false;
 335        }
 336    }
 337
 338    function startPolling() {
 339        if (pollTimer) clearInterval(pollTimer);
 340        pollTimer = setInterval(refreshImapStatus, pollMs);
 341    }
 342
 343    function stopPolling() {
 344        if (pollTimer) {
 345            clearInterval(pollTimer);
 346            pollTimer = null;
 347        }
 348    }
 349
 350    if (refreshBtn) {
 351        refreshBtn.addEventListener('click', refreshImapStatus);
 352    }
 353
 354    document.addEventListener('visibilitychange', function () {
 355        if (document.hidden) {
 356            stopPolling();
 357        } else {
 358            refreshImapStatus();
 359            startPolling();
 360        }
 361    });
 362
 363    // Карточка рендерится сервером пустой (страница больше не ждёт живое IMAP-подключение) —
 364    // сразу подтягиваем реальный статус, дальше по таймеру.
 365    refreshImapStatus();
 366    startPolling();
 367
 368    // --- Настройки IMAP (Admin) ---
 369    const settingsPanel = document.getElementById('imapSettingsPanel');
 370    const settingsToggleBtn = document.getElementById('imapSettingsToggleBtn');
 371    const testSettingsBtn = document.getElementById('imapTestSettingsBtn');
 372    const saveSettingsBtn = document.getElementById('imapSaveSettingsBtn');
 373    const settingsFeedback = document.getElementById('imapSettingsFeedback');
 0374    const testUrl = '@Url.Action("TestImapSettings", "EmailRoutingResults", new { area = "Studio2" })';
 0375    const saveUrl = '@Url.Action("SaveImapSettings", "EmailRoutingResults", new { area = "Studio2" })';
 376
 377    function getAntiforgeryToken() {
 378        return document.querySelector('input[name="__RequestVerificationToken"]')?.value || '';
 379    }
 380
 381    function readSettingsForm() {
 382        return {
 383            host: document.getElementById('imapEditHost')?.value?.trim() || '',
 384            port: parseInt(document.getElementById('imapEditPort')?.value || '993', 10),
 385            userName: document.getElementById('imapEditUserName')?.value?.trim() || '',
 386            password: document.getElementById('imapEditPassword')?.value || '',
 387            useSsl: document.getElementById('imapEditUseSsl')?.checked ?? true,
 388            folder: document.getElementById('imapEditFolder')?.value?.trim() || 'INBOX',
 389            fetchLimit: parseInt(document.getElementById('imapEditFetchLimit')?.value || '20', 10)
 390        };
 391    }
 392
 393    function showSettingsFeedback(type, message) {
 394        if (!settingsFeedback) return;
 395        settingsFeedback.className = `alert alert-${type} mt-3 mb-0`;
 396        settingsFeedback.textContent = message;
 397        settingsFeedback.classList.remove('d-none');
 398    }
 399
 400    async function postImapSettings(url) {
 401        const token = getAntiforgeryToken();
 402        const resp = await fetch(url, {
 403            method: 'POST',
 404            headers: {
 405                'Content-Type': 'application/json',
 406                'Accept': 'application/json',
 407                'RequestVerificationToken': token
 408            },
 409            body: JSON.stringify(readSettingsForm())
 410        });
 411        const data = await resp.json().catch(() => ({}));
 412        if (!resp.ok) {
 413            throw new Error(data.error || `HTTP ${resp.status}`);
 414        }
 415        return data;
 416    }
 417
 418    if (settingsToggleBtn && settingsPanel) {
 419        settingsToggleBtn.addEventListener('click', function () {
 420            settingsPanel.classList.toggle('d-none');
 421        });
 422    }
 423
 424    if (testSettingsBtn) {
 425        testSettingsBtn.addEventListener('click', async function () {
 426            testSettingsBtn.disabled = true;
 427            saveSettingsBtn.disabled = true;
 428            showSettingsFeedback('info', 'Проверка подключения…');
 429            try {
 430                const data = await postImapSettings(testUrl);
 431                renderImapStatus(data);
 432                if (data.isConnected) {
 433                    showSettingsFeedback('success', `Подключение успешно. Непрочитанных: ${data.unseenCount ?? 0}, в окн
 434                } else {
 435                    showSettingsFeedback('danger', data.errorMessage || 'Не удалось подключиться.');
 436                }
 437            } catch (e) {
 438                showSettingsFeedback('danger', e.message || 'Ошибка проверки.');
 439            } finally {
 440                testSettingsBtn.disabled = false;
 441                saveSettingsBtn.disabled = false;
 442            }
 443        });
 444    }
 445
 446    if (saveSettingsBtn) {
 447        saveSettingsBtn.addEventListener('click', async function () {
 448            testSettingsBtn.disabled = true;
 449            saveSettingsBtn.disabled = true;
 450            showSettingsFeedback('info', 'Сохранение…');
 451            try {
 452                const data = await postImapSettings(saveUrl);
 453                if (data.status) {
 454                    renderImapStatus(data.status);
 455                }
 456                document.getElementById('imapEditPassword').value = '';
 457                showSettingsFeedback('success', data.message || 'Настройки сохранены.');
 458            } catch (e) {
 459                showSettingsFeedback('danger', e.message || 'Ошибка сохранения.');
 460            } finally {
 461                testSettingsBtn.disabled = false;
 462                saveSettingsBtn.disabled = false;
 463            }
 464        });
 465    }
 466})();
 467</script>
 468}