< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_AssemblyList_Index
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/AssemblyList/Index.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 55
Coverable lines: 55
Total lines: 629
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 84
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%6806820%
<ExecuteAsync()100%210%
<ExecuteAsync()0%2040%

File(s)

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

#LineLine coverage
 1@using OzonOrdersWeb.Areas.Studio2.ViewModels.AssemblyList
 2@model AssemblyListIndexViewModel
 3@{
 04    Layout = "~/Views/Shared/_Layout.cshtml";
 05    ViewData["Title"] = "Лист подбора FBS";
 6}
 7
 8<link rel="stylesheet" href="~/css/Studio2/OzonClients/ozon-clients-form.css" asp-append-version="true" />
 9
 10@section HeaderActions {
 11<div class="header-page-actions">
 12    <span class="header-page-actions-sep"></span>
 13    <div class="btn-group">
 14        <button type="button" class="hpa-btn" onclick="startGenerate()">
 15            <i class="fas fa-play"></i><span class="hpa-label">Сформировать</span>
 16        </button>
 17    </div>
 18    <div class="btn-group">
 19        <a href="/Studio2/AssemblyList/History" class="hpa-btn">
 20            <i class="fas fa-history"></i><span class="hpa-label">История</span>
 21        </a>
 22    </div>
 23    <div class="btn-group">
 24        <a href="/Studio2/AssemblyListAccessKeys" class="hpa-btn">
 25            <i class="fas fa-key text-warning"></i><span class="hpa-label">Ключи доступа</span>
 26        </a>
 27    </div>
 28    <div class="btn-group" id="download-actions" style="display:none;">
 29        <button type="button" class="hpa-btn" onclick="downloadFile('excel')">
 30            <i class="fas fa-file-excel text-success"></i><span class="hpa-label">Excel</span>
 31        </button>
 32        <button type="button" class="hpa-btn" onclick="downloadFile('pdf')">
 33            <i class="fas fa-file-pdf text-danger"></i><span class="hpa-label">PDF</span>
 34        </button>
 35        <button type="button" class="hpa-btn" onclick="openAllLabels()">
 36            <i class="fas fa-tags text-warning"></i><span class="hpa-label">Этикетки</span>
 37        </button>
 38    </div>
 39</div>
 40}
 41
 42<div class="container-fluid py-3">
 43
 44    <!-- Фильтры -->
 45    <div class="oc-form-card mb-3">
 46        <div class="oc-form-card-header">
 47            <h2><i class="fas fa-sliders-h me-2 text-muted"></i>Параметры генерации</h2>
 48        </div>
 49        <div class="oc-form-grid" style="grid-template-columns: 1fr 1fr 160px 160px auto;">
 50            <div>
 51                <label class="form-label">Клиент Ozon</label>
 52                <select id="filterClient" class="form-select form-select-sm custom-select-init">
 53                    <option value="">— выберите клиента —</option>
 054                    @foreach (var c in Model.Clients)
 55                    {
 056                        <option value="@c.Id">@c.Name</option>
 57                    }
 58                </select>
 59            </div>
 60            <div>
 61                <label class="form-label">Статус</label>
 62                <select id="filterStatus" class="form-select form-select-sm custom-select-init">
 63                    <option value="awaiting_deliver" selected>Ожидает отгрузки</option>
 64                    <option value="awaiting_packaging">Ожидает упаковки</option>
 65                    <option value="awaiting_registration">Ожидает регистрации</option>
 66                    <option value="acceptance_in_progress">Идёт приёмка</option>
 67                    <option value="awaiting_approve">Ожидает подтверждения</option>
 68                    <option value="arbitration">Арбитраж</option>
 69                    <option value="client_arbitration">Клиентский арбитраж</option>
 70                    <option value="delivering">Доставляется</option>
 71                    <option value="driver_pickup">У водителя</option>
 72                    <option value="cancelled">Отменено</option>
 73                    <option value="not_accepted">Не принято на СЦ</option>
 74                </select>
 75            </div>
 76            <div>
 77                <label class="form-label">Дата С</label>
 78                <input type="date" id="filterDateFrom" class="form-control form-control-sm"
 79                       value="@DateTime.Today.ToString("yyyy-MM-dd")">
 80            </div>
 81            <div>
 82                <label class="form-label">Дата По</label>
 83                <input type="date" id="filterDateTo" class="form-control form-control-sm"
 84                       value="@DateTime.Today.AddDays(7).ToString("yyyy-MM-dd")">
 85            </div>
 86            <div class="d-flex align-items-end">
 87                <button class="btn btn-primary btn-sm w-100" onclick="startGenerate()">
 88                    <i class="fas fa-play me-1"></i>Сформировать
 89                </button>
 90            </div>
 91        </div>
 92    </div>
 93
 94    <!-- Прогресс -->
 95    <div id="progressSection" style="display:none;" class="mb-3">
 96        <div class="oc-form-card">
 97            <div class="oc-form-card-header">
 98                <h2><i class="fas fa-cogs me-2 text-muted"></i>Формирование листа подбора</h2>
 99            </div>
 100            <div class="assembly-steps">
 0101                @for (int i = 1; i <= 5; i++)
 102                {
 0103                    var icons = new[] { "fa-download", "fa-images", "fa-tags", "fa-database", "fa-table" };
 0104                    var labels = new[] { "Получение отправлений", "Загрузка товаров", "Генерация этикеток", "Остатки 1С"
 105                    <div class="assembly-step" id="step-@i">
 106                        <div class="step-icon">
 107                            <i class="fas @icons[i-1]" id="step-icon-@i"></i>
 108                            <span class="step-spinner spinner-border spinner-border-sm" style="display:none;"></span>
 109                        </div>
 110                        <div class="step-content">
 0111                            <div class="step-title">Шаг @i: @labels[i-1]</div>
 112                            <div class="step-msg text-muted small" id="step-msg-@i"></div>
 113                        </div>
 114                        <div class="step-status" id="step-status-@i">
 115                            <i class="fas fa-circle text-secondary opacity-25"></i>
 116                        </div>
 117                    </div>
 118                }
 119            </div>
 120            <div id="progressError" class="alert alert-danger mt-3" style="display:none;"></div>
 121        </div>
 122    </div>
 123
 124    <!-- Результат -->
 0125    <div id="resultSection" @(Model.LastRun != null ? "" : "style=\"display:none;\"")>
 126        <div class="oc-form-card p-0">
 127            <div class="oc-form-card-header px-3 pt-3 pb-2">
 128                <div>
 129                    <h2 style="margin:0;">
 130                        <i class="fas fa-list-alt text-muted me-2"></i>Лист подбора
 131                        <span id="resultBadge" class="badge bg-primary ms-2" style="font-size:0.75rem;">
 0132                            @(Model.LastRun != null ? $"{Model.LastRun.TotalPostings} отправлений" : "")
 133                        </span>
 134                    </h2>
 135                    <div class="oc-field-hint mt-1" id="resultMeta">
 0136                        @if (Model.LastRun != null)
 137                        {
 0138                            @($"Сформировано: {Model.LastRun.StartedAtUtc.ToLocalTime():dd.MM.yyyy HH:mm} · {Model.LastR
 139                        }
 140                    </div>
 141                </div>
 142                <div class="d-flex gap-2">
 143                    <button class="btn btn-sm btn-outline-success" id="btnExcel" onclick="downloadFile('excel')">
 144                        <i class="fas fa-file-excel me-1"></i>Excel
 145                    </button>
 146                    <button class="btn btn-sm btn-outline-danger" id="btnPdf" onclick="downloadFile('pdf')">
 147                        <i class="fas fa-file-pdf me-1"></i>PDF
 148                    </button>
 149                </div>
 150            </div>
 151            <div class="p-0">
 152                <div class="table-responsive">
 153                    <table class="table table-sm table-hover align-middle mb-0" id="assemblyTable">
 154                        <thead class="table-light">
 155                            <tr>
 156                                <th class="text-center" style="width:40px;">№</th>
 157                                <th>Отправление</th>
 158                                <th>Артикул</th>
 159                                <th>Название</th>
 160                                <th>Склад</th>
 161                                <th>Статус</th>
 162                                <th class="text-center" style="width:60px;">Кол-во</th>
 163                                <th>Поставщик</th>
 164                                <th>Заказан поставщику</th>
 165                                <th class="text-center text-nowrap">Остаток 1С</th>
 166                                <th>Документ 1С</th>
 167                                <th>Этикетка</th>
 168                            </tr>
 169                        </thead>
 170                        <tbody id="assemblyTableBody">
 0171                            @if (Model.LastRun != null && !Model.LastRunItems.Any())
 172                            {
 173                                // пусто — JS заполнит после генерации
 174                            }
 0175                            @if (Model.LastRun != null)
 176                            {
 0177                                string lastPosting = "";
 0178                                foreach (var item in Model.LastRunItems)
 179                                {
 0180                                    var borderTop = item.PostingNumber != lastPosting ? "border-top: 2px solid #dee2e6;"
 0181                                    lastPosting = item.PostingNumber;
 0182                                    var statusClass = item.Status switch
 0183                                    {
 0184                                        "awaiting_deliver"   => "bg-primary",
 0185                                        "awaiting_packaging" => "bg-warning text-dark",
 0186                                        "delivering"         => "bg-info text-dark",
 0187                                        "cancelled"          => "bg-danger",
 0188                                        "not_accepted"       => "bg-danger",
 0189                                        _                    => "bg-secondary",
 0190                                    };
 0191                                    var ssrMovArticle2 = item.OfferId.Contains('=')
 0192                                        ? item.OfferId[..item.OfferId.IndexOf('=')]
 0193                                        : item.OfferId;
 0194                                    var ssrMovUrl = $"/Studio2/WarehouseStock/movement-map?article={Uri.EscapeDataString
 195                                    <tr style="@borderTop">
 0196                                        <td class="text-center text-muted small">@item.ItemIndex</td>
 0197                                        <td class="text-nowrap"><code class="small copy-value" onclick="copyArticle(this
 0198                                        <td><code class="small copy-value" onclick="copyArticle(this)" role="button" tit
 199                                        <td class="small">
 200                                            <div class="d-flex align-items-center gap-2">
 0201                                                @if (!string.IsNullOrEmpty(item.ImageUrl))
 202                                                {
 203                                                    <img src="/Studio2/AssemblyList/ImageProxy?url=@Uri.EscapeDataString
 204                                                         class="assembly-table-img flex-shrink-0" loading="lazy"
 205                                                         onerror="this.parentNode.innerHTML=''" />
 206                                                }
 0207                                                <span>@item.ProductName</span>
 208                                            </div>
 209                                        </td>
 0210                                        <td class="small text-nowrap">@item.WarehouseName</td>
 0211                                        <td><span class="badge @statusClass">@item.StatusLabel</span></td>
 0212                                        <td class="text-center fw-bold">@item.Quantity</td>
 0213                                        <td class="small text-nowrap">@(item.SupplierName ?? "—")</td>
 0214                                        <td class="small text-nowrap">@(item.OrderedToSupplierDate.HasValue ? item.Order
 215                                        <td class="text-center small text-nowrap">
 216                                            <a href="@ssrMovUrl" target="_blank" class="text-decoration-none d-inline-bl
 0217                                                @if (item.OneCBalance.HasValue)
 218                                                {
 0219                                                    if (item.OneCBalance.Value > 0)
 220                                                    {
 0221                                                        <span class="text-success fw-bold">@item.OneCBalance.Value.ToStr
 222                                                    }
 223                                                    else
 224                                                    {
 225                                                        <span class="text-danger fw-bold">0</span>
 0226                                                        @if (!string.IsNullOrEmpty(item.OneCOtherStock))
 227                                                        {
 0228                                                            foreach (var stockPart in item.OneCOtherStock.Split(',', Str
 229                                                            {
 0230                                                                <br /><span class="text-muted text-nowrap" style="font-s
 231                                                            }
 232                                                        }
 233                                                    }
 234                                                }
 235                                                else
 236                                                {
 237                                                    <span class="text-muted">—</span>
 238                                                }
 239                                            </a>
 240                                        </td>
 241                                        <td class="small text-nowrap">
 0242                                            @if (item.OneCReceiptDocKey.HasValue && !string.IsNullOrEmpty(item.OneCRecei
 243                                            {
 244                                                <a href="/Studio2/OneCDocument/Details?refKey=@item.OneCReceiptDocKey.Va
 0245                                                    @item.OneCReceiptDoc
 246                                                </a>
 247                                            }
 248                                            else
 249                                            {
 0250                                                @(item.OneCReceiptDoc ?? "—")
 251                                            }
 252                                        </td>
 253                                        <td style="width:70px;">
 0254                                            @if (!string.IsNullOrEmpty(item.LabelUrl))
 255                                            {
 0256                                                var proxyUrl2 = "/Studio2/AssemblyList/LabelProxy?url=" + Uri.EscapeData
 257                                                <a href="@proxyUrl2" download class="label-thumb-link" title="Скачать эт
 0258                                                    <canvas class="label-thumb" data-pdf="@proxyUrl2"></canvas>
 259                                                </a>
 260                                            }
 0261                                            else if (!string.IsNullOrEmpty(item.LabelError))
 262                                            {
 263                                                <span class="text-muted small" title="@item.LabelError"><i class="fas fa
 264                                            }
 265                                            else
 266                                            {
 267                                                <span class="text-muted">—</span>
 268                                            }
 269                                        </td>
 270                                    </tr>
 271                                }
 272                            }
 273                        </tbody>
 274                    </table>
 275                </div>
 276            </div>
 277        </div>
 278    </div>
 279</div>
 280
 281
 282@section Scripts {
 283<style>
 284.assembly-steps { display: flex; flex-direction: column; gap: 12px; }
 285.assembly-step {
 286    display: flex;
 287    align-items: center;
 288    gap: 14px;
 289    padding: 10px 14px;
 290    border-radius: 8px;
 291    background: #f8f9fa;
 292    transition: background 0.2s;
 293}
 294.assembly-step.active { background: #e8f4fd; }
 295.assembly-step.done { background: #eafaf1; }
 296.assembly-step.error { background: #fdf2f2; }
 297.step-icon {
 298    width: 36px; height: 36px;
 299    border-radius: 50%;
 300    background: #dee2e6;
 301    display: flex; align-items: center; justify-content: center;
 302    font-size: 14px; color: #6c757d;
 303    flex-shrink: 0;
 304}
 305.assembly-step.active .step-icon { background: #0d6efd; color: #fff; }
 306.assembly-step.done .step-icon { background: #198754; color: #fff; }
 307.assembly-step.error .step-icon { background: #dc3545; color: #fff; }
 308.step-content { flex: 1; }
 309.step-title { font-weight: 600; font-size: 14px; }
 310.assembly-table-img { width: 48px; height: 48px; object-fit: contain; border-radius: 4px; border: 1px solid #dee2e6; bac
 311.label-thumb { display: block; max-width: 72px; max-height: 108px; width: auto; height: auto; border: 1px solid #dee2e6;
 312.label-thumb-link:hover .label-thumb { box-shadow: 0 0 0 2px #ffc107; }
 313.copy-value { cursor: pointer; }
 314.copy-value:hover { background: #fff3cd; border-radius: 3px; }
 315.copy-value.copied { background: #d1e7dd; border-radius: 3px; }
 316</style>
 317
 318<script src="~/js/copyArticle.js" asp-append-version="true"></script>
 319<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js" crossorigin="anonymous"></script>
 320<script>
 321let currentJobId = null;
 322let currentResult = null;
 0323let lastRunId = @(Model.LastRun != null ? Model.LastRun.Id.ToString() : "null");
 324
 325function startGenerate() {
 326    const clientId = parseInt(document.getElementById('filterClient').value);
 327    if (!clientId) { alert('Выберите клиента'); return; }
 328
 329    const status = document.getElementById('filterStatus').value;
 330    const dateFrom = document.getElementById('filterDateFrom').value;
 331    const dateTo = document.getElementById('filterDateTo').value;
 332
 333    document.getElementById('progressSection').style.display = '';
 334    document.getElementById('resultSection').style.display = 'none';
 335    document.getElementById('download-actions').style.display = 'none';
 336    document.getElementById('progressError').style.display = 'none';
 337    resetSteps();
 338
 339    PageLoadingBar.show();
 340
 341    fetch('/Studio2/AssemblyList/Generate', {
 342        method: 'POST',
 343        headers: { 'Content-Type': 'application/json' },
 344        body: JSON.stringify({
 345            ozonClientId: clientId,
 346            status: status,
 347            cutoffFrom: dateFrom ? dateFrom + 'T00:00:00' : null,
 348            cutoffTo: dateTo ? dateTo + 'T23:59:59' : null
 349        })
 350    })
 351    .then(function(r) {
 352        if (!r.ok) return r.json().then(function(d) { throw new Error(d.error || 'Ошибка сервера ' + r.status); });
 353        return r.json();
 354    })
 355    .then(function(data) {
 356        if (data.error) { showError(data.error); return; }
 357        currentJobId = data.jobId;
 358        if (data.dbRunId) lastRunId = data.dbRunId;
 359        listenProgress(data.jobId);
 360    })
 361    .catch(function(err) { showError(err.message); });
 362}
 363
 364function listenProgress(jobId) {
 365    const es = new EventSource('/Studio2/AssemblyList/Progress?jobId=' + jobId);
 366    es.addEventListener('progress', function(e) {
 367        const p = JSON.parse(e.data);
 368        updateStep(p);
 369        if (p.isComplete) {
 370            es.close();
 371            PageLoadingBar.hide();
 372            loadResult(jobId);
 373        }
 374        if (p.isError) {
 375            es.close();
 376            PageLoadingBar.hide();
 377            showError(p.errorMessage || p.message || 'Ошибка');
 378        }
 379    });
 380    es.onerror = function() {
 381        es.close();
 382        PageLoadingBar.hide();
 383    };
 384}
 385
 386function updateStep(p) {
 387    if (!p.step) return;
 388    for (let i = 1; i <= 5; i++) {
 389        const el = document.getElementById('step-' + i);
 390        const spinner = el ? el.querySelector('.step-spinner') : null;
 391        const icon = document.getElementById('step-icon-' + i);
 392        const statusEl = document.getElementById('step-status-' + i);
 393        const msgEl = document.getElementById('step-msg-' + i);
 394        if (!el) continue;
 395
 396        if (i < p.step) {
 397            el.className = 'assembly-step done';
 398            if (spinner) spinner.style.display = 'none';
 399            if (icon) icon.style.display = '';
 400            if (statusEl) statusEl.innerHTML = '<i class="fas fa-check-circle text-success"></i>';
 401        } else if (i === p.step) {
 402            el.className = 'assembly-step active';
 403            if (spinner) spinner.style.display = '';
 404            if (icon) icon.style.display = 'none';
 405            if (statusEl) statusEl.innerHTML = '<i class="fas fa-circle-notch fa-spin text-primary"></i>';
 406            if (msgEl) msgEl.textContent = p.message || '';
 407        }
 408    }
 409    if (p.isComplete) {
 410        for (let i = 1; i <= 5; i++) {
 411            const el = document.getElementById('step-' + i);
 412            if (el) el.className = 'assembly-step done';
 413            const statusEl = document.getElementById('step-status-' + i);
 414            if (statusEl) statusEl.innerHTML = '<i class="fas fa-check-circle text-success"></i>';
 415            const spinner = el ? el.querySelector('.step-spinner') : null;
 416            if (spinner) spinner.style.display = 'none';
 417            const icon = document.getElementById('step-icon-' + i);
 418            if (icon) icon.style.display = '';
 419        }
 420        const msgEl = document.getElementById('step-msg-5');
 421        if (msgEl) msgEl.textContent = p.message || '';
 422    }
 423}
 424
 425function loadResult(jobId) {
 426    fetch('/Studio2/AssemblyList/Result?jobId=' + jobId)
 427        .then(function(r) { return r.json(); })
 428        .then(function(data) {
 429            currentResult = data;
 430            renderTable(data);
 431            document.getElementById('resultSection').style.display = '';
 432            document.getElementById('download-actions').style.display = '';
 433            document.getElementById('resultBadge').textContent = data.totalPostings + ' отправлений';
 434            document.getElementById('resultMeta').textContent =
 435                'Сформировано: ' + new Date(data.generatedAt).toLocaleString('ru-RU') +
 436                ' · ' + data.items.length + ' строк';
 437        })
 438        .catch(function(err) { showError(err.message); });
 439}
 440
 441function renderTable(data) {
 442    const tbody = document.getElementById('assemblyTableBody');
 443    tbody.innerHTML = '';
 444    let lastPosting = '';
 445    data.items.forEach(function(item) {
 446        const tr = document.createElement('tr');
 447        if (item.postingNumber !== lastPosting) {
 448            tr.style.borderTop = '2px solid #dee2e6';
 449            lastPosting = item.postingNumber;
 450        }
 451
 452        const labelProxyUrl = item.labelUrl
 453            ? '/Studio2/AssemblyList/LabelProxy?url=' + encodeURIComponent(item.labelUrl)
 454            : null;
 455        const labelHtml = labelProxyUrl
 456            ? '<a href="' + labelProxyUrl + '" download class="label-thumb-link" title="Скачать этикетку"><canvas class=
 457            : (item.labelError ? '<span class="text-muted small" title="' + escHtml(item.labelError) + '"><i class="fas 
 458
 459        const imgSrc = item.imageUrl
 460            ? '/Studio2/AssemblyList/ImageProxy?url=' + encodeURIComponent(item.imageUrl)
 461            : null;
 462        const imgHtml = imgSrc
 463            ? '<img src="' + imgSrc + '" class="assembly-table-img" loading="lazy" onerror="this.parentNode.innerHTML=\'
 464            : '<span class="text-muted"><i class="fas fa-image"></i></span>';
 465
 466        const statusBadge = '<span class="badge ' + statusBadgeClass(item.status) + '">' + escHtml(item.statusLabel) + '
 467
 468        const orderedDate = item.orderedToSupplierDate
 469            ? new Date(item.orderedToSupplierDate).toLocaleString('ru-RU', {day:'2-digit',month:'2-digit',year:'numeric'
 470            : '—';
 471
 472        const movementArticle = item.offerId ? item.offerId.split('=')[0] : '';
 473        const movUrl = movementArticle
 474            ? '/Studio2/WarehouseStock/movement-map?article=' + encodeURIComponent(movementArticle)
 475            : null;
 476
 477        let balanceInner;
 478        if (item.oneCBalance != null) {
 479            balanceInner = '<span class="' + (item.oneCBalance > 0 ? 'text-success' : 'text-danger') + ' fw-bold">' + it
 480            if (item.oneCBalance === 0 && item.oneCOtherStock) {
 481                item.oneCOtherStock.split(',').forEach(function(part) {
 482                    part = part.trim();
 483                    if (part) balanceInner += '<br><span class="text-muted text-nowrap" style="font-size:0.8em;">' + esc
 484                });
 485            }
 486        } else {
 487            balanceInner = '<span class="text-muted">—</span>';
 488        }
 489        const balanceHtml = movUrl
 490            ? '<a href="' + movUrl + '" target="_blank" class="text-decoration-none d-inline-block">' + balanceInner + '
 491            : balanceInner;
 492
 493        const nameHtml = '<div class="d-flex align-items-center gap-2">' +
 494            (imgSrc ? '<img src="' + imgSrc + '" class="assembly-table-img flex-shrink-0" loading="lazy" onerror="this.p
 495            '<span>' + escHtml(item.productName) + '</span></div>';
 496
 497        tr.innerHTML =
 498            '<td class="text-center text-muted small">' + item.index + '</td>' +
 499            '<td class="text-nowrap"><code class="small copy-value" onclick="copyArticle(this)" role="button" title="Наж
 500            '<td><code class="small copy-value" onclick="copyArticle(this)" role="button" title="Нажмите, чтобы скопиров
 501            '<td class="small">' + nameHtml + '</td>' +
 502            '<td class="small text-nowrap">' + escHtml(item.warehouseName) + '</td>' +
 503            '<td>' + statusBadge + '</td>' +
 504            '<td class="text-center fw-bold">' + item.quantity + '</td>' +
 505            '<td class="small text-nowrap">' + escHtml(item.supplierName || '—') + '</td>' +
 506            '<td class="small text-nowrap">' + orderedDate + '</td>' +
 507            '<td class="text-center small text-nowrap">' + balanceHtml + '</td>' +
 508            '<td class="small text-nowrap">' + (item.oneCReceiptDocKey && item.oneCReceiptDoc ? '<a href="/Studio2/OneCD
 509            '<td>' + labelHtml + '</td>';
 510        tbody.appendChild(tr);
 511        if (labelProxyUrl) renderLabelThumb(tr.querySelector('canvas.label-thumb'));
 512    });
 513}
 514
 515function renderLabelThumb(canvas) {
 516    if (!canvas || !window.pdfjsLib) return;
 517    pdfjsLib.getDocument(canvas.dataset.pdf).promise.then(function(pdf) {
 518        return pdf.getPage(1);
 519    }).then(function(page) {
 520        var vp = page.getViewport({ scale: 1 });
 521        var scale = Math.min(72 / vp.width, 108 / vp.height);
 522        var viewport = page.getViewport({ scale: scale });
 523        canvas.width = viewport.width;
 524        canvas.height = viewport.height;
 525        page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport });
 526    }).catch(function() {
 527        canvas.width = 58; canvas.height = 20;
 528        var ctx = canvas.getContext('2d');
 529        ctx.fillStyle = '#fff3cd';
 530        ctx.fillRect(0, 0, 58, 20);
 531        ctx.fillStyle = '#856404';
 532        ctx.font = '10px sans-serif';
 533        ctx.textAlign = 'center';
 534        ctx.fillText('PDF', 29, 14);
 535    });
 536}
 537
 538function statusBadgeClass(status) {
 539    const map = {
 540        'awaiting_deliver': 'bg-primary',
 541        'awaiting_packaging': 'bg-warning text-dark',
 542        'awaiting_registration': 'bg-secondary',
 543        'delivering': 'bg-info text-dark',
 544        'cancelled': 'bg-danger',
 545        'not_accepted': 'bg-danger',
 546    };
 547    return map[status] || 'bg-secondary';
 548}
 549
 550function downloadFile(type) {
 551    if (currentJobId) {
 552        // Свежая генерация — данные в кэше
 553        if (type === 'excel')
 554            window.location.href = '/Studio2/AssemblyList/DownloadExcel?jobId=' + currentJobId;
 555        else
 556            window.location.href = '/Studio2/AssemblyList/DownloadPdf?jobId=' + currentJobId;
 557    } else if (lastRunId) {
 558        // Данные из БД (последний сохранённый запуск)
 559        if (type === 'excel')
 560            window.location.href = '/Studio2/AssemblyList/DownloadHistoryExcel?runId=' + lastRunId;
 561        else
 562            window.location.href = '/Studio2/AssemblyList/DownloadHistoryPdf?runId=' + lastRunId;
 563    }
 564}
 565
 566function openAllLabels() {
 567    const links = document.querySelectorAll('#assemblyTableBody a[target="_blank"]');
 568    const urls = [...new Set([...links].map(function(a) { return a.href; }))];
 569    if (!urls.length) { alert('Этикетки не сгенерированы'); return; }
 570    urls.forEach(function(url) { window.open(url, '_blank'); });
 571}
 572
 573function resetSteps() {
 574    for (let i = 1; i <= 5; i++) {
 575        const el = document.getElementById('step-' + i);
 576        if (el) el.className = 'assembly-step';
 577        const statusEl = document.getElementById('step-status-' + i);
 578        if (statusEl) statusEl.innerHTML = '<i class="fas fa-circle text-secondary opacity-25"></i>';
 579        const msgEl = document.getElementById('step-msg-' + i);
 580        if (msgEl) msgEl.textContent = '';
 581        const spinner = el ? el.querySelector('.step-spinner') : null;
 582        if (spinner) spinner.style.display = 'none';
 583        const icon = document.getElementById('step-icon-' + i);
 584        if (icon) icon.style.display = '';
 585    }
 586}
 587
 588function showError(msg) {
 589    const el = document.getElementById('progressError');
 590    el.innerHTML = '<i class="fas fa-exclamation-circle me-2"></i>' + escHtml(msg);
 591    el.style.display = '';
 592    PageLoadingBar.hide();
 593    for (let i = 1; i <= 4; i++) {
 594        const stepEl = document.getElementById('step-' + i);
 595        if (stepEl && stepEl.classList.contains('active')) stepEl.className = 'assembly-step error';
 596    }
 597}
 598
 599function escHtml(str) {
 600    if (!str) return '';
 601    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 602}
 603
 604function showNotification(message, type) {
 605    type = type || 'success';
 606    var container = document.getElementById('notification-container');
 607    if (!container) {
 608        container = document.createElement('div');
 609        container.id = 'notification-container';
 610        container.style.cssText = 'position:fixed;top:20px;right:20px;z-index:99999;display:flex;flex-direction:column;g
 611        document.body.appendChild(container);
 612    }
 613    var cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-danger' : 'alert-warning';
 614    var toast = document.createElement('div');
 615    toast.className = 'alert ' + cls + ' alert-dismissible fade show';
 616    toast.style.cssText = 'min-width:220px;max-width:350px;box-shadow:0 0.5rem 1rem rgba(0,0,0,.15);';
 617    toast.innerHTML = message + '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
 618    container.appendChild(toast);
 619    var bsAlert = new bootstrap.Alert(toast);
 620    setTimeout(function() { if (toast.parentNode) bsAlert.close(); }, 2000);
 621}
 622
 623// PDF.js init + SSR thumbnails
 624if (window.pdfjsLib) {
 625    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
 626    document.querySelectorAll('canvas.label-thumb').forEach(renderLabelThumb);
 627}
 628</script>
 629}