| | | 1 | | @{ |
| | 0 | 2 | | Layout = null; |
| | 0 | 3 | | var accessKey = (string)ViewBag.AccessKey; |
| | 0 | 4 | | var baseUrl = $"/public/assembly/{accessKey}"; |
| | 0 | 5 | | var periodFrom = (string)ViewBag.From!; |
| | 0 | 6 | | var periodTo = (string)ViewBag.To!; |
| | | 7 | | } |
| | | 8 | | <!DOCTYPE html> |
| | | 9 | | <html lang="ru"> |
| | | 10 | | <head> |
| | | 11 | | <meta charset="utf-8" /> |
| | | 12 | | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| | 0 | 13 | | <title>@Html.Raw(OgHtml.Attribute((string)ViewBag.OgTitle))</title> |
| | | 14 | | <meta name="description" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgDescription))" /> |
| | | 15 | | <link rel="icon" type="image/svg+xml" href="~/favicon.svg" asp-append-version="true" /> |
| | | 16 | | <link rel="apple-touch-icon" href="~/favicon.svg" asp-append-version="true" /> |
| | | 17 | | |
| | | 18 | | <meta property="og:type" content="website" /> |
| | | 19 | | <meta property="og:site_name" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgSiteName))" /> |
| | | 20 | | <meta property="og:locale" content="ru_RU" /> |
| | | 21 | | <meta property="og:title" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgTitle))" /> |
| | | 22 | | <meta property="og:description" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgDescription))" /> |
| | | 23 | | <meta property="og:url" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgUrl))" /> |
| | | 24 | | <meta property="og:image" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgImageUrl))" /> |
| | | 25 | | <meta property="og:image:secure_url" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgImageUrl))" /> |
| | | 26 | | <meta property="og:image:type" content="image/jpeg" /> |
| | | 27 | | <meta property="og:image:width" content="1200" /> |
| | | 28 | | <meta property="og:image:height" content="630" /> |
| | | 29 | | <meta property="og:image:alt" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgImageAlt))" /> |
| | | 30 | | |
| | | 31 | | <meta name="twitter:card" content="summary_large_image" /> |
| | | 32 | | <meta name="twitter:title" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgTitle))" /> |
| | | 33 | | <meta name="twitter:description" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgDescription))" /> |
| | | 34 | | <meta name="twitter:image" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgImageUrl))" /> |
| | | 35 | | <meta name="twitter:image:alt" content="@Html.Raw(OgHtml.Attribute((string)ViewBag.OgImageAlt))" /> |
| | | 36 | | |
| | | 37 | | <meta name="theme-color" content="#2563EB" /> |
| | | 38 | | <meta name="application-name" content="Interparts" /> |
| | | 39 | | |
| | | 40 | | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="a |
| | | 41 | | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" crossorigin= |
| | | 42 | | <link rel="stylesheet" href="~/css/design-tokens.css" asp-append-version="true" /> |
| | | 43 | | <link rel="stylesheet" href="~/css/Public/assembly-list-public.css" asp-append-version="true" /> |
| | | 44 | | </head> |
| | | 45 | | <body class="pal-page"> |
| | | 46 | | |
| | | 47 | | <div id="splash" class="pal-splash"> |
| | | 48 | | <div class="pal-splash-logo"> |
| | | 49 | | <img src="~/favicon.svg" asp-append-version="true" alt="" width="36" height="36" /> |
| | | 50 | | <span>Interparts</span> |
| | | 51 | | </div> |
| | | 52 | | <div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div> |
| | | 53 | | <div class="pal-splash-text" id="splashText">Подготавливаем лист подбора...</div> |
| | | 54 | | </div> |
| | | 55 | | |
| | | 56 | | <div class="pal-container"> |
| | | 57 | | |
| | | 58 | | <header class="pal-hero"> |
| | | 59 | | <div class="pal-hero-brand"> |
| | | 60 | | <img src="~/favicon.svg" asp-append-version="true" alt="" width="22" height="22" /> |
| | | 61 | | <span>Interparts · FBS</span> |
| | | 62 | | </div> |
| | | 63 | | <div class="pal-hero-top"> |
| | | 64 | | <div> |
| | | 65 | | <h1><i class="fas fa-list-alt text-primary me-2"></i>Лист подбора FBS</h1> |
| | | 66 | | <div class="pal-hero-subtitle" id="pageSubtitle"></div> |
| | | 67 | | </div> |
| | | 68 | | </div> |
| | | 69 | | <div class="pal-hero-meta" id="heroMeta"></div> |
| | | 70 | | </header> |
| | | 71 | | |
| | | 72 | | <div id="progressSection" class="pal-panel" style="display:none;"> |
| | | 73 | | <div class="pal-panel-header"><i class="fas fa-cogs me-2"></i>Формирование листа</div> |
| | | 74 | | <div class="pal-panel-body"> |
| | | 75 | | <div class="pal-steps"> |
| | | 76 | | <div class="pal-step" id="step-1"> |
| | | 77 | | <div class="pal-step-icon"><i class="fas fa-download" id="step-icon-1"></i><span class="spinner-bord |
| | | 78 | | <div><div class="pal-step-title">Шаг 1: Отправления</div><div class="pal-step-msg" id="step-msg-1">< |
| | | 79 | | </div> |
| | | 80 | | <div class="pal-step" id="step-2"> |
| | | 81 | | <div class="pal-step-icon"><i class="fas fa-images" id="step-icon-2"></i><span class="spinner-border |
| | | 82 | | <div><div class="pal-step-title">Шаг 2: Товары</div><div class="pal-step-msg" id="step-msg-2"></div> |
| | | 83 | | </div> |
| | | 84 | | <div class="pal-step" id="step-3"> |
| | | 85 | | <div class="pal-step-icon"><i class="fas fa-tags" id="step-icon-3"></i><span class="spinner-border s |
| | | 86 | | <div><div class="pal-step-title">Шаг 3: Этикетки</div><div class="pal-step-msg" id="step-msg-3"></di |
| | | 87 | | </div> |
| | | 88 | | <div class="pal-step" id="step-4"> |
| | | 89 | | <div class="pal-step-icon"><i class="fas fa-database" id="step-icon-4"></i><span class="spinner-bord |
| | | 90 | | <div><div class="pal-step-title">Шаг 4: Остатки 1С</div><div class="pal-step-msg" id="step-msg-4"></ |
| | | 91 | | </div> |
| | | 92 | | <div class="pal-step" id="step-5"> |
| | | 93 | | <div class="pal-step-icon"><i class="fas fa-table" id="step-icon-5"></i><span class="spinner-border |
| | | 94 | | <div><div class="pal-step-title">Шаг 5: Сборка</div><div class="pal-step-msg" id="step-msg-5"></div> |
| | | 95 | | </div> |
| | | 96 | | </div> |
| | | 97 | | <div id="progressError" class="alert alert-danger mt-3 d-none mb-0"></div> |
| | | 98 | | </div> |
| | | 99 | | </div> |
| | | 100 | | |
| | | 101 | | <div id="resultSection" class="pal-panel" style="display:none;"> |
| | | 102 | | <div class="pal-result-header"> |
| | | 103 | | <div> |
| | | 104 | | <p class="pal-result-title mb-0"><i class="fas fa-boxes-stacked text-primary me-2"></i>Позиции к подбору |
| | | 105 | | <div class="pal-result-meta" id="resultMeta"></div> |
| | | 106 | | </div> |
| | | 107 | | <div class="pal-result-actions"> |
| | | 108 | | <a id="downloadAllLabelsBtn" |
| | | 109 | | class="pal-download-all-btn d-none" |
| | | 110 | | href="#"> |
| | | 111 | | <i class="fas fa-file-pdf pal-download-all-btn-icon" aria-hidden="true"></i> |
| | | 112 | | <span class="pal-download-all-btn-text">Этикетки одним PDF</span> |
| | | 113 | | <span class="spinner-border spinner-border-sm pal-download-all-btn-spinner d-none" role="status" ari |
| | | 114 | | </a> |
| | | 115 | | <a id="exportExcelBtn" |
| | | 116 | | class="pal-download-all-btn pal-export-excel-btn d-none" |
| | | 117 | | href="#"> |
| | | 118 | | <i class="fas fa-file-excel pal-download-all-btn-icon" aria-hidden="true"></i> |
| | | 119 | | <span class="pal-download-all-btn-text">Экспорт в Excel</span> |
| | | 120 | | <span class="spinner-border spinner-border-sm pal-download-all-btn-spinner d-none" role="status" ari |
| | | 121 | | </a> |
| | | 122 | | <span class="pal-result-badge" id="resultBadge"></span> |
| | | 123 | | </div> |
| | | 124 | | </div> |
| | | 125 | | |
| | | 126 | | <div class="pal-1c-hint" id="onecHint"> |
| | | 127 | | <i class="fas fa-circle-info"></i> |
| | | 128 | | <span>Поля <strong>Отправление</strong> и <strong>Артикул</strong> копируются по клику.</span> |
| | | 129 | | </div> |
| | | 130 | | |
| | | 131 | | <div class="pal-table-wrap"> |
| | | 132 | | <table class="table pal-table mb-0" id="assemblyTable"> |
| | | 133 | | <thead> |
| | | 134 | | <tr> |
| | | 135 | | <th class="text-center" style="width:40px;">№</th> |
| | | 136 | | <th>Отправление</th> |
| | | 137 | | <th>Артикул</th> |
| | | 138 | | <th>Название</th> |
| | | 139 | | <th>Склад</th> |
| | | 140 | | <th>Статус</th> |
| | | 141 | | <th class="text-center" style="width:52px;">Кол.</th> |
| | | 142 | | <th>Поставщик</th> |
| | | 143 | | <th>Заказан</th> |
| | | 144 | | <th class="text-center">1С</th> |
| | | 145 | | <th>Приход</th> |
| | | 146 | | <th>Этикетка</th> |
| | | 147 | | </tr> |
| | | 148 | | </thead> |
| | | 149 | | <tbody id="assemblyTableBody"></tbody> |
| | | 150 | | </table> |
| | | 151 | | </div> |
| | | 152 | | |
| | | 153 | | <div class="pal-cards" id="assemblyCards" aria-label="Список позиций"></div> |
| | | 154 | | |
| | | 155 | | <div id="emptyWarehouse" class="pal-empty d-none"> |
| | | 156 | | <div><i class="fas fa-box-open d-block"></i></div> |
| | | 157 | | <div>Нет отправлений для выбранного склада.</div> |
| | | 158 | | </div> |
| | | 159 | | </div> |
| | | 160 | | |
| | | 161 | | </div> |
| | | 162 | | |
| | | 163 | | <div id="palImageLightbox" class="pal-image-lightbox d-none" role="dialog" aria-modal="true" aria-label="Просмотр изобра |
| | | 164 | | <button type="button" id="palImageLightboxClose" class="pal-image-lightbox-close" aria-label="Закрыть"> |
| | | 165 | | <i class="fas fa-times" aria-hidden="true"></i> |
| | | 166 | | </button> |
| | | 167 | | <img id="palImageLightboxImg" class="pal-image-lightbox-img" src="" alt="Изображение товара" /> |
| | | 168 | | </div> |
| | | 169 | | |
| | | 170 | | <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></scr |
| | | 171 | | <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js" crossorigin="anonymous"></script> |
| | | 172 | | <script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js" crossorigin="anonymous"></script> |
| | | 173 | | <script> |
| | | 174 | | pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; |
| | | 175 | | |
| | | 176 | | (function () { |
| | 0 | 177 | | var BASE_URL = '@Html.Raw(baseUrl)'; |
| | | 178 | | var params = new URLSearchParams(location.search); |
| | | 179 | | var clientId = params.get('clientId'); |
| | | 180 | | var status = params.get('status') || 'awaiting_deliver'; |
| | 0 | 181 | | var from = '@periodFrom'; |
| | 0 | 182 | | var to = '@periodTo'; |
| | | 183 | | var warehouse = params.get('warehouse') || ''; |
| | | 184 | | |
| | | 185 | | var splash = document.getElementById('splash'); |
| | | 186 | | var splashText = document.getElementById('splashText'); |
| | | 187 | | var heroMeta = document.getElementById('heroMeta'); |
| | | 188 | | var labelThumbDataUrlCache = new Map(); |
| | | 189 | | var labelThumbRenderPromiseCache = new Map(); |
| | | 190 | | var labelExportImageCache = new Map(); |
| | | 191 | | var labelExportRenderPromiseCache = new Map(); |
| | | 192 | | var productExportImageCache = new Map(); |
| | | 193 | | var productExportRenderPromiseCache = new Map(); |
| | | 194 | | var currentJobId = ''; |
| | | 195 | | var isDownloadingLabels = false; |
| | | 196 | | var isExportingExcel = false; |
| | | 197 | | var lastResultItems = []; |
| | | 198 | | var lastResultMeta = { generatedAt: null }; |
| | | 199 | | var imageLightbox = document.getElementById('palImageLightbox'); |
| | | 200 | | var imageLightboxImg = document.getElementById('palImageLightboxImg'); |
| | | 201 | | |
| | | 202 | | var splashPhrases = [ |
| | | 203 | | 'Подготавливаем лист подбора...', |
| | | 204 | | 'Запрашиваем данные у Ozon...', |
| | | 205 | | 'Собираем отправления...', |
| | | 206 | | 'Загружаем информацию о товарах...', |
| | | 207 | | 'Проверяем остатки на складе...', |
| | | 208 | | 'Запрашиваем этикетки...', |
| | | 209 | | 'Сверяем данные с 1С...', |
| | | 210 | | 'Ещё немного, почти готово...' |
| | | 211 | | ]; |
| | | 212 | | var splashIdx = 0; |
| | | 213 | | var splashTimer = setInterval(function () { |
| | | 214 | | splashIdx = (splashIdx + 1) % splashPhrases.length; |
| | | 215 | | if (splashText) splashText.textContent = splashPhrases[splashIdx]; |
| | | 216 | | }, 2000); |
| | | 217 | | |
| | | 218 | | function hideSplash() { |
| | | 219 | | clearInterval(splashTimer); |
| | | 220 | | splash.classList.add('hidden'); |
| | | 221 | | setTimeout(function () { splash.style.display = 'none'; }, 450); |
| | | 222 | | } |
| | | 223 | | |
| | | 224 | | function showError(msg) { |
| | | 225 | | hideSplash(); |
| | | 226 | | document.getElementById('progressSection').style.display = ''; |
| | | 227 | | document.getElementById('progressError').textContent = msg; |
| | | 228 | | document.getElementById('progressError').classList.remove('d-none'); |
| | | 229 | | } |
| | | 230 | | |
| | | 231 | | function escHtml(s) { |
| | | 232 | | if (!s) return ''; |
| | | 233 | | return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| | | 234 | | } |
| | | 235 | | |
| | | 236 | | function fmtDate(d) { |
| | | 237 | | try { return new Date(d).toLocaleDateString('ru-RU'); } catch { return d; } |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | function fmtDateTime(d) { |
| | | 241 | | try { |
| | | 242 | | return new Date(d).toLocaleString('ru-RU', { |
| | | 243 | | day: '2-digit', month: '2-digit', year: 'numeric', |
| | | 244 | | hour: '2-digit', minute: '2-digit' |
| | | 245 | | }); |
| | | 246 | | } catch { return d; } |
| | | 247 | | } |
| | | 248 | | |
| | | 249 | | function statusBadgeClass(st) { |
| | | 250 | | return { |
| | | 251 | | awaiting_deliver: 'bg-primary', |
| | | 252 | | awaiting_packaging: 'bg-warning text-dark', |
| | | 253 | | delivering: 'bg-info text-dark', |
| | | 254 | | cancelled: 'bg-danger', |
| | | 255 | | not_accepted: 'bg-danger' |
| | | 256 | | }[st] || 'bg-secondary'; |
| | | 257 | | } |
| | | 258 | | |
| | | 259 | | function copyPalValue(el) { |
| | | 260 | | var text = (el.getAttribute('data-copy') || el.textContent || '').trim(); |
| | | 261 | | if (!text) return; |
| | | 262 | | |
| | | 263 | | function onCopied() { |
| | | 264 | | el.classList.add('pal-copied'); |
| | | 265 | | setTimeout(function () { el.classList.remove('pal-copied'); }, 1200); |
| | | 266 | | var short = text.length > 36 ? text.substring(0, 36) + '…' : text; |
| | | 267 | | showPalToast('<i class="fas fa-check-circle"></i><span>Скопировано: <code class="pal-code">' + escHtml(short |
| | | 268 | | } |
| | | 269 | | |
| | | 270 | | if (navigator.clipboard && window.isSecureContext) { |
| | | 271 | | navigator.clipboard.writeText(text).then(onCopied).catch(function () { fallbackCopy(text, onCopied); }); |
| | | 272 | | } else { |
| | | 273 | | fallbackCopy(text, onCopied); |
| | | 274 | | } |
| | | 275 | | } |
| | | 276 | | |
| | | 277 | | function fallbackCopy(text, onSuccess) { |
| | | 278 | | var ta = document.createElement('textarea'); |
| | | 279 | | ta.value = text; |
| | | 280 | | ta.style.position = 'fixed'; |
| | | 281 | | ta.style.opacity = '0'; |
| | | 282 | | document.body.appendChild(ta); |
| | | 283 | | ta.select(); |
| | | 284 | | try { |
| | | 285 | | if (document.execCommand('copy')) onSuccess(); |
| | | 286 | | else showPalToast('<i class="fas fa-times-circle text-danger"></i><span>Не удалось скопировать</span>'); |
| | | 287 | | } catch (e) { |
| | | 288 | | showPalToast('<i class="fas fa-times-circle text-danger"></i><span>Не удалось скопировать</span>'); |
| | | 289 | | } finally { |
| | | 290 | | document.body.removeChild(ta); |
| | | 291 | | } |
| | | 292 | | } |
| | | 293 | | |
| | | 294 | | window.copyPalValue = copyPalValue; |
| | | 295 | | |
| | | 296 | | function showPalToast(html) { |
| | | 297 | | var container = document.getElementById('pal-toast-container'); |
| | | 298 | | if (!container) { |
| | | 299 | | container = document.createElement('div'); |
| | | 300 | | container.id = 'pal-toast-container'; |
| | | 301 | | container.className = 'pal-toast-container'; |
| | | 302 | | document.body.appendChild(container); |
| | | 303 | | } |
| | | 304 | | var toast = document.createElement('div'); |
| | | 305 | | toast.className = 'pal-toast'; |
| | | 306 | | toast.innerHTML = html; |
| | | 307 | | container.appendChild(toast); |
| | | 308 | | setTimeout(function () { |
| | | 309 | | toast.style.opacity = '0'; |
| | | 310 | | toast.style.transition = 'opacity 0.3s'; |
| | | 311 | | setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 320); |
| | | 312 | | }, 2200); |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | function copyCell(value) { |
| | | 316 | | return '<span class="pal-code pal-copy pal-code-nowrap" role="button" tabindex="0" data-copy="' + escHtml(value) |
| | | 317 | | } |
| | | 318 | | |
| | | 319 | | function productImageHtml(imgSrc, wrapExtraClass) { |
| | | 320 | | if (!imgSrc) return ''; |
| | | 321 | | var wrapCls = 'pal-media-wrap pal-img-wrap' + (wrapExtraClass ? ' ' + wrapExtraClass : ''); |
| | | 322 | | return '<span class="' + wrapCls + '">' + |
| | | 323 | | '<span class="pal-skeleton pal-skeleton-img" aria-hidden="true"></span>' + |
| | | 324 | | '<img src="' + imgSrc + '" class="pal-table-img pal-media-item pal-product-zoomable" loading="lazy" alt="Тов |
| | | 325 | | 'onload="palOnImgLoad(this)" onerror="palOnImgError(this)">' + |
| | | 326 | | '</span>'; |
| | | 327 | | } |
| | | 328 | | |
| | | 329 | | function labelThumbHtml(labelProxyUrl) { |
| | | 330 | | return '<a href="' + labelProxyUrl + '" download class="pal-label-thumb-link pal-media-wrap pal-label-wrap" titl |
| | | 331 | | '<span class="pal-skeleton pal-skeleton-label" aria-hidden="true"></span>' + |
| | | 332 | | '<canvas class="pal-label-thumb pal-media-item" data-pdf="' + escHtml(labelProxyUrl) + '"></canvas></a>'; |
| | | 333 | | } |
| | | 334 | | |
| | | 335 | | window.palOnImgLoad = function (img) { |
| | | 336 | | var wrap = img.closest('.pal-media-wrap'); |
| | | 337 | | if (wrap) wrap.classList.add('is-loaded'); |
| | | 338 | | }; |
| | | 339 | | |
| | | 340 | | window.palOnImgError = function (img) { |
| | | 341 | | var wrap = img.closest('.pal-media-wrap'); |
| | | 342 | | if (wrap) { |
| | | 343 | | wrap.classList.add('is-error'); |
| | | 344 | | img.remove(); |
| | | 345 | | } |
| | | 346 | | }; |
| | | 347 | | |
| | | 348 | | function hydrateCachedImages(root) { |
| | | 349 | | (root || document).querySelectorAll('.pal-img-wrap img').forEach(function (img) { |
| | | 350 | | if (!img.complete) return; |
| | | 351 | | if (img.naturalWidth > 0) palOnImgLoad(img); |
| | | 352 | | else palOnImgError(img); |
| | | 353 | | }); |
| | | 354 | | } |
| | | 355 | | |
| | | 356 | | function buildItemParts(item) { |
| | | 357 | | var statusClass = statusBadgeClass(item.status); |
| | | 358 | | var imgSrc = item.imageUrl ? (BASE_URL + '/image-proxy?url=' + encodeURIComponent(item.imageUrl)) : ''; |
| | | 359 | | |
| | | 360 | | var balanceInner; |
| | | 361 | | if (item.oneCBalance != null) { |
| | | 362 | | var balanceClass = item.oneCBalance > 0 ? 'text-success' : 'text-danger'; |
| | | 363 | | balanceInner = '<span class="pal-1c-balance-main ' + balanceClass + '">' + item.oneCBalance + '</span>'; |
| | | 364 | | } else { |
| | | 365 | | balanceInner = '<span class="text-muted">—</span>'; |
| | | 366 | | } |
| | | 367 | | if (item.oneCWarehouseName) { |
| | | 368 | | balanceInner += '<span class="pal-1c-warehouse">' + escHtml(item.oneCWarehouseName) + '</span>'; |
| | | 369 | | } |
| | | 370 | | |
| | | 371 | | var balanceHtml = balanceInner; |
| | | 372 | | |
| | | 373 | | var receiptHtml = escHtml(item.oneCReceiptDoc || '—'); |
| | | 374 | | |
| | | 375 | | var labelProxyUrl = item.labelUrl ? (BASE_URL + '/label-proxy?url=' + encodeURIComponent(item.labelUrl)) : ''; |
| | | 376 | | var labelHtml = labelProxyUrl |
| | | 377 | | ? labelThumbHtml(labelProxyUrl) |
| | | 378 | | : (item.labelError |
| | | 379 | | ? '<span class="text-muted small" title="' + escHtml(item.labelError) + '"><i class="fas fa-exclamation- |
| | | 380 | | : '<span class="text-muted">—</span>'); |
| | | 381 | | |
| | | 382 | | var orderedDate = item.orderedToSupplierDate ? fmtDateTime(item.orderedToSupplierDate) : '—'; |
| | | 383 | | var nameHtml = '<div class="pal-product-cell">' + |
| | | 384 | | productImageHtml(imgSrc) + |
| | | 385 | | '<span class="pal-product-name">' + escHtml(item.productName) + '</span></div>'; |
| | | 386 | | |
| | | 387 | | return { |
| | | 388 | | statusClass: statusClass, |
| | | 389 | | balanceHtml: balanceHtml, |
| | | 390 | | receiptHtml: receiptHtml, |
| | | 391 | | labelHtml: labelHtml, |
| | | 392 | | labelProxyUrl: labelProxyUrl, |
| | | 393 | | orderedDate: orderedDate, |
| | | 394 | | nameHtml: nameHtml, |
| | | 395 | | imgSrc: imgSrc |
| | | 396 | | }; |
| | | 397 | | } |
| | | 398 | | |
| | | 399 | | function renderLabelThumb(canvas) { |
| | | 400 | | if (!canvas || !window.pdfjsLib) return; |
| | | 401 | | var wrap = canvas.closest('.pal-label-wrap'); |
| | | 402 | | var pdfUrl = canvas.dataset.pdf; |
| | | 403 | | if (!pdfUrl) return; |
| | | 404 | | var inCardFooter = !!canvas.closest('.pal-card-footer'); |
| | | 405 | | var cacheVariant = inCardFooter ? 'card' : 'table'; |
| | | 406 | | var cacheKey = pdfUrl + '|' + cacheVariant; |
| | | 407 | | |
| | | 408 | | function markLoaded() { |
| | | 409 | | if (wrap) wrap.classList.add('is-loaded'); |
| | | 410 | | } |
| | | 411 | | |
| | | 412 | | function markError() { |
| | | 413 | | if (wrap) { |
| | | 414 | | wrap.classList.add('is-error', 'is-loaded'); |
| | | 415 | | } |
| | | 416 | | canvas.width = 58; |
| | | 417 | | canvas.height = 20; |
| | | 418 | | var ctx = canvas.getContext('2d'); |
| | | 419 | | ctx.fillStyle = '#fff3cd'; |
| | | 420 | | ctx.fillRect(0, 0, 58, 20); |
| | | 421 | | ctx.fillStyle = '#856404'; |
| | | 422 | | ctx.font = '10px sans-serif'; |
| | | 423 | | ctx.textAlign = 'center'; |
| | | 424 | | ctx.fillText('PDF', 29, 14); |
| | | 425 | | } |
| | | 426 | | |
| | | 427 | | function applyDataUrl(dataUrl) { |
| | | 428 | | var img = new Image(); |
| | | 429 | | img.onload = function () { |
| | | 430 | | canvas.width = img.width; |
| | | 431 | | canvas.height = img.height; |
| | | 432 | | var ctx = canvas.getContext('2d'); |
| | | 433 | | ctx.clearRect(0, 0, canvas.width, canvas.height); |
| | | 434 | | ctx.drawImage(img, 0, 0); |
| | | 435 | | markLoaded(); |
| | | 436 | | }; |
| | | 437 | | img.onerror = markError; |
| | | 438 | | img.src = dataUrl; |
| | | 439 | | } |
| | | 440 | | |
| | | 441 | | var cachedDataUrl = labelThumbDataUrlCache.get(cacheKey); |
| | | 442 | | if (cachedDataUrl) { |
| | | 443 | | applyDataUrl(cachedDataUrl); |
| | | 444 | | return; |
| | | 445 | | } |
| | | 446 | | |
| | | 447 | | var existingPromise = labelThumbRenderPromiseCache.get(cacheKey); |
| | | 448 | | if (existingPromise) { |
| | | 449 | | existingPromise.then(applyDataUrl).catch(markError); |
| | | 450 | | return; |
| | | 451 | | } |
| | | 452 | | |
| | | 453 | | var renderPromise = pdfjsLib.getDocument(pdfUrl).promise.then(function (pdf) { |
| | | 454 | | return pdf.getPage(1); |
| | | 455 | | }).then(function (page) { |
| | | 456 | | var vp = page.getViewport({ scale: 1 }); |
| | | 457 | | var maxW = inCardFooter ? 96 : 72; |
| | | 458 | | var maxH = inCardFooter ? 132 : 108; |
| | | 459 | | var scale = Math.min(maxW / vp.width, maxH / vp.height); |
| | | 460 | | var viewport = page.getViewport({ scale: scale }); |
| | | 461 | | var offscreen = document.createElement('canvas'); |
| | | 462 | | offscreen.width = viewport.width; |
| | | 463 | | offscreen.height = viewport.height; |
| | | 464 | | return page.render({ canvasContext: offscreen.getContext('2d'), viewport: viewport }).promise |
| | | 465 | | .then(function () { return offscreen.toDataURL('image/png'); }); |
| | | 466 | | }); |
| | | 467 | | |
| | | 468 | | labelThumbRenderPromiseCache.set(cacheKey, renderPromise); |
| | | 469 | | renderPromise.then(function (dataUrl) { |
| | | 470 | | labelThumbDataUrlCache.set(cacheKey, dataUrl); |
| | | 471 | | applyDataUrl(dataUrl); |
| | | 472 | | }).catch(function () { |
| | | 473 | | markError(); |
| | | 474 | | }).finally(function () { |
| | | 475 | | labelThumbRenderPromiseCache.delete(cacheKey); |
| | | 476 | | }); |
| | | 477 | | } |
| | | 478 | | |
| | | 479 | | function renderResults(items) { |
| | | 480 | | var tbody = document.getElementById('assemblyTableBody'); |
| | | 481 | | var cards = document.getElementById('assemblyCards'); |
| | | 482 | | tbody.innerHTML = ''; |
| | | 483 | | cards.innerHTML = ''; |
| | | 484 | | |
| | | 485 | | var lastPosting = ''; |
| | | 486 | | var labelCanvases = []; |
| | | 487 | | |
| | | 488 | | items.forEach(function (item) { |
| | | 489 | | var parts = buildItemParts(item); |
| | | 490 | | var isNewPosting = item.postingNumber !== lastPosting; |
| | | 491 | | |
| | | 492 | | var tr = document.createElement('tr'); |
| | | 493 | | if (isNewPosting) { |
| | | 494 | | tr.className = 'pal-row-posting-start'; |
| | | 495 | | lastPosting = item.postingNumber; |
| | | 496 | | } |
| | | 497 | | |
| | | 498 | | tr.innerHTML = |
| | | 499 | | '<td class="text-center text-muted small">' + item.index + '</td>' + |
| | | 500 | | '<td class="pal-col-posting">' + copyCell(item.postingNumber) + '</td>' + |
| | | 501 | | '<td>' + copyCell(item.offerId) + '</td>' + |
| | | 502 | | '<td class="small">' + parts.nameHtml + '</td>' + |
| | | 503 | | '<td class="small">' + escHtml(item.warehouseName) + '</td>' + |
| | | 504 | | '<td><span class="badge ' + parts.statusClass + '">' + escHtml(item.statusLabel) + '</span></td>' + |
| | | 505 | | '<td class="text-center fw-bold">' + item.quantity + '</td>' + |
| | | 506 | | '<td class="small">' + escHtml(item.supplierName || '—') + '</td>' + |
| | | 507 | | '<td class="small">' + parts.orderedDate + '</td>' + |
| | | 508 | | '<td class="text-center small">' + parts.balanceHtml + '</td>' + |
| | | 509 | | '<td class="small">' + parts.receiptHtml + '</td>' + |
| | | 510 | | '<td>' + parts.labelHtml + '</td>'; |
| | | 511 | | tbody.appendChild(tr); |
| | | 512 | | |
| | | 513 | | var postingHeaderHtml = isNewPosting |
| | | 514 | | ? '<div class="pal-posting-divider pal-card-posting"><i class="fas fa-box"></i><span>Отправление</span>' |
| | | 515 | | : ''; |
| | | 516 | | |
| | | 517 | | var card = document.createElement('article'); |
| | | 518 | | card.className = 'pal-card'; |
| | | 519 | | card.innerHTML = |
| | | 520 | | postingHeaderHtml + |
| | | 521 | | '<div class="pal-card-head">' + |
| | | 522 | | '<span class="pal-card-index">#' + item.index + '</span>' + |
| | | 523 | | '<span class="badge ' + parts.statusClass + '">' + escHtml(item.statusLabel) + '</span>' + |
| | | 524 | | '<span class="pal-card-qty">×' + item.quantity + '</span>' + |
| | | 525 | | '</div>' + |
| | | 526 | | '<div class="pal-card-product">' + |
| | | 527 | | productImageHtml(parts.imgSrc) + |
| | | 528 | | '<div class="pal-card-name">' + escHtml(item.productName) + '</div>' + |
| | | 529 | | '</div>' + |
| | | 530 | | '<dl class="pal-card-meta">' + |
| | | 531 | | '<div class="pal-meta-full"><dt>Артикул</dt><dd>' + copyCell(item.offerId) + '</dd></div>' + |
| | | 532 | | '<div><dt>Склад</dt><dd>' + escHtml(item.warehouseName) + '</dd></div>' + |
| | | 533 | | '<div class="pal-meta-full"><dt>Поставщик</dt><dd>' + escHtml(item.supplierName || '—') + '</dd></di |
| | | 534 | | '<div><dt>Заказан</dt><dd>' + parts.orderedDate + '</dd></div>' + |
| | | 535 | | '<div><dt>Остаток 1С</dt><dd>' + parts.balanceHtml + '</dd></div>' + |
| | | 536 | | '<div class="pal-meta-full"><dt>Приход 1С</dt><dd>' + parts.receiptHtml + '</dd></div>' + |
| | | 537 | | '</dl>' + |
| | | 538 | | '<div class="pal-card-footer">' + |
| | | 539 | | '<span class="pal-card-footer-label">Этикетка</span>' + |
| | | 540 | | parts.labelHtml + |
| | | 541 | | '</div>'; |
| | | 542 | | cards.appendChild(card); |
| | | 543 | | |
| | | 544 | | if (parts.labelProxyUrl) { |
| | | 545 | | var canvas = card.querySelector('canvas.pal-label-thumb'); |
| | | 546 | | if (canvas) labelCanvases.push(canvas); |
| | | 547 | | } |
| | | 548 | | }); |
| | | 549 | | |
| | | 550 | | labelCanvases.forEach(renderLabelThumb); |
| | | 551 | | tbody.querySelectorAll('canvas.pal-label-thumb').forEach(renderLabelThumb); |
| | | 552 | | hydrateCachedImages(tbody); |
| | | 553 | | hydrateCachedImages(cards); |
| | | 554 | | } |
| | | 555 | | |
| | | 556 | | function setGeneratedMeta(data) { |
| | | 557 | | var generatedAt = data.generatedAt ? fmtDateTime(data.generatedAt) : '—'; |
| | | 558 | | var metaText = 'Сформировано: ' + generatedAt + |
| | | 559 | | ' · ' + data.items.length + ' строк · ' + (data.totalPostings || 0) + ' отправлений'; |
| | | 560 | | document.getElementById('resultMeta').textContent = metaText; |
| | | 561 | | |
| | | 562 | | heroMeta.innerHTML = |
| | | 563 | | '<span class="pal-chip"><i class="fas fa-calendar"></i>' + fmtDate(from) + ' – ' + fmtDate(to) + '</span>' + |
| | | 564 | | (warehouse ? '<span class="pal-chip"><i class="fas fa-warehouse"></i>' + escHtml(warehouse) + '</span>' : '' |
| | | 565 | | '<span class="pal-chip pal-chip-generated" id="generatedAtChip"><i class="fas fa-clock"></i>' + generatedAt |
| | | 566 | | } |
| | | 567 | | |
| | | 568 | | function updateDownloadAllLabelsButton(items) { |
| | | 569 | | var btn = document.getElementById('downloadAllLabelsBtn'); |
| | | 570 | | if (!btn) return; |
| | | 571 | | |
| | | 572 | | var hasLabels = Array.isArray(items) && items.some(function (it) { return !!it.labelUrl; }); |
| | | 573 | | if (!currentJobId || !hasLabels) { |
| | | 574 | | btn.classList.add('d-none'); |
| | | 575 | | btn.removeAttribute('href'); |
| | | 576 | | return; |
| | | 577 | | } |
| | | 578 | | |
| | | 579 | | var qs = new URLSearchParams({ |
| | | 580 | | jobId: currentJobId, |
| | | 581 | | clientId: String(parseInt(clientId)) |
| | | 582 | | }); |
| | | 583 | | btn.href = BASE_URL + '/download-labels?' + qs.toString(); |
| | | 584 | | btn.classList.remove('d-none'); |
| | | 585 | | } |
| | | 586 | | |
| | | 587 | | function setActionButtonLoading(btn, loading, loadingText, idleText) { |
| | | 588 | | if (!btn) return; |
| | | 589 | | var text = btn.querySelector('.pal-download-all-btn-text'); |
| | | 590 | | var spinner = btn.querySelector('.pal-download-all-btn-spinner'); |
| | | 591 | | if (loading) { |
| | | 592 | | btn.classList.add('is-loading'); |
| | | 593 | | btn.setAttribute('aria-disabled', 'true'); |
| | | 594 | | if (text) text.textContent = loadingText; |
| | | 595 | | if (spinner) spinner.classList.remove('d-none'); |
| | | 596 | | } else { |
| | | 597 | | btn.classList.remove('is-loading'); |
| | | 598 | | btn.removeAttribute('aria-disabled'); |
| | | 599 | | if (text) text.textContent = idleText; |
| | | 600 | | if (spinner) spinner.classList.add('d-none'); |
| | | 601 | | } |
| | | 602 | | } |
| | | 603 | | |
| | | 604 | | function setDownloadBtnState(loading) { |
| | | 605 | | setActionButtonLoading(document.getElementById('downloadAllLabelsBtn'), loading, 'Подготавливаем PDF...', 'Этике |
| | | 606 | | } |
| | | 607 | | |
| | | 608 | | function updateExportExcelButton(items) { |
| | | 609 | | var btn = document.getElementById('exportExcelBtn'); |
| | | 610 | | if (!btn) return; |
| | | 611 | | btn.classList.toggle('d-none', !Array.isArray(items) || items.length === 0); |
| | | 612 | | } |
| | | 613 | | |
| | | 614 | | function fmtFileDate(d) { |
| | | 615 | | try { |
| | | 616 | | var dt = new Date(d || Date.now()); |
| | | 617 | | var pad = function (n) { return (n < 10 ? '0' : '') + n; }; |
| | | 618 | | return dt.getFullYear() + pad(dt.getMonth() + 1) + pad(dt.getDate()) + '_' + pad(dt.getHours()) + pad(dt.get |
| | | 619 | | } catch { return 'export'; } |
| | | 620 | | } |
| | | 621 | | |
| | | 622 | | function getLabelExportImage(pdfUrl) { |
| | | 623 | | if (!pdfUrl || !window.pdfjsLib) return Promise.resolve(null); |
| | | 624 | | if (labelExportImageCache.has(pdfUrl)) return Promise.resolve(labelExportImageCache.get(pdfUrl)); |
| | | 625 | | |
| | | 626 | | var existingPromise = labelExportRenderPromiseCache.get(pdfUrl); |
| | | 627 | | if (existingPromise) return existingPromise; |
| | | 628 | | |
| | | 629 | | var renderPromise = pdfjsLib.getDocument(pdfUrl).promise.then(function (pdf) { |
| | | 630 | | return pdf.getPage(1); |
| | | 631 | | }).then(function (page) { |
| | | 632 | | var vp = page.getViewport({ scale: 1 }); |
| | | 633 | | var scale = Math.min(140 / vp.width, 200 / vp.height); |
| | | 634 | | var viewport = page.getViewport({ scale: scale }); |
| | | 635 | | var canvas = document.createElement('canvas'); |
| | | 636 | | canvas.width = viewport.width; |
| | | 637 | | canvas.height = viewport.height; |
| | | 638 | | return page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise.then(function () |
| | | 639 | | var result = { dataUrl: canvas.toDataURL('image/png'), extension: 'png', width: viewport.width, height: |
| | | 640 | | labelExportImageCache.set(pdfUrl, result); |
| | | 641 | | return result; |
| | | 642 | | }); |
| | | 643 | | }).catch(function () { |
| | | 644 | | return null; |
| | | 645 | | }).finally(function () { |
| | | 646 | | labelExportRenderPromiseCache.delete(pdfUrl); |
| | | 647 | | }); |
| | | 648 | | |
| | | 649 | | labelExportRenderPromiseCache.set(pdfUrl, renderPromise); |
| | | 650 | | return renderPromise; |
| | | 651 | | } |
| | | 652 | | |
| | | 653 | | function getProductExportImage(imgUrl) { |
| | | 654 | | if (!imgUrl) return Promise.resolve(null); |
| | | 655 | | if (productExportImageCache.has(imgUrl)) return Promise.resolve(productExportImageCache.get(imgUrl)); |
| | | 656 | | |
| | | 657 | | var existingPromise = productExportRenderPromiseCache.get(imgUrl); |
| | | 658 | | if (existingPromise) return existingPromise; |
| | | 659 | | |
| | | 660 | | var renderPromise = fetch(imgUrl) |
| | | 661 | | .then(function (r) { if (!r.ok) throw new Error('image fetch failed'); return r.blob(); }) |
| | | 662 | | .then(function (blob) { |
| | | 663 | | return new Promise(function (resolve, reject) { |
| | | 664 | | var reader = new FileReader(); |
| | | 665 | | reader.onload = function () { resolve(reader.result); }; |
| | | 666 | | reader.onerror = reject; |
| | | 667 | | reader.readAsDataURL(blob); |
| | | 668 | | }); |
| | | 669 | | }) |
| | | 670 | | .then(function (dataUrl) { |
| | | 671 | | return new Promise(function (resolve) { |
| | | 672 | | var img = new Image(); |
| | | 673 | | img.onload = function () { |
| | | 674 | | var extMatch = /^data:image\/(png|jpe?g|gif)/i.exec(dataUrl); |
| | | 675 | | var extension = extMatch ? (extMatch[1].toLowerCase() === 'jpg' ? 'jpeg' : extMatch[1].toLowerCa |
| | | 676 | | var result = { dataUrl: dataUrl, extension: extension, width: img.naturalWidth || 60, height: im |
| | | 677 | | productExportImageCache.set(imgUrl, result); |
| | | 678 | | resolve(result); |
| | | 679 | | }; |
| | | 680 | | img.onerror = function () { resolve(null); }; |
| | | 681 | | img.src = dataUrl; |
| | | 682 | | }); |
| | | 683 | | }) |
| | | 684 | | .catch(function () { return null; }) |
| | | 685 | | .finally(function () { productExportRenderPromiseCache.delete(imgUrl); }); |
| | | 686 | | |
| | | 687 | | productExportRenderPromiseCache.set(imgUrl, renderPromise); |
| | | 688 | | return renderPromise; |
| | | 689 | | } |
| | | 690 | | |
| | | 691 | | function placeCellImage(workbook, sheet, imgPromise, col0, row0, maxW, maxH) { |
| | | 692 | | return imgPromise.then(function (img) { |
| | | 693 | | if (!img) return; |
| | | 694 | | var imgId = workbook.addImage({ base64: img.dataUrl, extension: img.extension }); |
| | | 695 | | var scale = Math.min(1, maxW / img.width, maxH / img.height); |
| | | 696 | | sheet.addImage(imgId, { |
| | | 697 | | tl: { col: col0 + 0.05, row: row0 + 0.05 }, |
| | | 698 | | ext: { width: img.width * scale, height: img.height * scale } |
| | | 699 | | }); |
| | | 700 | | }); |
| | | 701 | | } |
| | | 702 | | |
| | | 703 | | function exportToExcel() { |
| | | 704 | | if (!lastResultItems.length || !window.ExcelJS) return; |
| | | 705 | | isExportingExcel = true; |
| | | 706 | | setActionButtonLoading(document.getElementById('exportExcelBtn'), true, 'Формируем Excel...', 'Экспорт в Excel') |
| | | 707 | | |
| | | 708 | | var workbook = new ExcelJS.Workbook(); |
| | | 709 | | workbook.creator = 'Interparts Studio2'; |
| | | 710 | | workbook.created = new Date(); |
| | | 711 | | |
| | | 712 | | var sheet = workbook.addWorksheet('Лист подбора', { views: [{ state: 'frozen', ySplit: 1 }] }); |
| | | 713 | | var columns = [ |
| | | 714 | | { header: '№', key: 'index', width: 6 }, |
| | | 715 | | { header: 'Фото', key: 'photo', width: 12 }, |
| | | 716 | | { header: 'Отправление', key: 'posting', width: 20 }, |
| | | 717 | | { header: 'Артикул', key: 'offerId', width: 16 }, |
| | | 718 | | { header: 'Название', key: 'name', width: 42 }, |
| | | 719 | | { header: 'Склад', key: 'warehouse', width: 16 }, |
| | | 720 | | { header: 'Статус', key: 'status', width: 16 }, |
| | | 721 | | { header: 'Кол.', key: 'qty', width: 7 }, |
| | | 722 | | { header: 'Поставщик', key: 'supplier', width: 20 }, |
| | | 723 | | { header: 'Заказан', key: 'ordered', width: 16 }, |
| | | 724 | | { header: 'Остаток 1С', key: 'balance', width: 11 }, |
| | | 725 | | { header: 'Склад 1С', key: 'warehouse1c', width: 16 }, |
| | | 726 | | { header: 'Приход 1С', key: 'receipt', width: 14 }, |
| | | 727 | | { header: 'Этикетка', key: 'label', width: 14 } |
| | | 728 | | ]; |
| | | 729 | | sheet.columns = columns; |
| | | 730 | | |
| | | 731 | | var headerRow = sheet.getRow(1); |
| | | 732 | | headerRow.eachCell(function (cell) { |
| | | 733 | | cell.font = { bold: true }; |
| | | 734 | | cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; |
| | | 735 | | cell.border = { bottom: { style: 'thin', color: { argb: 'FF999999' } } }; |
| | | 736 | | }); |
| | | 737 | | |
| | | 738 | | var imagePromises = []; |
| | | 739 | | var rowIndex = 2; |
| | | 740 | | var photoCol = 2, labelCol = columns.length; |
| | | 741 | | |
| | | 742 | | lastResultItems.forEach(function (item) { |
| | | 743 | | var row = sheet.getRow(rowIndex); |
| | | 744 | | row.getCell(1).value = item.index; |
| | | 745 | | row.getCell(3).value = item.postingNumber; |
| | | 746 | | row.getCell(4).value = item.offerId; |
| | | 747 | | row.getCell(5).value = item.productName; |
| | | 748 | | row.getCell(6).value = item.warehouseName; |
| | | 749 | | row.getCell(7).value = item.statusLabel; |
| | | 750 | | row.getCell(8).value = item.quantity; |
| | | 751 | | row.getCell(9).value = item.supplierName || '—'; |
| | | 752 | | row.getCell(10).value = item.orderedToSupplierDate ? fmtDateTime(item.orderedToSupplierDate) : '—'; |
| | | 753 | | row.getCell(11).value = item.oneCBalance != null ? item.oneCBalance : '—'; |
| | | 754 | | row.getCell(12).value = item.oneCWarehouseName || '—'; |
| | | 755 | | row.getCell(13).value = item.oneCReceiptDoc || '—'; |
| | | 756 | | if (!item.labelUrl) row.getCell(labelCol).value = item.labelError ? 'Ошибка' : '—'; |
| | | 757 | | if (!item.imageUrl) row.getCell(photoCol).value = '—'; |
| | | 758 | | |
| | | 759 | | for (var c = 1; c <= columns.length; c++) { |
| | | 760 | | row.getCell(c).border = { bottom: { style: 'thin', color: { argb: 'FFD9D9D9' } } }; |
| | | 761 | | row.getCell(c).alignment = { vertical: 'middle' }; |
| | | 762 | | } |
| | | 763 | | row.getCell(8).alignment = { vertical: 'middle', horizontal: 'center' }; |
| | | 764 | | row.getCell(11).alignment = { vertical: 'middle', horizontal: 'center' }; |
| | | 765 | | |
| | | 766 | | row.height = 60; |
| | | 767 | | |
| | | 768 | | if (item.imageUrl) { |
| | | 769 | | var imgProxyUrl = BASE_URL + '/image-proxy?url=' + encodeURIComponent(item.imageUrl); |
| | | 770 | | imagePromises.push(placeCellImage(workbook, sheet, getProductExportImage(imgProxyUrl), photoCol - 1, row |
| | | 771 | | } |
| | | 772 | | |
| | | 773 | | if (item.labelUrl) { |
| | | 774 | | var labelProxyUrl = BASE_URL + '/label-proxy?url=' + encodeURIComponent(item.labelUrl); |
| | | 775 | | imagePromises.push(placeCellImage(workbook, sheet, getLabelExportImage(labelProxyUrl), labelCol - 1, row |
| | | 776 | | } |
| | | 777 | | |
| | | 778 | | rowIndex++; |
| | | 779 | | }); |
| | | 780 | | |
| | | 781 | | Promise.all(imagePromises) |
| | | 782 | | .then(function () { return workbook.xlsx.writeBuffer(); }) |
| | | 783 | | .then(function (buffer) { |
| | | 784 | | var blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| | | 785 | | var fileName = 'assembly_list_' + fmtFileDate(lastResultMeta.generatedAt) + '.xlsx'; |
| | | 786 | | triggerBrowserDownload(blob, fileName); |
| | | 787 | | showPalToast('<i class="fas fa-check-circle"></i><span>Excel-файл сформирован</span>'); |
| | | 788 | | }) |
| | | 789 | | .catch(function (err) { |
| | | 790 | | showPalToast('<i class="fas fa-times-circle text-danger"></i><span>' + escHtml(err.message || 'Не удалос |
| | | 791 | | }) |
| | | 792 | | .finally(function () { |
| | | 793 | | isExportingExcel = false; |
| | | 794 | | setActionButtonLoading(document.getElementById('exportExcelBtn'), false, 'Формируем Excel...', 'Экспорт |
| | | 795 | | }); |
| | | 796 | | } |
| | | 797 | | |
| | | 798 | | function triggerBrowserDownload(blob, fileName) { |
| | | 799 | | var objUrl = URL.createObjectURL(blob); |
| | | 800 | | var a = document.createElement('a'); |
| | | 801 | | a.href = objUrl; |
| | | 802 | | a.download = fileName; |
| | | 803 | | document.body.appendChild(a); |
| | | 804 | | a.click(); |
| | | 805 | | a.remove(); |
| | | 806 | | setTimeout(function () { URL.revokeObjectURL(objUrl); }, 5000); |
| | | 807 | | } |
| | | 808 | | |
| | | 809 | | function closeImageLightbox() { |
| | | 810 | | if (!imageLightbox || imageLightbox.classList.contains('d-none')) return; |
| | | 811 | | imageLightbox.classList.add('d-none'); |
| | | 812 | | document.body.classList.remove('pal-lightbox-open'); |
| | | 813 | | if (imageLightboxImg) imageLightboxImg.src = ''; |
| | | 814 | | } |
| | | 815 | | |
| | | 816 | | function openImageLightbox(src) { |
| | | 817 | | if (!imageLightbox || !imageLightboxImg || !src) return; |
| | | 818 | | imageLightboxImg.src = src; |
| | | 819 | | imageLightbox.classList.remove('d-none'); |
| | | 820 | | document.body.classList.add('pal-lightbox-open'); |
| | | 821 | | } |
| | | 822 | | |
| | | 823 | | document.addEventListener('click', function (e) { |
| | | 824 | | var btn = e.target.closest('#downloadAllLabelsBtn'); |
| | | 825 | | if (!btn) return; |
| | | 826 | | e.preventDefault(); |
| | | 827 | | if (isDownloadingLabels) return; |
| | | 828 | | var href = btn.getAttribute('href') || ''; |
| | | 829 | | if (!href || href === '#') return; |
| | | 830 | | |
| | | 831 | | isDownloadingLabels = true; |
| | | 832 | | setDownloadBtnState(true); |
| | | 833 | | |
| | | 834 | | fetch(href) |
| | | 835 | | .then(function (r) { |
| | | 836 | | if (!r.ok) throw new Error('Ошибка скачивания (код ' + r.status + ')'); |
| | | 837 | | var dispo = r.headers.get('Content-Disposition') || ''; |
| | | 838 | | var match = /filename="([^"]+)"/i.exec(dispo); |
| | | 839 | | var fileName = (match && match[1]) ? match[1] : ('assembly_labels_' + Date.now() + '.pdf'); |
| | | 840 | | return r.blob().then(function (blob) { return { blob: blob, fileName: fileName }; }); |
| | | 841 | | }) |
| | | 842 | | .then(function (payload) { |
| | | 843 | | triggerBrowserDownload(payload.blob, payload.fileName); |
| | | 844 | | showPalToast('<i class="fas fa-check-circle"></i><span>Скачивание началось</span>'); |
| | | 845 | | }) |
| | | 846 | | .catch(function (err) { |
| | | 847 | | showPalToast('<i class="fas fa-times-circle text-danger"></i><span>' + escHtml(err.message || 'Не удалос |
| | | 848 | | }) |
| | | 849 | | .finally(function () { |
| | | 850 | | isDownloadingLabels = false; |
| | | 851 | | setDownloadBtnState(false); |
| | | 852 | | }); |
| | | 853 | | }); |
| | | 854 | | |
| | | 855 | | document.addEventListener('click', function (e) { |
| | | 856 | | var excelBtn = e.target.closest('#exportExcelBtn'); |
| | | 857 | | if (!excelBtn) return; |
| | | 858 | | e.preventDefault(); |
| | | 859 | | if (isExportingExcel) return; |
| | | 860 | | exportToExcel(); |
| | | 861 | | }); |
| | | 862 | | |
| | | 863 | | document.addEventListener('click', function (e) { |
| | | 864 | | var img = e.target.closest('.pal-product-zoomable'); |
| | | 865 | | if (img) { |
| | | 866 | | e.preventDefault(); |
| | | 867 | | openImageLightbox(img.getAttribute('src') || ''); |
| | | 868 | | return; |
| | | 869 | | } |
| | | 870 | | |
| | | 871 | | if (!imageLightbox || imageLightbox.classList.contains('d-none')) return; |
| | | 872 | | var closeBtn = e.target.closest('#palImageLightboxClose'); |
| | | 873 | | if (closeBtn || e.target === imageLightbox) { |
| | | 874 | | e.preventDefault(); |
| | | 875 | | closeImageLightbox(); |
| | | 876 | | } |
| | | 877 | | }); |
| | | 878 | | |
| | | 879 | | document.addEventListener('keydown', function (e) { |
| | | 880 | | if (e.key === 'Escape') closeImageLightbox(); |
| | | 881 | | }); |
| | | 882 | | |
| | | 883 | | if (!clientId) { |
| | | 884 | | showError('Неверная ссылка: отсутствует параметр clientId.'); |
| | | 885 | | return; |
| | | 886 | | } |
| | | 887 | | |
| | | 888 | | document.getElementById('pageSubtitle').textContent = |
| | | 889 | | 'Период: ' + fmtDate(from) + ' – ' + fmtDate(to) + (warehouse ? ' · Склад: ' + warehouse : ''); |
| | | 890 | | |
| | | 891 | | heroMeta.innerHTML = |
| | | 892 | | '<span class="pal-chip"><i class="fas fa-calendar"></i>' + fmtDate(from) + ' – ' + fmtDate(to) + '</span>' + |
| | | 893 | | (warehouse ? '<span class="pal-chip"><i class="fas fa-warehouse"></i>' + escHtml(warehouse) + '</span>' : '') + |
| | | 894 | | '<span class="pal-chip"><i class="fas fa-spinner fa-spin"></i>Обновление данных...</span>'; |
| | | 895 | | |
| | | 896 | | document.getElementById('progressSection').style.display = ''; |
| | | 897 | | |
| | | 898 | | fetch(BASE_URL + '/generate', { |
| | | 899 | | method: 'POST', |
| | | 900 | | headers: { 'Content-Type': 'application/json' }, |
| | | 901 | | body: JSON.stringify({ clientId: parseInt(clientId), status: status, from: from, to: to, warehouse: warehouse }) |
| | | 902 | | }) |
| | | 903 | | .then(function (r) { |
| | | 904 | | if (!r.ok) { showError('Ошибка запуска генерации (код ' + r.status + ')'); return; } |
| | | 905 | | return r.json(); |
| | | 906 | | }) |
| | | 907 | | .then(function (data) { |
| | | 908 | | if (!data || !data.jobId) return; |
| | | 909 | | currentJobId = data.jobId; |
| | | 910 | | listenProgress(data.jobId); |
| | | 911 | | }) |
| | | 912 | | .catch(function (e) { showError('Ошибка соединения: ' + e.message); }); |
| | | 913 | | |
| | | 914 | | function listenProgress(jobId) { |
| | | 915 | | var es = new EventSource(BASE_URL + '/progress?jobId=' + encodeURIComponent(jobId)); |
| | | 916 | | es.addEventListener('progress', function (e) { |
| | | 917 | | var p = JSON.parse(e.data); |
| | | 918 | | if (p.step && p.step >= 1 && p.step <= 5) { |
| | | 919 | | var el = document.getElementById('step-' + p.step); |
| | | 920 | | if (el) { |
| | | 921 | | el.classList.add('active'); |
| | | 922 | | var icon = document.getElementById('step-icon-' + p.step); |
| | | 923 | | var spin = document.getElementById('step-spin-' + p.step); |
| | | 924 | | if (icon) icon.classList.add('d-none'); |
| | | 925 | | if (spin) spin.classList.remove('d-none'); |
| | | 926 | | var msg = document.getElementById('step-msg-' + p.step); |
| | | 927 | | if (msg) msg.textContent = p.message || ''; |
| | | 928 | | } |
| | | 929 | | if (p.step > 1) { |
| | | 930 | | var prev = document.getElementById('step-' + (p.step - 1)); |
| | | 931 | | if (prev) { |
| | | 932 | | prev.classList.remove('active'); |
| | | 933 | | prev.classList.add('done'); |
| | | 934 | | var pIcon = document.getElementById('step-icon-' + (p.step - 1)); |
| | | 935 | | var pSpin = document.getElementById('step-spin-' + (p.step - 1)); |
| | | 936 | | if (pIcon) { pIcon.className = 'fas fa-check-circle'; pIcon.classList.remove('d-none'); } |
| | | 937 | | if (pSpin) pSpin.classList.add('d-none'); |
| | | 938 | | } |
| | | 939 | | } |
| | | 940 | | if (p.step === 1) hideSplash(); |
| | | 941 | | } |
| | | 942 | | if (p.isComplete) { |
| | | 943 | | es.close(); |
| | | 944 | | for (var s = 1; s <= 5; s++) { |
| | | 945 | | var stepEl = document.getElementById('step-' + s); |
| | | 946 | | if (stepEl) { stepEl.classList.remove('active'); stepEl.classList.add('done'); } |
| | | 947 | | var ic = document.getElementById('step-icon-' + s); |
| | | 948 | | var sp = document.getElementById('step-spin-' + s); |
| | | 949 | | if (ic) { ic.className = 'fas fa-check-circle'; ic.classList.remove('d-none'); } |
| | | 950 | | if (sp) sp.classList.add('d-none'); |
| | | 951 | | } |
| | | 952 | | loadResult(jobId); |
| | | 953 | | } |
| | | 954 | | if (p.isError) { |
| | | 955 | | es.close(); |
| | | 956 | | hideSplash(); |
| | | 957 | | showError(p.errorMessage || p.message || 'неизвестная ошибка'); |
| | | 958 | | } |
| | | 959 | | }); |
| | | 960 | | es.onerror = function () { es.close(); }; |
| | | 961 | | } |
| | | 962 | | |
| | | 963 | | function loadResult(jobId) { |
| | | 964 | | fetch(BASE_URL + '/result?jobId=' + encodeURIComponent(jobId)) |
| | | 965 | | .then(function (r) { return r.json(); }) |
| | | 966 | | .then(function (data) { |
| | | 967 | | document.getElementById('resultSection').style.display = ''; |
| | | 968 | | if (!data.items || data.items.length === 0) { |
| | | 969 | | document.getElementById('emptyWarehouse').classList.remove('d-none'); |
| | | 970 | | document.getElementById('resultBadge').textContent = '0 позиций'; |
| | | 971 | | setGeneratedMeta({ generatedAt: data.generatedAt, items: [], totalPostings: 0 }); |
| | | 972 | | updateDownloadAllLabelsButton([]); |
| | | 973 | | lastResultItems = []; |
| | | 974 | | updateExportExcelButton([]); |
| | | 975 | | return; |
| | | 976 | | } |
| | | 977 | | document.getElementById('resultBadge').textContent = data.items.length + ' поз.'; |
| | | 978 | | setGeneratedMeta(data); |
| | | 979 | | renderResults(data.items); |
| | | 980 | | updateDownloadAllLabelsButton(data.items); |
| | | 981 | | lastResultItems = data.items; |
| | | 982 | | lastResultMeta = { generatedAt: data.generatedAt }; |
| | | 983 | | updateExportExcelButton(data.items); |
| | | 984 | | }) |
| | | 985 | | .catch(function (e) { showError('Ошибка загрузки результата: ' + e.message); }); |
| | | 986 | | } |
| | | 987 | | })(); |
| | | 988 | | </script> |
| | | 989 | | </body> |
| | | 990 | | </html> |