| | | 1 | | @using OzonOrdersWeb.ViewModels.OrderViewModels |
| | | 2 | | @model MultiplayEditOrderViewModel |
| | | 3 | | |
| | | 4 | | |
| | | 5 | | <script> |
| | | 6 | | window.txGetCommentFieldValue = function () { |
| | | 7 | | var el = document.getElementById('commentField'); |
| | | 8 | | if (!el) return ''; |
| | | 9 | | return el.value || ''; |
| | | 10 | | }; |
| | | 11 | | </script> |
| | | 12 | | |
| | | 13 | | <script> |
| | | 14 | | //Логика черновика |
| | | 15 | | document.addEventListener('DOMContentLoaded', (event) => { |
| | 0 | 16 | | const clearPurchasePriceOnLoad = @Json.Serialize(ViewBag.ClearPurchasePriceOnLoad is true); |
| | | 17 | | |
| | | 18 | | // Функция для сохранения значения поля в куки |
| | | 19 | | function saveToCookies(key, value) { |
| | | 20 | | document.cookie = key + "=" + encodeURIComponent(value) + "; path=/"; |
| | | 21 | | console.log(`Сохранено: поле ${key}, значение ${value}`); |
| | | 22 | | } |
| | | 23 | | |
| | | 24 | | // Функция для загрузки значения из куки |
| | | 25 | | function loadFromCookies(key) { |
| | | 26 | | const name = key + "="; |
| | | 27 | | const decodedCookie = decodeURIComponent(document.cookie); |
| | | 28 | | const ca = decodedCookie.split(';'); |
| | | 29 | | for (let i = 0; i < ca.length; i++) { |
| | | 30 | | let c = ca[i].trim(); |
| | | 31 | | if (c.indexOf(name) === 0) { |
| | | 32 | | return c.substring(name.length, c.length); |
| | | 33 | | } |
| | | 34 | | } |
| | | 35 | | return ""; |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | // Для каждого select с классом 'status-select' |
| | | 39 | | document.querySelectorAll('.status-select').forEach(select => { |
| | | 40 | | const id = select.id; |
| | | 41 | | const savedValue = loadFromCookies(id); |
| | | 42 | | |
| | | 43 | | // Если есть сохраненное значение, устанавливаем его |
| | | 44 | | if (savedValue) { |
| | | 45 | | select.value = savedValue; |
| | | 46 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | // Сохраняем значение в куки при изменении |
| | | 50 | | select.addEventListener('change', function () { |
| | | 51 | | saveToCookies(id, this.value); |
| | | 52 | | }); |
| | | 53 | | }); |
| | | 54 | | |
| | | 55 | | // Для каждого select с классом 'supplier-select' |
| | | 56 | | document.querySelectorAll('.supplier-select').forEach(select => { |
| | | 57 | | const id = select.id; |
| | | 58 | | const savedValue = loadFromCookies(id); |
| | | 59 | | |
| | | 60 | | if (savedValue) { |
| | | 61 | | select.value = savedValue; |
| | | 62 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | select.addEventListener('change', function () { |
| | | 66 | | saveToCookies(id, this.value); |
| | | 67 | | }); |
| | | 68 | | }); |
| | | 69 | | |
| | | 70 | | // Для каждого input с классом 'orderNumberToSupplie-field' |
| | | 71 | | document.querySelectorAll('.orderNumberToSupplie-field').forEach(input => { |
| | | 72 | | const id = input.id; |
| | | 73 | | const savedValue = loadFromCookies(id); |
| | | 74 | | |
| | | 75 | | if (savedValue) { |
| | | 76 | | input.value = savedValue; |
| | | 77 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | input.addEventListener('input', function () { |
| | | 81 | | saveToCookies(id, this.value); |
| | | 82 | | }); |
| | | 83 | | }); |
| | | 84 | | |
| | | 85 | | // Для каждого input с классом 'product-weight-field' |
| | | 86 | | document.querySelectorAll('.product-weight-field').forEach(input => { |
| | | 87 | | const id = input.id; |
| | | 88 | | const savedValue = loadFromCookies(id); |
| | | 89 | | |
| | | 90 | | if (savedValue) { |
| | | 91 | | input.value = savedValue; |
| | | 92 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | input.addEventListener('input', function () { |
| | | 96 | | saveToCookies(id, this.value); |
| | | 97 | | }); |
| | | 98 | | }); |
| | | 99 | | |
| | | 100 | | // Для каждого input с классом 'purchase-price-field' |
| | | 101 | | document.querySelectorAll('.purchase-price-field').forEach(input => { |
| | | 102 | | const id = input.id; |
| | | 103 | | const savedValue = clearPurchasePriceOnLoad ? "" : loadFromCookies(id); |
| | | 104 | | |
| | | 105 | | if (savedValue) { |
| | | 106 | | input.value = savedValue; |
| | | 107 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | input.addEventListener('input', function () { |
| | | 111 | | saveToCookies(id, this.value); |
| | | 112 | | }); |
| | | 113 | | }); |
| | | 114 | | |
| | | 115 | | // Для каждого input с классом 'ozon-commission-field' |
| | | 116 | | document.querySelectorAll('.ozon-commission-field').forEach(input => { |
| | | 117 | | const id = input.id; |
| | | 118 | | const savedValue = loadFromCookies(id); |
| | | 119 | | |
| | | 120 | | if (savedValue) { |
| | | 121 | | input.value = savedValue; |
| | | 122 | | console.log(`Загружено из куки: поле ${id}, значение ${savedValue}`); |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | input.addEventListener('input', function () { |
| | | 126 | | saveToCookies(id, this.value); |
| | | 127 | | }); |
| | | 128 | | }); |
| | | 129 | | }); |
| | | 130 | | |
| | | 131 | | </script> |
| | | 132 | | |
| | | 133 | | <script> |
| | | 134 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 135 | | const form = document.getElementById('createOrderForm'); |
| | | 136 | | if (!form) return; |
| | | 137 | | |
| | | 138 | | form.dataset.noLoading = ''; |
| | | 139 | | |
| | | 140 | | const styleId = 'tx-ux-enhancements-style'; |
| | | 141 | | if (!document.getElementById(styleId)) { |
| | | 142 | | const style = document.createElement('style'); |
| | | 143 | | style.id = styleId; |
| | | 144 | | style.textContent = ` |
| | | 145 | | .tx-section{border:1px solid #e9ecef;border-radius:8px;margin-top:10px} |
| | | 146 | | .tx-section-header{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;backgr |
| | | 147 | | .tx-section-body{padding:10px} |
| | | 148 | | .tx-submit-overlay{ |
| | | 149 | | position:fixed;inset:0;background:rgba(20, 24, 31, .42);backdrop-filter:blur(2px); |
| | | 150 | | display:none;align-items:center;justify-content:center;z-index:20000 |
| | | 151 | | } |
| | | 152 | | .tx-submit-overlay-card{ |
| | | 153 | | min-width:320px;max-width:440px;background:#fff;border-radius:12px;padding:16px 18px; |
| | | 154 | | box-shadow:0 10px 30px rgba(0,0,0,.22);display:flex;flex-direction:column;gap:10px |
| | | 155 | | } |
| | | 156 | | .tx-submit-overlay-card .spinner-border{width:1.5rem;height:1.5rem;flex-shrink:0} |
| | | 157 | | .tx-submit-overlay-top{display:flex;align-items:center;gap:12px} |
| | | 158 | | .tx-submit-overlay-elapsed{ |
| | | 159 | | margin-left:auto;padding:2px 8px;border-radius:999px;background:#f1f5f9;color:#475569; |
| | | 160 | | font-size:.75rem;font-weight:700;font-variant-numeric:tabular-nums |
| | | 161 | | } |
| | | 162 | | .tx-submit-overlay-stages{list-style:none;margin:0;padding:0;border-top:1px solid #e2e8f0;padding-top:8p |
| | | 163 | | .tx-submit-overlay-stages li{ |
| | | 164 | | display:flex;align-items:center;gap:8px;padding:2px 0;color:#94a3b8;font-size:.78rem |
| | | 165 | | } |
| | | 166 | | .tx-submit-overlay-stages li.is-done{color:#16a34a} |
| | | 167 | | .tx-submit-overlay-stages li.is-current{color:#1e293b;font-weight:600} |
| | | 168 | | .tx-submit-overlay-stages .tx-stage-icon{width:14px;text-align:center;flex-shrink:0} |
| | | 169 | | .tx-submit-overlay-hint{ |
| | | 170 | | padding:7px 9px;border-radius:8px;background:#fffbeb;border:1px solid #fde68a;color:#b45309; |
| | | 171 | | font-size:.75rem |
| | | 172 | | } |
| | | 173 | | .tx-submit-overlay-details{font-size:.75rem;color:#64748b} |
| | | 174 | | .tx-submit-overlay-details summary{cursor:pointer;color:#2563eb} |
| | | 175 | | .tx-submit-overlay-details p{margin:6px 0 0} |
| | | 176 | | #staticBackdrop,#loadingModal{display:none!important;visibility:hidden!important;pointer-events:none!imp |
| | | 177 | | #createOrderForm .price-sale-display.is-invalid, |
| | | 178 | | #createOrderForm .price-sale-display.highlight-error, |
| | | 179 | | #createOrderForm .purchase-price-field.is-invalid, |
| | | 180 | | #createOrderForm .purchase-price-field.highlight-error, |
| | | 181 | | #createOrderForm .orderNumberToSupplie-field.is-invalid, |
| | | 182 | | #createOrderForm .product-weight-field.is-invalid, |
| | | 183 | | #createOrderForm .quantity-input-field.is-invalid, |
| | | 184 | | #createOrderForm #warehouseSender.is-invalid, |
| | | 185 | | #createOrderForm #warehouseRecipient.is-invalid, |
| | | 186 | | #createOrderForm textarea.is-invalid, |
| | | 187 | | #createOrderForm input.form-control.is-invalid:not([type="hidden"]) { |
| | | 188 | | border-color: #dc3545 !important; |
| | | 189 | | background: #fef2f2 !important; |
| | | 190 | | box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.55) !important; |
| | | 191 | | } |
| | | 192 | | #createOrderForm .cs-wrapper:has(select.is-invalid) .cs-trigger, |
| | | 193 | | #createOrderForm .cs-wrapper:has(select.highlight-error) .cs-trigger, |
| | | 194 | | #createOrderForm .cs-trigger.highlight-error { |
| | | 195 | | border-color: #dc3545 !important; |
| | | 196 | | background: #fef2f2 !important; |
| | | 197 | | box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.55) !important; |
| | | 198 | | } |
| | | 199 | | #createOrderForm tr.tx-row-has-invalid > td { |
| | | 200 | | box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.18); |
| | | 201 | | } |
| | | 202 | | #createOrderForm #shipmentTable td.tx-field-invalid { |
| | | 203 | | background: rgba(254, 242, 242, 0.72) !important; |
| | | 204 | | box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.55) !important; |
| | | 205 | | } |
| | | 206 | | #createOrderForm #shipmentTable td.tx-field-invalid .cs-trigger { |
| | | 207 | | color: #991b1b !important; |
| | | 208 | | font-weight: 600 !important; |
| | | 209 | | outline: none; |
| | | 210 | | box-shadow: inset 0 0 0 1px #dc3545 !important; |
| | | 211 | | } |
| | | 212 | | `; |
| | | 213 | | document.head.appendChild(style); |
| | | 214 | | } |
| | | 215 | | |
| | | 216 | | function initUnifiedSubmitAnimation() { |
| | | 217 | | let overlay = document.getElementById('txSubmitOverlay'); |
| | | 218 | | if (!overlay) { |
| | | 219 | | overlay = document.createElement('div'); |
| | | 220 | | overlay.id = 'txSubmitOverlay'; |
| | | 221 | | overlay.className = 'tx-submit-overlay'; |
| | | 222 | | overlay.innerHTML = ` |
| | | 223 | | <div class="tx-submit-overlay-card"> |
| | | 224 | | <div class="tx-submit-overlay-top"> |
| | | 225 | | <div class="spinner-border text-primary" role="status"></div> |
| | | 226 | | <div> |
| | | 227 | | <div class="fw-semibold">Отправляем транзакцию...</div> |
| | | 228 | | <div class="small text-muted">Пожалуйста, не закрывайте страницу</div> |
| | | 229 | | </div> |
| | | 230 | | <span class="tx-submit-overlay-elapsed">00:00</span> |
| | | 231 | | </div> |
| | | 232 | | <ul class="tx-submit-overlay-stages"></ul> |
| | | 233 | | <div class="tx-submit-overlay-hint" style="display:none"></div> |
| | | 234 | | <details class="tx-submit-overlay-details"> |
| | | 235 | | <summary>Что происходит?</summary> |
| | | 236 | | <p>Studio2 проводит документ и меняет статусы заказов, затем при необходимости создаёт докум |
| | | 237 | | <p>Этапы ниже — приблизительные: сервер не сообщает свой прогресс, поэтому они оцениваются п |
| | | 238 | | </details> |
| | | 239 | | </div> |
| | | 240 | | `; |
| | | 241 | | document.body.appendChild(overlay); |
| | | 242 | | } |
| | | 243 | | |
| | | 244 | | function disableLegacyLoaders() { |
| | | 245 | | ['staticBackdrop', 'loadingModal'].forEach(function (id) { |
| | | 246 | | const el = document.getElementById(id); |
| | | 247 | | if (!el) return; |
| | | 248 | | el.classList.remove('show'); |
| | | 249 | | el.style.display = 'none'; |
| | | 250 | | el.setAttribute('aria-hidden', 'true'); |
| | | 251 | | if (window.bootstrap && bootstrap.Modal) { |
| | | 252 | | const instance = bootstrap.Modal.getInstance(el); |
| | | 253 | | if (instance) instance.hide(); |
| | | 254 | | } |
| | | 255 | | }); |
| | | 256 | | } |
| | | 257 | | |
| | | 258 | | function hidePageLoadingBar() { |
| | | 259 | | if (window.PageLoadingBar && typeof window.PageLoadingBar.hide === 'function') { |
| | | 260 | | window.PageLoadingBar.hide(); |
| | | 261 | | return; |
| | | 262 | | } |
| | | 263 | | if (window.hidePageLoadingBar) { |
| | | 264 | | window.hidePageLoadingBar(); |
| | | 265 | | return; |
| | | 266 | | } |
| | | 267 | | const bar = document.getElementById('page-loading-bar'); |
| | | 268 | | if (bar) bar.classList.remove('active'); |
| | | 269 | | } |
| | | 270 | | |
| | | 271 | | // Клиент не знает заранее, потребуется ли создавать новую номенклатуру в 1С (это решает |
| | | 272 | | // сервер по ходу обработки) — обычное проведение без новых товаров укладывается в пару |
| | | 273 | | // секунд, а создание карточки номенклатуры даёт 7-11 последовательных запросов к 1С. |
| | | 274 | | // Если оверлей всё ещё виден спустя NOMENCLATURE_HINT_DELAY_MS — это сильный сигнал, что |
| | | 275 | | // идёт создание номенклатуры, и стоит явно предупредить пользователя, а не оставлять его |
| | | 276 | | // с ощущением, что страница зависла. |
| | | 277 | | /* Настоящий ход проведения: сервер шлёт этапы через JobProgressHub в группу |
| | | 278 | | с ключом, который уходит вместе с формой. Пока событий нет (SignalR не поднялся, |
| | | 279 | | старый сервер), оверлей работает по прежней оценке времени. */ |
| | | 280 | | const txProgress = { |
| | | 281 | | key: null, |
| | | 282 | | stages: [], |
| | | 283 | | finished: false, |
| | | 284 | | |
| | | 285 | | reset() { |
| | | 286 | | this.stages = []; |
| | | 287 | | this.finished = false; |
| | | 288 | | }, |
| | | 289 | | |
| | | 290 | | add(stage) { |
| | | 291 | | if (!stage || !stage.code) return; |
| | | 292 | | const existing = this.stages.find(x => x.code === stage.code); |
| | | 293 | | if (existing) { |
| | | 294 | | existing.title = stage.title || existing.title; |
| | | 295 | | existing.detail = stage.detail ?? existing.detail; |
| | | 296 | | } else { |
| | | 297 | | this.stages.push({ code: stage.code, title: stage.title || stage.code, detail: stage.detail || n |
| | | 298 | | } |
| | | 299 | | renderOverlayProgress(buildStages()); |
| | | 300 | | }, |
| | | 301 | | |
| | | 302 | | finish() { |
| | | 303 | | this.finished = true; |
| | | 304 | | renderOverlayProgress(buildStages()); |
| | | 305 | | } |
| | | 306 | | }; |
| | | 307 | | |
| | | 308 | | function ensureProgressKey() { |
| | | 309 | | if (txProgress.key) return txProgress.key; |
| | | 310 | | txProgress.key = 'tx-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); |
| | | 311 | | let field = form.querySelector('input[name="progressKey"]'); |
| | | 312 | | if (!field) { |
| | | 313 | | field = document.createElement('input'); |
| | | 314 | | field.type = 'hidden'; |
| | | 315 | | field.name = 'progressKey'; |
| | | 316 | | form.appendChild(field); |
| | | 317 | | } |
| | | 318 | | field.value = txProgress.key; |
| | | 319 | | return txProgress.key; |
| | | 320 | | } |
| | | 321 | | |
| | | 322 | | async function subscribeToTransactionProgress() { |
| | | 323 | | if (!window.signalR) { |
| | | 324 | | // Библиотека приходит с внешнего CDN и может отстать от разметки. |
| | | 325 | | window.setTimeout(subscribeToTransactionProgress, 1000); |
| | | 326 | | return; |
| | | 327 | | } |
| | | 328 | | try { |
| | | 329 | | const connection = new signalR.HubConnectionBuilder() |
| | | 330 | | .withUrl('/jobProgressHub') |
| | | 331 | | .withAutomaticReconnect() |
| | | 332 | | .build(); |
| | | 333 | | |
| | | 334 | | connection.on('TransactionStage', stage => txProgress.add(stage)); |
| | | 335 | | connection.on('TransactionFinished', () => txProgress.finish()); |
| | | 336 | | |
| | | 337 | | await connection.start(); |
| | | 338 | | await connection.invoke('JoinJobGroup', ensureProgressKey()); |
| | | 339 | | } catch (error) { |
| | | 340 | | // Без подписки оверлей просто останется на прежней оценке по времени. |
| | | 341 | | console.debug('Прогресс проведения недоступен', error); |
| | | 342 | | } |
| | | 343 | | } |
| | | 344 | | |
| | | 345 | | ensureProgressKey(); |
| | | 346 | | void subscribeToTransactionProgress(); |
| | | 347 | | |
| | | 348 | | const NOMENCLATURE_HINT_DELAY_MS = 4000; |
| | | 349 | | const SLOW_HINT_DELAY_MS = 12000; |
| | | 350 | | const STUCK_HINT_DELAY_MS = 30000; |
| | | 351 | | let overlayTimer = null; |
| | | 352 | | let overlayStartedAt = 0; |
| | | 353 | | |
| | | 354 | | // Этапы оценочные: сервер не стримит прогресс, поэтому курсор двигается по времени ожидания. |
| | | 355 | | function buildStages() { |
| | | 356 | | if (txProgress.stages.length > 0) { |
| | | 357 | | return txProgress.stages.map(stage => ({ |
| | | 358 | | real: true, |
| | | 359 | | label: stage.detail ? `${stage.title} — ${stage.detail}` : stage.title |
| | | 360 | | })); |
| | | 361 | | } |
| | | 362 | | |
| | | 363 | | const stages = [ |
| | | 364 | | { atMs: 0, label: 'Проверяем данные и готовим документ' }, |
| | | 365 | | { atMs: 1500, label: 'Проводим транзакцию в Studio2' }, |
| | | 366 | | { atMs: NOMENCLATURE_HINT_DELAY_MS, label: 'Отправляем документы в 1С' } |
| | | 367 | | ]; |
| | | 368 | | if (hasQueuedSupplierCartItems()) { |
| | | 369 | | stages.push({ atMs: SLOW_HINT_DELAY_MS, label: 'Размещаем позиции в корзинах поставщиков' }); |
| | | 370 | | } |
| | | 371 | | stages.push({ atMs: STUCK_HINT_DELAY_MS, label: 'Ждём ответ сервера' }); |
| | | 372 | | return stages; |
| | | 373 | | } |
| | | 374 | | |
| | | 375 | | function hasQueuedSupplierCartItems() { |
| | | 376 | | return ['avdBasketQueueJson', 'armtekBasketQueueJson', 'motexBasketQueueJson', 'mlAutoBasketQueueJson', |
| | | 377 | | const field = document.getElementById(id); |
| | | 378 | | if (!field || !field.value) return false; |
| | | 379 | | try { |
| | | 380 | | const parsed = JSON.parse(field.value); |
| | | 381 | | return Array.isArray(parsed) && parsed.length > 0; |
| | | 382 | | } catch { |
| | | 383 | | return false; |
| | | 384 | | } |
| | | 385 | | }); |
| | | 386 | | } |
| | | 387 | | |
| | | 388 | | function escalationText(elapsed) { |
| | | 389 | | if (elapsed >= STUCK_HINT_DELAY_MS) { |
| | | 390 | | return 'Запрос всё ещё выполняется. Повторная отправка создаст дубль документа: если через минуту ни |
| | | 391 | | } |
| | | 392 | | if (elapsed >= SLOW_HINT_DELAY_MS) { |
| | | 393 | | return '1С отвечает медленно, проведение занимает больше обычного. Не закрывайте страницу и не отпра |
| | | 394 | | } |
| | | 395 | | if (elapsed >= NOMENCLATURE_HINT_DELAY_MS) { |
| | | 396 | | return 'Похоже, в 1С создаётся новая номенклатура — это может занять больше времени.'; |
| | | 397 | | } |
| | | 398 | | return ''; |
| | | 399 | | } |
| | | 400 | | |
| | | 401 | | function renderOverlayProgress(stages) { |
| | | 402 | | const elapsed = Date.now() - overlayStartedAt; |
| | | 403 | | const elapsedNode = overlay.querySelector('.tx-submit-overlay-elapsed'); |
| | | 404 | | if (elapsedNode) { |
| | | 405 | | const totalSeconds = Math.floor(elapsed / 1000); |
| | | 406 | | elapsedNode.textContent = `${String(Math.floor(totalSeconds / 60)).padStart(2, '0')}:${String(totalS |
| | | 407 | | } |
| | | 408 | | |
| | | 409 | | // Реальные этапы: пройденные отмечены, последний пришедший — текущий. |
| | | 410 | | // Оценочные — по секундомеру, как раньше. |
| | | 411 | | const real = stages.length > 0 && stages[0].real === true; |
| | | 412 | | let currentIndex = real ? stages.length - 1 : 0; |
| | | 413 | | if (!real) { |
| | | 414 | | stages.forEach(function (stage, index) { |
| | | 415 | | if (elapsed >= stage.atMs) currentIndex = index; |
| | | 416 | | }); |
| | | 417 | | } else if (txProgress.finished) { |
| | | 418 | | currentIndex = stages.length; |
| | | 419 | | } |
| | | 420 | | |
| | | 421 | | const list = overlay.querySelector('.tx-submit-overlay-stages'); |
| | | 422 | | if (list) { |
| | | 423 | | list.innerHTML = stages.map(function (stage, index) { |
| | | 424 | | // Последний этап никогда не помечаем выполненным: оверлей исчезает только по |
| | | 425 | | // завершению запроса, поэтому полностью отмеченный список был бы неправдой. |
| | | 426 | | const state = index < currentIndex ? 'is-done' : (index === currentIndex ? 'is-current' : ''); |
| | | 427 | | const icon = index < currentIndex |
| | | 428 | | ? '<i class="fas fa-check"></i>' |
| | | 429 | | : (index === currentIndex ? '<i class="fas fa-circle-notch fa-spin"></i>' : '<i class="fas f |
| | | 430 | | return `<li class="${state}"><span class="tx-stage-icon">${icon}</span><span>${stage.label}</spa |
| | | 431 | | }).join(''); |
| | | 432 | | } |
| | | 433 | | |
| | | 434 | | const note = overlay.querySelector('.tx-submit-overlay-details p:last-child'); |
| | | 435 | | if (note) { |
| | | 436 | | note.textContent = txProgress.stages.length > 0 |
| | | 437 | | ? 'Этапы ниже приходят с сервера по мере выполнения — это фактический ход проведения.' |
| | | 438 | | : 'Этапы ниже — приблизительные: сервер не сообщает свой прогресс, поэтому они оцениваются по вр |
| | | 439 | | } |
| | | 440 | | |
| | | 441 | | const hint = overlay.querySelector('.tx-submit-overlay-hint'); |
| | | 442 | | if (hint) { |
| | | 443 | | const text = txProgress.stages.length > 0 && elapsed < STUCK_HINT_DELAY_MS |
| | | 444 | | ? '' |
| | | 445 | | : escalationText(elapsed); |
| | | 446 | | hint.textContent = text; |
| | | 447 | | hint.style.display = text ? 'block' : 'none'; |
| | | 448 | | } |
| | | 449 | | } |
| | | 450 | | |
| | | 451 | | const showOverlay = function () { |
| | | 452 | | // Идемпотентность обязательна: showOverlay дёргают и мост legacy-модалок, и патч |
| | | 453 | | // jQuery.fn.modal, и хук #confirmSubmit — иначе таймер сбрасывался бы на 00:00. |
| | | 454 | | if (overlay.style.display === 'flex') return; |
| | | 455 | | txProgress.reset(); |
| | | 456 | | clampQuantityInputsBeforeSubmit(); |
| | | 457 | | disableLegacyLoaders(); |
| | | 458 | | overlayStartedAt = Date.now(); |
| | | 459 | | renderOverlayProgress(buildStages()); |
| | | 460 | | if (overlayTimer) clearInterval(overlayTimer); |
| | | 461 | | // Список пересобираем на каждом тике: этапы приходят с сервера по ходу проведения, |
| | | 462 | | // и зафиксированный при показе набор затирал бы их обратно на оценочный. |
| | | 463 | | overlayTimer = setInterval(function () { renderOverlayProgress(buildStages()); }, 500); |
| | | 464 | | overlay.style.display = 'flex'; |
| | | 465 | | }; |
| | | 466 | | const hideOverlay = function () { |
| | | 467 | | overlay.style.display = 'none'; |
| | | 468 | | hidePageLoadingBar(); |
| | | 469 | | if (overlayTimer) { |
| | | 470 | | clearInterval(overlayTimer); |
| | | 471 | | overlayTimer = null; |
| | | 472 | | } |
| | | 473 | | }; |
| | | 474 | | |
| | | 475 | | window.txShowSubmitOverlay = showOverlay; |
| | | 476 | | window.txHideSubmitOverlay = hideOverlay; |
| | | 477 | | |
| | | 478 | | function blockBootstrapLegacyModal() { |
| | | 479 | | ['staticBackdrop', 'loadingModal'].forEach(function (id) { |
| | | 480 | | const el = document.getElementById(id); |
| | | 481 | | if (!el || el.__txModalBlocked) return; |
| | | 482 | | el.addEventListener('show.bs.modal', function (e) { |
| | | 483 | | e.preventDefault(); |
| | | 484 | | e.stopImmediatePropagation(); |
| | | 485 | | showOverlay(); |
| | | 486 | | }); |
| | | 487 | | el.__txModalBlocked = true; |
| | | 488 | | }); |
| | | 489 | | } |
| | | 490 | | |
| | | 491 | | function bridgeLegacyModalCalls() { |
| | | 492 | | if (!window.jQuery || !jQuery.fn || !jQuery.fn.modal) return; |
| | | 493 | | if (jQuery.fn.modal.__txPatched) return; |
| | | 494 | | |
| | | 495 | | const originalModal = jQuery.fn.modal; |
| | | 496 | | jQuery.fn.modal = function (arg) { |
| | | 497 | | const el = this && this.length ? this[0] : null; |
| | | 498 | | const id = el && el.id ? el.id : ''; |
| | | 499 | | const isLegacyLoader = id === 'staticBackdrop' || id === 'loadingModal'; |
| | | 500 | | const isValidationModal = id === 'myModal'; |
| | | 501 | | if (isLegacyLoader) { |
| | | 502 | | const action = typeof arg === 'string' ? arg : (arg && arg.show ? 'show' : ''); |
| | | 503 | | if (action === 'hide') { |
| | | 504 | | hideOverlay(); |
| | | 505 | | } else { |
| | | 506 | | showOverlay(); |
| | | 507 | | } |
| | | 508 | | return this; |
| | | 509 | | } |
| | | 510 | | if (isValidationModal) { |
| | | 511 | | const action = typeof arg === 'string' ? arg : (arg && arg.show ? 'show' : ''); |
| | | 512 | | if (action !== 'hide') { |
| | | 513 | | hideOverlay(); |
| | | 514 | | setTimeout(function () { |
| | | 515 | | if (window.txValidateRequiredFields) { |
| | | 516 | | window.txValidateRequiredFields(form); |
| | | 517 | | } |
| | | 518 | | if (window.txSyncInvalidFieldHighlights) { |
| | | 519 | | window.txSyncInvalidFieldHighlights(form); |
| | | 520 | | } |
| | | 521 | | }, 0); |
| | | 522 | | } |
| | | 523 | | } |
| | | 524 | | return originalModal.apply(this, arguments); |
| | | 525 | | }; |
| | | 526 | | jQuery.fn.modal.__txPatched = true; |
| | | 527 | | } |
| | | 528 | | |
| | | 529 | | disableLegacyLoaders(); |
| | | 530 | | blockBootstrapLegacyModal(); |
| | | 531 | | bridgeLegacyModalCalls(); |
| | | 532 | | |
| | | 533 | | function hookValidationModal() { |
| | | 534 | | const myModal = document.getElementById('myModal'); |
| | | 535 | | if (!myModal || myModal.__txValidationModalHooked) return; |
| | | 536 | | const titleElement = myModal.querySelector('.modal-title'); |
| | | 537 | | const bodyElement = myModal.querySelector('.modal-body'); |
| | | 538 | | // Модалка общая для клиентской валидации и для серверных ошибок проведения: |
| | | 539 | | // запоминаем исходный вид, чтобы вернуть его после показа серверной ошибки. |
| | | 540 | | myModal.__txDefaultTitle = titleElement ? titleElement.textContent : ''; |
| | | 541 | | myModal.__txDefaultBody = bodyElement ? bodyElement.innerHTML : ''; |
| | | 542 | | const highlightRequiredFields = function () { |
| | | 543 | | if (window.txValidateRequiredFields) { |
| | | 544 | | window.txValidateRequiredFields(form); |
| | | 545 | | } |
| | | 546 | | if (window.txSyncInvalidFieldHighlights) { |
| | | 547 | | window.txSyncInvalidFieldHighlights(form); |
| | | 548 | | } |
| | | 549 | | }; |
| | | 550 | | myModal.addEventListener('show.bs.modal', function () { |
| | | 551 | | hideOverlay(); |
| | | 552 | | if (!myModal.__txServerErrorShown) { |
| | | 553 | | if (titleElement) titleElement.textContent = myModal.__txDefaultTitle; |
| | | 554 | | // Тело восстанавливаем только если перезаписывали его сами: вьюхи |
| | | 555 | | // с #modalErrorMessage подставляют собственный текст перед показом. |
| | | 556 | | if (bodyElement && myModal.__txBodyOverwritten) { |
| | | 557 | | bodyElement.innerHTML = myModal.__txDefaultBody; |
| | | 558 | | myModal.__txBodyOverwritten = false; |
| | | 559 | | } |
| | | 560 | | } |
| | | 561 | | highlightRequiredFields(); |
| | | 562 | | }); |
| | | 563 | | myModal.addEventListener('shown.bs.modal', function () { |
| | | 564 | | highlightRequiredFields(); |
| | | 565 | | requestAnimationFrame(highlightRequiredFields); |
| | | 566 | | }); |
| | | 567 | | myModal.addEventListener('hidden.bs.modal', function () { |
| | | 568 | | myModal.__txServerErrorShown = false; |
| | | 569 | | }); |
| | | 570 | | myModal.__txValidationModalHooked = true; |
| | | 571 | | } |
| | | 572 | | |
| | | 573 | | hookValidationModal(); |
| | | 574 | | |
| | | 575 | | if (window.showFriendlyRequestError && !window.showFriendlyRequestError.__txWrapped) { |
| | | 576 | | const originalShowFriendlyRequestError = window.showFriendlyRequestError; |
| | | 577 | | window.showFriendlyRequestError = function () { |
| | | 578 | | hideOverlay(); |
| | | 579 | | if (window.txValidateRequiredFields) { |
| | | 580 | | window.txValidateRequiredFields(); |
| | | 581 | | } |
| | | 582 | | if (window.txSyncInvalidFieldHighlights) { |
| | | 583 | | window.txSyncInvalidFieldHighlights(); |
| | | 584 | | } |
| | | 585 | | return originalShowFriendlyRequestError.apply(this, arguments); |
| | | 586 | | }; |
| | | 587 | | window.showFriendlyRequestError.__txWrapped = true; |
| | | 588 | | } |
| | | 589 | | |
| | | 590 | | function clampQuantityInputsBeforeSubmit() { |
| | | 591 | | document.querySelectorAll('.quantity-input-field').forEach(function (input) { |
| | | 592 | | const originalQuantity = parseInt(input.dataset.originalQuantity, 10); |
| | | 593 | | let currentQuantity = parseInt(input.value, 10); |
| | | 594 | | if (isNaN(currentQuantity) || currentQuantity < 0) { |
| | | 595 | | currentQuantity = 0; |
| | | 596 | | } |
| | | 597 | | if (!isNaN(originalQuantity) && currentQuantity > originalQuantity) { |
| | | 598 | | input.value = String(originalQuantity); |
| | | 599 | | input.dispatchEvent(new Event('input', { bubbles: true })); |
| | | 600 | | } |
| | | 601 | | }); |
| | | 602 | | } |
| | | 603 | | |
| | | 604 | | // Оверлей только после подтверждения в transactionModal, не при открытии формы. |
| | | 605 | | const confirmSubmitBtn = document.getElementById('confirmSubmit'); |
| | | 606 | | if (confirmSubmitBtn) { |
| | | 607 | | confirmSubmitBtn.addEventListener('click', function () { |
| | | 608 | | setTimeout(function () { |
| | | 609 | | if (document.body.contains(overlay) && overlay.style.display !== 'flex') { |
| | | 610 | | showOverlay(); |
| | | 611 | | } |
| | | 612 | | }, 120); |
| | | 613 | | }); |
| | | 614 | | } |
| | | 615 | | |
| | | 616 | | if (window.jQuery) { |
| | | 617 | | jQuery(document).ajaxComplete(function (_e, _xhr, settings) { |
| | | 618 | | const url = settings && settings.url ? String(settings.url) : ''; |
| | | 619 | | const isTransactionSubmit = url.indexOf('Transaction/Create') >= 0 |
| | | 620 | | || /\/(?:Studio2\/)?\w+Transaction\//.test(url); |
| | | 621 | | if (!isTransactionSubmit) return; |
| | | 622 | | setTimeout(hideOverlay, 250); |
| | | 623 | | }); |
| | | 624 | | jQuery(document).ajaxError(function (_e, _xhr, settings) { |
| | | 625 | | const url = settings && settings.url ? String(settings.url) : ''; |
| | | 626 | | const isTransactionSubmit = url.indexOf('Transaction/Create') >= 0 |
| | | 627 | | || /\/(?:Studio2\/)?\w+Transaction\//.test(url); |
| | | 628 | | if (!isTransactionSubmit) return; |
| | | 629 | | setTimeout(hideOverlay, 250); |
| | | 630 | | }); |
| | | 631 | | } |
| | | 632 | | |
| | | 633 | | window.addEventListener('pageshow', hideOverlay); |
| | | 634 | | window.addEventListener('error', hideOverlay); |
| | | 635 | | window.addEventListener('unhandledrejection', hideOverlay); |
| | | 636 | | } |
| | | 637 | | |
| | | 638 | | function initInvalidFieldHighlighting() { |
| | | 639 | | const invalidFieldSelector = [ |
| | | 640 | | '.purchase-price-field', |
| | | 641 | | '.supplier-select', |
| | | 642 | | '.warehouse-select', |
| | | 643 | | '.orderNumberToSupplie-field', |
| | | 644 | | '.product-weight-field', |
| | | 645 | | '.quantity-input-field', |
| | | 646 | | '#warehouseSender', |
| | | 647 | | '#warehouseRecipient', |
| | | 648 | | '.price-sale-display' |
| | | 649 | | ].join(', '); |
| | | 650 | | |
| | | 651 | | function syncCustomSelectTrigger(field, isInvalid) { |
| | | 652 | | if (!field) return; |
| | | 653 | | const wrapper = field.closest('.cs-wrapper'); |
| | | 654 | | if (!wrapper) return; |
| | | 655 | | const trigger = wrapper.querySelector('.cs-trigger'); |
| | | 656 | | if (trigger) { |
| | | 657 | | trigger.classList.toggle('highlight-error', !!isInvalid); |
| | | 658 | | if (isInvalid) { |
| | | 659 | | trigger.style.setProperty('border-color', '#dc3545', 'important'); |
| | | 660 | | trigger.style.setProperty('background-color', '#fef2f2', 'important'); |
| | | 661 | | trigger.style.setProperty('box-shadow', 'inset 0 0 0 1px rgba(220, 53, 69, 0.55)', 'important'); |
| | | 662 | | } else { |
| | | 663 | | trigger.style.removeProperty('border-color'); |
| | | 664 | | trigger.style.removeProperty('background-color'); |
| | | 665 | | trigger.style.removeProperty('box-shadow'); |
| | | 666 | | } |
| | | 667 | | } |
| | | 668 | | } |
| | | 669 | | |
| | | 670 | | function isFieldValueEmpty(field) { |
| | | 671 | | const val = (field.value || '').trim(); |
| | | 672 | | if (field.classList.contains('supplier-select')) { |
| | | 673 | | const selectedText = field.options[field.selectedIndex]?.text?.trim() || ''; |
| | | 674 | | return val === '' || val === '1' |
| | | 675 | | || selectedText.startsWith('Не указан') |
| | | 676 | | || selectedText.startsWith('Выберите'); |
| | | 677 | | } |
| | | 678 | | return val === ''; |
| | | 679 | | } |
| | | 680 | | |
| | | 681 | | function markInvalidRows() { |
| | | 682 | | form.querySelectorAll('tbody tr').forEach(function (row) { |
| | | 683 | | const hasInvalid = !!row.querySelector('.is-invalid, .highlight-error, .input-validation-error'); |
| | | 684 | | row.classList.toggle('tx-row-has-invalid', hasInvalid); |
| | | 685 | | }); |
| | | 686 | | } |
| | | 687 | | |
| | | 688 | | window.txMarkFieldInvalid = function (field) { |
| | | 689 | | if (!field) return; |
| | | 690 | | field.classList.add('is-invalid', 'highlight-error'); |
| | | 691 | | syncCustomSelectTrigger(field, true); |
| | | 692 | | field.closest('td')?.classList.add('tx-field-invalid'); |
| | | 693 | | markInvalidRows(); |
| | | 694 | | }; |
| | | 695 | | |
| | | 696 | | window.txClearFieldInvalid = function (field) { |
| | | 697 | | if (!field) return; |
| | | 698 | | field.classList.remove('is-invalid', 'highlight-error', 'input-validation-error'); |
| | | 699 | | syncCustomSelectTrigger(field, false); |
| | | 700 | | const cell = field.closest('td'); |
| | | 701 | | if (cell && !cell.querySelector('.is-invalid, .highlight-error, .input-validation-error')) { |
| | | 702 | | cell.classList.remove('tx-field-invalid'); |
| | | 703 | | } |
| | | 704 | | markInvalidRows(); |
| | | 705 | | }; |
| | | 706 | | |
| | | 707 | | window.txSyncInvalidFieldHighlights = function (scope) { |
| | | 708 | | const root = scope || form; |
| | | 709 | | root.querySelectorAll('.is-invalid, .highlight-error, .input-validation-error').forEach(function (field) |
| | | 710 | | window.txMarkFieldInvalid(field); |
| | | 711 | | }); |
| | | 712 | | markInvalidRows(); |
| | | 713 | | |
| | | 714 | | const firstInvalid = root.querySelector('.is-invalid, .highlight-error, .input-validation-error'); |
| | | 715 | | if (!firstInvalid) return; |
| | | 716 | | const scrollTarget = firstInvalid.closest('.cs-wrapper') |
| | | 717 | | ? firstInvalid.closest('.cs-wrapper').querySelector('.cs-trigger') |
| | | 718 | | : firstInvalid; |
| | | 719 | | if (scrollTarget && typeof scrollTarget.scrollIntoView === 'function') { |
| | | 720 | | scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| | | 721 | | } |
| | | 722 | | }; |
| | | 723 | | |
| | | 724 | | form.querySelectorAll(invalidFieldSelector).forEach(function (field) { |
| | | 725 | | const clearIfFilled = function () { |
| | | 726 | | if (!field.classList.contains('is-invalid') && !field.classList.contains('highlight-error')) { |
| | | 727 | | return; |
| | | 728 | | } |
| | | 729 | | if (!isFieldValueEmpty(field)) { |
| | | 730 | | window.txClearFieldInvalid(field); |
| | | 731 | | } |
| | | 732 | | }; |
| | | 733 | | field.addEventListener('input', clearIfFilled); |
| | | 734 | | field.addEventListener('change', clearIfFilled); |
| | | 735 | | }); |
| | | 736 | | |
| | | 737 | | // Автоочистка подсветки цены при изменении hidden price input |
| | | 738 | | form.querySelectorAll('.price-sale-field').forEach(function (field) { |
| | | 739 | | field.addEventListener('input', function () { |
| | | 740 | | const orderId = field.dataset.orderId; |
| | | 741 | | if (!orderId) return; |
| | | 742 | | const display = document.getElementById('Price_' + orderId); |
| | | 743 | | if (display && parseFloat(field.value) > 0) { |
| | | 744 | | display.classList.remove('is-invalid', 'highlight-error'); |
| | | 745 | | markInvalidRows(); |
| | | 746 | | } |
| | | 747 | | }); |
| | | 748 | | field.addEventListener('change', function () { field.dispatchEvent(new Event('input')); }); |
| | | 749 | | }); |
| | | 750 | | |
| | | 751 | | if (window.jQuery && !jQuery.fn.__txInvalidClassPatched) { |
| | | 752 | | const originalAddClass = jQuery.fn.addClass; |
| | | 753 | | jQuery.fn.addClass = function (className) { |
| | | 754 | | const result = originalAddClass.apply(this, arguments); |
| | | 755 | | if (typeof className === 'string' && className.indexOf('is-invalid') >= 0) { |
| | | 756 | | this.each(function () { |
| | | 757 | | if (this.closest && this.closest('#createOrderForm') && window.txMarkFieldInvalid) { |
| | | 758 | | window.txMarkFieldInvalid(this); |
| | | 759 | | } |
| | | 760 | | }); |
| | | 761 | | } |
| | | 762 | | return result; |
| | | 763 | | }; |
| | | 764 | | |
| | | 765 | | const originalRemoveClass = jQuery.fn.removeClass; |
| | | 766 | | jQuery.fn.removeClass = function (className) { |
| | | 767 | | const result = originalRemoveClass.apply(this, arguments); |
| | | 768 | | if (typeof className === 'string' && className.indexOf('is-invalid') >= 0) { |
| | | 769 | | this.each(function () { |
| | | 770 | | if (this.closest && this.closest('#createOrderForm') && window.txClearFieldInvalid) { |
| | | 771 | | window.txClearFieldInvalid(this); |
| | | 772 | | } |
| | | 773 | | }); |
| | | 774 | | } |
| | | 775 | | return result; |
| | | 776 | | }; |
| | | 777 | | jQuery.fn.__txInvalidClassPatched = true; |
| | | 778 | | } |
| | | 779 | | |
| | | 780 | | function shouldSkipOrderValidation(orderId, root) { |
| | | 781 | | const statusSelect = document.getElementById('AppStatusId_' + orderId); |
| | | 782 | | const selectedStatusText = statusSelect && statusSelect.options[statusSelect.selectedIndex] |
| | | 783 | | ? statusSelect.options[statusSelect.selectedIndex].text.trim() |
| | | 784 | | : ''; |
| | | 785 | | if (selectedStatusText === 'Отменен' || selectedStatusText === 'К отмене') { |
| | | 786 | | return true; |
| | | 787 | | } |
| | | 788 | | |
| | | 789 | | const quantityInput = (root || form).querySelector('.quantity-input-field[data-order-id="' + orderId + ' |
| | | 790 | | || document.getElementById('Quantity_' + orderId); |
| | | 791 | | if (quantityInput) { |
| | | 792 | | const quantity = parseFloat(quantityInput.value); |
| | | 793 | | if (!isNaN(quantity) && quantity === 0) { |
| | | 794 | | return true; |
| | | 795 | | } |
| | | 796 | | } |
| | | 797 | | return false; |
| | | 798 | | } |
| | | 799 | | |
| | | 800 | | window.txValidateRequiredFields = function (scope) { |
| | | 801 | | const root = scope || form; |
| | | 802 | | if (!root) { |
| | | 803 | | return true; |
| | | 804 | | } |
| | | 805 | | |
| | | 806 | | let allValid = true; |
| | | 807 | | |
| | | 808 | | function validateOrderField(field, isEmpty) { |
| | | 809 | | if (!field || field.disabled) { |
| | | 810 | | return; |
| | | 811 | | } |
| | | 812 | | const orderId = (field.id || '').split('_')[1]; |
| | | 813 | | if (orderId && shouldSkipOrderValidation(orderId, root)) { |
| | | 814 | | window.txClearFieldInvalid(field); |
| | | 815 | | return; |
| | | 816 | | } |
| | | 817 | | if (isEmpty(field)) { |
| | | 818 | | allValid = false; |
| | | 819 | | window.txMarkFieldInvalid(field); |
| | | 820 | | } else { |
| | | 821 | | window.txClearFieldInvalid(field); |
| | | 822 | | } |
| | | 823 | | } |
| | | 824 | | |
| | | 825 | | root.querySelectorAll('.purchase-price-field').forEach(function (field) { |
| | | 826 | | if (field.dataset.priceRequired === 'false') { |
| | | 827 | | window.txClearFieldInvalid(field); |
| | | 828 | | return; |
| | | 829 | | } |
| | | 830 | | validateOrderField(field, function (f) { |
| | | 831 | | return (f.value || '').trim() === ''; |
| | | 832 | | }); |
| | | 833 | | }); |
| | | 834 | | |
| | | 835 | | root.querySelectorAll('.price-sale-field').forEach(function (field) { |
| | | 836 | | const orderId = field.dataset.orderId; |
| | | 837 | | if (!orderId || shouldSkipOrderValidation(orderId, root)) return; |
| | | 838 | | const display = document.getElementById('Price_' + orderId); |
| | | 839 | | const isEmpty = !field.value || parseFloat(field.value) <= 0; |
| | | 840 | | if (isEmpty) { |
| | | 841 | | allValid = false; |
| | | 842 | | if (display) { display.classList.add('is-invalid', 'highlight-error'); } |
| | | 843 | | } else { |
| | | 844 | | if (display) { display.classList.remove('is-invalid', 'highlight-error'); } |
| | | 845 | | } |
| | | 846 | | }); |
| | | 847 | | |
| | | 848 | | root.querySelectorAll('.supplier-select').forEach(function (field) { |
| | | 849 | | validateOrderField(field, function (f) { |
| | | 850 | | const val = (f.value || '').trim(); |
| | | 851 | | const selectedText = f.options[f.selectedIndex]?.text?.trim() || ''; |
| | | 852 | | return val === '' || val === '1' |
| | | 853 | | || selectedText.startsWith('Не указан') |
| | | 854 | | || selectedText.startsWith('Выберите'); |
| | | 855 | | }); |
| | | 856 | | }); |
| | | 857 | | |
| | | 858 | | root.querySelectorAll('.warehouse-select').forEach(function (field) { |
| | | 859 | | validateOrderField(field, function (f) { |
| | | 860 | | return (f.value || '').trim() === ''; |
| | | 861 | | }); |
| | | 862 | | }); |
| | | 863 | | |
| | | 864 | | ['#warehouseSender', '#warehouseRecipient'].forEach(function (selector) { |
| | | 865 | | const field = root.querySelector(selector) || document.querySelector(selector); |
| | | 866 | | if (!field) { |
| | | 867 | | return; |
| | | 868 | | } |
| | | 869 | | if ((field.value || '').trim() === '') { |
| | | 870 | | allValid = false; |
| | | 871 | | window.txMarkFieldInvalid(field); |
| | | 872 | | } else { |
| | | 873 | | window.txClearFieldInvalid(field); |
| | | 874 | | } |
| | | 875 | | }); |
| | | 876 | | |
| | | 877 | | markInvalidRows(); |
| | | 878 | | if (!allValid) { |
| | | 879 | | const invalidFields = [...root.querySelectorAll('.is-invalid, .highlight-error, .input-validation-er |
| | | 880 | | .map(function (field) { |
| | | 881 | | return { |
| | | 882 | | id: field.id || null, |
| | | 883 | | name: field.name || null, |
| | | 884 | | value: field.value || null, |
| | | 885 | | text: field.options?.[field.selectedIndex]?.text?.trim() || null |
| | | 886 | | }; |
| | | 887 | | }); |
| | | 888 | | console.warn('[Transaction validation] Незаполненные поля', invalidFields); |
| | | 889 | | } |
| | | 890 | | return allValid; |
| | | 891 | | }; |
| | | 892 | | |
| | | 893 | | window.txHandleTransactionSubmitError = function (message) { |
| | | 894 | | window.txValidateRequiredFields(form); |
| | | 895 | | window.txSyncInvalidFieldHighlights(form); |
| | | 896 | | |
| | | 897 | | const fallback = 'Одно или несколько полей не заполнены. Пожалуйста, заполните все поля перед отправкой |
| | | 898 | | const hasServerMessage = String(message ?? '').trim() !== ''; |
| | | 899 | | const html = String(message || fallback).replace(/\n/g, '<br>'); |
| | | 900 | | const myModal = document.getElementById('myModal'); |
| | | 901 | | if (myModal && window.jQuery && jQuery.fn.modal) { |
| | | 902 | | const msgEl = document.getElementById('modalErrorMessage'); |
| | | 903 | | const body = myModal.querySelector('.modal-body'); |
| | | 904 | | const titleElement = myModal.querySelector('.modal-title'); |
| | | 905 | | // Серверная ошибка проведения — не «Поля не заполнены»: заголовок ставим по смыслу. |
| | | 906 | | myModal.__txServerErrorShown = hasServerMessage; |
| | | 907 | | if (titleElement) { |
| | | 908 | | titleElement.textContent = hasServerMessage |
| | | 909 | | ? 'Не удалось провести транзакцию' |
| | | 910 | | : (myModal.__txDefaultTitle || titleElement.textContent); |
| | | 911 | | } |
| | | 912 | | if (msgEl) { |
| | | 913 | | msgEl.innerHTML = html; |
| | | 914 | | } else if (body) { |
| | | 915 | | body.innerHTML = html; |
| | | 916 | | myModal.__txBodyOverwritten = true; |
| | | 917 | | } |
| | | 918 | | jQuery(myModal).modal('show'); |
| | | 919 | | return; |
| | | 920 | | } |
| | | 921 | | |
| | | 922 | | if (window.showFriendlyRequestError) { |
| | | 923 | | window.showFriendlyRequestError( |
| | | 924 | | { responseText: message || fallback, status: 400, statusText: 'Validation Error' }, |
| | | 925 | | 'error', |
| | | 926 | | '', |
| | | 927 | | 'Ошибка при отправке формы' |
| | | 928 | | ); |
| | | 929 | | } |
| | | 930 | | }; |
| | | 931 | | } |
| | | 932 | | |
| | | 933 | | function initTransactionSubmitAjaxHooks() { |
| | | 934 | | if (!window.jQuery || window.jQuery.__txSubmitPrefilterInstalled) { |
| | | 935 | | return; |
| | | 936 | | } |
| | | 937 | | |
| | | 938 | | window.jQuery.ajaxPrefilter(function (options) { |
| | | 939 | | if ((options.type || 'GET').toUpperCase() !== 'POST') { |
| | | 940 | | return; |
| | | 941 | | } |
| | | 942 | | const url = options.url || ''; |
| | | 943 | | if (!/\/(?:Studio2\/)?\w+Transaction\//.test(url)) { |
| | | 944 | | return; |
| | | 945 | | } |
| | | 946 | | |
| | | 947 | | const originalSuccess = options.success; |
| | | 948 | | options.success = function (data, textStatus, jqXHR) { |
| | | 949 | | if (data && data.error) { |
| | | 950 | | window.__criticalOperationInProgress = false; |
| | | 951 | | // Вьюха может держать собственный флаг «отправка идёт»; success страницы здесь |
| | | 952 | | // не вызывается, поэтому сбрасываем состояние явно. |
| | | 953 | | if (window.txResetSubmitState) { |
| | | 954 | | window.txResetSubmitState(); |
| | | 955 | | } |
| | | 956 | | if (window.txHideSubmitOverlay) { |
| | | 957 | | window.txHideSubmitOverlay(); |
| | | 958 | | } |
| | | 959 | | if (window.txHandleTransactionSubmitError) { |
| | | 960 | | window.txHandleTransactionSubmitError(data.error); |
| | | 961 | | } |
| | | 962 | | return; |
| | | 963 | | } |
| | | 964 | | if (originalSuccess) { |
| | | 965 | | return originalSuccess.apply(this, arguments); |
| | | 966 | | } |
| | | 967 | | }; |
| | | 968 | | |
| | | 969 | | const originalError = options.error; |
| | | 970 | | options.error = function (xhr, status, error) { |
| | | 971 | | window.__criticalOperationInProgress = false; |
| | | 972 | | if (window.txResetSubmitState) { |
| | | 973 | | window.txResetSubmitState(); |
| | | 974 | | } |
| | | 975 | | if (window.txHideSubmitOverlay) { |
| | | 976 | | window.txHideSubmitOverlay(); |
| | | 977 | | } |
| | | 978 | | // Сервер лежит (деплой/перезапуск) — причина не в данных формы. Наблюдатель |
| | | 979 | | // доступности покажет свою модалку, а «Не удалось провести транзакцию» |
| | | 980 | | // и подсветка «незаполненных» полей здесь только запутали бы. |
| | | 981 | | if (window.ServerAvailability |
| | | 982 | | && typeof window.ServerAvailability.handleRequestFailure === 'function' |
| | | 983 | | && window.ServerAvailability.handleRequestFailure(xhr, status, error)) { |
| | | 984 | | if (originalError) { |
| | | 985 | | return originalError.apply(this, arguments); |
| | | 986 | | } |
| | | 987 | | return; |
| | | 988 | | } |
| | | 989 | | if (window.txValidateRequiredFields) { |
| | | 990 | | window.txValidateRequiredFields(); |
| | | 991 | | } |
| | | 992 | | if (window.txSyncInvalidFieldHighlights) { |
| | | 993 | | window.txSyncInvalidFieldHighlights(); |
| | | 994 | | } |
| | | 995 | | if (window.txHandleTransactionSubmitError) { |
| | | 996 | | const detail = window.extractFriendlyErrorMessage |
| | | 997 | | ? window.extractFriendlyErrorMessage(xhr, status, error) |
| | | 998 | | : (xhr?.responseText || error || status || 'Ошибка при отправке формы'); |
| | | 999 | | window.txHandleTransactionSubmitError(detail); |
| | | 1000 | | } |
| | | 1001 | | if (originalError) { |
| | | 1002 | | return originalError.apply(this, arguments); |
| | | 1003 | | } |
| | | 1004 | | }; |
| | | 1005 | | }); |
| | | 1006 | | |
| | | 1007 | | window.jQuery.__txSubmitPrefilterInstalled = true; |
| | | 1008 | | } |
| | | 1009 | | |
| | | 1010 | | function initKeyboardNav() { |
| | | 1011 | | form.addEventListener('keydown', function (e) { |
| | | 1012 | | if (e.key !== 'Enter') return; |
| | | 1013 | | const target = e.target; |
| | | 1014 | | if (!target || target.tagName === 'TEXTAREA') return; |
| | | 1015 | | if (!target.matches('input, select')) return; |
| | | 1016 | | e.preventDefault(); |
| | | 1017 | | const fields = Array.prototype.slice.call(form.querySelectorAll( |
| | | 1018 | | '.supplier-select, .status-select, .purchase-price-field, .product-weight-field, .orderNumberToSuppl |
| | | 1019 | | )).filter(function (el) { return !el.disabled && el.offsetParent !== null; }); |
| | | 1020 | | const idx = fields.indexOf(target); |
| | | 1021 | | if (idx >= 0 && idx < fields.length - 1) fields[idx + 1].focus(); |
| | | 1022 | | }); |
| | | 1023 | | } |
| | | 1024 | | |
| | | 1025 | | function initLocalDraft() { |
| | | 1026 | | // Один черновик на форму вместо ключа на каждое поле. Контекст включает страницу |
| | | 1027 | | // и фактические ID заказов, а поле внутри черновика привязано к ID заказа, не к индексу строки. |
| | | 1028 | | const storageKey = 'txDraft:v3'; |
| | | 1029 | | const action = new URL(form.action || location.href, location.origin).pathname; |
| | | 1030 | | const page = form.querySelector('[name="RedirectPage"]')?.value || '1'; |
| | | 1031 | | const orderIds = Array.from(form.querySelectorAll('.transaction-order-row[data-order-id]')) |
| | | 1032 | | .map(row => row.dataset.orderId) |
| | | 1033 | | .filter(Boolean) |
| | | 1034 | | .sort((left, right) => Number(left) - Number(right)); |
| | | 1035 | | const scope = `${action}:page=${page}:orders=${orderIds.join(',')}`; |
| | | 1036 | | const fields = Array.from(form.querySelectorAll( |
| | | 1037 | | 'input:not([type="hidden"]):not([type="file"]):not([type="submit"]):not([type="button"]), select, textar |
| | | 1038 | | )).filter(field => field.id || field.name); |
| | | 1039 | | |
| | | 1040 | | const getFieldKey = function (field) { |
| | | 1041 | | const orderId = field.closest('.transaction-order-row')?.dataset.orderId || 'document'; |
| | | 1042 | | return `${orderId}:${field.id || field.name}`; |
| | | 1043 | | }; |
| | | 1044 | | |
| | | 1045 | | let drafts = {}; |
| | | 1046 | | try { |
| | | 1047 | | drafts = JSON.parse(sessionStorage.getItem(storageKey) || '{}') || {}; |
| | | 1048 | | } catch { |
| | | 1049 | | drafts = {}; |
| | | 1050 | | } |
| | | 1051 | | |
| | | 1052 | | const draft = drafts[scope]?.fields || {}; |
| | | 1053 | | fields.forEach(function (field) { |
| | | 1054 | | const saved = draft[getFieldKey(field)]; |
| | | 1055 | | if (!saved) return; |
| | | 1056 | | |
| | | 1057 | | if (field.type === 'checkbox' || field.type === 'radio') { |
| | | 1058 | | field.checked = saved.checked === true; |
| | | 1059 | | } else { |
| | | 1060 | | field.value = saved.value ?? ''; |
| | | 1061 | | } |
| | | 1062 | | // Combobox открывает меню на input. При восстановлении значение уже установлено, |
| | | 1063 | | // поэтому ему достаточно change; иначе раскрывается «Засыл» в каждой строке. |
| | | 1064 | | if (!field.classList.contains('combobox-input')) { |
| | | 1065 | | field.dispatchEvent(new Event('input', { bubbles: true })); |
| | | 1066 | | } |
| | | 1067 | | field.dispatchEvent(new Event('change', { bubbles: true })); |
| | | 1068 | | }); |
| | | 1069 | | window.closeAllCombobox?.(); |
| | | 1070 | | |
| | | 1071 | | const saveDraft = function () { |
| | | 1072 | | const values = {}; |
| | | 1073 | | fields.forEach(function (field) { |
| | | 1074 | | values[getFieldKey(field)] = (field.type === 'checkbox' || field.type === 'radio') |
| | | 1075 | | ? { checked: field.checked } |
| | | 1076 | | : { value: field.value || '' }; |
| | | 1077 | | }); |
| | | 1078 | | drafts[scope] = { fields: values, updatedAt: Date.now() }; |
| | | 1079 | | |
| | | 1080 | | // sessionStorage содержит одну запись; оставляем только последние 20 контекстов формы. |
| | | 1081 | | const scopes = Object.keys(drafts).sort((left, right) => |
| | | 1082 | | (drafts[right]?.updatedAt || 0) - (drafts[left]?.updatedAt || 0)); |
| | | 1083 | | scopes.slice(20).forEach(oldScope => delete drafts[oldScope]); |
| | | 1084 | | try { |
| | | 1085 | | sessionStorage.setItem(storageKey, JSON.stringify(drafts)); |
| | | 1086 | | } catch (error) { |
| | | 1087 | | console.warn('Не удалось сохранить черновик транзакции', error); |
| | | 1088 | | } |
| | | 1089 | | }; |
| | | 1090 | | |
| | | 1091 | | fields.forEach(function (field) { |
| | | 1092 | | field.addEventListener('input', saveDraft); |
| | | 1093 | | field.addEventListener('change', saveDraft); |
| | | 1094 | | }); |
| | | 1095 | | |
| | | 1096 | | window.txClearLocalDraft = function () { |
| | | 1097 | | delete drafts[scope]; |
| | | 1098 | | try { |
| | | 1099 | | sessionStorage.setItem(storageKey, JSON.stringify(drafts)); |
| | | 1100 | | } catch (error) { |
| | | 1101 | | console.warn('Не удалось очистить черновик транзакции', error); |
| | | 1102 | | } |
| | | 1103 | | }; |
| | | 1104 | | |
| | | 1105 | | // Удаляем старые многочисленные записи v2 для этого действия после перехода на единый черновик. |
| | | 1106 | | const legacyPrefix = 'txDraft:v2:' + (form.getAttribute('action') || location.pathname) + ':'; |
| | | 1107 | | Object.keys(localStorage) |
| | | 1108 | | .filter(key => key.startsWith(legacyPrefix)) |
| | | 1109 | | .forEach(key => localStorage.removeItem(key)); |
| | | 1110 | | } |
| | | 1111 | | |
| | | 1112 | | function initCollapsibleSections() { |
| | | 1113 | | const targets = [ |
| | | 1114 | | { id: 'fileUploadContainer', title: 'Основные файлы и вложения', open: true }, |
| | | 1115 | | { id: 'selectedSuppliersList', title: 'Диагностика по поставщикам', open: false } |
| | | 1116 | | ]; |
| | | 1117 | | targets.forEach(function (cfg) { |
| | | 1118 | | const el = document.getElementById(cfg.id); |
| | | 1119 | | if (!el || el.closest('.tx-section')) return; |
| | | 1120 | | const section = document.createElement('div'); |
| | | 1121 | | section.className = 'tx-section'; |
| | | 1122 | | const header = document.createElement('div'); |
| | | 1123 | | header.className = 'tx-section-header'; |
| | | 1124 | | header.innerHTML = `<strong>${cfg.title}</strong><i class="fas fa-chevron-${cfg.open ? 'up' : 'down'}">< |
| | | 1125 | | const body = document.createElement('div'); |
| | | 1126 | | body.className = 'tx-section-body'; |
| | | 1127 | | if (!cfg.open) body.style.display = 'none'; |
| | | 1128 | | el.parentNode.insertBefore(section, el); |
| | | 1129 | | body.appendChild(el); |
| | | 1130 | | section.appendChild(header); |
| | | 1131 | | section.appendChild(body); |
| | | 1132 | | header.addEventListener('click', function () { |
| | | 1133 | | const opened = body.style.display !== 'none'; |
| | | 1134 | | body.style.display = opened ? 'none' : 'block'; |
| | | 1135 | | header.querySelector('i').className = 'fas fa-chevron-' + (opened ? 'down' : 'up'); |
| | | 1136 | | }); |
| | | 1137 | | }); |
| | | 1138 | | } |
| | | 1139 | | |
| | | 1140 | | function normalizeFeedbackLook() { |
| | | 1141 | | document.querySelectorAll('.modal .modal-header').forEach(function (h) { |
| | | 1142 | | h.classList.add('bg-light'); |
| | | 1143 | | }); |
| | | 1144 | | document.querySelectorAll('#flashMessage .toast-header').forEach(function (h) { |
| | | 1145 | | h.classList.add('bg-light'); |
| | | 1146 | | }); |
| | | 1147 | | } |
| | | 1148 | | |
| | | 1149 | | initKeyboardNav(); |
| | | 1150 | | initInvalidFieldHighlighting(); |
| | | 1151 | | initTransactionSubmitAjaxHooks(); |
| | | 1152 | | initLocalDraft(); |
| | | 1153 | | initUnifiedSubmitAnimation(); |
| | | 1154 | | initCollapsibleSections(); |
| | | 1155 | | normalizeFeedbackLook(); |
| | | 1156 | | }); |
| | | 1157 | | </script> |
| | | 1158 | | |
| | | 1159 | | <script> |
| | | 1160 | | // Функция для извлечения числового значения из строки |
| | | 1161 | | /** |
| | | 1162 | | * Метрика в ячейке транзакции: разряды разделяются узким неразрывным |
| | | 1163 | | * пробелом, длинное значение по-прежнему обрезается — колонки узкие. |
| | | 1164 | | */ |
| | | 1165 | | function txFormatMetric(value) { |
| | | 1166 | | if (isNaN(value)) return ''; |
| | | 1167 | | var text = Math.abs(value) > 9999 |
| | | 1168 | | ? String(value).slice(0, 4) + '...' |
| | | 1169 | | : String(value); |
| | | 1170 | | return text.replace(/\B(?=(\d{3})+(?!\d))/g, '\u202F'); |
| | | 1171 | | } |
| | | 1172 | | |
| | | 1173 | | function txMarkSign($el, value) { |
| | | 1174 | | if (!$el || !$el.length) return; |
| | | 1175 | | $el.css('color', ''); |
| | | 1176 | | $el.addClass('orders-num').toggleClass('orders-num--negative', value < 0); |
| | | 1177 | | } |
| | | 1178 | | |
| | | 1179 | | function extractNumber(text) { |
| | | 1180 | | if (typeof text !== 'string') { |
| | | 1181 | | text = String(text); // Преобразуем в строку, если это не строка |
| | | 1182 | | } |
| | | 1183 | | const number = parseFloat(text.replace(/\s/g, '').replace(',', '.')); |
| | | 1184 | | return isNaN(number) ? null : number; |
| | | 1185 | | } |
| | | 1186 | | |
| | | 1187 | | function readSupplierDataNumber($option, key, fallback) { |
| | | 1188 | | if (!$option || !$option.length) return fallback; |
| | | 1189 | | var raw = $option.data(key); |
| | | 1190 | | if (raw === undefined || raw === null || raw === '') return fallback; |
| | | 1191 | | var parsed = extractNumber(String(raw)); |
| | | 1192 | | return parsed === null ? fallback : parsed; |
| | | 1193 | | } |
| | | 1194 | | </script> |
| | | 1195 | | |
| | | 1196 | | <script> |
| | | 1197 | | document.addEventListener('DOMContentLoaded', (event) => { |
| | | 1198 | | const supplierSelects = document.querySelectorAll('.supplier-select'); |
| | | 1199 | | const statusSelects = document.querySelectorAll('.status-select'); |
| | | 1200 | | const selectedSuppliers = {}; |
| | | 1201 | | |
| | | 1202 | | // Сохраняем курсы валют в глобальной области видимости для использования в updateSelectedSuppliers |
| | | 1203 | | // Они будут доступны из jQuery кода выше |
| | | 1204 | | if (typeof window.rateUSD === 'undefined') { |
| | 0 | 1205 | | window.rateUSD = @Model.RateUSD.ToString().Replace(',', '.'); |
| | 0 | 1206 | | window.rateEUR = @Model.RateEUR.ToString().Replace(',', '.'); |
| | 0 | 1207 | | window.rateBYN = @Model.RateBYN.ToString().Replace(',', '.'); |
| | | 1208 | | } |
| | | 1209 | | |
| | | 1210 | | function updateSelectedSuppliers() { |
| | | 1211 | | for (const key in selectedSuppliers) { |
| | | 1212 | | if (selectedSuppliers.hasOwnProperty(key)) { |
| | | 1213 | | selectedSuppliers[key] = { |
| | | 1214 | | count: 0, |
| | | 1215 | | totalAmount: 0, |
| | | 1216 | | totalQuantity: 0, |
| | | 1217 | | totalPurchasePrice: 0, |
| | | 1218 | | // Суммы закупки в валюте поставщика по каждому коду валюты |
| | | 1219 | | currencyTotals: {} |
| | | 1220 | | }; |
| | | 1221 | | } |
| | | 1222 | | } |
| | | 1223 | | |
| | | 1224 | | const shipmentTableRows = document.querySelectorAll('#shipmentTable tbody tr'); |
| | | 1225 | | if (shipmentTableRows && shipmentTableRows.length > 0) { |
| | | 1226 | | shipmentTableRows.forEach((row) => { |
| | | 1227 | | if (!row) return; |
| | | 1228 | | |
| | | 1229 | | const statusSelect = row.querySelector('.status-select'); |
| | | 1230 | | if (!statusSelect || !statusSelect.options || statusSelect.selectedIndex < 0) return; |
| | | 1231 | | |
| | | 1232 | | const selectedStatus = statusSelect.options[statusSelect.selectedIndex]?.text?.trim() || ''; |
| | | 1233 | | |
| | | 1234 | | if (selectedStatus !== "Отменен") { |
| | | 1235 | | const supplierSelect = row.querySelector('.supplier-select'); |
| | | 1236 | | if (!supplierSelect || !supplierSelect.options || supplierSelect.selectedIndex < 0) return; |
| | | 1237 | | |
| | | 1238 | | const shipmentAmountElement = row.querySelector('[id^="shipmentAmount_"]'); |
| | | 1239 | | const shipmentAmount = shipmentAmountElement ? (extractNumber(shipmentAmountElement.textContent) |
| | | 1240 | | |
| | | 1241 | | // Получаем количество из поля ввода (quantity-input-field) или из скрытого поля |
| | | 1242 | | let quantity = 0; |
| | | 1243 | | const quantityInputElement = row.querySelector('.quantity-input-field'); |
| | | 1244 | | if (quantityInputElement) { |
| | | 1245 | | quantity = parseFloat(quantityInputElement.value) || 0; |
| | | 1246 | | } else { |
| | | 1247 | | // Если поля ввода нет, пытаемся получить из скрытого поля |
| | | 1248 | | const quantityHiddenElement = row.querySelector('.quantity-hidden-input'); |
| | | 1249 | | if (quantityHiddenElement) { |
| | | 1250 | | quantity = parseFloat(quantityHiddenElement.value) || 0; |
| | | 1251 | | } else { |
| | | 1252 | | // Если и скрытого поля нет, пытаемся получить из элемента с ID Quantity_ |
| | | 1253 | | const quantityElement = row.querySelector('[id^="Quantity_"]'); |
| | | 1254 | | if (quantityElement) { |
| | | 1255 | | // Пытаемся получить из отображаемого текста |
| | | 1256 | | const quantityDisplay = quantityElement.querySelector('.quantity-display'); |
| | | 1257 | | if (quantityDisplay) { |
| | | 1258 | | quantity = extractNumber(quantityDisplay.textContent) || 0; |
| | | 1259 | | } else { |
| | | 1260 | | quantity = extractNumber(quantityElement.textContent) || 0; |
| | | 1261 | | } |
| | | 1262 | | } |
| | | 1263 | | } |
| | | 1264 | | } |
| | | 1265 | | |
| | | 1266 | | // Получаем цену закупки в RUB для единицы товара |
| | | 1267 | | // Используем ту же логику, что и в calculateTotalPurchasePriceRub |
| | | 1268 | | let purchasePricePerUnit = 0; |
| | | 1269 | | |
| | | 1270 | | // Сначала пытаемся получить из скрытого поля PurchasePriceHidden (самый надежный способ) |
| | | 1271 | | const purchasePriceHiddenElement = row.querySelector('[id^="PurchasePriceHidden_"]'); |
| | | 1272 | | if (purchasePriceHiddenElement && purchasePriceHiddenElement.value) { |
| | | 1273 | | purchasePricePerUnit = parseFloat(purchasePriceHiddenElement.value) || 0; |
| | | 1274 | | } |
| | | 1275 | | |
| | | 1276 | | // Если не нашли, пытаемся получить из элемента с классом 'purchase-price-rub' |
| | | 1277 | | if (!purchasePricePerUnit || purchasePricePerUnit === 0) { |
| | | 1278 | | const purchasePriceRubElement = row.querySelector('.purchase-price-rub'); |
| | | 1279 | | if (purchasePriceRubElement) { |
| | | 1280 | | const textContent = purchasePriceRubElement.textContent.trim(); |
| | | 1281 | | purchasePricePerUnit = extractNumber(textContent) || 0; |
| | | 1282 | | } |
| | | 1283 | | } |
| | | 1284 | | |
| | | 1285 | | // Если не нашли, пытаемся получить из PurchasePriceRub_ по ID |
| | | 1286 | | if (!purchasePricePerUnit || purchasePricePerUnit === 0) { |
| | | 1287 | | const purchasePriceElement = row.querySelector('[id^="PurchasePriceRub_"]'); |
| | | 1288 | | if (purchasePriceElement) { |
| | | 1289 | | let purchasePriceText = purchasePriceElement.textContent || '0'; |
| | | 1290 | | // Убираем текст "RUB" и другие нечисловые символы |
| | | 1291 | | purchasePriceText = purchasePriceText.replace(/RUB|₽|руб|рублей/gi, '').trim(); |
| | | 1292 | | purchasePricePerUnit = extractNumber(purchasePriceText) || 0; |
| | | 1293 | | } |
| | | 1294 | | } |
| | | 1295 | | |
| | | 1296 | | // Если все еще не нашли, пытаемся рассчитать из PurchasePrice |
| | | 1297 | | if (!purchasePricePerUnit || purchasePricePerUnit === 0) { |
| | | 1298 | | const purchasePriceInputElement = row.querySelector('[id^="PurchasePrice_"]'); |
| | | 1299 | | if (purchasePriceInputElement && supplierSelect && supplierSelect.selectedIndex >= 0) { |
| | | 1300 | | const selectedOption = supplierSelect.options[supplierSelect.selectedIndex]; |
| | | 1301 | | const currencyCode = selectedOption.getAttribute('data-currency-code') || 'RUB'; |
| | | 1302 | | const costFactor = parseFloat(selectedOption.getAttribute('data-cost-factor') || '1'); |
| | | 1303 | | const purchasePriceValue = parseFloat(purchasePriceInputElement.value) || 0; |
| | | 1304 | | |
| | | 1305 | | // Рассчитываем цену в RUB с учетом валюты и коэффициента |
| | | 1306 | | const rateUSD = window.rateUSD || 1; |
| | | 1307 | | const rateEUR = window.rateEUR || 1; |
| | | 1308 | | const rateBYN = window.rateBYN || 1; |
| | | 1309 | | |
| | | 1310 | | switch (currencyCode) { |
| | | 1311 | | case 'USD': |
| | | 1312 | | purchasePricePerUnit = purchasePriceValue * costFactor * rateUSD; |
| | | 1313 | | break; |
| | | 1314 | | case 'EUR': |
| | | 1315 | | purchasePricePerUnit = purchasePriceValue * costFactor * rateEUR; |
| | | 1316 | | break; |
| | | 1317 | | case 'BYN': |
| | | 1318 | | purchasePricePerUnit = purchasePriceValue * costFactor * rateBYN; |
| | | 1319 | | break; |
| | | 1320 | | default: |
| | | 1321 | | purchasePricePerUnit = purchasePriceValue * costFactor; |
| | | 1322 | | break; |
| | | 1323 | | } |
| | | 1324 | | } |
| | | 1325 | | } |
| | | 1326 | | |
| | | 1327 | | // Умножаем цену за единицу на количество для получения общей цены закупки в RUB |
| | | 1328 | | const purchasePrice = purchasePricePerUnit * quantity; |
| | | 1329 | | // Сумма закупки в валюте поставщика (до пересчёта в RUB) |
| | | 1330 | | let purchasePriceOriginalTotal = 0; |
| | | 1331 | | |
| | | 1332 | | const selectedOption = supplierSelect.options[supplierSelect.selectedIndex]; |
| | | 1333 | | if (!selectedOption) return; |
| | | 1334 | | |
| | | 1335 | | const supplierName = selectedOption.text || ''; |
| | | 1336 | | |
| | | 1337 | | if (selectedOption.value) { |
| | | 1338 | | const currencyCode = selectedOption.getAttribute('data-currency-code') || 'RUB'; |
| | | 1339 | | |
| | | 1340 | | // Пытаемся получить исходную цену закупки (в валюте поставщика) и умножаем на количество |
| | | 1341 | | const purchasePriceInputElement = row.querySelector('[id^="PurchasePrice_"]'); |
| | | 1342 | | if (purchasePriceInputElement) { |
| | | 1343 | | const purchasePriceValueOriginal = parseFloat(purchasePriceInputElement.value) || 0; |
| | | 1344 | | if (purchasePriceValueOriginal > 0 && quantity > 0) { |
| | | 1345 | | purchasePriceOriginalTotal = purchasePriceValueOriginal * quantity; |
| | | 1346 | | } |
| | | 1347 | | } |
| | | 1348 | | |
| | | 1349 | | // purchasePrice уже содержит цену закупки с учетом количества (purchasePricePerUnit * quant |
| | | 1350 | | // Используем ту же логику расчета, что и в calculateTotalPurchasePriceRub |
| | | 1351 | | |
| | | 1352 | | if (selectedSuppliers[supplierName]) { |
| | | 1353 | | selectedSuppliers[supplierName].count++; |
| | | 1354 | | selectedSuppliers[supplierName].totalAmount += shipmentAmount; |
| | | 1355 | | selectedSuppliers[supplierName].totalQuantity += quantity; |
| | | 1356 | | selectedSuppliers[supplierName].totalPurchasePrice += purchasePrice; |
| | | 1357 | | if (purchasePriceOriginalTotal > 0) { |
| | | 1358 | | const totals = selectedSuppliers[supplierName].currencyTotals; |
| | | 1359 | | if (!totals[currencyCode]) { |
| | | 1360 | | totals[currencyCode] = 0; |
| | | 1361 | | } |
| | | 1362 | | totals[currencyCode] += purchasePriceOriginalTotal; |
| | | 1363 | | } |
| | | 1364 | | } else { |
| | | 1365 | | selectedSuppliers[supplierName] = { |
| | | 1366 | | count: 1, |
| | | 1367 | | totalAmount: shipmentAmount, |
| | | 1368 | | totalQuantity: quantity, |
| | | 1369 | | totalPurchasePrice: purchasePrice, |
| | | 1370 | | currencyTotals: {} |
| | | 1371 | | }; |
| | | 1372 | | if (purchasePriceOriginalTotal > 0) { |
| | | 1373 | | selectedSuppliers[supplierName].currencyTotals[currencyCode] = purchasePriceOriginal |
| | | 1374 | | } |
| | | 1375 | | } |
| | | 1376 | | } |
| | | 1377 | | } |
| | | 1378 | | }); |
| | | 1379 | | } |
| | | 1380 | | |
| | | 1381 | | const selectedSuppliersList = document.getElementById('selectedSuppliersList'); |
| | | 1382 | | if (!selectedSuppliersList) return; |
| | | 1383 | | |
| | | 1384 | | selectedSuppliersList.innerHTML = ''; |
| | | 1385 | | |
| | | 1386 | | // Функция для форматирования чисел с разделителем тысяч (без десятичных знаков) |
| | | 1387 | | function formatNumber(num) { |
| | | 1388 | | if (num == null || isNaN(num)) return '0'; |
| | | 1389 | | return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); |
| | | 1390 | | } |
| | | 1391 | | |
| | | 1392 | | // Создаем контейнер для таблицы с карточкой в стиле приложения |
| | | 1393 | | // card |
| | | 1394 | | const card = document.createElement('div'); |
| | | 1395 | | card.className = 'card app-card mt-3 mb-3'; |
| | | 1396 | | |
| | | 1397 | | // header |
| | | 1398 | | const cardHeader = document.createElement('div'); |
| | | 1399 | | cardHeader.className = 'card-header app-card-header'; |
| | | 1400 | | |
| | | 1401 | | const cardTitle = document.createElement('h6'); |
| | | 1402 | | cardTitle.className = 'app-card-title'; |
| | | 1403 | | cardTitle.textContent = 'Сводка по поставщикам'; |
| | | 1404 | | cardTitle.dataset.originalText = cardTitle.textContent; |
| | | 1405 | | |
| | | 1406 | | cardHeader.appendChild(cardTitle); |
| | | 1407 | | card.appendChild(cardHeader); |
| | | 1408 | | |
| | | 1409 | | // body |
| | | 1410 | | const cardBody = document.createElement('div'); |
| | | 1411 | | cardBody.className = 'card-body app-card-body'; |
| | | 1412 | | |
| | | 1413 | | // table |
| | | 1414 | | const table = document.createElement('table'); |
| | | 1415 | | table.className = 'table table-sm table-striped mb-0 app-table'; |
| | | 1416 | | |
| | | 1417 | | const thead = document.createElement('thead'); |
| | | 1418 | | thead.className = 'app-table-head'; |
| | | 1419 | | |
| | | 1420 | | const headerRow = document.createElement('tr'); |
| | | 1421 | | |
| | | 1422 | | const headers = [ |
| | | 1423 | | {text: 'Поставщик', align: 'left'}, |
| | | 1424 | | {text: 'Заказы у поставщика', align: 'center'}, |
| | | 1425 | | {text: 'Количество заказов', align: 'center'}, |
| | | 1426 | | {text: 'Сумма отправления', align: 'right'}, |
| | | 1427 | | {text: 'Цена закупки RUB', align: 'right'}, |
| | | 1428 | | {text: 'Цена закупки (валюта поставщика)', align: 'right'} |
| | | 1429 | | ]; |
| | | 1430 | | |
| | | 1431 | | headers.forEach((header, index) => { |
| | | 1432 | | const th = document.createElement('th'); |
| | | 1433 | | th.textContent = header.text; |
| | | 1434 | | th.classList.add(`text-${header.align}`, 'app-th'); |
| | | 1435 | | headerRow.appendChild(th); |
| | | 1436 | | |
| | | 1437 | | // Добавляем возможность сортировки для колонок с данными (кроме первой - "Поставщик") |
| | | 1438 | | if (index > 0) { |
| | | 1439 | | th.style.cursor = 'pointer'; |
| | | 1440 | | th.style.userSelect = 'none'; |
| | | 1441 | | th.innerHTML = `${header.text} <span style="opacity: 0.5; font-size: 0.75rem;">↕</span>`; |
| | | 1442 | | th.setAttribute('data-sort-column', index); |
| | | 1443 | | th.setAttribute('data-sort-direction', 'none'); |
| | | 1444 | | |
| | | 1445 | | th.addEventListener('click', function () { |
| | | 1446 | | const sortColumn = parseInt(this.getAttribute('data-sort-column')); |
| | | 1447 | | const currentDirection = this.getAttribute('data-sort-direction'); |
| | | 1448 | | const newDirection = currentDirection === 'asc' ? 'desc' : 'asc'; |
| | | 1449 | | |
| | | 1450 | | // Сбрасываем сортировку у всех заголовков |
| | | 1451 | | headerRow.querySelectorAll('th').forEach(h => { |
| | | 1452 | | if (h !== this && h.getAttribute('data-sort-column')) { |
| | | 1453 | | h.setAttribute('data-sort-direction', 'none'); |
| | | 1454 | | const colIndex = parseInt(h.getAttribute('data-sort-column')); |
| | | 1455 | | h.innerHTML = `${headers[colIndex].text} <span style="opacity: 0.5; font-size: 0.75rem;" |
| | | 1456 | | } |
| | | 1457 | | }); |
| | | 1458 | | |
| | | 1459 | | // Устанавливаем новую сортировку |
| | | 1460 | | this.setAttribute('data-sort-direction', newDirection); |
| | | 1461 | | const sortIcon = newDirection === 'asc' ? '↑' : '↓'; |
| | | 1462 | | this.innerHTML = `${headers[sortColumn].text} <span style="opacity: 1; font-size: 0.75rem;">${so |
| | | 1463 | | |
| | | 1464 | | // Сортируем строки (кроме итоговой) |
| | | 1465 | | const footerRow = tbody.querySelector('tr:last-child'); |
| | | 1466 | | const rows = Array.from(tbody.querySelectorAll('tr')).filter(r => r !== footerRow); |
| | | 1467 | | |
| | | 1468 | | rows.sort((a, b) => { |
| | | 1469 | | let aValue, bValue; |
| | | 1470 | | |
| | | 1471 | | switch (sortColumn) { |
| | | 1472 | | case 1: // Заказы у поставщика |
| | | 1473 | | aValue = parseInt(a.children[1].textContent) || 0; |
| | | 1474 | | bValue = parseInt(b.children[1].textContent) || 0; |
| | | 1475 | | break; |
| | | 1476 | | case 2: // Количество заказов |
| | | 1477 | | aValue = parseInt(a.children[2].textContent) || 0; |
| | | 1478 | | bValue = parseInt(b.children[2].textContent) || 0; |
| | | 1479 | | break; |
| | | 1480 | | case 3: // Сумма отправления |
| | | 1481 | | aValue = parseFloat(a.children[3].getAttribute('data-amount') || a.children[3].textC |
| | | 1482 | | bValue = parseFloat(b.children[3].getAttribute('data-amount') || b.children[3].textC |
| | | 1483 | | break; |
| | | 1484 | | case 4: // Цена закупки |
| | | 1485 | | aValue = parseFloat(a.children[4].getAttribute('data-purchase-price') || a.children[ |
| | | 1486 | | bValue = parseFloat(b.children[4].getAttribute('data-purchase-price') || b.children[ |
| | | 1487 | | break; |
| | | 1488 | | default: |
| | | 1489 | | return 0; |
| | | 1490 | | } |
| | | 1491 | | |
| | | 1492 | | return newDirection === 'asc' ? aValue - bValue : bValue - aValue; |
| | | 1493 | | }); |
| | | 1494 | | |
| | | 1495 | | // Перемещаем отсортированные строки перед итоговой |
| | | 1496 | | rows.forEach(row => { |
| | | 1497 | | tbody.insertBefore(row, footerRow); |
| | | 1498 | | }); |
| | | 1499 | | }); |
| | | 1500 | | } else { |
| | | 1501 | | th.textContent = header.text; |
| | | 1502 | | } |
| | | 1503 | | |
| | | 1504 | | headerRow.appendChild(th); |
| | | 1505 | | }); |
| | | 1506 | | thead.appendChild(headerRow); |
| | | 1507 | | table.appendChild(thead); |
| | | 1508 | | |
| | | 1509 | | const tbody = document.createElement('tbody'); |
| | | 1510 | | let totalCount = 0; |
| | | 1511 | | let totalQuantity = 0; |
| | | 1512 | | let totalAmount = 0; |
| | | 1513 | | let totalPurchasePrice = 0; |
| | | 1514 | | // Итоги по валютам для всех поставщиков |
| | | 1515 | | const globalCurrencyTotals = {}; |
| | | 1516 | | |
| | | 1517 | | for (const [supplier, data] of Object.entries(selectedSuppliers)) { |
| | | 1518 | | if (data.count > 0) { |
| | | 1519 | | const row = document.createElement('tr'); |
| | | 1520 | | row.classList.add('supplier-row'); |
| | | 1521 | | row.setAttribute('data-supplier-name', supplier); |
| | | 1522 | | |
| | | 1523 | | // Добавляем обработчик клика для подсветки строк в таблице транзакций |
| | | 1524 | | row.addEventListener('click', function () { |
| | | 1525 | | const supplierName = this.getAttribute('data-supplier-name'); |
| | | 1526 | | const isCurrentlyHighlighted = this.classList.contains('is-active'); |
| | | 1527 | | |
| | | 1528 | | // Убираем подсветку со всех строк |
| | | 1529 | | const allRows = document.querySelectorAll('#shipmentTable tbody tr'); |
| | | 1530 | | allRows.forEach(r => { |
| | | 1531 | | r.classList.remove('table-warning', 'table-info'); |
| | | 1532 | | }); |
| | | 1533 | | |
| | | 1534 | | // Убираем подсветку со всех строк поставщиков |
| | | 1535 | | const allSupplierRows = document.querySelectorAll('#selectedSuppliersList tbody tr'); |
| | | 1536 | | allSupplierRows.forEach(r => { |
| | | 1537 | | r.classList.remove('is-active'); |
| | | 1538 | | }); |
| | | 1539 | | |
| | | 1540 | | // Если строка уже была выделена, просто снимаем выделение |
| | | 1541 | | if (isCurrentlyHighlighted) { |
| | | 1542 | | this.style.backgroundColor = ''; |
| | | 1543 | | // Восстанавливаем оригинальный текст заголовка |
| | | 1544 | | const cardTitle = document.querySelector('#selectedSuppliersList .card-header h6'); |
| | | 1545 | | if (cardTitle) { |
| | | 1546 | | const originalText = cardTitle.getAttribute('data-original-text') || 'Сводка по поставщи |
| | | 1547 | | cardTitle.textContent = originalText; |
| | | 1548 | | } |
| | | 1549 | | return; |
| | | 1550 | | } |
| | | 1551 | | |
| | | 1552 | | // Подсвечиваем текущую строку поставщика |
| | | 1553 | | this.classList.add('is-active'); |
| | | 1554 | | |
| | | 1555 | | // Находим и подсвечиваем все строки в таблице транзакций с этим поставщиком |
| | | 1556 | | let highlightedCount = 0; |
| | | 1557 | | let firstHighlightedRow = null; |
| | | 1558 | | |
| | | 1559 | | if (supplierName) { |
| | | 1560 | | allRows.forEach(transactionRow => { |
| | | 1561 | | const supplierSelect = transactionRow.querySelector('.supplier-select'); |
| | | 1562 | | if (supplierSelect && supplierSelect.selectedIndex >= 0) { |
| | | 1563 | | const selectedOption = supplierSelect.options[supplierSelect.selectedIndex]; |
| | | 1564 | | if (selectedOption && selectedOption.text === supplierName) { |
| | | 1565 | | transactionRow.classList.add('table-warning'); |
| | | 1566 | | highlightedCount++; |
| | | 1567 | | if (!firstHighlightedRow) { |
| | | 1568 | | firstHighlightedRow = transactionRow; |
| | | 1569 | | } |
| | | 1570 | | } |
| | | 1571 | | } |
| | | 1572 | | }); |
| | | 1573 | | |
| | | 1574 | | // Плавная прокрутка к первой выделенной строке |
| | | 1575 | | if (firstHighlightedRow) { |
| | | 1576 | | firstHighlightedRow.scrollIntoView({behavior: 'smooth', block: 'center'}); |
| | | 1577 | | } |
| | | 1578 | | |
| | | 1579 | | // Показываем количество выделенных строк в заголовке карточки |
| | | 1580 | | const cardTitle = document.querySelector('#selectedSuppliersList .card-header h6'); |
| | | 1581 | | if (cardTitle && highlightedCount > 0) { |
| | | 1582 | | const originalText = cardTitle.getAttribute('data-original-text') || 'Сводка по поставщи |
| | | 1583 | | cardTitle.setAttribute('data-original-text', originalText); |
| | | 1584 | | cardTitle.textContent = `${originalText} (выделено: ${highlightedCount})`; |
| | | 1585 | | } |
| | | 1586 | | } |
| | | 1587 | | }); |
| | | 1588 | | |
| | | 1589 | | // Добавляем эффект при наведении |
| | | 1590 | | row.addEventListener('mouseenter', function () { |
| | | 1591 | | if (!this.style.backgroundColor || this.style.backgroundColor === '') { |
| | | 1592 | | this.style.backgroundColor = '#f8f9fa'; |
| | | 1593 | | } |
| | | 1594 | | }); |
| | | 1595 | | |
| | | 1596 | | row.addEventListener('mouseleave', function () { |
| | | 1597 | | if (this.style.backgroundColor === '#f8f9fa') { |
| | | 1598 | | this.style.backgroundColor = ''; |
| | | 1599 | | } |
| | | 1600 | | }); |
| | | 1601 | | |
| | | 1602 | | const supplierCell = document.createElement('td'); |
| | | 1603 | | supplierCell.textContent = supplier; |
| | | 1604 | | row.appendChild(supplierCell); |
| | | 1605 | | |
| | | 1606 | | const countCell = document.createElement('td'); |
| | | 1607 | | countCell.textContent = data.count; |
| | | 1608 | | countCell.classList.add('text-center'); |
| | | 1609 | | row.appendChild(countCell); |
| | | 1610 | | |
| | | 1611 | | const quantityCell = document.createElement('td'); |
| | | 1612 | | quantityCell.textContent = data.totalQuantity; |
| | | 1613 | | quantityCell.classList.add('text-center'); |
| | | 1614 | | row.appendChild(quantityCell); |
| | | 1615 | | |
| | | 1616 | | const amountCell = document.createElement('td'); |
| | | 1617 | | const amountValue = (data.totalAmount != null && !isNaN(data.totalAmount)) |
| | | 1618 | | ? data.totalAmount |
| | | 1619 | | : 0; |
| | | 1620 | | amountCell.textContent = formatNumber(amountValue); |
| | | 1621 | | amountCell.classList.add('text-right', 'numeric'); |
| | | 1622 | | amountCell.setAttribute('data-amount', amountValue); |
| | | 1623 | | amountCell.title = `Сумма отправления: ${formatNumber(amountValue)}`; |
| | | 1624 | | row.appendChild(amountCell); |
| | | 1625 | | |
| | | 1626 | | const purchasePriceCell = document.createElement('td'); |
| | | 1627 | | const purchasePriceValue = (data.totalPurchasePrice != null && !isNaN(data.totalPurchasePrice)) |
| | | 1628 | | ? data.totalPurchasePrice |
| | | 1629 | | : 0; |
| | | 1630 | | purchasePriceCell.textContent = formatNumber(purchasePriceValue); |
| | | 1631 | | purchasePriceCell.classList.add('text-right', 'numeric'); |
| | | 1632 | | purchasePriceCell.setAttribute('data-purchase-price', purchasePriceValue); |
| | | 1633 | | purchasePriceCell.title = `Цена закупки: ${formatNumber(purchasePriceValue)}`; |
| | | 1634 | | row.appendChild(purchasePriceCell); |
| | | 1635 | | |
| | | 1636 | | // Новый столбец: цена закупки в валютах поставщика |
| | | 1637 | | const purchasePriceOriginalCell = document.createElement('td'); |
| | | 1638 | | purchasePriceOriginalCell.classList.add('text-right', 'numeric'); |
| | | 1639 | | |
| | | 1640 | | const currencyParts = []; |
| | | 1641 | | const currencyTotals = data.currencyTotals || {}; |
| | | 1642 | | Object.entries(currencyTotals).forEach(([currencyCode, amount]) => { |
| | | 1643 | | if (!globalCurrencyTotals[currencyCode]) { |
| | | 1644 | | globalCurrencyTotals[currencyCode] = 0; |
| | | 1645 | | } |
| | | 1646 | | globalCurrencyTotals[currencyCode] += amount; |
| | | 1647 | | currencyParts.push(`${amount.toFixed(2)} ${currencyCode}`); |
| | | 1648 | | }); |
| | | 1649 | | |
| | | 1650 | | purchasePriceOriginalCell.textContent = currencyParts.join(' / '); |
| | | 1651 | | purchasePriceOriginalCell.title = currencyParts.length |
| | | 1652 | | ? `Цена закупки в валютах поставщика: ${currencyParts.join(', ')}` |
| | | 1653 | | : ''; |
| | | 1654 | | row.appendChild(purchasePriceOriginalCell); |
| | | 1655 | | |
| | | 1656 | | tbody.appendChild(row); |
| | | 1657 | | |
| | | 1658 | | // Суммируем для итоговой строки |
| | | 1659 | | totalCount += data.count; |
| | | 1660 | | totalQuantity += data.totalQuantity; |
| | | 1661 | | totalAmount += amountValue; |
| | | 1662 | | totalPurchasePrice += purchasePriceValue; |
| | | 1663 | | } |
| | | 1664 | | } |
| | | 1665 | | |
| | | 1666 | | // Обновляем tooltip с процентами для каждой строки |
| | | 1667 | | if (totalAmount > 0 || totalPurchasePrice > 0) { |
| | | 1668 | | tbody.querySelectorAll('tr').forEach(row => { |
| | | 1669 | | const amountCell = row.querySelector('td[data-amount]'); |
| | | 1670 | | const purchasePriceCell = row.querySelector('td[data-purchase-price]'); |
| | | 1671 | | |
| | | 1672 | | if (amountCell && totalAmount > 0) { |
| | | 1673 | | const amountValue = parseFloat(amountCell.getAttribute('data-amount')) || 0; |
| | | 1674 | | const percentage = ((amountValue / totalAmount) * 100).toFixed(1); |
| | | 1675 | | amountCell.title = `Сумма отправления: ${formatNumber(amountValue)} (${percentage}% от общей сум |
| | | 1676 | | } |
| | | 1677 | | |
| | | 1678 | | if (purchasePriceCell && totalPurchasePrice > 0) { |
| | | 1679 | | const purchasePriceValue = parseFloat(purchasePriceCell.getAttribute('data-purchase-price')) || |
| | | 1680 | | const percentage = ((purchasePriceValue / totalPurchasePrice) * 100).toFixed(1); |
| | | 1681 | | purchasePriceCell.title = `Цена закупки: ${formatNumber(purchasePriceValue)} (${percentage}% от |
| | | 1682 | | } |
| | | 1683 | | }); |
| | | 1684 | | } |
| | | 1685 | | |
| | | 1686 | | // Добавляем итоговую строку |
| | | 1687 | | if (totalCount > 0) { |
| | | 1688 | | const footerRow = document.createElement('tr'); |
| | | 1689 | | footerRow.classList.add('table-total'); |
| | | 1690 | | |
| | | 1691 | | const emptyCell1 = document.createElement('td'); |
| | | 1692 | | emptyCell1.textContent = 'Итого:'; |
| | | 1693 | | emptyCell1.colSpan = 1; |
| | | 1694 | | footerRow.appendChild(emptyCell1); |
| | | 1695 | | |
| | | 1696 | | const totalCountCell = document.createElement('td'); |
| | | 1697 | | totalCountCell.textContent = totalCount; |
| | | 1698 | | totalCountCell.classList.add('text-center'); |
| | | 1699 | | footerRow.appendChild(totalCountCell); |
| | | 1700 | | |
| | | 1701 | | const totalQuantityCell = document.createElement('td'); |
| | | 1702 | | totalQuantityCell.textContent = totalQuantity; |
| | | 1703 | | totalQuantityCell.classList.add('text-center'); |
| | | 1704 | | footerRow.appendChild(totalQuantityCell); |
| | | 1705 | | |
| | | 1706 | | const totalAmountCell = document.createElement('td'); |
| | | 1707 | | totalAmountCell.textContent = formatNumber(totalAmount); |
| | | 1708 | | totalAmountCell.classList.add('text-right', 'numeric'); |
| | | 1709 | | footerRow.appendChild(totalAmountCell); |
| | | 1710 | | |
| | | 1711 | | const totalPurchasePriceCell = document.createElement('td'); |
| | | 1712 | | totalPurchasePriceCell.textContent = formatNumber(totalPurchasePrice); |
| | | 1713 | | totalPurchasePriceCell.classList.add('text-right', 'numeric'); |
| | | 1714 | | footerRow.appendChild(totalPurchasePriceCell); |
| | | 1715 | | |
| | | 1716 | | // Итоги по валютам поставщиков (по всем строкам) |
| | | 1717 | | const totalCurrencyCell = document.createElement('td'); |
| | | 1718 | | totalCurrencyCell.classList.add('text-right', 'numeric'); |
| | | 1719 | | const globalParts = []; |
| | | 1720 | | Object.entries(globalCurrencyTotals).forEach(([currencyCode, amount]) => { |
| | | 1721 | | globalParts.push(`${amount.toFixed(2)} ${currencyCode}`); |
| | | 1722 | | }); |
| | | 1723 | | totalCurrencyCell.textContent = globalParts.join(' / '); |
| | | 1724 | | totalCurrencyCell.title = globalParts.length |
| | | 1725 | | ? `Итого по валютам поставщиков: ${globalParts.join(', ')}` |
| | | 1726 | | : ''; |
| | | 1727 | | footerRow.appendChild(totalCurrencyCell); |
| | | 1728 | | |
| | | 1729 | | tbody.appendChild(footerRow); |
| | | 1730 | | } |
| | | 1731 | | |
| | | 1732 | | table.appendChild(tbody); |
| | | 1733 | | cardBody.appendChild(table); |
| | | 1734 | | card.appendChild(cardBody); |
| | | 1735 | | selectedSuppliersList.appendChild(card); |
| | | 1736 | | } |
| | | 1737 | | |
| | | 1738 | | window.updateSupplierList = updateSelectedSuppliers; |
| | | 1739 | | |
| | | 1740 | | // Обработчики для обновления таблицы при изменении полей |
| | | 1741 | | supplierSelects.forEach((select, index) => { |
| | | 1742 | | select.addEventListener('change', function () { |
| | | 1743 | | updateSelectedSuppliers(); |
| | | 1744 | | }); |
| | | 1745 | | }); |
| | | 1746 | | |
| | | 1747 | | statusSelects.forEach((select) => { |
| | | 1748 | | select.addEventListener('change', function () { |
| | | 1749 | | updateSelectedSuppliers(); |
| | | 1750 | | }); |
| | | 1751 | | }); |
| | | 1752 | | |
| | | 1753 | | // Обработчик для изменения количества |
| | | 1754 | | document.addEventListener('input', function (e) { |
| | | 1755 | | if (e.target && e.target.classList.contains('quantity-input-field')) { |
| | | 1756 | | // Используем debounce для оптимизации |
| | | 1757 | | clearTimeout(window.quantityUpdateTimeout); |
| | | 1758 | | window.quantityUpdateTimeout = setTimeout(function () { |
| | | 1759 | | updateSelectedSuppliers(); |
| | | 1760 | | }, 300); |
| | | 1761 | | } |
| | | 1762 | | }); |
| | | 1763 | | |
| | | 1764 | | // Обработчик для изменения цены закупки |
| | | 1765 | | document.addEventListener('input', function (e) { |
| | | 1766 | | if (e.target && e.target.classList.contains('purchase-price-field')) { |
| | | 1767 | | const orderId = e.target.id.split('_')[1]; |
| | | 1768 | | const selectedSupplier = document.querySelector(`#SupplierId_${orderId} option:checked`); |
| | | 1769 | | if (selectedSupplier) { |
| | | 1770 | | const currencyCode = selectedSupplier.getAttribute('data-currency-code') || 'RUB'; |
| | | 1771 | | const costFactor = parseFloat(selectedSupplier.getAttribute('data-cost-factor') || '1'); |
| | | 1772 | | |
| | | 1773 | | // Пересчитываем цену закупки в RUB |
| | | 1774 | | const purchasePrice = parseFloat(e.target.value) || 0; |
| | | 1775 | | let purchasePriceInRub = 0; |
| | | 1776 | | |
| | | 1777 | | // Получаем курсы валют (если доступны) |
| | | 1778 | | const rateUSD = parseFloat(document.querySelector('[data-rate-usd]')?.getAttribute('data-rate-usd') |
| | | 1779 | | window.rateUSD || 1); |
| | | 1780 | | const rateEUR = parseFloat(document.querySelector('[data-rate-eur]')?.getAttribute('data-rate-eur') |
| | | 1781 | | window.rateEUR || 1); |
| | | 1782 | | const rateBYN = parseFloat(document.querySelector('[data-rate-byn]')?.getAttribute('data-rate-byn') |
| | | 1783 | | window.rateBYN || 1); |
| | | 1784 | | |
| | | 1785 | | switch (currencyCode) { |
| | | 1786 | | case 'USD': |
| | | 1787 | | purchasePriceInRub = purchasePrice * costFactor * rateUSD; |
| | | 1788 | | break; |
| | | 1789 | | case 'EUR': |
| | | 1790 | | purchasePriceInRub = purchasePrice * costFactor * rateEUR; |
| | | 1791 | | break; |
| | | 1792 | | case 'BYN': |
| | | 1793 | | purchasePriceInRub = purchasePrice * costFactor * rateBYN; |
| | | 1794 | | break; |
| | | 1795 | | default: |
| | | 1796 | | purchasePriceInRub = purchasePrice * costFactor; |
| | | 1797 | | break; |
| | | 1798 | | } |
| | | 1799 | | |
| | | 1800 | | // Обновляем отображение цены закупки в RUB |
| | | 1801 | | const purchasePriceRubElement = document.getElementById(`PurchasePriceRub_${orderId}`); |
| | | 1802 | | if (purchasePriceRubElement) { |
| | | 1803 | | purchasePriceRubElement.textContent = !isNaN(purchasePriceInRub) ? Math.round(purchasePriceInRub |
| | | 1804 | | } |
| | | 1805 | | } |
| | | 1806 | | |
| | | 1807 | | // Используем debounce для оптимизации |
| | | 1808 | | clearTimeout(window.purchasePriceUpdateTimeout); |
| | | 1809 | | window.purchasePriceUpdateTimeout = setTimeout(function () { |
| | | 1810 | | updateSelectedSuppliers(); |
| | | 1811 | | }, 300); |
| | | 1812 | | } |
| | | 1813 | | }); |
| | | 1814 | | |
| | | 1815 | | // Обработчик для изменения суммы отправления (если она редактируется) |
| | | 1816 | | document.addEventListener('input', function (e) { |
| | | 1817 | | if (e.target && (e.target.id && e.target.id.startsWith('shipmentAmount_') || |
| | | 1818 | | e.target.classList.contains('shipment-amount-field'))) { |
| | | 1819 | | clearTimeout(window.shipmentAmountUpdateTimeout); |
| | | 1820 | | window.shipmentAmountUpdateTimeout = setTimeout(function () { |
| | | 1821 | | updateSelectedSuppliers(); |
| | | 1822 | | }, 300); |
| | | 1823 | | } |
| | | 1824 | | }); |
| | | 1825 | | |
| | | 1826 | | // Обработчик для изменения веса (так как он влияет на цену закупки) |
| | | 1827 | | document.addEventListener('input', function (e) { |
| | | 1828 | | if (e.target && e.target.classList.contains('product-weight-field')) { |
| | | 1829 | | clearTimeout(window.weightUpdateTimeout); |
| | | 1830 | | window.weightUpdateTimeout = setTimeout(function () { |
| | | 1831 | | updateSelectedSuppliers(); |
| | | 1832 | | }, 300); |
| | | 1833 | | } |
| | | 1834 | | }); |
| | | 1835 | | |
| | | 1836 | | // Инициализация при загрузке страницы |
| | | 1837 | | updateSelectedSuppliers(); |
| | | 1838 | | }); |
| | | 1839 | | </script> |
| | | 1840 | | |
| | | 1841 | | <script> |
| | | 1842 | | function updateShipmentAmount(orderId) { |
| | | 1843 | | var shipmentAmountElement = document.getElementById('shipmentAmount_' + orderId); |
| | | 1844 | | var priceElement = document.getElementById('Price_' + orderId); |
| | | 1845 | | var quantityElement = document.querySelector('#Quantity_' + orderId + ' .quantity-input-field'); |
| | | 1846 | | if (!shipmentAmountElement || !priceElement || !quantityElement) return; |
| | | 1847 | | |
| | | 1848 | | var price = extractNumber(priceElement.textContent); |
| | | 1849 | | var quantity = extractNumber(quantityElement.value); |
| | | 1850 | | if (price === null || quantity === null || isNaN(price) || isNaN(quantity)) return; |
| | | 1851 | | |
| | | 1852 | | shipmentAmountElement.textContent = new Intl.NumberFormat('ru-RU', { |
| | | 1853 | | maximumFractionDigits: 2 |
| | | 1854 | | }).format(price * quantity); |
| | | 1855 | | } |
| | | 1856 | | |
| | | 1857 | | function calculateSumAndDisplay() { |
| | | 1858 | | var total = 0; |
| | | 1859 | | var shipmentAmountElements = document.querySelectorAll('[id^="shipmentAmount_"]'); |
| | | 1860 | | var totalDiv = document.getElementById("totalShipmentAmount"); |
| | | 1861 | | // Итоговой ячейки нет, если таблица не отрисована (панель ошибки) или колонка скрыта правами |
| | | 1862 | | if (!totalDiv) return; |
| | | 1863 | | |
| | | 1864 | | shipmentAmountElements.forEach(function (element) { |
| | | 1865 | | var value = extractNumber(element.innerText); |
| | | 1866 | | total += value; |
| | | 1867 | | }); |
| | | 1868 | | |
| | | 1869 | | // Выводим сумму в элемент div |
| | | 1870 | | totalDiv.innerText = total; |
| | | 1871 | | } |
| | | 1872 | | |
| | | 1873 | | document.querySelectorAll('#shipmentTable .transaction-order-row').forEach(function (row) { |
| | | 1874 | | var orderId = row.getAttribute('data-order-id'); |
| | | 1875 | | if (orderId) updateShipmentAmount(orderId); |
| | | 1876 | | }); |
| | | 1877 | | |
| | | 1878 | | // Вызываем функцию для вычисления и вывода суммы |
| | | 1879 | | calculateSumAndDisplay(); |
| | | 1880 | | </script> |
| | | 1881 | | |
| | | 1882 | | |
| | | 1883 | | <script> |
| | | 1884 | | function calculateTotalPurchasePriceRub() { |
| | | 1885 | | var total = 0; |
| | | 1886 | | |
| | | 1887 | | // Находим все элементы с классом 'purchase-price-rub' |
| | | 1888 | | var elements = document.querySelectorAll('.purchase-price-rub'); |
| | | 1889 | | elements.forEach(function (element) { |
| | | 1890 | | var textContent = element.textContent.trim(); |
| | | 1891 | | var numberValue = extractNumber(textContent); |
| | | 1892 | | |
| | | 1893 | | if (!isNaN(numberValue)) { |
| | | 1894 | | total += numberValue; |
| | | 1895 | | } |
| | | 1896 | | }); |
| | | 1897 | | |
| | | 1898 | | // Выводим общую сумму в элемент div |
| | | 1899 | | var totalDiv = document.getElementById("totalPurchasePriceRub"); |
| | | 1900 | | if (!totalDiv) return; |
| | | 1901 | | totalDiv.innerText = total.toFixed(2) + " RUB"; |
| | | 1902 | | } |
| | | 1903 | | |
| | | 1904 | | // Функция для настройки MutationObserver |
| | | 1905 | | function setupMutationObserver() { |
| | | 1906 | | // Находим все элементы с классом 'purchase-price-rub' |
| | | 1907 | | var elements = document.querySelectorAll('.purchase-price-rub'); |
| | | 1908 | | elements.forEach(function (element) { |
| | | 1909 | | // Создаем новый экземпляр MutationObserver |
| | | 1910 | | var observer = new MutationObserver(function (mutationsList) { |
| | | 1911 | | // При изменении содержимого вызываем функцию для пересчета суммы |
| | | 1912 | | calculateTotalPurchasePriceRub(); |
| | | 1913 | | }); |
| | | 1914 | | |
| | | 1915 | | // Настраиваем наблюдение за изменениями внутри элемента 'element' |
| | | 1916 | | observer.observe(element, {childList: true, subtree: true}); |
| | | 1917 | | }); |
| | | 1918 | | } |
| | | 1919 | | |
| | | 1920 | | // Вызываем функцию для настройки MutationObserver при загрузке страницы |
| | | 1921 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 1922 | | calculateTotalPurchasePriceRub(); // Вызываем для первичного расчета суммы |
| | | 1923 | | setupMutationObserver(); // Вызываем для настройки MutationObserver |
| | | 1924 | | }); |
| | | 1925 | | </script> |
| | | 1926 | | |
| | | 1927 | | <script> |
| | | 1928 | | $(document).ready(function () { |
| | | 1929 | | // Обработка изменения поставщика в индивидуальных полях |
| | | 1930 | | $('.supplier-select').on('change', function () { |
| | | 1931 | | var selectedSupplier = $(this).find('option:selected'); |
| | | 1932 | | |
| | | 1933 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 1934 | | var isReSeller = selectedSupplier.data('is-reseller'); |
| | | 1935 | | var orderId = $(this).attr('id').split('_')[1]; // Пример: SupplierId_60956 → 60956 |
| | | 1936 | | |
| | | 1937 | | // Обновляем валюту |
| | | 1938 | | if (currencyCode) { |
| | | 1939 | | $('#CurrencyCode_' + orderId).text(currencyCode); |
| | | 1940 | | } else { |
| | | 1941 | | $('#CurrencyCode_' + orderId).text(''); |
| | | 1942 | | } |
| | | 1943 | | |
| | | 1944 | | // Если поставщик — реализатор → меняем статус заказа |
| | | 1945 | | if (isReSeller === true || isReSeller === "True") { |
| | | 1946 | | // Находим селект статуса по ID: AppStatusId_XXXX |
| | | 1947 | | var statusSelect = $('#AppStatusId_' + orderId); |
| | | 1948 | | var supplierSelect = $(this); |
| | | 1949 | | |
| | | 1950 | | if (statusSelect.length && statusSelect[0]) { |
| | | 1951 | | // Сохраняем текущее значение поставщика перед изменением статуса |
| | | 1952 | | var currentSupplierValue = supplierSelect.val(); |
| | | 1953 | | |
| | | 1954 | | // Устанавливаем флаг, чтобы предотвратить сброс поставщика |
| | | 1955 | | supplierSelect.data('preserve-supplier', true); |
| | | 1956 | | |
| | | 1957 | | // Ищем опцию "Заказан реализатору" по тексту |
| | | 1958 | | var statusOptions = statusSelect[0].options; |
| | | 1959 | | var resellerStatusValue = null; |
| | | 1960 | | |
| | | 1961 | | for (var i = 0; i < statusOptions.length; i++) { |
| | | 1962 | | var optionText = statusOptions[i].text.trim(); |
| | | 1963 | | if (optionText === "Заказан реализатору") { |
| | | 1964 | | resellerStatusValue = statusOptions[i].value; |
| | | 1965 | | break; |
| | | 1966 | | } |
| | | 1967 | | } |
| | | 1968 | | |
| | | 1969 | | // Если нашли статус "Заказан реализатору", устанавливаем его |
| | | 1970 | | if (resellerStatusValue) { |
| | | 1971 | | // Устанавливаем значение статуса без вызова события change сначала |
| | | 1972 | | statusSelect[0].value = resellerStatusValue; |
| | | 1973 | | |
| | | 1974 | | // Вызываем событие change синхронно |
| | | 1975 | | statusSelect.trigger('change'); |
| | | 1976 | | |
| | | 1977 | | // Используем requestAnimationFrame для асинхронного восстановления поставщика |
| | | 1978 | | requestAnimationFrame(function () { |
| | | 1979 | | // Проверяем и восстанавливаем значение поставщика |
| | | 1980 | | if (supplierSelect.val() !== currentSupplierValue && currentSupplierValue) { |
| | | 1981 | | supplierSelect.val(currentSupplierValue); |
| | | 1982 | | |
| | | 1983 | | // Обновляем связанные поля (валюта и т.д.) без повторного изменения статуса |
| | | 1984 | | var selectedSupplierOption = supplierSelect.find('option:selected'); |
| | | 1985 | | var currencyCode = selectedSupplierOption.data('currency-code'); |
| | | 1986 | | if (currencyCode) { |
| | | 1987 | | $('#CurrencyCode_' + orderId).text(currencyCode); |
| | | 1988 | | } |
| | | 1989 | | } |
| | | 1990 | | |
| | | 1991 | | // Снимаем флаг |
| | | 1992 | | supplierSelect.removeData('preserve-supplier'); |
| | | 1993 | | }); |
| | | 1994 | | } else { |
| | | 1995 | | supplierSelect.removeData('preserve-supplier'); |
| | | 1996 | | } |
| | | 1997 | | } |
| | | 1998 | | } |
| | | 1999 | | }); |
| | | 2000 | | |
| | | 2001 | | // Обработка изменения поставщика в общем поле |
| | | 2002 | | $('#universalSelect').on('change', function () { |
| | | 2003 | | var selectedSupplier = $(this).find('option:selected'); |
| | | 2004 | | var supplierId = selectedSupplier.val(); |
| | | 2005 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 2006 | | |
| | | 2007 | | $('.supplier-select').each(function () { |
| | | 2008 | | $(this).val(supplierId).trigger('change'); |
| | | 2009 | | }); |
| | | 2010 | | |
| | | 2011 | | $('.currency-code').each(function () { |
| | | 2012 | | if (currencyCode) { |
| | | 2013 | | $(this).text(currencyCode); |
| | | 2014 | | } else { |
| | | 2015 | | $(this).text(''); |
| | | 2016 | | } |
| | | 2017 | | }); |
| | | 2018 | | }); |
| | | 2019 | | }); |
| | | 2020 | | </script> |
| | | 2021 | | |
| | | 2022 | | <script> |
| | | 2023 | | $(document).ready(function () { |
| | | 2024 | | // Получаем значения курсов валют из Razor View |
| | 0 | 2025 | | var rateUSD = @Model.RateUSD.ToString().Replace(',', '.'); |
| | 0 | 2026 | | var rateEUR = @Model.RateEUR.ToString().Replace(',', '.'); |
| | 0 | 2027 | | var rateBYN = @Model.RateBYN.ToString().Replace(',', '.'); |
| | | 2028 | | |
| | | 2029 | | // Обработка изменения поставщика в индивидуальных полях |
| | | 2030 | | $('.supplier-select').on('change', function () { |
| | | 2031 | | var selectedSupplier = $(this).find('option:selected'); |
| | | 2032 | | var weightFactor = readSupplierDataNumber(selectedSupplier, 'weight-factor', 0); |
| | | 2033 | | var orderId = $(this).attr('id').split('_')[1]; // Извлекаем ID заказа из ID элемента |
| | | 2034 | | var weightInput = $('#ProductWeight_' + orderId); |
| | | 2035 | | |
| | | 2036 | | // Если весовой коэффициент равен 0, устанавливаем поле веса в 0 и делаем его readonly |
| | | 2037 | | if (weightFactor === 0) { |
| | | 2038 | | weightInput.val('0').prop('readonly', true); |
| | | 2039 | | } else { |
| | | 2040 | | weightInput.prop('readonly', false); // Включаем возможность редактирования |
| | | 2041 | | } |
| | | 2042 | | |
| | | 2043 | | // Пример: обновляем другие значения на странице |
| | | 2044 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 2045 | | var weightCurrencyCode = selectedSupplier.data('weight-currency-code'); |
| | | 2046 | | var costFactor = readSupplierDataNumber(selectedSupplier, 'cost-factor', 1); |
| | | 2047 | | |
| | | 2048 | | if (currencyCode) { |
| | | 2049 | | $('#CurrencyCode_' + orderId).text(currencyCode); |
| | | 2050 | | var purchasePriceInRub = updatePurchasePriceInRub(orderId, currencyCode, costFactor); |
| | | 2051 | | updateCostPrice(orderId, currencyCode, costFactor, weightCurrencyCode, purchasePriceInRub); |
| | | 2052 | | |
| | | 2053 | | // Обновляем таблицу поставщиков при изменении поставщика |
| | | 2054 | | if (window.updateSupplierList) { |
| | | 2055 | | clearTimeout(window.supplierChangeTimeout); |
| | | 2056 | | window.supplierChangeTimeout = setTimeout(function () { |
| | | 2057 | | window.updateSupplierList(); |
| | | 2058 | | }, 300); |
| | | 2059 | | } |
| | | 2060 | | } else { |
| | | 2061 | | $('#CurrencyCode_' + orderId).text(''); |
| | | 2062 | | $('#PurchasePriceRub_' + orderId).text(''); |
| | | 2063 | | $('#PurchasePriceHidden_' + orderId).text(''); |
| | | 2064 | | $('#CostPrice_' + orderId).text(''); |
| | | 2065 | | |
| | | 2066 | | // Обновляем таблицу поставщиков при сбросе поставщика |
| | | 2067 | | if (window.updateSupplierList) { |
| | | 2068 | | clearTimeout(window.supplierChangeTimeout); |
| | | 2069 | | window.supplierChangeTimeout = setTimeout(function () { |
| | | 2070 | | window.updateSupplierList(); |
| | | 2071 | | }, 300); |
| | | 2072 | | } |
| | | 2073 | | } |
| | | 2074 | | }); |
| | | 2075 | | |
| | | 2076 | | // Обработка изменения значения purchase price |
| | | 2077 | | $('.purchase-price-field').on('input', function () { |
| | | 2078 | | var orderId = $(this).attr('id').split('_')[1]; |
| | | 2079 | | var selectedSupplier = $('#SupplierId_' + orderId).find('option:selected'); |
| | | 2080 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 2081 | | var weightCurrencyCode = selectedSupplier.data('weight-currency-code'); |
| | | 2082 | | var costFactor = readSupplierDataNumber(selectedSupplier, 'cost-factor', 1); |
| | | 2083 | | |
| | | 2084 | | var purchasePriceInRub = updatePurchasePriceInRub(orderId, currencyCode, costFactor); |
| | | 2085 | | updateCostPrice(orderId, currencyCode, costFactor, weightCurrencyCode, purchasePriceInRub); |
| | | 2086 | | updateCalculations(orderId); |
| | | 2087 | | |
| | | 2088 | | // Обновляем таблицу поставщиков с задержкой для оптимизации |
| | | 2089 | | if (window.updateSupplierList) { |
| | | 2090 | | clearTimeout(window.purchasePriceUpdateTimeout); |
| | | 2091 | | window.purchasePriceUpdateTimeout = setTimeout(function () { |
| | | 2092 | | window.updateSupplierList(); |
| | | 2093 | | }, 300); |
| | | 2094 | | } |
| | | 2095 | | }); |
| | | 2096 | | |
| | | 2097 | | // Обработка изменения значения веса |
| | | 2098 | | $('.product-weight-field').on('input', function () { |
| | | 2099 | | var orderId = $(this).attr('id').split('_')[1]; |
| | | 2100 | | var selectedSupplier = $('#SupplierId_' + orderId).find('option:selected'); |
| | | 2101 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 2102 | | var weightCurrencyCode = selectedSupplier.data('weight-currency-code'); |
| | | 2103 | | var costFactor = readSupplierDataNumber(selectedSupplier, 'cost-factor', 1); |
| | | 2104 | | |
| | | 2105 | | var purchasePriceInRub = updatePurchasePriceInRub(orderId, currencyCode, costFactor); |
| | | 2106 | | updateCostPrice(orderId, currencyCode, costFactor, weightCurrencyCode, purchasePriceInRub); |
| | | 2107 | | updateCalculations(orderId); |
| | | 2108 | | |
| | | 2109 | | // Обновляем таблицу поставщиков с задержкой для оптимизации |
| | | 2110 | | if (window.updateSupplierList) { |
| | | 2111 | | clearTimeout(window.weightUpdateTimeout); |
| | | 2112 | | window.weightUpdateTimeout = setTimeout(function () { |
| | | 2113 | | window.updateSupplierList(); |
| | | 2114 | | }, 300); |
| | | 2115 | | } |
| | | 2116 | | }); |
| | | 2117 | | |
| | | 2118 | | // Обработка изменения сумм дополнительных расходов |
| | | 2119 | | $(document).on('input', '.expense-amount-input', function () { |
| | | 2120 | | var container = $(this).closest('.extra-expenses-container'); |
| | | 2121 | | if (!container.length) return; |
| | | 2122 | | |
| | | 2123 | | var orderId = container.data('order-id'); |
| | | 2124 | | if (!orderId) return; |
| | | 2125 | | |
| | | 2126 | | if (window.updateCostPriceWithExtras) { |
| | | 2127 | | window.updateCostPriceWithExtras(orderId); |
| | | 2128 | | } |
| | | 2129 | | }); |
| | | 2130 | | |
| | | 2131 | | // Обработка изменения количества |
| | | 2132 | | $(document).on('input', '.quantity-input-field', function () { |
| | | 2133 | | const orderId = $(this).data('order-id'); |
| | | 2134 | | if (orderId && typeof updateShipmentAmount === 'function') { |
| | | 2135 | | updateShipmentAmount(orderId); |
| | | 2136 | | calculateSumAndDisplay(); |
| | | 2137 | | } |
| | | 2138 | | // Обновляем таблицу поставщиков с задержкой для оптимизации |
| | | 2139 | | if (window.updateSupplierList) { |
| | | 2140 | | clearTimeout(window.quantityUpdateTimeout); |
| | | 2141 | | window.quantityUpdateTimeout = setTimeout(function () { |
| | | 2142 | | window.updateSupplierList(); |
| | | 2143 | | if (orderId && typeof window.refreshOffersTable === 'function') window.refreshOffersTable(orderId); |
| | | 2144 | | }, 300); |
| | | 2145 | | } |
| | | 2146 | | }); |
| | | 2147 | | |
| | | 2148 | | // Обработка изменения суммы отправления (если она редактируется) |
| | | 2149 | | $(document).on('input', '[id^="shipmentAmount_"]', function () { |
| | | 2150 | | // Обновляем таблицу поставщиков с задержкой для оптимизации |
| | | 2151 | | if (window.updateSupplierList) { |
| | | 2152 | | clearTimeout(window.shipmentAmountUpdateTimeout); |
| | | 2153 | | window.shipmentAmountUpdateTimeout = setTimeout(function () { |
| | | 2154 | | window.updateSupplierList(); |
| | | 2155 | | }, 300); |
| | | 2156 | | } |
| | | 2157 | | }); |
| | | 2158 | | |
| | | 2159 | | $('.ozon-commission-field').on('input', function () { |
| | | 2160 | | var orderId = $(this).attr('id').split('_')[1]; |
| | | 2161 | | updateCalculations(orderId); |
| | | 2162 | | }); |
| | | 2163 | | |
| | | 2164 | | // Вызываем обновление при загрузке страницы для каждого элемента |
| | | 2165 | | $('.supplier-select').each(function () { |
| | | 2166 | | var orderId = $(this).attr('id').split('_')[1]; |
| | | 2167 | | var selectedSupplier = $(this).find('option:selected'); |
| | | 2168 | | var currencyCode = selectedSupplier.data('currency-code'); |
| | | 2169 | | var weightCurrencyCode = selectedSupplier.data('weight-currency-code'); |
| | | 2170 | | var costFactor = readSupplierDataNumber(selectedSupplier, 'cost-factor', 1); |
| | | 2171 | | |
| | | 2172 | | if (currencyCode) { |
| | | 2173 | | $('#CurrencyCode_' + orderId).text(currencyCode); |
| | | 2174 | | var purchasePriceInRub = updatePurchasePriceInRub(orderId, currencyCode, costFactor); |
| | | 2175 | | updateCostPrice(orderId, currencyCode, costFactor, weightCurrencyCode, purchasePriceInRub); |
| | | 2176 | | } else { |
| | | 2177 | | $('#CurrencyCode_' + orderId).text(''); |
| | | 2178 | | $('#PurchasePriceRub_' + orderId).text(''); |
| | | 2179 | | $('#PurchasePriceHidden_' + orderId).text(''); |
| | | 2180 | | $('#CostPrice_' + orderId).text(''); |
| | | 2181 | | } |
| | | 2182 | | }); |
| | | 2183 | | |
| | | 2184 | | // Вызываем обновление вычислений при загрузке страницы для каждого элемента |
| | | 2185 | | $('.purchase-price-field, .ozon-commission-field').each(function () { |
| | | 2186 | | var orderId = $(this).attr('id').split('_')[1]; |
| | | 2187 | | updateCalculations(orderId); |
| | | 2188 | | }); |
| | | 2189 | | |
| | | 2190 | | function updatePurchasePriceInRub(orderId, currencyCode, costFactor) { |
| | | 2191 | | var purchasePrice = parseFloat($('#PurchasePrice_' + orderId).val()); |
| | | 2192 | | var purchasePriceInRub = 0; |
| | | 2193 | | var titleText = ''; |
| | | 2194 | | |
| | | 2195 | | switch (currencyCode) { |
| | | 2196 | | case 'USD': |
| | | 2197 | | purchasePriceInRub = purchasePrice * costFactor * rateUSD; |
| | | 2198 | | titleText = `${purchasePrice} (Цена закупки USD) * ${costFactor} (Коэффициент) * ${rateUSD} (Курс US |
| | | 2199 | | break; |
| | | 2200 | | case 'EUR': |
| | | 2201 | | purchasePriceInRub = purchasePrice * costFactor * rateEUR; |
| | | 2202 | | titleText = `${purchasePrice} (Цена закупки EUR) * ${costFactor} (Коэффициент) * ${rateEUR} (Курс EU |
| | | 2203 | | break; |
| | | 2204 | | case 'BYN': |
| | | 2205 | | purchasePriceInRub = purchasePrice * costFactor * rateBYN; |
| | | 2206 | | titleText = `${purchasePrice} (Цена закупки BYN) * ${costFactor} (Коэффициент) * ${rateBYN} (Курс BY |
| | | 2207 | | break; |
| | | 2208 | | default: |
| | | 2209 | | purchasePriceInRub = purchasePrice * costFactor; |
| | | 2210 | | titleText = `${purchasePrice} (Цена закупки RUB) * ${costFactor} = ${purchasePriceInRub}`; |
| | | 2211 | | break; |
| | | 2212 | | } |
| | | 2213 | | |
| | | 2214 | | $('#PurchasePrice_' + orderId).attr('title', titleText); |
| | | 2215 | | |
| | | 2216 | | $('#PurchasePriceRub_' + orderId).text(!isNaN(purchasePriceInRub) ? Math.round(purchasePriceInRub) + ' RUB' |
| | | 2217 | | $('#PurchasePriceRub_' + orderId).attr('title', titleText); |
| | | 2218 | | $('#PurchasePrice_' + orderId).data('purchase-price-rub', purchasePriceInRub); |
| | | 2219 | | $('#PurchasePriceHidden_' + orderId).val(!isNaN(purchasePriceInRub) ? Math.round(purchasePriceInRub) : ''); |
| | | 2220 | | |
| | | 2221 | | return purchasePriceInRub; |
| | | 2222 | | } |
| | | 2223 | | |
| | | 2224 | | // Подсчет суммы дополнительных расходов по заказу |
| | | 2225 | | // с приведением к расходу на одну единицу товара |
| | | 2226 | | function getExtraExpensesTotal(orderId) { |
| | | 2227 | | var totalPerUnit = 0; |
| | | 2228 | | |
| | | 2229 | | var container = document.getElementById('extraExpensesContainer_' + orderId); |
| | | 2230 | | if (!container) { |
| | | 2231 | | return 0; |
| | | 2232 | | } |
| | | 2233 | | |
| | | 2234 | | // Определяем количество товара по заказу |
| | | 2235 | | var quantity = 1; |
| | | 2236 | | |
| | | 2237 | | // 1) Пытаемся найти editable поле количества в текущей строке |
| | | 2238 | | var row = container.closest('tr'); |
| | | 2239 | | if (row) { |
| | | 2240 | | var quantityInput = row.querySelector('.quantity-input-field'); |
| | | 2241 | | if (quantityInput) { |
| | | 2242 | | var rawQty = (quantityInput.value || '').toString().replace(',', '.').replace(/\s/g, ''); |
| | | 2243 | | var parsedQty = parseFloat(rawQty); |
| | | 2244 | | if (!isNaN(parsedQty) && parsedQty > 0) { |
| | | 2245 | | quantity = parsedQty; |
| | | 2246 | | } |
| | | 2247 | | } |
| | | 2248 | | } |
| | | 2249 | | |
| | | 2250 | | // 2) Если не нашли/невалидно — пробуем взять из отображаемого количества |
| | | 2251 | | if (!quantity || quantity <= 0) { |
| | | 2252 | | var quantityElement = document.getElementById('Quantity_' + orderId); |
| | | 2253 | | if (quantityElement) { |
| | | 2254 | | // В некоторых представлениях количество может быть вложено в .quantity-display |
| | | 2255 | | var quantityDisplay = quantityElement.querySelector('.quantity-display'); |
| | | 2256 | | var quantityTextSource = quantityDisplay || quantityElement; |
| | | 2257 | | var qtyText = (quantityTextSource.textContent || '') |
| | | 2258 | | .toString() |
| | | 2259 | | .replace(/\s/g, '') |
| | | 2260 | | .replace(',', '.'); |
| | | 2261 | | var parsedQtyText = parseFloat(qtyText); |
| | | 2262 | | if (!isNaN(parsedQtyText) && parsedQtyText > 0) { |
| | | 2263 | | quantity = parsedQtyText; |
| | | 2264 | | } |
| | | 2265 | | } |
| | | 2266 | | } |
| | | 2267 | | |
| | | 2268 | | // 3) Fallback: если количество так и не определено — не делим, чтобы не ломать старое поведение |
| | | 2269 | | if (!quantity || quantity <= 0) { |
| | | 2270 | | quantity = 1; |
| | | 2271 | | } |
| | | 2272 | | |
| | | 2273 | | var amountInputs = container.querySelectorAll('.expense-amount-input'); |
| | | 2274 | | amountInputs.forEach(function (input) { |
| | | 2275 | | if (!input) return; |
| | | 2276 | | |
| | | 2277 | | var raw = (input.value || '').toString().replace(',', '.').replace(/\s/g, ''); |
| | | 2278 | | var val = parseFloat(raw); |
| | | 2279 | | if (!isNaN(val) && val > 0) { |
| | | 2280 | | // Каждый доп. расход распределяем по количеству (расход на единицу товара) |
| | | 2281 | | totalPerUnit += (val / quantity); |
| | | 2282 | | } |
| | | 2283 | | }); |
| | | 2284 | | |
| | | 2285 | | return totalPerUnit; |
| | | 2286 | | } |
| | | 2287 | | |
| | | 2288 | | // Применяет дополнительные расходы к базовой себестоимости и пересчитывает прибыли/наценки |
| | | 2289 | | function updateCostPriceWithExtras(orderId) { |
| | | 2290 | | var costPriceSpan = $('#CostPrice_' + orderId); |
| | | 2291 | | if (!costPriceSpan.length) { |
| | | 2292 | | return; |
| | | 2293 | | } |
| | | 2294 | | |
| | | 2295 | | var baseCostPrice = extractNumber(costPriceSpan.data('base-cost-price')); |
| | | 2296 | | |
| | | 2297 | | // Если базовая себестоимость не посчитана, просто выходим |
| | | 2298 | | if (baseCostPrice === null || isNaN(baseCostPrice) || baseCostPrice === 0) { |
| | | 2299 | | costPriceSpan.text(''); |
| | | 2300 | | costPriceSpan.attr('title', ''); |
| | | 2301 | | costPriceSpan.data('cost-price', ''); |
| | | 2302 | | updateCalculations(orderId); |
| | | 2303 | | return; |
| | | 2304 | | } |
| | | 2305 | | |
| | | 2306 | | // Сумма доп. расходов |
| | | 2307 | | var extraTotal = getExtraExpensesTotal(orderId); |
| | | 2308 | | |
| | | 2309 | | // Итоговая себестоимость = базовая + доп. расходы |
| | | 2310 | | var finalCostPrice = baseCostPrice + extraTotal; |
| | | 2311 | | |
| | | 2312 | | // Формируем title: базовая формула + часть про доп. расходы |
| | | 2313 | | var baseTitle = costPriceSpan.attr('data-base-title') || ''; |
| | | 2314 | | var extraTitlePart = extraTotal > 0 |
| | | 2315 | | ? ` + ${extraTotal} (Доп. расходы) = ${finalCostPrice}` |
| | | 2316 | | : ''; |
| | | 2317 | | |
| | | 2318 | | costPriceSpan.text(Math.round(finalCostPrice) + ' RUB'); |
| | | 2319 | | costPriceSpan.attr('title', baseTitle + extraTitlePart); |
| | | 2320 | | costPriceSpan.data('cost-price', finalCostPrice); |
| | | 2321 | | |
| | | 2322 | | // Пересчитываем прибыль и наценку от новой себестоимости |
| | | 2323 | | updateCalculations(orderId); |
| | | 2324 | | } |
| | | 2325 | | |
| | | 2326 | | // Делаем функцию доступной глобально (используется из _TransactionTable.cshtml) |
| | | 2327 | | window.updateCostPriceWithExtras = updateCostPriceWithExtras; |
| | | 2328 | | |
| | | 2329 | | // Нужна в generateInfoRow.js для предрасчёта прибыли по предложениям поставщиков |
| | | 2330 | | window.getExtraExpensesTotal = getExtraExpensesTotal; |
| | | 2331 | | |
| | | 2332 | | function updateCostPrice(orderId, currencyCode, costFactor, weightCurrencyCode, purchasePriceInRub) { |
| | | 2333 | | var weight = parseFloat($('#ProductWeight_' + orderId).val()); |
| | | 2334 | | var supplierWeightFactor = readSupplierDataNumber($('#SupplierId_' + orderId).find('option:selected'), 'weig |
| | | 2335 | | var supplierWeightFactorInRub = 0; |
| | | 2336 | | |
| | | 2337 | | var supplierWeightFactorInRubTitle = ``; |
| | | 2338 | | switch (weightCurrencyCode) { |
| | | 2339 | | case 'USD': |
| | | 2340 | | supplierWeightFactorInRub = supplierWeightFactor * rateUSD; |
| | | 2341 | | supplierWeightFactorInRubTitle = ` = ${supplierWeightFactor} * ${rateUSD}`; |
| | | 2342 | | break; |
| | | 2343 | | case 'EUR': |
| | | 2344 | | supplierWeightFactorInRub = supplierWeightFactor * rateEUR; |
| | | 2345 | | supplierWeightFactorInRubTitle = ` = ${supplierWeightFactor} * ${rateEUR}`; |
| | | 2346 | | break; |
| | | 2347 | | case 'BYN': |
| | | 2348 | | supplierWeightFactorInRub = supplierWeightFactor * rateBYN; |
| | | 2349 | | supplierWeightFactorInRubTitle = ` = ${supplierWeightFactor} * ${rateBYN}`; |
| | | 2350 | | break; |
| | | 2351 | | default: |
| | | 2352 | | supplierWeightFactorInRub = supplierWeightFactor; |
| | | 2353 | | break; |
| | | 2354 | | } |
| | | 2355 | | |
| | | 2356 | | var costPrice = 0; |
| | | 2357 | | var titleText = ''; |
| | | 2358 | | |
| | | 2359 | | if (!isNaN(weight) && !isNaN(supplierWeightFactor) && !isNaN(purchasePriceInRub) && supplierWeightFactorInRu |
| | | 2360 | | costPrice = (weight * supplierWeightFactorInRub) + purchasePriceInRub; |
| | | 2361 | | titleText = `${weight} (Вес) * ${supplierWeightFactorInRub} (Цена за кг. RUB${supplierWeightFactorInRubT |
| | | 2362 | | } else if (!isNaN(purchasePriceInRub)) { |
| | | 2363 | | costPrice = purchasePriceInRub; |
| | | 2364 | | titleText = `${purchasePriceInRub} (Цена закупки RUB) = ${costPrice}`; |
| | | 2365 | | } else { |
| | | 2366 | | $('#CostPrice_' + orderId).text(''); |
| | | 2367 | | $('#CostPrice_' + orderId).attr('title', ''); |
| | | 2368 | | $('#CostPrice_' + orderId).data('cost-price', ''); |
| | | 2369 | | $('#CostPrice_' + orderId).data('base-cost-price', ''); |
| | | 2370 | | $('#CostPrice_' + orderId).attr('data-base-title', ''); |
| | | 2371 | | updateCalculations(orderId); |
| | | 2372 | | return; |
| | | 2373 | | } |
| | | 2374 | | |
| | | 2375 | | // Сохраняем базовую себестоимость (без доп. расходов) и базовый title |
| | | 2376 | | $('#CostPrice_' + orderId).data('base-cost-price', costPrice); |
| | | 2377 | | $('#CostPrice_' + orderId).attr('data-base-title', titleText); |
| | | 2378 | | |
| | | 2379 | | // Применяем доп. расходы и пересчитываем отображаемую себестоимость/прибыль |
| | | 2380 | | updateCostPriceWithExtras(orderId); |
| | | 2381 | | } |
| | | 2382 | | |
| | | 2383 | | |
| | | 2384 | | function updateCalculations(orderId) { |
| | | 2385 | | var costPrice = extractNumber($('#CostPrice_' + orderId).data('cost-price')); |
| | | 2386 | | var currentPrice = extractNumber($('#CurrentPriceWithDiscount_' + orderId).text()); |
| | | 2387 | | var price = currentPrice !== null && currentPrice > 0 |
| | | 2388 | | ? currentPrice |
| | | 2389 | | : extractNumber($('#Price_' + orderId).text()); |
| | | 2390 | | |
| | | 2391 | | if (costPrice === null || costPrice === 0) { |
| | | 2392 | | // Если costPrice равен null или 0, не выполнять расчеты |
| | | 2393 | | $('#MinProfit_' + orderId).text(''); |
| | | 2394 | | $('#MaxProfit_' + orderId).text(''); |
| | | 2395 | | $('#MaxDiscount_' + orderId).text(''); |
| | | 2396 | | $('#MinDiscount_' + orderId).text(''); |
| | | 2397 | | if (typeof window.updateOfferMetrics === 'function') { |
| | | 2398 | | window.updateOfferMetrics(orderId); |
| | | 2399 | | } |
| | | 2400 | | return; |
| | | 2401 | | } |
| | | 2402 | | |
| | | 2403 | | var minOzonCommission = extractNumber($('#MinOzonCommission_' + orderId).val() || $('#MinOzonCommission_' + |
| | | 2404 | | var maxOzonCommission = extractNumber($('#MaxOzonCommission_' + orderId).val() || $('#MaxOzonCommission_' + |
| | | 2405 | | |
| | | 2406 | | var startCommission = price - costPrice; |
| | | 2407 | | var minProfit = startCommission - maxOzonCommission; |
| | | 2408 | | var maxProfit = startCommission - minOzonCommission; |
| | | 2409 | | var minDiscount = (minProfit / costPrice) * 100; |
| | | 2410 | | var maxDiscount = (maxProfit / costPrice) * 100; |
| | | 2411 | | |
| | | 2412 | | // Округляем до целого числа и обрезаем до 4 знаков, если нужно |
| | | 2413 | | minProfit = Math.floor(minProfit); |
| | | 2414 | | maxProfit = Math.floor(maxProfit); |
| | | 2415 | | minDiscount = Math.floor(minDiscount); |
| | | 2416 | | maxDiscount = Math.floor(maxDiscount); |
| | | 2417 | | |
| | | 2418 | | var displayMinProfit = txFormatMetric(minProfit); |
| | | 2419 | | var displayMaxProfit = txFormatMetric(maxProfit); |
| | | 2420 | | var displayMinDiscount = txFormatMetric(minDiscount); |
| | | 2421 | | var displayMaxDiscount = txFormatMetric(maxDiscount); |
| | | 2422 | | |
| | | 2423 | | // Единица измерения вторична визуально — как в списке заказов. |
| | | 2424 | | var pct = '<span class="orders-unit">%</span>'; |
| | | 2425 | | $('#MinProfit_' + orderId).text(!isNaN(minProfit) ? displayMinProfit : ""); |
| | | 2426 | | $('#MaxProfit_' + orderId).text(!isNaN(maxProfit) ? displayMaxProfit : ""); |
| | | 2427 | | $('#MaxDiscount_' + orderId).html(!isNaN(maxDiscount) ? displayMaxDiscount + pct : ""); |
| | | 2428 | | $('#MinDiscount_' + orderId).html(!isNaN(minDiscount) ? displayMinDiscount + pct : ""); |
| | | 2429 | | |
| | | 2430 | | // 13,5% от цены сайта (fallback: цена заказа) рядом с прибылью |
| | | 2431 | | var targetSpan = $('#TargetProfit_' + orderId); |
| | | 2432 | | var targetSpan2 = $('#TargetProfit2_' + orderId); |
| | | 2433 | | if (targetSpan.length) { |
| | | 2434 | | var basePrice = null; |
| | | 2435 | | |
| | | 2436 | | // Ищем строку заказа по ячейке с MaxProfit |
| | | 2437 | | var row = $('#MaxProfit_' + orderId).closest('tr'); |
| | | 2438 | | if (row.length) { |
| | | 2439 | | basePrice = currentPrice !== null && currentPrice > 0 |
| | | 2440 | | ? currentPrice |
| | | 2441 | | : extractNumber(row.find('[id^="Price_"]').text()); |
| | | 2442 | | } |
| | | 2443 | | |
| | | 2444 | | if (basePrice !== null && !isNaN(basePrice) && basePrice > 0) { |
| | | 2445 | | var targetProfit = Math.floor(basePrice * 0.135); |
| | | 2446 | | var suffixText = ' / -' + targetProfit; |
| | | 2447 | | targetSpan.text(suffixText).addClass('tx-metric-suffix'); |
| | | 2448 | | targetSpan2.text(suffixText).addClass('tx-metric-suffix'); |
| | | 2449 | | } else { |
| | | 2450 | | targetSpan.text('').removeClass('tx-metric-suffix'); |
| | | 2451 | | targetSpan2.text('').removeClass('tx-metric-suffix'); |
| | | 2452 | | } |
| | | 2453 | | } |
| | | 2454 | | |
| | | 2455 | | // Отрицательное значение — семантический класс, а не инлайновый цвет. |
| | | 2456 | | txMarkSign($('#MinProfit_' + orderId), minProfit); |
| | | 2457 | | txMarkSign($('#MaxProfit_' + orderId), maxProfit); |
| | | 2458 | | txMarkSign($('#MaxDiscount_' + orderId), maxDiscount); |
| | | 2459 | | txMarkSign($('#MinDiscount_' + orderId), minDiscount); |
| | | 2460 | | |
| | | 2461 | | // Добавляем title к полям |
| | | 2462 | | $('#MinProfit_' + orderId).attr('title', `${price} (Цена сайта) - ${costPrice} (Себестоимость) - ${maxOzonCo |
| | | 2463 | | $('#MaxProfit_' + orderId).attr('title', `${price} (Цена сайта) - ${costPrice} (Себестоимость) - ${minOzonCo |
| | | 2464 | | |
| | | 2465 | | $('#MaxDiscount_' + orderId).attr('title', `${maxProfit} (Макс. Прибыль) / ${costPrice} (Себестоимость) * 10 |
| | | 2466 | | $('#MinDiscount_' + orderId).attr('title', `${minProfit} (Мин. Прибыль) / ${costPrice} (Себестоимость) * 100 |
| | | 2467 | | |
| | | 2468 | | // Предрасчёт по предложениям поставщиков считается от тех же цены и комиссии |
| | | 2469 | | if (typeof window.updateOfferMetrics === 'function') { |
| | | 2470 | | window.updateOfferMetrics(orderId); |
| | | 2471 | | } |
| | | 2472 | | } |
| | | 2473 | | |
| | | 2474 | | |
| | | 2475 | | }); |
| | | 2476 | | </script> |
| | | 2477 | | |
| | | 2478 | | |
| | | 2479 | | |
| | | 2480 | | |
| | | 2481 | | <script> |
| | | 2482 | | var inputChangeEvent = new Event('inputchange', { |
| | | 2483 | | bubbles: true, |
| | | 2484 | | cancelable: true |
| | | 2485 | | }); |
| | | 2486 | | |
| | | 2487 | | // Находим общее поле для выбора из выпадающего списка и поля для обновления в цикле |
| | | 2488 | | var universalSelect = document.getElementById('universalSelect'); |
| | | 2489 | | var supplierSelects = document.querySelectorAll('.supplier-select'); |
| | | 2490 | | |
| | | 2491 | | // Добавляем обработчик события на изменение значения общего поля |
| | | 2492 | | if (universalSelect) { |
| | | 2493 | | universalSelect.addEventListener('change', function () { |
| | | 2494 | | var value = this.value; // Получаем выбранное значение из общего поля |
| | | 2495 | | |
| | | 2496 | | // Обновляем значения полей в цикле |
| | | 2497 | | if (supplierSelects && supplierSelects.length > 0) { |
| | | 2498 | | supplierSelects.forEach(function (select) { |
| | | 2499 | | if (select) { |
| | | 2500 | | select.value = value; |
| | | 2501 | | } |
| | | 2502 | | }); |
| | | 2503 | | } |
| | | 2504 | | }); |
| | | 2505 | | } |
| | | 2506 | | </script> |
| | | 2507 | | |
| | | 2508 | | <script> |
| | | 2509 | | function getStatusContrastColor(hexColor) { |
| | | 2510 | | if (!hexColor || hexColor === '#ffffff') return '#000000'; |
| | | 2511 | | hexColor = hexColor.replace('#', ''); |
| | | 2512 | | if (hexColor.length === 3) { |
| | | 2513 | | hexColor = hexColor[0] + hexColor[0] + hexColor[1] + hexColor[1] + hexColor[2] + hexColor[2]; |
| | | 2514 | | } |
| | | 2515 | | var r = parseInt(hexColor.substr(0, 2), 16); |
| | | 2516 | | var g = parseInt(hexColor.substr(2, 2), 16); |
| | | 2517 | | var b = parseInt(hexColor.substr(4, 2), 16); |
| | | 2518 | | var luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; |
| | | 2519 | | return luminance > 0.5 ? '#000000' : '#ffffff'; |
| | | 2520 | | } |
| | | 2521 | | |
| | | 2522 | | function applyStatusSelectColor(select) { |
| | | 2523 | | if (!select || !select.options || select.selectedIndex < 0) return; |
| | | 2524 | | var selectedOption = select.options[select.selectedIndex]; |
| | | 2525 | | var color = selectedOption.getAttribute('data-color'); |
| | | 2526 | | select.classList.remove('highlight-null', 'highlight-success'); |
| | | 2527 | | if (color) { |
| | | 2528 | | select.style.setProperty('background-color', color, 'important'); |
| | | 2529 | | select.style.setProperty('color', getStatusContrastColor(color), 'important'); |
| | | 2530 | | return; |
| | | 2531 | | } |
| | | 2532 | | select.style.removeProperty('background-color'); |
| | | 2533 | | select.style.removeProperty('color'); |
| | | 2534 | | var selectedText = (selectedOption.text || '').trim(); |
| | | 2535 | | if (selectedText === 'Заказан поставщику') { |
| | | 2536 | | select.classList.add('highlight-success'); |
| | | 2537 | | } else if (selectedText === 'Отменен') { |
| | | 2538 | | select.classList.add('highlight-null'); |
| | | 2539 | | } |
| | | 2540 | | } |
| | | 2541 | | |
| | | 2542 | | function syncStatusSelectAppearance(select) { |
| | | 2543 | | if (!select) return; |
| | | 2544 | | if (select.dataset.customSelect === '1' && typeof window.syncCustomSelect === 'function') { |
| | | 2545 | | window.syncCustomSelect(select); |
| | | 2546 | | return; |
| | | 2547 | | } |
| | | 2548 | | applyStatusSelectColor(select); |
| | | 2549 | | } |
| | | 2550 | | |
| | | 2551 | | window.handleStatusSelectChange = function (selectElement) { |
| | | 2552 | | if (!selectElement) return; |
| | | 2553 | | if (selectElement.value === 'create') { |
| | 0 | 2554 | | window.location.href = '@Url.Action("Create", "AppStatus")'; |
| | | 2555 | | return; |
| | | 2556 | | } |
| | | 2557 | | |
| | | 2558 | | var selectedStatus = selectElement.value; |
| | | 2559 | | var rowStatusSelects = document.querySelectorAll('#shipmentTable tbody .status-select'); |
| | | 2560 | | var supplierValuesMap = {}; |
| | | 2561 | | |
| | | 2562 | | rowStatusSelects.forEach(function (statusSelect) { |
| | | 2563 | | if (!statusSelect || !statusSelect.id) return; |
| | | 2564 | | var orderId = statusSelect.id.split('_')[1]; |
| | | 2565 | | var supplierSelect = document.getElementById('SupplierId_' + orderId); |
| | | 2566 | | if (supplierSelect) { |
| | | 2567 | | supplierValuesMap[orderId] = supplierSelect.value; |
| | | 2568 | | } |
| | | 2569 | | statusSelect.value = selectedStatus; |
| | | 2570 | | syncStatusSelectAppearance(statusSelect); |
| | | 2571 | | statusSelect.dispatchEvent(new Event('change', { bubbles: true })); |
| | | 2572 | | }); |
| | | 2573 | | |
| | | 2574 | | syncStatusSelectAppearance(selectElement); |
| | | 2575 | | |
| | | 2576 | | requestAnimationFrame(function () { |
| | | 2577 | | Object.keys(supplierValuesMap).forEach(function (orderId) { |
| | | 2578 | | var supplierValue = supplierValuesMap[orderId]; |
| | | 2579 | | if (!supplierValue) return; |
| | | 2580 | | var supplierSelect = document.getElementById('SupplierId_' + orderId); |
| | | 2581 | | if (supplierSelect && supplierSelect.value !== supplierValue) { |
| | | 2582 | | supplierSelect.value = supplierValue; |
| | | 2583 | | supplierSelect.dispatchEvent(new Event('change', { bubbles: true })); |
| | | 2584 | | } |
| | | 2585 | | }); |
| | | 2586 | | }); |
| | | 2587 | | }; |
| | | 2588 | | |
| | | 2589 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 2590 | | document.querySelectorAll('#shipmentTable .status-select').forEach(syncStatusSelectAppearance); |
| | | 2591 | | var mainSelect = document.getElementById('statusSelect'); |
| | | 2592 | | if (mainSelect) { |
| | | 2593 | | syncStatusSelectAppearance(mainSelect); |
| | | 2594 | | } |
| | | 2595 | | initTransactionRowStatusHandlers(); |
| | | 2596 | | initPurchasePriceFieldValidation(); |
| | | 2597 | | setTimeout(function () { |
| | | 2598 | | initTransactionRowStatusHandlers(); |
| | | 2599 | | document.querySelectorAll('#shipmentTable .status-select').forEach(syncStatusSelectAppearance); |
| | | 2600 | | }, 0); |
| | | 2601 | | }); |
| | | 2602 | | |
| | | 2603 | | var TRANSACTION_ROW_COLOR_CLASSES = ['table-secondary', 'table-success', 'table-info', 'table-warning', 'table-dange |
| | | 2604 | | |
| | | 2605 | | function isWhiteStatusColor(color) { |
| | | 2606 | | var c = (color || '').trim().toLowerCase(); |
| | | 2607 | | return !c || c === '#ffffff' || c === '#fff' || c === 'white' || c === 'rgb(255, 255, 255)'; |
| | | 2608 | | } |
| | | 2609 | | |
| | | 2610 | | function applyRowBackgroundFromStatus(row, select) { |
| | | 2611 | | if (!row || !select || !select.options || select.selectedIndex < 0) return; |
| | | 2612 | | var selectedOption = select.options[select.selectedIndex]; |
| | | 2613 | | var selectedText = (selectedOption.text || '').trim(); |
| | | 2614 | | row.classList.remove.apply(row.classList, TRANSACTION_ROW_COLOR_CLASSES); |
| | | 2615 | | row.querySelectorAll('td').forEach(function (td) { |
| | | 2616 | | td.style.backgroundColor = ''; |
| | | 2617 | | }); |
| | | 2618 | | delete row.dataset.statusColor; |
| | | 2619 | | if (selectedText === 'Заказан поставщику') { |
| | | 2620 | | row.classList.add('table-success'); |
| | | 2621 | | } else if (selectedText === 'Отгружен поставщиком') { |
| | | 2622 | | row.classList.add('table-info'); |
| | | 2623 | | } else if (selectedText === 'Отгружен реализатору') { |
| | | 2624 | | row.classList.add('table-warning'); |
| | | 2625 | | } else if (selectedText === 'Заказан реализатору') { |
| | | 2626 | | row.classList.add('table-secondary'); |
| | | 2627 | | } else if (selectedText === 'Отменен' || selectedText === 'Отменён') { |
| | | 2628 | | row.classList.add('table-danger'); |
| | | 2629 | | } else { |
| | | 2630 | | var originalRowClass = row.getAttribute('data-original-row-class') || ''; |
| | | 2631 | | originalRowClass.split(/\s+/).forEach(function (cls) { |
| | | 2632 | | if (cls && TRANSACTION_ROW_COLOR_CLASSES.indexOf(cls) >= 0) { |
| | | 2633 | | row.classList.add(cls); |
| | | 2634 | | } |
| | | 2635 | | }); |
| | | 2636 | | } |
| | | 2637 | | } |
| | | 2638 | | |
| | | 2639 | | window.applyTransactionRowStatus = function (select) { |
| | | 2640 | | if (!select || !select.id || !select.id.startsWith('AppStatusId_')) return; |
| | | 2641 | | if (!select.options || select.selectedIndex < 0) return; |
| | | 2642 | | |
| | | 2643 | | var selectedText = (select.options[select.selectedIndex].text || '').trim(); |
| | | 2644 | | var orderId = select.id.split('_')[1]; |
| | | 2645 | | var purchasePriceField = document.getElementById('PurchasePrice_' + orderId); |
| | | 2646 | | var row = document.getElementById('orderRow_' + orderId); |
| | | 2647 | | |
| | | 2648 | | select.classList.remove('highlight-null', 'highlight-info'); |
| | | 2649 | | if (selectedText === 'Отгружен поставщиком') { |
| | | 2650 | | select.classList.add('highlight-info'); |
| | | 2651 | | } else if (selectedText === 'Отменен' || selectedText === 'Отменён') { |
| | | 2652 | | select.classList.add('highlight-null'); |
| | | 2653 | | } |
| | | 2654 | | |
| | | 2655 | | if (purchasePriceField) { |
| | | 2656 | | var isCancelled = selectedText === 'Отменен' || selectedText === 'Отменён'; |
| | | 2657 | | purchasePriceField.disabled = isCancelled; |
| | | 2658 | | purchasePriceField.classList.toggle('disabled-field', isCancelled); |
| | | 2659 | | } |
| | | 2660 | | |
| | | 2661 | | applyRowBackgroundFromStatus(row, select); |
| | | 2662 | | syncStatusSelectAppearance(select); |
| | | 2663 | | }; |
| | | 2664 | | |
| | | 2665 | | function initTransactionRowStatusHandlers() { |
| | | 2666 | | document.querySelectorAll('#shipmentTable tbody .status-select[id^="AppStatusId_"]').forEach(function (select) { |
| | | 2667 | | if (select.dataset.rowStatusBound === '1') return; |
| | | 2668 | | select.dataset.rowStatusBound = '1'; |
| | | 2669 | | select.addEventListener('change', function () { |
| | | 2670 | | window.applyTransactionRowStatus(select); |
| | | 2671 | | }); |
| | | 2672 | | window.applyTransactionRowStatus(select); |
| | | 2673 | | }); |
| | | 2674 | | } |
| | | 2675 | | |
| | | 2676 | | function initPurchasePriceFieldValidation() { |
| | | 2677 | | document.querySelectorAll('#shipmentTable .purchase-price-field').forEach(function (field) { |
| | | 2678 | | if (field.dataset.priceValidationBound === '1') return; |
| | | 2679 | | field.dataset.priceValidationBound = '1'; |
| | | 2680 | | if (field.dataset.priceRequired === 'false') { |
| | | 2681 | | field.classList.remove('highlight-error', 'is-invalid'); |
| | | 2682 | | return; |
| | | 2683 | | } |
| | | 2684 | | field.addEventListener('input', function () { |
| | | 2685 | | this.classList.toggle('highlight-error', this.value.trim() === ''); |
| | | 2686 | | }); |
| | | 2687 | | if (field.value.trim() === '') { |
| | | 2688 | | field.classList.add('highlight-error'); |
| | | 2689 | | } |
| | | 2690 | | }); |
| | | 2691 | | } |
| | | 2692 | | </script> |
| | | 2693 | | |
| | | 2694 | | <script> |
| | | 2695 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 2696 | | // Находим общее поле для ввода и все целевые поля для обновления |
| | | 2697 | | var universalField = document.getElementById('universalField'); |
| | | 2698 | | var supplierFields = document.querySelectorAll('.supplier-field'); |
| | | 2699 | | |
| | | 2700 | | // Добавляем обработчик события на изменение значения общего поля |
| | | 2701 | | if (universalField) { |
| | | 2702 | | universalField.addEventListener('input', function () { |
| | | 2703 | | var value = this.value; // Получаем текущее значение из общего поля |
| | | 2704 | | |
| | | 2705 | | // Обновляем значения целевых полей в цикле |
| | | 2706 | | if (supplierFields && supplierFields.length > 0) { |
| | | 2707 | | supplierFields.forEach(function (field) { |
| | | 2708 | | if (field) { |
| | | 2709 | | field.value = value; |
| | | 2710 | | field.dispatchEvent(new Event('change')); |
| | | 2711 | | } |
| | | 2712 | | }); |
| | | 2713 | | } |
| | | 2714 | | }); |
| | | 2715 | | } |
| | | 2716 | | }); |
| | | 2717 | | |
| | | 2718 | | </script> |
| | | 2719 | | |
| | | 2720 | | <script> |
| | | 2721 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 2722 | | // Находим общее поле для ввода и все целевые поля для обновления |
| | | 2723 | | var universalField = document.getElementById('universalOrderNumberToSupplier'); |
| | | 2724 | | var universalOrderNumberToSupplier = document.querySelectorAll('.orderNumberToSupplie-field'); |
| | | 2725 | | |
| | | 2726 | | // Добавляем обработчик события на изменение значения общего поля |
| | | 2727 | | if (universalField) { |
| | | 2728 | | universalField.addEventListener('input', function () { |
| | | 2729 | | var value = this.value; // Получаем текущее значение из общего поля |
| | | 2730 | | |
| | | 2731 | | // Обновляем значения целевых полей в цикле |
| | | 2732 | | if (universalOrderNumberToSupplier && universalOrderNumberToSupplier.length > 0) { |
| | | 2733 | | universalOrderNumberToSupplier.forEach(function (field) { |
| | | 2734 | | if (field) { |
| | | 2735 | | field.value = value; |
| | | 2736 | | } |
| | | 2737 | | }); |
| | | 2738 | | } |
| | | 2739 | | }); |
| | | 2740 | | } |
| | | 2741 | | }); |
| | | 2742 | | </script> |
| | | 2743 | | |
| | | 2744 | | <script> |
| | | 2745 | | |
| | | 2746 | | // Получаем блок modal-body |
| | | 2747 | | const modalBody = document.querySelector('.modal-body-checkbox'); |
| | | 2748 | | |
| | | 2749 | | // Получаем все чекбоксы внутри блока modal-body |
| | | 2750 | | if (modalBody) { |
| | | 2751 | | const checkboxes = modalBody.querySelectorAll('input[type="checkbox"]'); |
| | | 2752 | | |
| | | 2753 | | // Перебираем чекбоксы и добавляем обработчик события change |
| | | 2754 | | checkboxes.forEach(function (checkbox) { |
| | | 2755 | | if (checkbox) { |
| | | 2756 | | checkbox.addEventListener('change', function (event) { |
| | | 2757 | | if (event && event.target) { |
| | | 2758 | | const checkboxId = event.target.id; |
| | | 2759 | | const checkboxState = event.target.checked; |
| | | 2760 | | if (typeof processCheckboxState === 'function') { |
| | | 2761 | | processCheckboxState(checkboxId, checkboxState); |
| | | 2762 | | } |
| | | 2763 | | } |
| | | 2764 | | }); |
| | | 2765 | | } |
| | | 2766 | | }); |
| | | 2767 | | } |
| | | 2768 | | |
| | | 2769 | | // Функция для скрытия и раскрытия столбца таблицы по его ID |
| | | 2770 | | function toggleColumnVisibility(columnIndex, isVisible) { |
| | | 2771 | | const table = document.getElementById("shipmentTable"); |
| | | 2772 | | if (table) { |
| | | 2773 | | const rows = table.rows; |
| | | 2774 | | for (let i = 0; i < rows.length; i++) { |
| | | 2775 | | const cells = rows[i].cells; |
| | | 2776 | | if (!cells[columnIndex]) continue; |
| | | 2777 | | if (cells[columnIndex].classList.contains('permanent-col')) continue; |
| | | 2778 | | |
| | | 2779 | | isVisible = typeof isVisible === 'string' ? isVisible.toLowerCase() === 'true' : isVisible; |
| | | 2780 | | |
| | | 2781 | | if (!isVisible) { |
| | | 2782 | | cells[columnIndex].style.display = 'none'; |
| | | 2783 | | } else { |
| | | 2784 | | cells[columnIndex].style.display = ''; |
| | | 2785 | | } |
| | | 2786 | | |
| | | 2787 | | } |
| | | 2788 | | } |
| | | 2789 | | } |
| | | 2790 | | </script> |
| | | 2791 | | |
| | | 2792 | | |
| | | 2793 | | <script> |
| | | 2794 | | document.addEventListener('DOMContentLoaded', (event) => { |
| | | 2795 | | const supplierSelects = document.querySelectorAll('.supplier-select'); |
| | | 2796 | | |
| | | 2797 | | function checkField(select) { |
| | | 2798 | | const value = select.value.trim().toLowerCase(); |
| | | 2799 | | const selectedOption = select.options[select.selectedIndex]; |
| | | 2800 | | const selectedText = selectedOption ? selectedOption.text : ''; |
| | | 2801 | | |
| | | 2802 | | if (value === "" || selectedText.startsWith('Не указан')) { |
| | | 2803 | | select.classList.add('highlight-error'); |
| | | 2804 | | } else { |
| | | 2805 | | select.classList.remove('highlight-error'); |
| | | 2806 | | } |
| | | 2807 | | } |
| | | 2808 | | |
| | | 2809 | | supplierSelects.forEach(select => { |
| | | 2810 | | select.addEventListener('change', function () { |
| | | 2811 | | checkField(this); |
| | | 2812 | | }); |
| | | 2813 | | |
| | | 2814 | | checkField(select); |
| | | 2815 | | }); |
| | | 2816 | | }); |
| | | 2817 | | </script> |
| | | 2818 | | |
| | | 2819 | | |
| | | 2820 | | <script> |
| | | 2821 | | // Guard: на некоторых страницах/вариантах шаблона функция `isDataTypeEnabled` может не быть загружена, |
| | | 2822 | | // а она используется при раскрытии доп. информации. |
| | | 2823 | | // Логика: |
| | | 2824 | | // 1) если есть чекбокс с id === dataType — используем его состояние |
| | | 2825 | | // 2) иначе пробуем прочитать cookie dataType (ключ: `${dataType}=`) как "true/false" |
| | | 2826 | | // 3) если и cookie нет — по умолчанию разрешаем (true) |
| | | 2827 | | window.isDataTypeEnabled = window.isDataTypeEnabled || function (dataType) { |
| | | 2828 | | try { |
| | | 2829 | | if (!dataType) return true; |
| | | 2830 | | |
| | | 2831 | | const type = String(dataType); |
| | | 2832 | | const checkbox = document.getElementById(type); |
| | | 2833 | | if (checkbox) return !!checkbox.checked; |
| | | 2834 | | |
| | | 2835 | | const cookieKey = type + "="; |
| | | 2836 | | const cookies = (document.cookie || "") |
| | | 2837 | | .split(";") |
| | | 2838 | | .map(c => c.trim()) |
| | | 2839 | | .filter(Boolean); |
| | | 2840 | | |
| | | 2841 | | const cookie = cookies.find(c => c.startsWith(cookieKey)); |
| | | 2842 | | if (!cookie) return true; |
| | | 2843 | | |
| | | 2844 | | const rawValue = cookie.substring(cookieKey.length); |
| | | 2845 | | const value = decodeURIComponent(rawValue); |
| | | 2846 | | return value === "true"; |
| | | 2847 | | } catch { |
| | | 2848 | | return true; |
| | | 2849 | | } |
| | | 2850 | | }; |
| | | 2851 | | |
| | | 2852 | | // Глобальный кеш для хранения данных |
| | | 2853 | | const orderDataCache = {}; |
| | | 2854 | | let lastCartBadgeCount = 0; |
| | | 2855 | | const rowCache = new Map(); |
| | 0 | 2856 | | const armtekQuotationEnabled = @Json.Serialize(Model.ArmtekQuotationEnabled); |
| | 0 | 2857 | | const armtekByQuotationEnabled = @Json.Serialize(Model.ArmtekByQuotationEnabled); |
| | 0 | 2858 | | const shateMQuotationEnabled = @Json.Serialize(Model.ShateMQuotationEnabled); |
| | 0 | 2859 | | const avdQuotationEnabled = @Json.Serialize(Model.AvdQuotationEnabled); |
| | 0 | 2860 | | const bergQuotationEnabled = @Json.Serialize(Model.BergQuotationEnabled); |
| | 0 | 2861 | | const motexQuotationEnabled = @Json.Serialize(Model.MotexQuotationEnabled); |
| | 0 | 2862 | | const mlAutoQuotationEnabled = @Json.Serialize(Model.MlAutoQuotationEnabled); |
| | 0 | 2863 | | const mlAutoRuQuotationEnabled = @Json.Serialize(Model.MlAutoRuQuotationEnabled); |
| | 0 | 2864 | | const forumAutoQuotationEnabled = @Json.Serialize(Model.ForumAutoQuotationEnabled); |
| | 0 | 2865 | | const forumAutoInterpartsQuotationEnabled = @Json.Serialize(Model.ForumAutoInterpartsQuotationEnabled); |
| | 0 | 2866 | | const forumAutoPiterQuotationEnabled = @Json.Serialize(Model.ForumAutoPiterQuotationEnabled); |
| | 0 | 2867 | | const forumAutoRostovQuotationEnabled = @Json.Serialize(Model.ForumAutoRostovQuotationEnabled); |
| | 0 | 2868 | | const forumAutoIstraQuotationEnabled = @Json.Serialize(Model.ForumAutoIstraQuotationEnabled); |
| | 0 | 2869 | | const forumAutoDisplayName = @Json.Serialize(Model.ForumAutoDisplayName); |
| | 0 | 2870 | | const forumAutoInterpartsDisplayName = @Json.Serialize(Model.ForumAutoInterpartsDisplayName); |
| | 0 | 2871 | | const forumAutoPiterDisplayName = @Json.Serialize(Model.ForumAutoPiterDisplayName); |
| | 0 | 2872 | | const forumAutoRostovDisplayName = @Json.Serialize(Model.ForumAutoRostovDisplayName); |
| | 0 | 2873 | | const forumAutoIstraDisplayName = @Json.Serialize(Model.ForumAutoIstraDisplayName); |
| | 0 | 2874 | | const favoritPartsQuotationEnabled = @Json.Serialize(Model.FavoritPartsQuotationEnabled); |
| | 0 | 2875 | | const favoritPartsIstraQuotationEnabled = @Json.Serialize(Model.FavoritPartsIstraQuotationEnabled); |
| | 0 | 2876 | | const favoritPartsRostovQuotationEnabled = @Json.Serialize(Model.FavoritPartsRostovQuotationEnabled); |
| | 0 | 2877 | | const moskvorechieIstraQuotationEnabled = @Json.Serialize(Model.MoskvorechieIstraQuotationEnabled); |
| | 0 | 2878 | | const profitLigaQuotationEnabled = @Json.Serialize(Model.ProfitLigaQuotationEnabled); |
| | 0 | 2879 | | const nikeiQuotationEnabled = @Json.Serialize(Model.NikeiQuotationEnabled); |
| | 0 | 2880 | | const mikadoMskHod20QuotationEnabled = @Json.Serialize(Model.MikadoMskHod20QuotationEnabled); |
| | 0 | 2881 | | const zzapMoscowQuotationEnabled = @Json.Serialize(Model.ZZapMoscowQuotationEnabled); |
| | 0 | 2882 | | const japartsQuotationEnabled = @Json.Serialize(Model.JapartsQuotationEnabled); |
| | | 2883 | | const apiSupplierCurrencyRates = { |
| | | 2884 | | RUB: 1, |
| | 0 | 2885 | | USD: parseFloat('@Model.RateUSD'.replace(',', '.')), |
| | 0 | 2886 | | EUR: parseFloat('@Model.RateEUR'.replace(',', '.')), |
| | 0 | 2887 | | BYN: parseFloat('@Model.RateBYN'.replace(',', '.')) |
| | | 2888 | | }; |
| | 0 | 2889 | | const apiSupplierAllowedWarehouses = @Json.Serialize(Model.ApiSupplierAllowedWarehouses ?? new Dictionary<string, Li |
| | | 2890 | | const studioWarehouseNameToId = { |
| | 0 | 2891 | | @if (Model.Warehouses != null) |
| | | 2892 | | { |
| | 0 | 2893 | | @foreach (var w in Model.Warehouses) |
| | | 2894 | | { |
| | 0 | 2895 | | if (!string.IsNullOrWhiteSpace(w.Name)) |
| | | 2896 | | { |
| | 0 | 2897 | | @:"@Html.Raw(w.Name.Trim().ToLowerInvariant().Replace("\"", "\\\""))": @w.Id, |
| | | 2898 | | } |
| | | 2899 | | } |
| | | 2900 | | } |
| | | 2901 | | }; |
| | | 2902 | | |
| | | 2903 | | function getEffectiveOrderWarehouseId(orderId) { |
| | | 2904 | | const zasyl = getZasylWarehouseName(orderId); |
| | | 2905 | | if (zasyl) { |
| | | 2906 | | const key = zasyl.trim().toLowerCase(); |
| | | 2907 | | if (studioWarehouseNameToId[key]) { |
| | | 2908 | | return studioWarehouseNameToId[key]; |
| | | 2909 | | } |
| | | 2910 | | } |
| | | 2911 | | const select = document.getElementById(`ShipmentWarehouseId_${orderId}`); |
| | | 2912 | | if (select && select.value) { |
| | | 2913 | | const id = parseInt(select.value, 10); |
| | | 2914 | | if (!isNaN(id) && id > 0) return id; |
| | | 2915 | | } |
| | | 2916 | | const row = document.querySelector(`.transaction-order-row[data-order-id="${orderId}"]`); |
| | | 2917 | | if (row && row.dataset.shipmentWarehouseId) { |
| | | 2918 | | const id = parseInt(row.dataset.shipmentWarehouseId, 10); |
| | | 2919 | | if (!isNaN(id) && id > 0) return id; |
| | | 2920 | | } |
| | | 2921 | | return null; |
| | | 2922 | | } |
| | | 2923 | | |
| | | 2924 | | function isSupplierAllowedForOrderWarehouse(providerKey, orderId) { |
| | | 2925 | | if (!providerKey || !apiSupplierAllowedWarehouses) return true; |
| | | 2926 | | const allowed = apiSupplierAllowedWarehouses[providerKey]; |
| | | 2927 | | if (!allowed || !Array.isArray(allowed) || allowed.length === 0) return true; |
| | | 2928 | | const warehouseId = getEffectiveOrderWarehouseId(orderId); |
| | | 2929 | | if (!warehouseId) return true; |
| | | 2930 | | return allowed.includes(warehouseId); |
| | | 2931 | | } |
| | | 2932 | | const apiSupplierQuotationSources = [ |
| | | 2933 | | { |
| | | 2934 | | providerKey: 'Armtek', |
| | | 2935 | | displayName: 'Армтек', |
| | | 2936 | | cacheKey: 'armtekPriceData', |
| | 0 | 2937 | | endpoint: '@Url.Action("Search", "ArmtekPrice")', |
| | 0 | 2938 | | warmupEndpoint: '@Url.Action("Warmup", "ArmtekPrice")', |
| | | 2939 | | isEnabled: () => armtekQuotationEnabled, |
| | | 2940 | | process: processArmtekPriceData |
| | | 2941 | | }, |
| | | 2942 | | { |
| | | 2943 | | providerKey: 'ArmtekBy', |
| | | 2944 | | displayName: 'Армтек BY', |
| | | 2945 | | cacheKey: 'armtekByPriceData', |
| | 0 | 2946 | | endpoint: '@Url.Action("Search", "ArmtekByPrice")', |
| | 0 | 2947 | | warmupEndpoint: '@Url.Action("Warmup", "ArmtekByPrice")', |
| | | 2948 | | isEnabled: () => armtekByQuotationEnabled, |
| | | 2949 | | process: processArmtekByPriceData |
| | | 2950 | | }, |
| | | 2951 | | { |
| | | 2952 | | providerKey: 'ShateM', |
| | | 2953 | | displayName: 'Шате‑М', |
| | | 2954 | | cacheKey: 'shateMPriceData', |
| | 0 | 2955 | | endpoint: '@Url.Action("Search", "ShateMPrice")', |
| | | 2956 | | isEnabled: () => shateMQuotationEnabled, |
| | | 2957 | | process: processShateMPriceData |
| | | 2958 | | }, |
| | | 2959 | | { |
| | | 2960 | | providerKey: 'Avd', |
| | | 2961 | | displayName: 'АВД', |
| | | 2962 | | cacheKey: 'avdPriceData', |
| | 0 | 2963 | | endpoint: '@Url.Action("Search", "AvdPrice")', |
| | | 2964 | | isEnabled: () => avdQuotationEnabled, |
| | | 2965 | | process: processAvdPriceData |
| | | 2966 | | }, |
| | | 2967 | | { |
| | | 2968 | | providerKey: 'Berg', |
| | | 2969 | | displayName: 'Берг', |
| | | 2970 | | cacheKey: 'bergPriceData', |
| | 0 | 2971 | | endpoint: '@Url.Action("Search", "BergPrice")', |
| | | 2972 | | isEnabled: () => bergQuotationEnabled, |
| | | 2973 | | process: processBergPriceData |
| | | 2974 | | }, |
| | | 2975 | | { |
| | | 2976 | | providerKey: 'Japarts', |
| | | 2977 | | displayName: 'Japarts', |
| | | 2978 | | cacheKey: 'japartsPriceData', |
| | 0 | 2979 | | endpoint: '@Url.Action("Search", "JapartsPrice")', |
| | | 2980 | | isEnabled: () => japartsQuotationEnabled, |
| | | 2981 | | process: processJapartsPriceData |
| | | 2982 | | }, |
| | | 2983 | | { |
| | | 2984 | | providerKey: 'Motex', |
| | | 2985 | | displayName: 'МоТехС', |
| | | 2986 | | cacheKey: 'motexPriceData', |
| | 0 | 2987 | | endpoint: '@Url.Action("Search", "MotexPrice")', |
| | | 2988 | | isEnabled: () => motexQuotationEnabled, |
| | | 2989 | | process: processMotexPriceData |
| | | 2990 | | }, |
| | | 2991 | | { |
| | | 2992 | | providerKey: 'MlAuto', |
| | | 2993 | | displayName: 'ML-Auto BY', |
| | | 2994 | | cacheKey: 'mlAutoPriceData', |
| | 0 | 2995 | | endpoint: '@Url.Action("Search", "MlAutoPrice")', |
| | | 2996 | | requiresBrand: true, |
| | | 2997 | | isEnabled: () => mlAutoQuotationEnabled, |
| | | 2998 | | process: processMlAutoPriceData |
| | | 2999 | | }, |
| | | 3000 | | { |
| | | 3001 | | providerKey: 'MlAutoRu', |
| | | 3002 | | displayName: 'ML-Auto RU', |
| | | 3003 | | cacheKey: 'mlAutoRuPriceData', |
| | 0 | 3004 | | endpoint: '@Url.Action("Search", "MlAutoRuPrice")', |
| | | 3005 | | requiresBrand: true, |
| | | 3006 | | isEnabled: () => mlAutoRuQuotationEnabled, |
| | | 3007 | | process: processMlAutoRuPriceData |
| | | 3008 | | }, |
| | | 3009 | | { |
| | | 3010 | | providerKey: 'ForumAuto', |
| | | 3011 | | displayName: forumAutoDisplayName, |
| | | 3012 | | cacheKey: 'forumAutoPriceData', |
| | 0 | 3013 | | endpoint: '@Url.Action("Search", "ForumAutoPrice")', |
| | | 3014 | | isEnabled: () => forumAutoQuotationEnabled, |
| | | 3015 | | process: processForumAutoPriceData |
| | | 3016 | | }, |
| | | 3017 | | { |
| | | 3018 | | providerKey: 'ForumAutoInterparts', |
| | | 3019 | | displayName: forumAutoInterpartsDisplayName, |
| | | 3020 | | cacheKey: 'forumAutoInterpartsPriceData', |
| | 0 | 3021 | | endpoint: '@Url.Action("Search", "ForumAutoInterpartsPrice")', |
| | | 3022 | | isEnabled: () => forumAutoInterpartsQuotationEnabled, |
| | | 3023 | | process: processForumAutoInterpartsPriceData |
| | | 3024 | | }, |
| | | 3025 | | { |
| | | 3026 | | providerKey: 'ForumAutoPiter', |
| | | 3027 | | displayName: forumAutoPiterDisplayName, |
| | | 3028 | | cacheKey: 'forumAutoPiterPriceData', |
| | 0 | 3029 | | endpoint: '@Url.Action("Search", "ForumAutoPiterPrice")', |
| | | 3030 | | isEnabled: () => forumAutoPiterQuotationEnabled, |
| | | 3031 | | process: processForumAutoPiterPriceData |
| | | 3032 | | }, |
| | | 3033 | | { |
| | | 3034 | | providerKey: 'ForumAutoRostov', |
| | | 3035 | | displayName: forumAutoRostovDisplayName, |
| | | 3036 | | cacheKey: 'forumAutoRostovPriceData', |
| | 0 | 3037 | | endpoint: '@Url.Action("Search", "ForumAutoRostovPrice")', |
| | | 3038 | | isEnabled: () => forumAutoRostovQuotationEnabled, |
| | | 3039 | | process: processForumAutoRostovPriceData |
| | | 3040 | | }, |
| | | 3041 | | { |
| | | 3042 | | providerKey: 'ForumAutoIstra', |
| | | 3043 | | displayName: forumAutoIstraDisplayName, |
| | | 3044 | | cacheKey: 'forumAutoIstraPriceData', |
| | 0 | 3045 | | endpoint: '@Url.Action("Search", "ForumAutoIstraPrice")', |
| | | 3046 | | isEnabled: () => forumAutoIstraQuotationEnabled, |
| | | 3047 | | process: processForumAutoIstraPriceData |
| | | 3048 | | }, |
| | | 3049 | | { |
| | | 3050 | | providerKey: 'FavoritParts', |
| | | 3051 | | displayName: 'Фаворит Союз', |
| | | 3052 | | cacheKey: 'favoritPartsPriceData', |
| | 0 | 3053 | | endpoint: '@Url.Action("Search", "FavoritPartsPrice")', |
| | | 3054 | | isEnabled: () => favoritPartsQuotationEnabled, |
| | | 3055 | | process: processFavoritPartsPriceData |
| | | 3056 | | }, |
| | | 3057 | | { |
| | | 3058 | | providerKey: 'FavoritPartsIstra', |
| | | 3059 | | displayName: 'Фаворит Истра', |
| | | 3060 | | cacheKey: 'favoritPartsIstraPriceData', |
| | 0 | 3061 | | endpoint: '@Url.Action("Search", "FavoritPartsIstraPrice")', |
| | | 3062 | | isEnabled: () => favoritPartsIstraQuotationEnabled, |
| | | 3063 | | process: processFavoritPartsIstraPriceData |
| | | 3064 | | }, |
| | | 3065 | | { |
| | | 3066 | | providerKey: 'FavoritPartsRostov', |
| | | 3067 | | displayName: 'Фаворит Ростов', |
| | | 3068 | | cacheKey: 'favoritPartsRostovPriceData', |
| | 0 | 3069 | | endpoint: '@Url.Action("Search", "FavoritPartsRostovPrice")', |
| | | 3070 | | isEnabled: () => favoritPartsRostovQuotationEnabled, |
| | | 3071 | | process: processFavoritPartsRostovPriceData |
| | | 3072 | | }, |
| | | 3073 | | { |
| | | 3074 | | providerKey: 'MoskvorechieIstra', |
| | | 3075 | | displayName: 'Москворечье (Истра)', |
| | | 3076 | | cacheKey: 'moskvorechieIstraPriceData', |
| | 0 | 3077 | | endpoint: '@Url.Action("Search", "MoskvorechieIstraPrice")', |
| | | 3078 | | isEnabled: () => moskvorechieIstraQuotationEnabled, |
| | | 3079 | | process: processMoskvorechieIstraPriceData |
| | | 3080 | | }, |
| | | 3081 | | { |
| | | 3082 | | providerKey: 'ProfitLiga', |
| | | 3083 | | displayName: 'Профит-Лига', |
| | | 3084 | | cacheKey: 'profitLigaPriceData', |
| | 0 | 3085 | | endpoint: '@Url.Action("Search", "ProfitLigaPrice")', |
| | | 3086 | | isEnabled: () => profitLigaQuotationEnabled, |
| | | 3087 | | process: processProfitLigaPriceData |
| | | 3088 | | }, |
| | | 3089 | | { |
| | | 3090 | | providerKey: 'Nikei', |
| | | 3091 | | displayName: 'Nikei', |
| | | 3092 | | cacheKey: 'nikeiPriceData', |
| | 0 | 3093 | | endpoint: '@Url.Action("Search", "NikeiPrice")', |
| | | 3094 | | requiresBrand: true, |
| | | 3095 | | isEnabled: () => nikeiQuotationEnabled, |
| | | 3096 | | process: processNikeiPriceData |
| | | 3097 | | }, |
| | | 3098 | | { |
| | | 3099 | | providerKey: 'MikadoMskHod20', |
| | | 3100 | | displayName: 'Микадо МскХод20', |
| | | 3101 | | cacheKey: 'mikadoMskHod20PriceData', |
| | 0 | 3102 | | endpoint: '@Url.Action("Search", "MikadoMskHod20Price")', |
| | | 3103 | | requiresBrand: true, |
| | | 3104 | | isEnabled: () => mikadoMskHod20QuotationEnabled, |
| | | 3105 | | process: processMikadoMskHod20PriceData |
| | | 3106 | | }, |
| | | 3107 | | { |
| | | 3108 | | providerKey: 'ZZapMoscow', |
| | | 3109 | | displayName: 'ZZap Москва', |
| | | 3110 | | cacheKey: 'zzapMoscowPriceData', |
| | 0 | 3111 | | endpoint: '@Url.Action("Search", "ZZapMoscowPrice")', |
| | | 3112 | | requiresBrand: true, |
| | | 3113 | | isEnabled: () => zzapMoscowQuotationEnabled, |
| | | 3114 | | process: processZZapMoscowPriceData |
| | | 3115 | | } |
| | | 3116 | | ]; |
| | | 3117 | | |
| | | 3118 | | const TX_ORDER_INFO_BTN_ICONS = { |
| | | 3119 | | loading: ` |
| | | 3120 | | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-hourglass-sp |
| | | 3121 | | <path d="M2.5 15a.5 .5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154 |
| | | 3122 | | </svg> |
| | | 3123 | | <span class="visually-hidden">Loading</span> |
| | | 3124 | | `, |
| | | 3125 | | expanded: ` |
| | | 3126 | | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-chevron-doub |
| | | 3127 | | <path fill-rule="evenodd" d="M7.646 2.646a.5 .5 0 0 1 .708 0l6 6a.5 .5 0 0 1-.708.708L8 3.707 2.354 9.35 |
| | | 3128 | | <path fill-rule="evenodd" d="M7.646 6.646a.5 .5 0 0 1 .708 0l6 6a.5 .5 0 0 1-.708.708L8 7.707l-5.646 5.6 |
| | | 3129 | | </svg> |
| | | 3130 | | <span class="visually-hidden">Info</span> |
| | | 3131 | | `, |
| | | 3132 | | collapsed: ` |
| | | 3133 | | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-chevron-doub |
| | | 3134 | | <path fill-rule="evenodd" d="M1.646 6.646a.5 .5 0 0 1 .708 0L8 12.293l5.646-5.647a.5 .5 0 0 1 .708.708l- |
| | | 3135 | | <path fill-rule="evenodd" d="M1.646 2.646a.5 .5 0 0 1 .708 0L8 8.293l5.646-5.647a.5 .5 0 0 1 .708.708l-6 |
| | | 3136 | | </svg> |
| | | 3137 | | <span class="visually-hidden">Info</span> |
| | | 3138 | | ` |
| | | 3139 | | }; |
| | | 3140 | | |
| | | 3141 | | function setOrderInfoButtonState(button, state) { |
| | | 3142 | | if (!button) return; |
| | | 3143 | | const icon = TX_ORDER_INFO_BTN_ICONS[state] || TX_ORDER_INFO_BTN_ICONS.collapsed; |
| | | 3144 | | button.innerHTML = icon; |
| | | 3145 | | button.disabled = state === 'loading'; |
| | | 3146 | | } |
| | | 3147 | | |
| | | 3148 | | /* ── Фоновая проценка ────────────────────────────────────── |
| | | 3149 | | Заказы процениваются сразу после загрузки страницы, молча — блок |
| | | 3150 | | прогресса не показывается. Состояние этапов копится здесь, чтобы |
| | | 3151 | | раскрытая позже строка показала актуальный прогресс, а не начала |
| | | 3152 | | запросы заново. Включается флагом window.txPrefetchQuotationOnLoad. */ |
| | | 3153 | | const txQuotationPrefetch = { |
| | | 3154 | | byOrder: new Map(), // orderId => { stages, promise, done, stale } |
| | | 3155 | | queue: [], |
| | | 3156 | | running: 0, |
| | | 3157 | | // Внутри заказа все API поставщиков вызываются через Promise.all. |
| | | 3158 | | // Одновременно запускаем пять заказов, чтобы проценка всей таблицы |
| | | 3159 | | // шла параллельно, но не создавала неконтролируемую нагрузку на API. |
| | | 3160 | | concurrency: 5, |
| | | 3161 | | started: false |
| | | 3162 | | }; |
| | | 3163 | | |
| | | 3164 | | function getQuotationPrefetch(orderId) { |
| | | 3165 | | return txQuotationPrefetch.byOrder.get(String(orderId)); |
| | | 3166 | | } |
| | | 3167 | | |
| | | 3168 | | function markQuotationStage(orderId, stageKey, state = 'complete') { |
| | | 3169 | | const entry = getQuotationPrefetch(orderId); |
| | | 3170 | | if (entry) entry.stages.set(stageKey, state); |
| | | 3171 | | updateSupplierInfoLoadingStage(orderId, stageKey, state); |
| | | 3172 | | } |
| | | 3173 | | |
| | | 3174 | | // Заказ уже раскрыт или грузится по кнопке — фоном его не трогаем. |
| | | 3175 | | function isOrderQuotationBusy(orderId) { |
| | | 3176 | | return rowCache.has(String(orderId)) |
| | | 3177 | | || document.querySelector(`.tx-info-loading-row[data-order-id="${orderId}"]`) !== null |
| | | 3178 | | || document.querySelector(`#orderRow_${orderId} + .info-row`) !== null; |
| | | 3179 | | } |
| | | 3180 | | |
| | | 3181 | | /* Все API-источники получают zasylWarehouseName, поэтому смена засыла |
| | | 3182 | | обесценивает проценку целиком, а не только четыре провайдера. */ |
| | | 3183 | | function clearQuotationCacheForOrder(orderId) { |
| | | 3184 | | const cache = orderDataCache[orderId]; |
| | | 3185 | | if (cache) apiSupplierQuotationSources.forEach(source => delete cache[source.cacheKey]); |
| | | 3186 | | rowCache.delete(String(orderId)); |
| | | 3187 | | } |
| | | 3188 | | |
| | | 3189 | | function invalidateQuotationPrefetch(orderId) { |
| | | 3190 | | const key = String(orderId); |
| | | 3191 | | const entry = txQuotationPrefetch.byOrder.get(key); |
| | | 3192 | | // Запросы уже в полёте: их ответы придут со старым складом, поэтому |
| | | 3193 | | // помечаем прогрев устаревшим — очередь перезапустит его сама. |
| | | 3194 | | if (entry && !entry.done) { |
| | | 3195 | | entry.stale = true; |
| | | 3196 | | return; |
| | | 3197 | | } |
| | | 3198 | | txQuotationPrefetch.byOrder.delete(key); |
| | | 3199 | | } |
| | | 3200 | | |
| | | 3201 | | function enqueueQuotationPrefetch(button) { |
| | | 3202 | | if (!button) return; |
| | | 3203 | | txQuotationPrefetch.queue.push(button); |
| | | 3204 | | void pumpQuotationPrefetchQueue(); |
| | | 3205 | | } |
| | | 3206 | | |
| | | 3207 | | function startBackgroundQuotationPrefetch() { |
| | | 3208 | | if (window.txPrefetchQuotationOnLoad !== true) { |
| | | 3209 | | console.info('Фоновая проценка выключена: window.txPrefetchQuotationOnLoad не выставлен'); |
| | | 3210 | | return; |
| | | 3211 | | } |
| | | 3212 | | if (txQuotationPrefetch.started) return; |
| | | 3213 | | |
| | | 3214 | | const buttons = document.querySelectorAll('.get-full-order-info'); |
| | | 3215 | | if (buttons.length === 0) { |
| | | 3216 | | console.info('Фоновая проценка: строки заказов ещё не отрисованы, пробуем позже'); |
| | | 3217 | | window.setTimeout(startBackgroundQuotationPrefetch, 1000); |
| | | 3218 | | return; |
| | | 3219 | | } |
| | | 3220 | | |
| | | 3221 | | txQuotationPrefetch.started = true; |
| | | 3222 | | |
| | | 3223 | | txQuotationPrefetch.queue = Array.from(buttons); |
| | | 3224 | | console.info(`Фоновая проценка: ${txQuotationPrefetch.queue.length} заказ(ов), по ${txQuotationPrefetch.concurre |
| | | 3225 | | for (let i = 0; i < txQuotationPrefetch.concurrency; i++) { |
| | | 3226 | | void pumpQuotationPrefetchQueue(); |
| | | 3227 | | } |
| | | 3228 | | } |
| | | 3229 | | |
| | | 3230 | | /* Запускаем сразу после построения DOM. Не ждём фиксированные 1.2 секунды: |
| | | 3231 | | на большой таблице за это время уже должны идти первые запросы, а не |
| | | 3232 | | простаивать вся очередь. */ |
| | | 3233 | | (function scheduleBackgroundQuotationPrefetch() { |
| | | 3234 | | const run = () => { |
| | | 3235 | | try { |
| | | 3236 | | startBackgroundQuotationPrefetch(); |
| | | 3237 | | } catch (error) { |
| | | 3238 | | console.warn('Фоновая проценка не запустилась', error); |
| | | 3239 | | } |
| | | 3240 | | }; |
| | | 3241 | | |
| | | 3242 | | if (document.readyState === 'loading') { |
| | | 3243 | | document.addEventListener('DOMContentLoaded', run, { once: true }); |
| | | 3244 | | } else { |
| | | 3245 | | run(); |
| | | 3246 | | } |
| | | 3247 | | })(); |
| | | 3248 | | |
| | | 3249 | | async function pumpQuotationPrefetchQueue() { |
| | | 3250 | | if (txQuotationPrefetch.running >= txQuotationPrefetch.concurrency) return; |
| | | 3251 | | |
| | | 3252 | | const button = txQuotationPrefetch.queue.shift(); |
| | | 3253 | | if (!button) return; |
| | | 3254 | | |
| | | 3255 | | txQuotationPrefetch.running++; |
| | | 3256 | | try { |
| | | 3257 | | const orderId = String(button.getAttribute('data-order-id')); |
| | | 3258 | | if (!getQuotationPrefetch(orderId) && !isOrderQuotationBusy(orderId)) { |
| | | 3259 | | const entry = { stages: new Map(), done: false, stale: false, promise: null }; |
| | | 3260 | | txQuotationPrefetch.byOrder.set(orderId, entry); |
| | | 3261 | | // Прогрев не должен падать наружу: на этот промис завязано |
| | | 3262 | | // ручное раскрытие строки, и его отказ закрыл бы её с ошибкой. |
| | | 3263 | | entry.promise = Promise.resolve(fetchSupplierInfoInBg(button)) |
| | | 3264 | | .catch(error => console.warn('Фоновая проценка заказа ' + orderId + ' не удалась', error)) |
| | | 3265 | | .finally(() => { |
| | | 3266 | | entry.done = true; |
| | | 3267 | | console.debug(`Фоновая проценка заказа ${orderId}: готово, этапов ${entry.stages.size}`); |
| | | 3268 | | }); |
| | | 3269 | | await entry.promise; |
| | | 3270 | | |
| | | 3271 | | // Засыл сменили, пока шли запросы — данные пришли по старому |
| | | 3272 | | // складу, выбрасываем их и греем заказ заново. |
| | | 3273 | | if (entry.stale) { |
| | | 3274 | | txQuotationPrefetch.byOrder.delete(orderId); |
| | | 3275 | | clearQuotationCacheForOrder(orderId); |
| | | 3276 | | txQuotationPrefetch.queue.push(button); |
| | | 3277 | | } |
| | | 3278 | | } |
| | | 3279 | | } finally { |
| | | 3280 | | txQuotationPrefetch.running--; |
| | | 3281 | | void pumpQuotationPrefetchQueue(); |
| | | 3282 | | } |
| | | 3283 | | } |
| | | 3284 | | |
| | | 3285 | | function showSupplierInfoLoading(rowId, orderId) { |
| | | 3286 | | const prefetch = getQuotationPrefetch(orderId); |
| | | 3287 | | // Этап показываем, если он ещё не загружен либо его уже трогал фоновый |
| | | 3288 | | // прогрев — иначе строка, открытая после прогрева, выглядела бы пустой. |
| | | 3289 | | const isStageShown = key => !orderDataCache[orderId]?.[key] || prefetch?.stages.has(key); |
| | | 3290 | | |
| | | 3291 | | const stages = [ |
| | | 3292 | | isStageShown('orderDetails') && isDataTypeEnabled('orderDetails') ? { key: 'orderDetails', label: 'Карточка' |
| | | 3293 | | isStageShown('bitrixData') && isDataTypeEnabled('bitrixData') ? { key: 'bitrixData', label: 'Остатки' } : nu |
| | | 3294 | | isStageShown('interpartsPriceData') && isDataTypeEnabled('interpartsPriceData') ? { key: 'interpartsPriceDat |
| | | 3295 | | ...apiSupplierQuotationSources |
| | | 3296 | | .filter(source => source.isEnabled() |
| | | 3297 | | && isStageShown(source.cacheKey) |
| | | 3298 | | && isDataTypeEnabled(source.cacheKey) |
| | | 3299 | | && isSupplierAllowedForOrderWarehouse(source.providerKey, orderId)) |
| | | 3300 | | .map(source => ({ key: source.cacheKey, label: source.displayName })), |
| | | 3301 | | isStageShown('ozonAttributesData') && isDataTypeEnabled('ozonAttributesData') ? { key: 'ozonAttributesData', |
| | | 3302 | | ].filter(Boolean); |
| | | 3303 | | |
| | | 3304 | | $(rowId).after(` |
| | | 3305 | | <tr class="info-row tx-info-row tx-info-loading-row" data-order-id="${orderId}" aria-live="polite"> |
| | | 3306 | | <td colspan="16"> |
| | | 3307 | | <div class="tx-info-loading-card"> |
| | | 3308 | | <div class="tx-info-loading-card__header"> |
| | | 3309 | | <span class="tx-info-loading-card__spinner" data-loading-spinner aria-hidden="true"></span> |
| | | 3310 | | <div><strong>Подбираем предложения поставщиков</strong><span>Запросы выполняются параллельно |
| | | 3311 | | </div> |
| | | 3312 | | <div class="tx-info-loading-stages">${stages.map(stage => ` |
| | | 3313 | | <span class="tx-info-loading-stage is-pending" data-stage="${stage.key}"> |
| | | 3314 | | <i class="fas fa-circle-notch fa-spin" data-loading-spinner aria-hidden="true"></i><span |
| | | 3315 | | </span>`).join('')}</div> |
| | | 3316 | | </div> |
| | | 3317 | | </td> |
| | | 3318 | | </tr>`); |
| | | 3319 | | |
| | | 3320 | | document.querySelectorAll(`.tx-info-loading-row[data-order-id="${orderId}"] [data-loading-spinner]`) |
| | | 3321 | | .forEach(spinner => startSupplierLoadingSpinner(spinner)); |
| | | 3322 | | |
| | | 3323 | | // Догоняем прогресс, набежавший в фоне до раскрытия строки. |
| | | 3324 | | prefetch?.stages.forEach((state, key) => updateSupplierInfoLoadingStage(orderId, key, state)); |
| | | 3325 | | } |
| | | 3326 | | |
| | | 3327 | | // Web Animations API не зависит от Font Awesome и CSS-настроек reduced motion. |
| | | 3328 | | function startSupplierLoadingSpinner(spinner) { |
| | | 3329 | | if (!spinner || typeof spinner.animate !== 'function') return; |
| | | 3330 | | spinner.animate( |
| | | 3331 | | [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], |
| | | 3332 | | { duration: 800, iterations: Infinity, easing: 'linear' }); |
| | | 3333 | | } |
| | | 3334 | | |
| | | 3335 | | function updateSupplierInfoLoadingStage(orderId, stageKey, state = 'complete') { |
| | | 3336 | | const stage = document.querySelector(`.tx-info-loading-row[data-order-id="${orderId}"] [data-stage="${stageKey}" |
| | | 3337 | | if (!stage) return; |
| | | 3338 | | stage.classList.remove('is-pending', 'is-complete', 'is-error'); |
| | | 3339 | | stage.classList.add(`is-${state}`); |
| | | 3340 | | const icon = stage.querySelector('i'); |
| | | 3341 | | if (icon) icon.className = state === 'complete' |
| | | 3342 | | ? 'fas fa-circle-check' |
| | | 3343 | | : state === 'error' ? 'fas fa-circle-exclamation' : 'fas fa-circle-notch fa-spin'; |
| | | 3344 | | |
| | | 3345 | | const label = stage.querySelector('span'); |
| | | 3346 | | if (label && state !== 'pending') { |
| | | 3347 | | const data = orderDataCache[orderId]?.[stageKey]; |
| | | 3348 | | if (state === 'error') { |
| | | 3349 | | label.textContent = `${label.textContent} · ошибка`; |
| | | 3350 | | } else if (Array.isArray(data?.items)) { |
| | | 3351 | | label.textContent = `${label.textContent} · ${data.items.length} поз.`; |
| | | 3352 | | } else { |
| | | 3353 | | label.textContent = `${label.textContent} · готово`; |
| | | 3354 | | } |
| | | 3355 | | } |
| | | 3356 | | } |
| | | 3357 | | |
| | | 3358 | | async function hideSupplierInfoLoading(orderId) { |
| | | 3359 | | const loadingRow = $(`.tx-info-loading-row[data-order-id="${orderId}"]`); |
| | | 3360 | | // Блока может не быть вовсе: строку открыли после фонового прогрева. |
| | | 3361 | | if (loadingRow.length === 0) return; |
| | | 3362 | | loadingRow.addClass('is-complete'); |
| | | 3363 | | await new Promise(resolve => window.setTimeout(resolve, 180)); |
| | | 3364 | | loadingRow.remove(); |
| | | 3365 | | } |
| | | 3366 | | |
| | | 3367 | | async function withPageLoadingBar(task) { |
| | | 3368 | | if (window.PageLoadingBar && typeof window.PageLoadingBar.run === 'function') { |
| | | 3369 | | return window.PageLoadingBar.run(task); |
| | | 3370 | | } |
| | | 3371 | | if (window.showPageLoadingBar) window.showPageLoadingBar(); |
| | | 3372 | | try { |
| | | 3373 | | return await task(); |
| | | 3374 | | } finally { |
| | | 3375 | | if (window.hidePageLoadingBar) window.hidePageLoadingBar(); |
| | | 3376 | | } |
| | | 3377 | | } |
| | | 3378 | | |
| | | 3379 | | /* Очередь запросов проценки держим у себя, а не в браузере. Лимит чуть выше числа |
| | | 3380 | | HTTP/1.1-соединений на хост (их шесть), поэтому сеть загружена полностью и проценка |
| | | 3381 | | идёт с прежней скоростью — но лишние запросы ждут здесь, и при отправке формы мы |
| | | 3382 | | выбрасываем их мгновенно. Раньше они висели в сетевой очереди браузера, и POST |
| | | 3383 | | проведения вставал за ними на минуты. */ |
| | | 3384 | | const txQuotationGate = { |
| | | 3385 | | limit: 8, |
| | | 3386 | | active: 0, |
| | | 3387 | | waiting: [], |
| | | 3388 | | |
| | | 3389 | | run(task) { |
| | | 3390 | | if (this.active < this.limit) { |
| | | 3391 | | this.active++; |
| | | 3392 | | return this.finish(task()); |
| | | 3393 | | } |
| | | 3394 | | return new Promise(resolve => this.waiting.push(resolve)) |
| | | 3395 | | .then(() => this.finish(task())); |
| | | 3396 | | }, |
| | | 3397 | | |
| | | 3398 | | finish(promise) { |
| | | 3399 | | return Promise.resolve(promise).finally(() => { |
| | | 3400 | | const next = this.waiting.shift(); |
| | | 3401 | | if (next) next(); |
| | | 3402 | | else this.active--; |
| | | 3403 | | }); |
| | | 3404 | | }, |
| | | 3405 | | |
| | | 3406 | | // При отправке формы ждущие запросы уже не нужны — очищаем очередь. |
| | | 3407 | | drain() { |
| | | 3408 | | this.waiting.length = 0; |
| | | 3409 | | } |
| | | 3410 | | }; |
| | | 3411 | | |
| | | 3412 | | function fetchApiSupplierQuotationData(source, code, producerName, orderId) { |
| | | 3413 | | const cleanCode = (code || '').trim(); |
| | | 3414 | | const cleanBrand = (producerName || '').trim(); |
| | | 3415 | | |
| | | 3416 | | if (!cleanCode) { |
| | | 3417 | | return Promise.resolve({ error: `Не указан артикул (code) для поиска цен ${source.displayName}`, items: [] } |
| | | 3418 | | } |
| | | 3419 | | if (source.requiresBrand && !cleanBrand) { |
| | | 3420 | | return Promise.resolve({ items: [], skippedByMissingBrand: true }); |
| | | 3421 | | } |
| | | 3422 | | if (!isSupplierAllowedForOrderWarehouse(source.providerKey, orderId)) { |
| | | 3423 | | return Promise.resolve({ items: [], skippedByWarehouse: true }); |
| | | 3424 | | } |
| | | 3425 | | |
| | | 3426 | | const startedAt = performance.now(); |
| | | 3427 | | return txQuotationGate.run(() => $.ajax({ |
| | | 3428 | | url: source.endpoint, |
| | | 3429 | | type: 'GET', |
| | | 3430 | | data: { code: cleanCode, brand: cleanBrand, orderId: orderId, zasylWarehouseName: getZasylWarehouseName(orde |
| | | 3431 | | })).then(response => { |
| | | 3432 | | if (response && typeof response === 'object') response.responseTimeMs = Math.round(performance.now() - start |
| | | 3433 | | return response; |
| | | 3434 | | }).catch(error => { |
| | | 3435 | | const message = txExtractAjaxErrorDetail(error, `Не удалось загрузить цены ${source.displayName}`); |
| | | 3436 | | console.warn(`${source.providerKey} price API:`, message, error); |
| | | 3437 | | return { error: message, items: [], responseTimeMs: Math.round(performance.now() - startedAt) }; |
| | | 3438 | | }); |
| | | 3439 | | } |
| | | 3440 | | |
| | | 3441 | | function getZasylWarehouseName(orderId) { |
| | | 3442 | | const input = document.getElementById(`orderSupplierMap_${orderId}`); |
| | | 3443 | | return input ? input.value.trim() : null; |
| | | 3444 | | } |
| | | 3445 | | |
| | | 3446 | | function addApiSupplierQuotationRequests(requests, code, producerName, orderId, onFinished = null) { |
| | | 3447 | | apiSupplierQuotationSources |
| | | 3448 | | .filter(source => { |
| | | 3449 | | if (!source.isEnabled() || orderDataCache[orderId]?.[source.cacheKey] || !isDataTypeEnabled(source.cache |
| | | 3450 | | return false; |
| | | 3451 | | } |
| | | 3452 | | if (source.requiresBrand && !producerName?.trim()) { |
| | | 3453 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 3454 | | orderDataCache[orderId][source.cacheKey] = { |
| | | 3455 | | items: [], |
| | | 3456 | | empty: true, |
| | | 3457 | | skippedByMissingBrand: true, |
| | | 3458 | | lastUpdated: new Date().toISOString() |
| | | 3459 | | }; |
| | | 3460 | | updateApiSupplierIndicator(orderId, source); |
| | | 3461 | | onFinished?.(source); |
| | | 3462 | | return false; |
| | | 3463 | | } |
| | | 3464 | | if (!isSupplierAllowedForOrderWarehouse(source.providerKey, orderId)) { |
| | | 3465 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 3466 | | orderDataCache[orderId][source.cacheKey] = { |
| | | 3467 | | items: [], |
| | | 3468 | | empty: true, |
| | | 3469 | | skippedByWarehouse: true, |
| | | 3470 | | lastUpdated: new Date().toISOString() |
| | | 3471 | | }; |
| | | 3472 | | updateApiSupplierIndicator(orderId, source); |
| | | 3473 | | onFinished?.(source); |
| | | 3474 | | return false; |
| | | 3475 | | } |
| | | 3476 | | return true; |
| | | 3477 | | }) |
| | | 3478 | | .forEach(source => requests.push( |
| | | 3479 | | Promise.resolve(fetchApiSupplierQuotationData(source, code, producerName, orderId)) |
| | | 3480 | | .then(data => { |
| | | 3481 | | source.process(data, orderId); |
| | | 3482 | | const cache = orderDataCache[orderId]?.[source.cacheKey]; |
| | | 3483 | | if (cache) cache.responseTimeMs = Number(data?.responseTimeMs) || null; |
| | | 3484 | | updateApiSupplierIndicator(orderId, source); |
| | | 3485 | | }) |
| | | 3486 | | .finally(() => onFinished?.(source)))); |
| | | 3487 | | } |
| | | 3488 | | |
| | | 3489 | | const apiSupplierIndicatorClasses = { |
| | | 3490 | | Armtek: 'armtek', ArmtekBy: 'armtek', ShateM: 'shatem', Avd: 'avd', Berg: 'berg', Japarts: 'japarts', Motex: 'mo |
| | | 3491 | | MlAuto: 'mlauto', MlAutoRu: 'mlauto', ForumAuto: 'forum-auto', ForumAutoInterparts: 'forum-auto', |
| | | 3492 | | ForumAutoPiter: 'forum-auto', ForumAutoRostov: 'forum-auto', ForumAutoIstra: 'forum-auto', |
| | | 3493 | | FavoritParts: 'favorit-parts', FavoritPartsIstra: 'favorit-parts', FavoritPartsRostov: 'favorit-parts', Moskvore |
| | | 3494 | | ProfitLiga: 'profit-liga', Nikei: 'nikei', MikadoMskHod20: 'mikado', ZZapMoscow: 'zzap' |
| | | 3495 | | }; |
| | | 3496 | | |
| | | 3497 | | const apiSupplierIndicatorLogoData = { |
| | | 3498 | | Armtek: { siteUrl: 'https://ws.armtek.ru/', title: 'Армтек' }, |
| | | 3499 | | ArmtekBy: { siteUrl: 'https://ws.armtek.ru/', title: 'Армтек BY' }, |
| | | 3500 | | ShateM: { siteUrl: 'https://api.shate-m.by/', title: 'Шате‑М' }, |
| | | 3501 | | Avd: { siteUrl: 'https://www.avdmotors.ru/', title: 'АВД' }, |
| | | 3502 | | Berg: { siteUrl: 'https://berg.ru/', title: 'Берг' }, |
| | | 3503 | | Japarts: { siteUrl: 'https://www.japarts.ru/', title: 'Japarts' }, |
| | | 3504 | | Motex: { siteUrl: 'https://api.motexc.by/', title: 'МоТехС' }, |
| | | 3505 | | MlAuto: { siteUrl: 'https://www.ml-auto.by/', title: 'ML-Auto BY' }, |
| | | 3506 | | MlAutoRu: { siteUrl: 'https://www.ml-auto.ru/', title: 'ML-Auto RU' }, |
| | | 3507 | | ForumAuto: { siteUrl: 'https://itrade.forum-auto.ru/', title: forumAutoDisplayName }, |
| | | 3508 | | ForumAutoInterparts: { siteUrl: 'https://itrade.forum-auto.ru/', title: forumAutoInterpartsDisplayName }, |
| | | 3509 | | ForumAutoPiter: { siteUrl: 'https://itrade.forum-auto.ru/', title: forumAutoPiterDisplayName }, |
| | | 3510 | | ForumAutoRostov: { siteUrl: 'https://itrade.forum-auto.ru/', title: forumAutoRostovDisplayName }, |
| | | 3511 | | ForumAutoIstra: { siteUrl: 'https://itrade.forum-auto.ru/', title: forumAutoIstraDisplayName }, |
| | | 3512 | | FavoritParts: { siteUrl: 'https://lk.favorit-parts.ru/', title: 'Фаворит Союз' }, |
| | | 3513 | | FavoritPartsIstra: { siteUrl: 'https://lk.favorit-parts.ru/', title: 'Фаворит Истра' }, |
| | | 3514 | | FavoritPartsRostov: { siteUrl: 'https://lk.favorit-parts.ru/', title: 'Фаворит Ростов' }, |
| | | 3515 | | MoskvorechieIstra: { siteUrl: 'https://portal.moskvorechie.ru/', title: 'Москворечье (Истра)' }, |
| | | 3516 | | Nikei: { siteUrl: 'https://nikei.ru/', title: 'Nikei' }, |
| | | 3517 | | MikadoMskHod20: { siteUrl: 'https://mikado-parts.ru', title: 'Микадо МскХод20' }, |
| | | 3518 | | ZZapMoscow: { siteUrl: 'https://www.zzap.ru/', title: 'ZZap Москва' } |
| | | 3519 | | }; |
| | | 3520 | | |
| | | 3521 | | function getApiSupplierIndicatorPopover() { |
| | | 3522 | | let popover = document.getElementById('tx-api-supplier-popover'); |
| | | 3523 | | if (popover) return popover; |
| | | 3524 | | popover = document.createElement('div'); |
| | | 3525 | | popover.id = 'tx-api-supplier-popover'; |
| | | 3526 | | popover.className = 'tx-api-supplier-popover'; |
| | | 3527 | | popover.setAttribute('role', 'tooltip'); |
| | | 3528 | | document.body.appendChild(popover); |
| | | 3529 | | return popover; |
| | | 3530 | | } |
| | | 3531 | | |
| | | 3532 | | function showApiSupplierIndicatorPopover(host) { |
| | | 3533 | | const items = Array.from(host.querySelectorAll(':scope > .tx-api-supplier-indicator')); |
| | | 3534 | | if (items.length === 0) return; |
| | | 3535 | | |
| | | 3536 | | const popover = getApiSupplierIndicatorPopover(); |
| | | 3537 | | popover.innerHTML = items.map(indicator => { |
| | | 3538 | | const providerKey = indicator.dataset.providerKey || ''; |
| | | 3539 | | const source = apiSupplierIndicatorLogoData[providerKey] || {}; |
| | | 3540 | | const logo = typeof getSupplierLogoMarkup === 'function' |
| | | 3541 | | ? getSupplierLogoMarkup(source) |
| | | 3542 | | : '<i class="fas fa-store" aria-hidden="true"></i>'; |
| | | 3543 | | return `<div class="tx-api-supplier-popover__item">${logo}<span>${indicator.dataset.providerName || ''}</spa |
| | | 3544 | | }).join(''); |
| | | 3545 | | |
| | | 3546 | | const rect = host.getBoundingClientRect(); |
| | | 3547 | | popover.style.visibility = 'hidden'; |
| | | 3548 | | popover.classList.add('is-visible'); |
| | | 3549 | | const width = popover.offsetWidth; |
| | | 3550 | | const height = popover.offsetHeight; |
| | | 3551 | | popover.style.left = Math.max(6, Math.min(rect.left, window.innerWidth - width - 6)) + 'px'; |
| | | 3552 | | popover.style.top = Math.max(6, Math.min(rect.bottom + 5, window.innerHeight - height - 6)) + 'px'; |
| | | 3553 | | popover.style.visibility = 'visible'; |
| | | 3554 | | } |
| | | 3555 | | |
| | | 3556 | | function hideApiSupplierIndicatorPopover() { |
| | | 3557 | | document.getElementById('tx-api-supplier-popover')?.classList.remove('is-visible'); |
| | | 3558 | | } |
| | | 3559 | | |
| | | 3560 | | function getCartSelectionPopover() { |
| | | 3561 | | let popover = document.getElementById('tx-cart-selection-popover'); |
| | | 3562 | | if (popover) return popover; |
| | | 3563 | | popover = document.createElement('div'); |
| | | 3564 | | popover.id = 'tx-cart-selection-popover'; |
| | | 3565 | | popover.className = 'tx-cart-selection-popover'; |
| | | 3566 | | popover.setAttribute('role', 'tooltip'); |
| | | 3567 | | document.body.appendChild(popover); |
| | | 3568 | | return popover; |
| | | 3569 | | } |
| | | 3570 | | |
| | | 3571 | | function formatCartSelectionPrice(value, currency) { |
| | | 3572 | | const price = Number.parseFloat(String(value ?? '').replace(',', '.')); |
| | | 3573 | | return Number.isFinite(price) ? `${price.toFixed(2)} ${currency || ''}`.trim() : '—'; |
| | | 3574 | | } |
| | | 3575 | | |
| | | 3576 | | function showCartSelectionPopover(indicator) { |
| | | 3577 | | const orderId = indicator.dataset.orderId || ''; |
| | | 3578 | | const cart = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 3579 | | const item = cart.find(x => String(x.orderId) === orderId); |
| | | 3580 | | if (!item) return; |
| | | 3581 | | |
| | | 3582 | | const esc = typeof escapeHtml === 'function' ? escapeHtml : value => String(value ?? ''); |
| | | 3583 | | const popover = getCartSelectionPopover(); |
| | | 3584 | | const apiPrice = item.sourcePrice ? formatCartSelectionPrice(item.sourcePrice, item.sourceCurrencyCode) : '—'; |
| | | 3585 | | const price = formatCartSelectionPrice(item.priceValue, item.currencyCode); |
| | | 3586 | | popover.innerHTML = ` |
| | | 3587 | | <div class="tx-cart-selection-popover__title"><i class="fas fa-cart-check" aria-hidden="true"></i> Выбрано в |
| | | 3588 | | <div class="tx-cart-selection-popover__scroll"> |
| | | 3589 | | <table> |
| | | 3590 | | <thead><tr><th>Наименование</th><th>Направление</th><th>Поставщик</th><th>Налич.</th><th>API</th><th |
| | | 3591 | | <tbody><tr> |
| | | 3592 | | <td title="${esc(item.itemName || item.article || '—')}">${esc(item.itemName || item.article || |
| | | 3593 | | <td>${esc(item.direction || '—')}</td> |
| | | 3594 | | <td>${esc(item.produser || '—')}</td> |
| | | 3595 | | <td>${esc(item.rest || '—')}</td> |
| | | 3596 | | <td>${esc(apiPrice)}</td> |
| | | 3597 | | <td><b>${esc(price)}</b></td> |
| | | 3598 | | <td>${esc(item.deliveryDays || '—')}</td> |
| | | 3599 | | </tr></tbody> |
| | | 3600 | | </table> |
| | | 3601 | | </div>`; |
| | | 3602 | | |
| | | 3603 | | const rect = indicator.getBoundingClientRect(); |
| | | 3604 | | popover.style.visibility = 'hidden'; |
| | | 3605 | | popover.classList.add('is-visible'); |
| | | 3606 | | const width = popover.offsetWidth; |
| | | 3607 | | const height = popover.offsetHeight; |
| | | 3608 | | popover.style.left = Math.max(6, Math.min(rect.left, window.innerWidth - width - 6)) + 'px'; |
| | | 3609 | | popover.style.top = Math.max(6, Math.min(rect.bottom + 5, window.innerHeight - height - 6)) + 'px'; |
| | | 3610 | | popover.style.visibility = 'visible'; |
| | | 3611 | | } |
| | | 3612 | | |
| | | 3613 | | function hideCartSelectionPopover() { |
| | | 3614 | | document.getElementById('tx-cart-selection-popover')?.classList.remove('is-visible'); |
| | | 3615 | | } |
| | | 3616 | | |
| | | 3617 | | function updateOrderCartIndicators(cart) { |
| | | 3618 | | document.querySelectorAll('.tx-cart-selection-slot[data-order-id]').forEach(slot => { |
| | | 3619 | | const orderId = slot.dataset.orderId || ''; |
| | | 3620 | | const item = cart.find(x => String(x.orderId) === orderId); |
| | | 3621 | | if (!item) { |
| | | 3622 | | slot.replaceChildren(); |
| | | 3623 | | return; |
| | | 3624 | | } |
| | | 3625 | | let indicator = slot.querySelector('.tx-cart-selection-indicator'); |
| | | 3626 | | if (!indicator) { |
| | | 3627 | | indicator = document.createElement('button'); |
| | | 3628 | | indicator.type = 'button'; |
| | | 3629 | | indicator.className = 'tx-cart-selection-indicator'; |
| | | 3630 | | indicator.innerHTML = '<i class="fas fa-cart-shopping" aria-hidden="true"></i><span class="visually-hidd |
| | | 3631 | | slot.appendChild(indicator); |
| | | 3632 | | } |
| | | 3633 | | indicator.dataset.orderId = orderId; |
| | | 3634 | | indicator.setAttribute('aria-label', `В корзине: ${item.produser || 'поставщик'}, ${formatCartSelectionPrice |
| | | 3635 | | }); |
| | | 3636 | | } |
| | | 3637 | | |
| | | 3638 | | if (!window.__txApiSupplierPopoverBound) { |
| | | 3639 | | window.__txApiSupplierPopoverBound = true; |
| | | 3640 | | document.addEventListener('pointerover', event => { |
| | | 3641 | | const host = event.target.closest('.tx-api-supplier-indicators'); |
| | | 3642 | | if (host) showApiSupplierIndicatorPopover(host); |
| | | 3643 | | }); |
| | | 3644 | | document.addEventListener('pointerout', event => { |
| | | 3645 | | const host = event.target.closest('.tx-api-supplier-indicators'); |
| | | 3646 | | if (host && !host.contains(event.relatedTarget)) hideApiSupplierIndicatorPopover(); |
| | | 3647 | | }); |
| | | 3648 | | document.addEventListener('pointerover', event => { |
| | | 3649 | | const indicator = event.target.closest('.tx-cart-selection-indicator'); |
| | | 3650 | | if (indicator) showCartSelectionPopover(indicator); |
| | | 3651 | | }); |
| | | 3652 | | document.addEventListener('pointerout', event => { |
| | | 3653 | | const indicator = event.target.closest('.tx-cart-selection-indicator'); |
| | | 3654 | | if (indicator && !indicator.contains(event.relatedTarget)) hideCartSelectionPopover(); |
| | | 3655 | | }); |
| | | 3656 | | window.addEventListener('scroll', hideApiSupplierIndicatorPopover, true); |
| | | 3657 | | window.addEventListener('scroll', hideCartSelectionPopover, true); |
| | | 3658 | | window.addEventListener('resize', hideApiSupplierIndicatorPopover); |
| | | 3659 | | window.addEventListener('resize', hideCartSelectionPopover); |
| | | 3660 | | } |
| | | 3661 | | |
| | | 3662 | | function updateApiSupplierIndicator(orderId, source) { |
| | | 3663 | | const host = document.querySelector(`.tx-api-supplier-indicators[data-order-id="${orderId}"]`); |
| | | 3664 | | if (!host) return; |
| | | 3665 | | |
| | | 3666 | | const cache = orderDataCache[orderId]?.[source.cacheKey]; |
| | | 3667 | | const count = Array.isArray(cache?.items) ? cache.items.length : 0; |
| | | 3668 | | const id = `tx-api-indicator-${orderId}-${source.providerKey}`; |
| | | 3669 | | let indicator = document.getElementById(id); |
| | | 3670 | | |
| | | 3671 | | if (count === 0) { |
| | | 3672 | | indicator?.remove(); |
| | | 3673 | | return; |
| | | 3674 | | } |
| | | 3675 | | |
| | | 3676 | | if (!indicator) { |
| | | 3677 | | indicator = document.createElement('button'); |
| | | 3678 | | indicator.id = id; |
| | | 3679 | | indicator.type = 'button'; |
| | | 3680 | | indicator.className = `tx-api-supplier-indicator tx-api-supplier-indicator--${apiSupplierIndicatorClasses[so |
| | | 3681 | | indicator.innerHTML = '<i class="fas fa-circle-check" aria-hidden="true"></i><span class="tx-api-supplier-in |
| | | 3682 | | indicator.addEventListener('click', () => document.querySelector(`.get-full-order-info[data-order-id="${orde |
| | | 3683 | | } |
| | | 3684 | | |
| | | 3685 | | // В компактной строке видны три последних готовых источника. По мере |
| | | 3686 | | // прихода новых ответов они занимают освободившееся первое место, а |
| | | 3687 | | // полный список доступен при наведении. |
| | | 3688 | | host.insertBefore(indicator, host.firstChild); |
| | | 3689 | | |
| | | 3690 | | const label = `${source.displayName}: ${count} предложен${count === 1 ? 'ие' : 'ий'}`; |
| | | 3691 | | indicator.removeAttribute('title'); |
| | | 3692 | | indicator.setAttribute('aria-label', label); |
| | | 3693 | | indicator.dataset.providerKey = source.providerKey; |
| | | 3694 | | indicator.dataset.providerName = source.displayName; |
| | | 3695 | | indicator.dataset.count = String(count); |
| | | 3696 | | const badge = indicator.querySelector('.tx-api-supplier-indicator__count'); |
| | | 3697 | | if (badge) badge.textContent = String(count); |
| | | 3698 | | } |
| | | 3699 | | |
| | | 3700 | | async function refreshQuotationHistorySummary(orderId) { |
| | | 3701 | | try { |
| | 0 | 3702 | | const response = await fetch('@Url.Action("GetSupplierQuotationHistorySummary", "OrderHistory", new { area = |
| | | 3703 | | if (!response.ok) return; |
| | | 3704 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 3705 | | const summary = await response.json(); |
| | | 3706 | | orderDataCache[orderId].quotationHistorySummary = summary; |
| | | 3707 | | const quotationTabCount = document.getElementById('oh-quotation-tab-count'); |
| | | 3708 | | if (quotationTabCount) quotationTabCount.textContent = String(summary.offers || 0); |
| | | 3709 | | } catch (error) { |
| | | 3710 | | console.debug('Supplier quotation history summary was skipped', error); |
| | | 3711 | | } |
| | | 3712 | | } |
| | | 3713 | | |
| | | 3714 | | function setStok(userOrderId) { |
| | | 3715 | | const data = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 3716 | | data.forEach(({orderId, indexRow}) => { |
| | | 3717 | | if (userOrderId == orderId) { |
| | | 3718 | | const button = document.querySelector(`.stok-order[data-order-id="${orderId}"][data-index-row="${indexRo |
| | | 3719 | | if (button) { |
| | | 3720 | | const tableRow = button.closest('tr'); |
| | | 3721 | | const rowKey = tableRow?.dataset.rowKey || `${indexRow}__${button.dataset.stokId || ''}`; |
| | | 3722 | | setCartButtonState($(button), true); |
| | | 3723 | | highlightRow(orderId, rowKey, tableRow); |
| | | 3724 | | } |
| | | 3725 | | } |
| | | 3726 | | }); |
| | | 3727 | | } |
| | | 3728 | | |
| | | 3729 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 3730 | | const buttons = document.querySelectorAll('.get-full-order-info'); |
| | | 3731 | | buttons.forEach((button, index) => { |
| | | 3732 | | button.addEventListener('click', function () { |
| | | 3733 | | fetchSupplierInfo(this); |
| | | 3734 | | }); |
| | | 3735 | | }); |
| | | 3736 | | // Смена засыла или склада отгрузки меняет склад, с которым уходят запросы к API поставщиков, |
| | | 3737 | | // поэтому проценку заказа надо перечитать. Раскрытую строку перезагружаем |
| | | 3738 | | // сразу и с блоком прогресса, свёрнутую — молча, через фоновую очередь. |
| | | 3739 | | $(document).on('change', '.zasyl-input, .warehouse-select', function () { |
| | | 3740 | | const orderId = this.id.replace('orderSupplierMap_', '').replace('ShipmentWarehouseId_', ''); |
| | | 3741 | | if (!orderId) return; |
| | | 3742 | | |
| | | 3743 | | const isExpanded = document.querySelector(`#orderRow_${orderId} + .info-row`) !== null; |
| | | 3744 | | const apiProviders = new Set(apiSupplierQuotationSources.map(source => source.providerKey)); |
| | | 3745 | | |
| | | 3746 | | clearQuotationCacheForOrder(orderId); |
| | | 3747 | | invalidateQuotationPrefetch(orderId); |
| | | 3748 | | |
| | | 3749 | | let cart = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 3750 | | const hadApiItem = cart.some(item => String(item.orderId) === String(orderId) && apiProviders.has(item.provi |
| | | 3751 | | cart = cart.filter(item => String(item.orderId) !== String(orderId) || !apiProviders.has(item.providerKey)); |
| | | 3752 | | inMemoryCartStorage.setItem('selectedStockList' + ids, JSON.stringify(cart)); |
| | | 3753 | | if (hadApiItem) { |
| | | 3754 | | clearRowHighlight(orderId); |
| | | 3755 | | updateCartBadge(); |
| | | 3756 | | document.dispatchEvent(new CustomEvent('tx-cart-changed')); |
| | | 3757 | | } |
| | | 3758 | | |
| | | 3759 | | const button = document.querySelector(`.get-full-order-info[data-order-id="${orderId}"]`); |
| | | 3760 | | if (!button) return; |
| | | 3761 | | |
| | | 3762 | | if (isExpanded) { |
| | | 3763 | | $(`#orderRow_${orderId}`).next('.info-row').remove(); |
| | | 3764 | | fetchSupplierInfo(button); |
| | | 3765 | | return; |
| | | 3766 | | } |
| | | 3767 | | |
| | | 3768 | | // Массовая простановка засыла шлёт change сразу по всем строкам — |
| | | 3769 | | // очередь удерживает нагрузку на том же лимите, что и первичный прогрев. |
| | | 3770 | | if (window.txPrefetchQuotationOnLoad === true) { |
| | | 3771 | | enqueueQuotationPrefetch(button); |
| | | 3772 | | } |
| | | 3773 | | }); |
| | | 3774 | | document.getElementById('startButton')?.addEventListener('click', function () { |
| | | 3775 | | this.style.display = 'none'; |
| | | 3776 | | buttons.forEach((button, index) => { |
| | | 3777 | | setTimeout(() => fetchSupplierInfoInBg(button), index * 4500); |
| | | 3778 | | }); |
| | | 3779 | | }); |
| | | 3780 | | |
| | | 3781 | | // Армтек выполняет первый запрос медленнее из-за холодного TLS-соединения и справочников. |
| | | 3782 | | // Прогрев не запрашивает товары: все реальные API-запросы остаются в этапах раскрытой строки. |
| | | 3783 | | void warmupApiSupplierQuotationSources(); |
| | | 3784 | | |
| | | 3785 | | // Для partial, добавленных после DOMContentLoaded, IIFE выше уже запустит |
| | | 3786 | | // очередь. Здесь повторный безопасный вызов нужен обычной загрузке страницы: |
| | | 3787 | | // строки и обработчики точно готовы до первого запроса. |
| | | 3788 | | startBackgroundQuotationPrefetch(); |
| | | 3789 | | |
| | | 3790 | | }); |
| | | 3791 | | |
| | | 3792 | | function warmupApiSupplierQuotationSources() { |
| | | 3793 | | apiSupplierQuotationSources |
| | | 3794 | | .filter(source => source.isEnabled() && source.warmupEndpoint) |
| | | 3795 | | .forEach(source => { |
| | | 3796 | | $.ajax({ url: source.warmupEndpoint, type: 'GET' }) |
| | | 3797 | | .fail(() => console.debug(`${source.providerKey} quotation warmup was skipped`)); |
| | | 3798 | | }); |
| | | 3799 | | } |
| | | 3800 | | |
| | | 3801 | | async function preloadInterpartsPriceApi(buttons) { |
| | | 3802 | | // Ограничиваем количество параллельных запросов, чтобы не перегружать сервер. |
| | | 3803 | | const concurrency = 3; |
| | | 3804 | | const shouldPreloadInterparts = document.getElementById('interpartsPriceData')?.checked === true; |
| | | 3805 | | |
| | | 3806 | | const unique = new Map(); |
| | | 3807 | | (buttons || []).forEach(btn => { |
| | | 3808 | | const orderId = btn.getAttribute('data-order-id'); |
| | | 3809 | | const code = btn.getAttribute('data-order-article'); |
| | | 3810 | | const producerId = parseInt(btn.getAttribute('data-order-producer-id') || '0', 10); |
| | | 3811 | | const producerName = btn.getAttribute('data-order-producer-name') || ''; |
| | | 3812 | | |
| | | 3813 | | const cleanCode = (code || '').trim(); |
| | | 3814 | | if (!orderId || !cleanCode || !producerId || producerId <= 0) return; |
| | | 3815 | | unique.set(orderId, { orderId: orderId, code: cleanCode, producerId: producerId, producerName: producerName |
| | | 3816 | | }); |
| | | 3817 | | |
| | | 3818 | | const items = Array.from(unique.values()); |
| | | 3819 | | let cursor = 0; |
| | | 3820 | | |
| | | 3821 | | async function worker() { |
| | | 3822 | | while (cursor < items.length) { |
| | | 3823 | | const currentIndex = cursor++; |
| | | 3824 | | const current = items[currentIndex]; |
| | | 3825 | | |
| | | 3826 | | if (!current) continue; |
| | | 3827 | | |
| | | 3828 | | // Если уже подгрузили — пропускаем |
| | | 3829 | | if (orderDataCache[current.orderId]?.interpartsPriceData) continue; |
| | | 3830 | | |
| | | 3831 | | try { |
| | | 3832 | | const interpartsRequest = shouldPreloadInterparts |
| | | 3833 | | ? fetchInterpartsPriceData(current.code, current.producerId, current.orderId) |
| | | 3834 | | .then(data => { |
| | | 3835 | | if (data) processInterpartsPriceData(data, current.orderId); |
| | | 3836 | | }) |
| | | 3837 | | : Promise.resolve(); |
| | | 3838 | | const quotationRequests = []; |
| | | 3839 | | addApiSupplierQuotationRequests(quotationRequests, current.code, current.producerName, current.order |
| | | 3840 | | await Promise.all([interpartsRequest, ...quotationRequests]); |
| | | 3841 | | await refreshQuotationHistorySummary(current.orderId); |
| | | 3842 | | } catch (e) { |
| | | 3843 | | // Ошибки не ломаем основную логику страницы |
| | | 3844 | | console.warn('Preload InterpartsPriceApi error:', e); |
| | | 3845 | | } |
| | | 3846 | | } |
| | | 3847 | | } |
| | | 3848 | | |
| | | 3849 | | const workers = []; |
| | | 3850 | | for (let i = 0; i < concurrency; i++) { |
| | | 3851 | | workers.push(worker()); |
| | | 3852 | | } |
| | | 3853 | | |
| | | 3854 | | await Promise.allSettled(workers); |
| | | 3855 | | } |
| | | 3856 | | |
| | | 3857 | | async function preloadOzonAttributesApi(buttons) { |
| | | 3858 | | // Ограничиваем количество параллельных запросов, чтобы не перегружать сервер. |
| | | 3859 | | const concurrency = 3; |
| | | 3860 | | |
| | | 3861 | | const unique = new Map(); |
| | | 3862 | | (buttons || []).forEach(btn => { |
| | | 3863 | | const orderId = btn.getAttribute('data-order-id'); |
| | | 3864 | | if (!orderId) return; |
| | | 3865 | | unique.set(orderId, { orderId: orderId }); |
| | | 3866 | | }); |
| | | 3867 | | |
| | | 3868 | | const items = Array.from(unique.values()); |
| | | 3869 | | let cursor = 0; |
| | | 3870 | | |
| | | 3871 | | async function worker() { |
| | | 3872 | | while (cursor < items.length) { |
| | | 3873 | | const currentIndex = cursor++; |
| | | 3874 | | const current = items[currentIndex]; |
| | | 3875 | | |
| | | 3876 | | if (!current) continue; |
| | | 3877 | | |
| | | 3878 | | // Если уже подгрузили — пропускаем |
| | | 3879 | | if (orderDataCache[current.orderId]?.ozonAttributesData) continue; |
| | | 3880 | | |
| | | 3881 | | try { |
| | | 3882 | | const data = await fetchOzonAttributesData(current.orderId); |
| | | 3883 | | if (data) { |
| | | 3884 | | processOzonAttributesData(data, current.orderId); |
| | | 3885 | | } |
| | | 3886 | | } catch (e) { |
| | | 3887 | | // Ошибки не ломаем основную логику страницы |
| | | 3888 | | console.warn('Preload Ozon attributes error:', e); |
| | | 3889 | | } |
| | | 3890 | | } |
| | | 3891 | | } |
| | | 3892 | | |
| | | 3893 | | const workers = []; |
| | | 3894 | | for (let i = 0; i < concurrency; i++) { |
| | | 3895 | | workers.push(worker()); |
| | | 3896 | | } |
| | | 3897 | | |
| | | 3898 | | await Promise.allSettled(workers); |
| | | 3899 | | } |
| | | 3900 | | |
| | | 3901 | | |
| | | 3902 | | async function fetchSupplierInfo(button) { |
| | | 3903 | | const orderId = button.getAttribute('data-order-id'); |
| | | 3904 | | const orderArticle = button.getAttribute('data-order-article'); |
| | | 3905 | | const orderProducerId = parseInt(button.getAttribute('data-order-producer-id') || '0', 10); |
| | | 3906 | | const orderProducerName = button.getAttribute('data-order-producer-name') || ''; |
| | | 3907 | | const rowId = `#orderRow_${orderId}`; |
| | | 3908 | | |
| | | 3909 | | // Если строка уже открыта — закрываем без загрузки |
| | | 3910 | | if ($(rowId).next().hasClass('info-row')) { |
| | | 3911 | | $(rowId).next().remove(); |
| | | 3912 | | rowCache.delete(orderId); |
| | | 3913 | | setOrderInfoButtonState(button, 'collapsed'); |
| | | 3914 | | return; |
| | | 3915 | | } |
| | | 3916 | | |
| | | 3917 | | // Кеш — показываем сразу, без полосы загрузки |
| | | 3918 | | if (rowCache.has(orderId)) { |
| | | 3919 | | $(rowId).after(rowCache.get(orderId)); |
| | | 3920 | | if (typeof initTooltip === 'function') { |
| | | 3921 | | document.querySelectorAll('.tx-info-row[data-order-id="' + orderId + '"] [title]').forEach(initTooltip); |
| | | 3922 | | } |
| | | 3923 | | if (typeof applyColumnState === 'function') { |
| | | 3924 | | applyColumnState('.stock-trade-table[data-order-id="' + orderId + '"]'); |
| | | 3925 | | } |
| | | 3926 | | setOrderInfoButtonState(button, 'expanded'); |
| | | 3927 | | setStok(orderId); |
| | | 3928 | | return; |
| | | 3929 | | } |
| | | 3930 | | |
| | | 3931 | | setOrderInfoButtonState(button, 'loading'); |
| | | 3932 | | |
| | | 3933 | | // Блок прогресса показываем всегда: завершённый прогрев ещё не значит, |
| | | 3934 | | // что данные есть — он мог упасть или отработать вхолостую. |
| | | 3935 | | showSupplierInfoLoading(rowId, orderId); |
| | | 3936 | | |
| | | 3937 | | // Заказ греется в фоне — дожидаемся его вместо второго комплекта тех же |
| | | 3938 | | // запросов; этапы в блоке отмечает сам прогрев. |
| | | 3939 | | const prefetch = getQuotationPrefetch(orderId); |
| | | 3940 | | const isPrefetchRunning = !!prefetch && !prefetch.done; |
| | | 3941 | | |
| | | 3942 | | try { |
| | | 3943 | | await withPageLoadingBar(async () => { |
| | | 3944 | | if (isPrefetchRunning) { |
| | | 3945 | | await prefetch.promise.catch(() => {}); |
| | | 3946 | | } |
| | | 3947 | | |
| | | 3948 | | const requests = []; |
| | | 3949 | | |
| | | 3950 | | if (!orderDataCache[orderId]?.orderDetails && isDataTypeEnabled('orderDetails')) { |
| | | 3951 | | requests.push(fetchOrderDetails(orderId).then(data => { |
| | | 3952 | | if (data && Object.keys(data).length > 0) { |
| | | 3953 | | processOrderDetails(data, orderId); |
| | | 3954 | | } |
| | | 3955 | | }).finally(() => updateSupplierInfoLoadingStage(orderId, 'orderDetails'))); |
| | | 3956 | | } |
| | | 3957 | | |
| | | 3958 | | if (!orderDataCache[orderId]?.bitrixData && isDataTypeEnabled('bitrixData')) { |
| | | 3959 | | requests.push(Promise.resolve(fetchBitrixData(orderArticle)).then(data => processBitrixData(data, or |
| | | 3960 | | .finally(() => updateSupplierInfoLoadingStage(orderId, 'bitrixData'))); |
| | | 3961 | | } |
| | | 3962 | | |
| | | 3963 | | if (!orderDataCache[orderId]?.interpartsPriceData && isDataTypeEnabled('interpartsPriceData')) { |
| | | 3964 | | requests.push(Promise.resolve(fetchInterpartsPriceData(orderArticle, orderProducerId, orderId)).then |
| | | 3965 | | .finally(() => updateSupplierInfoLoadingStage(orderId, 'interpartsPriceData', orderDataCache[ord |
| | | 3966 | | } |
| | | 3967 | | |
| | | 3968 | | addApiSupplierQuotationRequests(requests, orderArticle, orderProducerName, orderId, source => |
| | | 3969 | | updateSupplierInfoLoadingStage(orderId, source.cacheKey, orderDataCache[orderId]?.[source.cacheKey]? |
| | | 3970 | | |
| | | 3971 | | if (!orderDataCache[orderId]?.ozonAttributesData && isDataTypeEnabled('ozonAttributesData')) { |
| | | 3972 | | requests.push(fetchOzonAttributesData(orderId).then(data => { |
| | | 3973 | | if (data) { |
| | | 3974 | | processOzonAttributesData(data, orderId); |
| | | 3975 | | } |
| | | 3976 | | }).finally(() => updateSupplierInfoLoadingStage(orderId, 'ozonAttributesData'))); |
| | | 3977 | | } |
| | | 3978 | | |
| | | 3979 | | await Promise.all(requests); |
| | | 3980 | | await refreshQuotationHistorySummary(orderId); |
| | | 3981 | | |
| | | 3982 | | const rates = { |
| | | 3983 | | 'RUB': 1, |
| | 0 | 3984 | | 'USD': parseFloat('@Model.RateUSD'.replace(',', '.')), |
| | 0 | 3985 | | 'EUR': parseFloat('@Model.RateEUR'.replace(',', '.')), |
| | 0 | 3986 | | 'BYN': parseFloat('@Model.RateBYN'.replace(',', '.')) |
| | | 3987 | | }; |
| | | 3988 | | |
| | | 3989 | | await hideSupplierInfoLoading(orderId); |
| | | 3990 | | const infoRow = generateInfoRow(orderId, rates); |
| | | 3991 | | rowCache.set(orderId, infoRow); |
| | | 3992 | | $(rowId).after(infoRow); |
| | | 3993 | | if (typeof initTooltip === 'function') { |
| | | 3994 | | document.querySelectorAll('.tx-info-row[data-order-id="' + orderId + '"] [title]').forEach(initToolt |
| | | 3995 | | } |
| | | 3996 | | if (typeof applyColumnState === 'function') { |
| | | 3997 | | applyColumnState('.stock-trade-table[data-order-id="' + orderId + '"]'); |
| | | 3998 | | } |
| | | 3999 | | setStok(orderId); |
| | | 4000 | | updateAllPricesForOrder(orderId); |
| | | 4001 | | }); |
| | | 4002 | | |
| | | 4003 | | setOrderInfoButtonState(button, 'expanded'); |
| | | 4004 | | } catch (error) { |
| | | 4005 | | console.error("Ошибка при загрузке данных:", error); |
| | | 4006 | | $(`.tx-info-loading-row[data-order-id="${orderId}"]`).remove(); |
| | | 4007 | | setOrderInfoButtonState(button, 'collapsed'); |
| | | 4008 | | } |
| | | 4009 | | } |
| | | 4010 | | |
| | | 4011 | | async function fetchSupplierInfoInBg(button) { |
| | | 4012 | | const orderId = button.getAttribute('data-order-id'); |
| | | 4013 | | const orderArticle = button.getAttribute('data-order-article'); |
| | | 4014 | | const orderProducerId = parseInt(button.getAttribute('data-order-producer-id') || '0', 10); |
| | | 4015 | | const orderProducerName = button.getAttribute('data-order-producer-name') || ''; |
| | | 4016 | | |
| | | 4017 | | try { |
| | | 4018 | | // Фоновый режим не загружает только карточку заказа и атрибуты Ozon. |
| | | 4019 | | // Остатки Bitrix и Interparts нужны в раскрытой строке так же, как |
| | | 4020 | | // предложения API-поставщиков, поэтому прогреваем их в этой очереди. |
| | | 4021 | | const requests = []; |
| | | 4022 | | |
| | | 4023 | | if (!orderDataCache[orderId]?.bitrixData && isDataTypeEnabled('bitrixData')) { |
| | | 4024 | | requests.push(Promise.resolve(fetchBitrixData(orderArticle)) |
| | | 4025 | | .then(data => processBitrixData(data, orderId)) |
| | | 4026 | | .finally(() => markQuotationStage(orderId, 'bitrixData', orderDataCache[orderId]?.bitrixData?.error |
| | | 4027 | | } |
| | | 4028 | | |
| | | 4029 | | if (!orderDataCache[orderId]?.interpartsPriceData && isDataTypeEnabled('interpartsPriceData')) { |
| | | 4030 | | requests.push(Promise.resolve(fetchInterpartsPriceData(orderArticle, orderProducerId, orderId)) |
| | | 4031 | | .then(data => processInterpartsPriceData(data, orderId)) |
| | | 4032 | | .finally(() => markQuotationStage(orderId, 'interpartsPriceData', orderDataCache[orderId]?.interpart |
| | | 4033 | | } |
| | | 4034 | | |
| | | 4035 | | addApiSupplierQuotationRequests(requests, orderArticle, orderProducerName, orderId, source => |
| | | 4036 | | markQuotationStage(orderId, source.cacheKey, orderDataCache[orderId]?.[source.cacheKey]?.error ? 'error' |
| | | 4037 | | |
| | | 4038 | | await Promise.all(requests); |
| | | 4039 | | await refreshQuotationHistorySummary(orderId); |
| | | 4040 | | |
| | | 4041 | | } catch (error) { |
| | | 4042 | | console.error("Ошибка при загрузке данных:", error); |
| | | 4043 | | } |
| | | 4044 | | } |
| | | 4045 | | |
| | | 4046 | | // Обработчики для кнопок выбора всех чекбоксов (есть только в navbar части транзакций поставщика/клиента) |
| | | 4047 | | document.getElementById('selectAllData')?.addEventListener('click', function () { |
| | | 4048 | | document.querySelectorAll('.data-type-checkbox').forEach(checkbox => { |
| | | 4049 | | checkbox.checked = true; |
| | | 4050 | | }); |
| | | 4051 | | }); |
| | | 4052 | | |
| | | 4053 | | document.getElementById('deselectAllData')?.addEventListener('click', function () { |
| | | 4054 | | document.querySelectorAll('.data-type-checkbox').forEach(checkbox => { |
| | | 4055 | | checkbox.checked = false; |
| | | 4056 | | }); |
| | | 4057 | | }); |
| | | 4058 | | |
| | | 4059 | | function txExtractAjaxErrorDetail(xhr, fallback) { |
| | | 4060 | | const fb = fallback || 'Неизвестная ошибка'; |
| | | 4061 | | if (!xhr) { |
| | | 4062 | | return fb; |
| | | 4063 | | } |
| | | 4064 | | if (typeof window.extractFriendlyErrorMessage === 'function') { |
| | | 4065 | | return window.extractFriendlyErrorMessage(xhr, xhr.statusText || '', xhr.statusText || ''); |
| | | 4066 | | } |
| | | 4067 | | if (xhr.responseJSON) { |
| | | 4068 | | if (typeof xhr.responseJSON === 'string') { |
| | | 4069 | | return xhr.responseJSON; |
| | | 4070 | | } |
| | | 4071 | | if (xhr.responseJSON.error) { |
| | | 4072 | | return String(xhr.responseJSON.error); |
| | | 4073 | | } |
| | | 4074 | | if (xhr.responseJSON.message) { |
| | | 4075 | | return String(xhr.responseJSON.message); |
| | | 4076 | | } |
| | | 4077 | | if (xhr.responseJSON.title) { |
| | | 4078 | | return String(xhr.responseJSON.title); |
| | | 4079 | | } |
| | | 4080 | | } |
| | | 4081 | | const text = (xhr.responseText || '').trim(); |
| | | 4082 | | if (text) { |
| | | 4083 | | return text.length > 4000 ? text.slice(0, 4000) + '…' : text; |
| | | 4084 | | } |
| | | 4085 | | if (xhr.status) { |
| | | 4086 | | return `HTTP ${xhr.status}${xhr.statusText ? ' ' + xhr.statusText : ''}`; |
| | | 4087 | | } |
| | | 4088 | | return fb; |
| | | 4089 | | } |
| | | 4090 | | |
| | | 4091 | | function fetchOrderDetails(orderId) { |
| | | 4092 | | // Legacy endpoint removed вместе с PartsInfo; intentionally skip network call. |
| | | 4093 | | return Promise.resolve({}); |
| | | 4094 | | } |
| | | 4095 | | |
| | | 4096 | | function fetchTradesoftData(orderId) { |
| | | 4097 | | return Promise.resolve([]); |
| | | 4098 | | } |
| | | 4099 | | |
| | | 4100 | | function fetchAbcpData(orderId) { |
| | | 4101 | | return Promise.resolve([]); |
| | | 4102 | | } |
| | | 4103 | | |
| | | 4104 | | |
| | | 4105 | | function fetchOneAbcpData(orderId) { |
| | | 4106 | | return $.ajax({ |
| | 0 | 4107 | | url: '@Url.Action("GetOneAbcpData", "Tradesoft")', |
| | | 4108 | | type: "GET", |
| | | 4109 | | data: { |
| | | 4110 | | orderId: orderId, |
| | | 4111 | | } |
| | | 4112 | | }).catch(error => { |
| | | 4113 | | if (error.status === 500 || error.status === 204) { |
| | | 4114 | | console.error('Ошибка 500 при получении данных от ABCP:', error.responseText); |
| | | 4115 | | } |
| | | 4116 | | return null; |
| | | 4117 | | }); |
| | | 4118 | | } |
| | | 4119 | | |
| | | 4120 | | function fetchAvdData(orderId) { |
| | | 4121 | | return Promise.resolve([]); |
| | | 4122 | | } |
| | | 4123 | | |
| | | 4124 | | function fetchBitrixData(orderArticle) { |
| | | 4125 | | return $.ajax({ |
| | 0 | 4126 | | url: '@Url.Action("GetBitrixData", "Bitrix")', |
| | | 4127 | | type: "GET", |
| | | 4128 | | data: { |
| | | 4129 | | article: orderArticle |
| | | 4130 | | } |
| | | 4131 | | }).catch(error => { |
| | | 4132 | | const message = txExtractAjaxErrorDetail(error, 'Не удалось загрузить данные Bitrix'); |
| | | 4133 | | console.error('Bitrix:', message, error); |
| | | 4134 | | return { error: message }; |
| | | 4135 | | }); |
| | | 4136 | | } |
| | | 4137 | | |
| | | 4138 | | function fetchInterpartsPriceData(code, producerId, orderId) { |
| | | 4139 | | const cleanCode = (code || '').trim(); |
| | | 4140 | | const numProducerId = Number(producerId) || 0; |
| | | 4141 | | if (!cleanCode || numProducerId <= 0) { |
| | | 4142 | | const message = !cleanCode |
| | | 4143 | | ? 'Не указан артикул (code) для поиска цен Interparts' |
| | | 4144 | | : 'Не указан производитель (producerId) для поиска цен Interparts'; |
| | | 4145 | | return Promise.resolve({ error: message, items: [] }); |
| | | 4146 | | } |
| | | 4147 | | |
| | | 4148 | | const useCrosses = document.getElementById('useCrosses')?.checked === true; |
| | | 4149 | | const startedAt = performance.now(); |
| | | 4150 | | |
| | | 4151 | | return $.ajax({ |
| | 0 | 4152 | | url: '@Url.Action("Search", "InterpartsPriceApi")', |
| | | 4153 | | type: "GET", |
| | | 4154 | | data: { |
| | | 4155 | | code: cleanCode, |
| | | 4156 | | producerId: numProducerId, |
| | | 4157 | | orderId: orderId, |
| | | 4158 | | useCrosses: useCrosses |
| | | 4159 | | } |
| | | 4160 | | }).then(response => { |
| | | 4161 | | // Свойство на массиве не сериализуется и не влияет на предложения, но доступно обработчику ниже. |
| | | 4162 | | response.responseTimeMs = Math.round(performance.now() - startedAt); |
| | | 4163 | | return response; |
| | | 4164 | | }).catch(error => { |
| | | 4165 | | const message = txExtractAjaxErrorDetail(error, 'Не удалось загрузить цены Interparts'); |
| | | 4166 | | console.warn('Interparts price API:', message, error); |
| | | 4167 | | return { |
| | | 4168 | | error: message, |
| | | 4169 | | items: [], |
| | | 4170 | | responseTimeMs: Math.round(performance.now() - startedAt), |
| | | 4171 | | upstreamStatusCode: error?.responseJSON?.upstreamStatusCode ?? null, |
| | | 4172 | | diagnosticsUrl: error?.responseJSON?.diagnosticsUrl ?? null |
| | | 4173 | | }; |
| | | 4174 | | }); |
| | | 4175 | | } |
| | | 4176 | | |
| | | 4177 | | function fetchOzonAttributesData(orderId) { |
| | | 4178 | | if (!orderId || orderId <= 0) { |
| | | 4179 | | return Promise.resolve(null); |
| | | 4180 | | } |
| | | 4181 | | // Optional block: skip request to prevent noisy 400 for orders without attributes. |
| | | 4182 | | return Promise.resolve(null); |
| | | 4183 | | } |
| | | 4184 | | |
| | | 4185 | | function fetchYumboData(orderId) { |
| | | 4186 | | return Promise.resolve([]); |
| | | 4187 | | } |
| | | 4188 | | |
| | | 4189 | | // Функции обработки данных |
| | | 4190 | | function processOrderDetails(response, orderId) { |
| | | 4191 | | const safeResponse = response || {}; |
| | | 4192 | | const detailInfo = safeResponse.detailInfo || {}; |
| | | 4193 | | const images = detailInfo.imgUrls?.length > 0 |
| | | 4194 | | ? detailInfo.imgUrls |
| | | 4195 | | : ["https://www.interparts.store/_sysimg/no-photo.png"]; |
| | | 4196 | | |
| | | 4197 | | // Обновление иконок |
| | | 4198 | | const hidden_info = document.querySelector(`.hidden-info-icon[data-order-id="${orderId}"]`); |
| | | 4199 | | if (hidden_info) { |
| | | 4200 | | hidden_info.style.display = ( |
| | | 4201 | | (safeResponse.detailAttributes?.length !== 0) || |
| | | 4202 | | (safeResponse.productInfo && Object.keys(safeResponse.productInfo).length > 0) |
| | | 4203 | | ) ? 'inline' : 'none'; |
| | | 4204 | | } |
| | | 4205 | | |
| | | 4206 | | const preferred_supplier = document.querySelector(`.preferred-supplier[data-order-id="${orderId}"]`); |
| | | 4207 | | if (preferred_supplier) { |
| | | 4208 | | preferred_supplier.style.display = 'none'; |
| | | 4209 | | } |
| | | 4210 | | |
| | | 4211 | | // Сохранение в кеш |
| | | 4212 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4213 | | orderDataCache[orderId].orderDetails = { |
| | | 4214 | | response: safeResponse, |
| | | 4215 | | mainImage: images[0], |
| | | 4216 | | limitedMiniatures: images.slice(1, 5) |
| | | 4217 | | }; |
| | | 4218 | | } |
| | | 4219 | | |
| | | 4220 | | function processOzonAttributesData(response, orderId) { |
| | | 4221 | | // response: ProductAttributesResponse (DTO сервера). |
| | | 4222 | | const safeResponse = response || {}; |
| | | 4223 | | |
| | | 4224 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4225 | | orderDataCache[orderId].ozonAttributesData = { |
| | | 4226 | | response: safeResponse, |
| | | 4227 | | lastUpdated: new Date().toISOString() |
| | | 4228 | | }; |
| | | 4229 | | } |
| | | 4230 | | |
| | | 4231 | | function processTradesoftData(tradesofResponse, orderId) { |
| | | 4232 | | // Проверяем, что tradesofResponse не null и имеет свойство length |
| | | 4233 | | const tradesofData = tradesofResponse || []; |
| | | 4234 | | |
| | | 4235 | | // Обновление иконки Tradesoft |
| | | 4236 | | const preferred_tradesoft = document.querySelector(`.preferred-tradesoft[data-order-id="${orderId}"]`); |
| | | 4237 | | if (preferred_tradesoft) { |
| | | 4238 | | preferred_tradesoft.style.display = tradesofData.length > 0 ? 'inline' : 'none'; |
| | | 4239 | | } |
| | | 4240 | | |
| | | 4241 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4242 | | orderDataCache[orderId].tradesoftData = { |
| | | 4243 | | tradesofData: tradesofData |
| | | 4244 | | }; |
| | | 4245 | | } |
| | | 4246 | | |
| | | 4247 | | function processAbcpData(abcpResponse, orderId) { |
| | | 4248 | | const abcpData = abcpResponse || []; |
| | | 4249 | | const preferredAbcp = document.querySelector(`.preferred-abcp[data-order-id="${orderId}"]`); |
| | | 4250 | | if (preferredAbcp) { |
| | | 4251 | | preferredAbcp.style.display = abcpData.length > 0 ? 'inline' : 'none'; |
| | | 4252 | | preferredAbcp.title = abcpData.length > 0 |
| | | 4253 | | ? `Найдено ${abcpData.length} позиций в ABCP` |
| | | 4254 | | : 'Нет данных в ABCP'; |
| | | 4255 | | } |
| | | 4256 | | |
| | | 4257 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4258 | | orderDataCache[orderId].abcpData = { |
| | | 4259 | | items: abcpData, |
| | | 4260 | | lastUpdated: new Date().toISOString() |
| | | 4261 | | }; |
| | | 4262 | | if (abcpData.length > 0) { |
| | | 4263 | | updatePriceComparison(orderId, abcpData); |
| | | 4264 | | } |
| | | 4265 | | } |
| | | 4266 | | |
| | | 4267 | | function processOneAbcpData(abcpOneDataResponse, orderId) { |
| | | 4268 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4269 | | orderDataCache[orderId].abcpOneData = { |
| | | 4270 | | ...abcpOneDataResponse, // сохраняем плоско |
| | | 4271 | | lastUpdated: new Date().toISOString() |
| | | 4272 | | }; |
| | | 4273 | | } |
| | | 4274 | | |
| | | 4275 | | function processAvdData(avdResponse, orderId) { |
| | | 4276 | | const avdData = avdResponse || []; |
| | | 4277 | | const preferredAvd = document.querySelector(`.preferred-avd[data-order-id="${orderId}"]`); |
| | | 4278 | | |
| | | 4279 | | if (preferredAvd) { |
| | | 4280 | | preferredAvd.style.display = avdData.length > 0 ? 'inline' : 'none'; |
| | | 4281 | | preferredAvd.title = avdData.length > 0 |
| | | 4282 | | ? `Найдено ${avdData.length} позиций в AVD` |
| | | 4283 | | : 'Нет данных в AVD'; |
| | | 4284 | | } |
| | | 4285 | | |
| | | 4286 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4287 | | orderDataCache[orderId].avdData = { |
| | | 4288 | | items: avdData, |
| | | 4289 | | lastUpdated: new Date().toISOString() |
| | | 4290 | | }; |
| | | 4291 | | |
| | | 4292 | | if (avdData.length > 0) { |
| | | 4293 | | updatePriceComparison(orderId, avdData); |
| | | 4294 | | } |
| | | 4295 | | } |
| | | 4296 | | |
| | | 4297 | | function processBitrixData(bitrixResponse, orderId) { |
| | | 4298 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4299 | | |
| | | 4300 | | if (bitrixResponse && bitrixResponse.error) { |
| | | 4301 | | const stockElement = document.querySelector(`.bitrix-stock[data-order-id="${orderId}"]`); |
| | | 4302 | | if (stockElement) { |
| | | 4303 | | stockElement.style.display = 'none'; |
| | | 4304 | | stockElement.title = String(bitrixResponse.error); |
| | | 4305 | | } |
| | | 4306 | | orderDataCache[orderId].bitrixData = { |
| | | 4307 | | data: {}, |
| | | 4308 | | error: String(bitrixResponse.error), |
| | | 4309 | | lastUpdated: new Date().toISOString() |
| | | 4310 | | }; |
| | | 4311 | | return; |
| | | 4312 | | } |
| | | 4313 | | |
| | | 4314 | | const bitrixData = bitrixResponse || {}; |
| | | 4315 | | const stockElement = document.querySelector(`.bitrix-stock[data-order-id="${orderId}"]`); |
| | | 4316 | | |
| | | 4317 | | if (stockElement) { |
| | | 4318 | | stockElement.style.display = Object.keys(bitrixData.stores || {}).length > 0 ? 'inline' : 'none'; |
| | | 4319 | | stockElement.title = Object.keys(bitrixData.stores || {}).length > 0 |
| | | 4320 | | ? `Цена: ${bitrixData.price} ${bitrixData.currency}, остатки: ${JSON.stringify(bitrixData.stores)}` |
| | | 4321 | | : 'Нет данных в Bitrix'; |
| | | 4322 | | } |
| | | 4323 | | |
| | | 4324 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4325 | | orderDataCache[orderId].bitrixData = { |
| | | 4326 | | data: bitrixData, |
| | | 4327 | | lastUpdated: new Date().toISOString() |
| | | 4328 | | }; |
| | | 4329 | | } |
| | | 4330 | | |
| | | 4331 | | function processInterpartsPriceData(response, orderId) { |
| | | 4332 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4333 | | |
| | | 4334 | | if (response == null) { |
| | | 4335 | | orderDataCache[orderId].interpartsPriceData = { |
| | | 4336 | | items: [], |
| | | 4337 | | error: 'Запрос цен Interparts не выполнен (пустой ответ)', |
| | | 4338 | | lastUpdated: new Date().toISOString() |
| | | 4339 | | }; |
| | | 4340 | | return; |
| | | 4341 | | } |
| | | 4342 | | |
| | | 4343 | | if (response && !Array.isArray(response) && response.error) { |
| | | 4344 | | orderDataCache[orderId].interpartsPriceData = { |
| | | 4345 | | items: [], |
| | | 4346 | | error: String(response.error), |
| | | 4347 | | responseTimeMs: Number(response.responseTimeMs) || null, |
| | | 4348 | | upstreamStatusCode: response.upstreamStatusCode ?? null, |
| | | 4349 | | diagnosticsUrl: response.diagnosticsUrl ?? null, |
| | | 4350 | | lastUpdated: new Date().toISOString() |
| | | 4351 | | }; |
| | | 4352 | | const preferredInterparts = document.querySelector(`.preferred-interparts[data-order-id="${orderId}"]`); |
| | | 4353 | | if (preferredInterparts) { |
| | | 4354 | | preferredInterparts.style.display = 'none'; |
| | | 4355 | | preferredInterparts.title = String(response.error); |
| | | 4356 | | } |
| | | 4357 | | return; |
| | | 4358 | | } |
| | | 4359 | | |
| | | 4360 | | // Новый бэкенд-вариант: controller отдаёт сразу List<PreOrderItem>. |
| | | 4361 | | // Старый фронт-вариант: отдавался объект с offers/Offers. |
| | | 4362 | | let items = []; |
| | | 4363 | | if (Array.isArray(response)) { |
| | | 4364 | | items = response; |
| | | 4365 | | } else { |
| | | 4366 | | const offers = response?.offers || response?.Offers || []; |
| | | 4367 | | items = (offers || []).map(offer => { |
| | | 4368 | | // Берем и поля в camelCase, и исходные имена C# (на случай различий сериализации) |
| | | 4369 | | const partCodeShort = offer?.PartCodeShort ?? offer?.partCodeShort ?? ''; |
| | | 4370 | | const partDescription = offer?.PartDescription ?? offer?.partDescription ?? ''; |
| | | 4371 | | const producerName = offer?.ProducerName ?? offer?.producerName ?? ''; |
| | | 4372 | | const supplierName = offer?.SupplierName ?? offer?.supplierName ?? ''; |
| | | 4373 | | |
| | | 4374 | | const ourPrice2Decimal = offer?.OurPrice2Decimal ?? offer?.ourPrice2Decimal ?? null; |
| | | 4375 | | const supplierPriceValue = ourPrice2Decimal !== null && ourPrice2Decimal !== undefined && !isNaN(Number( |
| | | 4376 | | ? Math.round(Number(ourPrice2Decimal)) |
| | | 4377 | | : undefined; |
| | | 4378 | | |
| | | 4379 | | const inputPriceDecimal = offer?.InputPriceDecimal ?? offer?.inputPriceDecimal ?? null; |
| | | 4380 | | const inputCostValue = inputPriceDecimal !== null && inputPriceDecimal !== undefined && !isNaN(Number(in |
| | | 4381 | | ? Math.round(Number(inputPriceDecimal)) |
| | | 4382 | | : undefined; |
| | | 4383 | | |
| | | 4384 | | const term = offer?.Term ?? offer?.term ?? 0; |
| | | 4385 | | const termInt = term !== null && term !== undefined && !isNaN(parseInt(term, 10)) ? parseInt(term, 10) : |
| | | 4386 | | |
| | | 4387 | | const amount = offer?.Amount ?? offer?.amount ?? 0; |
| | | 4388 | | |
| | | 4389 | | return { |
| | | 4390 | | caption: partDescription || partCodeShort || '', |
| | | 4391 | | rest: amount != null ? String(amount) : '0', |
| | | 4392 | | deliveryDays: termInt ? String(termInt) : '0', |
| | | 4393 | | deliveryDaysMin: termInt, |
| | | 4394 | | deliveryDaysMax: termInt, |
| | | 4395 | | currencyCode: 'RUB', |
| | | 4396 | | producer: producerName, |
| | | 4397 | | Code: partCodeShort, |
| | | 4398 | | priceDecimal: supplierPriceValue, |
| | | 4399 | | costPrice: inputCostValue, |
| | | 4400 | | siteUrl: 'http://studio-api.interparts.ru/', |
| | | 4401 | | title: supplierName || 'Interparts', |
| | | 4402 | | direction: supplierName || 'Interparts', |
| | | 4403 | | itemId: partCodeShort || partCodeShort, |
| | | 4404 | | description: partDescription |
| | | 4405 | | }; |
| | | 4406 | | }); |
| | | 4407 | | } |
| | | 4408 | | |
| | | 4409 | | // Приводим priceDecimal (цена постав.) и costPrice (себест.) к числам; currencyCode/currencycode — одно поле. |
| | | 4410 | | items = (items || []) |
| | | 4411 | | .map(item => { |
| | | 4412 | | if (!item) return item; |
| | | 4413 | | |
| | | 4414 | | let priceDecimal = item.priceDecimal; |
| | | 4415 | | if (priceDecimal === undefined && item.price !== undefined && item.price !== null) { |
| | | 4416 | | // `price` из PreOrderItem — строка, например "100.23" или "100,23" |
| | | 4417 | | const raw = String(item.price).replace(/\s/g, '').replace(',', '.'); |
| | | 4418 | | const parsed = parseFloat(raw); |
| | | 4419 | | if (!isNaN(parsed)) priceDecimal = parsed; |
| | | 4420 | | } |
| | | 4421 | | |
| | | 4422 | | if (priceDecimal !== undefined && priceDecimal !== null && typeof priceDecimal !== 'number') { |
| | | 4423 | | const raw = String(priceDecimal).replace(/\s/g, '').replace(',', '.'); |
| | | 4424 | | const parsed = parseFloat(raw); |
| | | 4425 | | if (!isNaN(parsed)) priceDecimal = parsed; |
| | | 4426 | | } |
| | | 4427 | | |
| | | 4428 | | if (priceDecimal !== undefined && priceDecimal !== null && typeof priceDecimal === 'number') { |
| | | 4429 | | priceDecimal = Math.round(priceDecimal); |
| | | 4430 | | } |
| | | 4431 | | |
| | | 4432 | | let costPrice = item.costPrice ?? item.CostPrice; |
| | | 4433 | | if (costPrice !== undefined && costPrice !== null && costPrice !== '') { |
| | | 4434 | | const rawC = String(costPrice).replace(/\s/g, '').replace(',', '.'); |
| | | 4435 | | const parsedC = parseFloat(rawC); |
| | | 4436 | | costPrice = !isNaN(parsedC) ? Math.round(parsedC) : undefined; |
| | | 4437 | | } else { |
| | | 4438 | | costPrice = undefined; |
| | | 4439 | | } |
| | | 4440 | | |
| | | 4441 | | const costPriceFormatted = item.costPriceFormatted ?? item.CostPriceFormatted; |
| | | 4442 | | const currencyCode = item.currencyCode || item.currencycode; |
| | | 4443 | | |
| | | 4444 | | return {...item, priceDecimal, costPrice, costPriceFormatted, currencyCode}; |
| | | 4445 | | }) |
| | | 4446 | | .filter(item => item && item.priceDecimal !== undefined); |
| | | 4447 | | |
| | | 4448 | | const preferredInterparts = document.querySelector(`.preferred-interparts[data-order-id="${orderId}"]`); |
| | | 4449 | | if (preferredInterparts) { |
| | | 4450 | | preferredInterparts.style.display = items.length > 0 ? 'inline' : 'none'; |
| | | 4451 | | preferredInterparts.title = items.length > 0 |
| | | 4452 | | ? `Найдено ${items.length} предложений в Interparts price API` |
| | | 4453 | | : 'Нет данных в Interparts price API'; |
| | | 4454 | | } |
| | | 4455 | | |
| | | 4456 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4457 | | orderDataCache[orderId].interpartsPriceData = { |
| | | 4458 | | items: items, |
| | | 4459 | | empty: items.length === 0, |
| | | 4460 | | responseTimeMs: Number(response?.responseTimeMs) || null, |
| | | 4461 | | lastUpdated: new Date().toISOString() |
| | | 4462 | | }; |
| | | 4463 | | |
| | | 4464 | | // generateInfoRow.js использует items напрямую, поэтому не вызываем updatePriceComparison здесь. |
| | | 4465 | | } |
| | | 4466 | | |
| | | 4467 | | function processArmtekPriceData(response, orderId) { |
| | | 4468 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4469 | | |
| | | 4470 | | if (response == null) { |
| | | 4471 | | orderDataCache[orderId].armtekPriceData = { |
| | | 4472 | | items: [], |
| | | 4473 | | error: 'Запрос цен Армтека не выполнен (пустой ответ)', |
| | | 4474 | | lastUpdated: new Date().toISOString() |
| | | 4475 | | }; |
| | | 4476 | | return; |
| | | 4477 | | } |
| | | 4478 | | |
| | | 4479 | | if (response && !Array.isArray(response) && response.error) { |
| | | 4480 | | orderDataCache[orderId].armtekPriceData = { |
| | | 4481 | | items: [], |
| | | 4482 | | error: String(response.error), |
| | | 4483 | | lastUpdated: new Date().toISOString() |
| | | 4484 | | }; |
| | | 4485 | | const preferredArmtek = document.querySelector(`.preferred-armtek[data-order-id="${orderId}"]`); |
| | | 4486 | | if (preferredArmtek) { |
| | | 4487 | | preferredArmtek.style.display = 'none'; |
| | | 4488 | | preferredArmtek.title = String(response.error); |
| | | 4489 | | } |
| | | 4490 | | return; |
| | | 4491 | | } |
| | | 4492 | | |
| | | 4493 | | const warning = !Array.isArray(response) ? response?.warning : null; |
| | | 4494 | | const settingsUrl = !Array.isArray(response) ? response?.settingsUrl : null; |
| | | 4495 | | let items = Array.isArray(response) ? response : (response?.items || []); |
| | | 4496 | | |
| | | 4497 | | items = (items || []) |
| | | 4498 | | .map(item => { |
| | | 4499 | | if (!item) return item; |
| | | 4500 | | |
| | | 4501 | | let priceDecimal = item.priceDecimal; |
| | | 4502 | | if (priceDecimal === undefined && item.price !== undefined && item.price !== null) { |
| | | 4503 | | const raw = String(item.price).replace(/\s/g, '').replace(',', '.'); |
| | | 4504 | | const parsed = parseFloat(raw); |
| | | 4505 | | if (!isNaN(parsed)) priceDecimal = parsed; |
| | | 4506 | | } |
| | | 4507 | | if (priceDecimal !== undefined && priceDecimal !== null && typeof priceDecimal !== 'number') { |
| | | 4508 | | const raw = String(priceDecimal).replace(/\s/g, '').replace(',', '.'); |
| | | 4509 | | const parsed = parseFloat(raw); |
| | | 4510 | | if (!isNaN(parsed)) priceDecimal = parsed; |
| | | 4511 | | } |
| | | 4512 | | if (typeof priceDecimal === 'number') { |
| | | 4513 | | priceDecimal = Math.round(priceDecimal); |
| | | 4514 | | } |
| | | 4515 | | |
| | | 4516 | | let costPrice = item.costPrice ?? item.CostPrice; |
| | | 4517 | | if (costPrice !== undefined && costPrice !== null && costPrice !== '') { |
| | | 4518 | | const raw = String(costPrice).replace(/\s/g, '').replace(',', '.'); |
| | | 4519 | | const parsed = parseFloat(raw); |
| | | 4520 | | costPrice = !isNaN(parsed) ? Math.round(parsed) : undefined; |
| | | 4521 | | } else { |
| | | 4522 | | costPrice = undefined; |
| | | 4523 | | } |
| | | 4524 | | |
| | | 4525 | | const costPriceFormatted = item.costPriceFormatted ?? item.CostPriceFormatted; |
| | | 4526 | | const currencyCode = item.currencyCode || item.currencycode; |
| | | 4527 | | |
| | | 4528 | | return {...item, priceDecimal, costPrice, costPriceFormatted, currencyCode}; |
| | | 4529 | | }) |
| | | 4530 | | .filter(item => item && item.priceDecimal !== undefined); |
| | | 4531 | | |
| | | 4532 | | orderDataCache[orderId].armtekPriceData = { |
| | | 4533 | | items: items, |
| | | 4534 | | empty: items.length === 0, |
| | | 4535 | | warning: warning ? String(warning) : null, |
| | | 4536 | | settingsUrl: settingsUrl ? String(settingsUrl) : null, |
| | | 4537 | | lastUpdated: new Date().toISOString() |
| | | 4538 | | }; |
| | | 4539 | | |
| | | 4540 | | const preferredArmtek = document.querySelector(`.preferred-armtek[data-order-id="${orderId}"]`); |
| | | 4541 | | if (preferredArmtek) { |
| | | 4542 | | preferredArmtek.style.display = items.length > 0 ? 'inline' : 'none'; |
| | | 4543 | | preferredArmtek.title = items.length > 0 |
| | | 4544 | | ? `Найдено ${items.length} предложений в API Армтека` |
| | | 4545 | | : 'Нет данных в API Армтека'; |
| | | 4546 | | } |
| | | 4547 | | } |
| | | 4548 | | |
| | | 4549 | | function processArmtekByPriceData(response, orderId) { |
| | | 4550 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4551 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4552 | | orderDataCache[orderId].armtekByPriceData = { items: [], error: response?.error || 'Запрос цен Армтек BY не |
| | | 4553 | | return; |
| | | 4554 | | } |
| | | 4555 | | const warning = !Array.isArray(response) ? response?.warning : null; |
| | | 4556 | | const settingsUrl = !Array.isArray(response) ? response?.settingsUrl : null; |
| | | 4557 | | const items = (Array.isArray(response) ? response : (response?.items || [])).map(item => { |
| | | 4558 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4559 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4560 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4561 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4562 | | orderDataCache[orderId].armtekByPriceData = { items: items, empty: items.length === 0, warning: warning ? String |
| | | 4563 | | } |
| | | 4564 | | |
| | | 4565 | | function processShateMPriceData(response, orderId) { |
| | | 4566 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4567 | | |
| | | 4568 | | if (response == null) { |
| | | 4569 | | orderDataCache[orderId].shateMPriceData = { |
| | | 4570 | | items: [], |
| | | 4571 | | error: 'Запрос цен Шате‑М не выполнен (пустой ответ)', |
| | | 4572 | | lastUpdated: new Date().toISOString() |
| | | 4573 | | }; |
| | | 4574 | | return; |
| | | 4575 | | } |
| | | 4576 | | |
| | | 4577 | | if (!Array.isArray(response) && response.error) { |
| | | 4578 | | orderDataCache[orderId].shateMPriceData = { |
| | | 4579 | | items: [], |
| | | 4580 | | error: String(response.error), |
| | | 4581 | | lastUpdated: new Date().toISOString() |
| | | 4582 | | }; |
| | | 4583 | | return; |
| | | 4584 | | } |
| | | 4585 | | |
| | | 4586 | | const items = (Array.isArray(response) ? response : []) |
| | | 4587 | | .map(item => { |
| | | 4588 | | if (!item) return item; |
| | | 4589 | | const priceRaw = item.priceDecimal ?? item.price; |
| | | 4590 | | const priceDecimal = Number.parseFloat(String(priceRaw ?? '').replace(/\s/g, '').replace(',', '.')); |
| | | 4591 | | const costRaw = item.costPrice ?? item.CostPrice ?? priceDecimal; |
| | | 4592 | | const costPrice = Number.parseFloat(String(costRaw ?? '').replace(/\s/g, '').replace(',', '.')); |
| | | 4593 | | return { |
| | | 4594 | | ...item, |
| | | 4595 | | priceDecimal: Number.isFinite(priceDecimal) ? Math.round(priceDecimal) : undefined, |
| | | 4596 | | costPrice: Number.isFinite(costPrice) ? Math.round(costPrice) : undefined, |
| | | 4597 | | costPriceFormatted: item.costPriceFormatted ?? item.CostPriceFormatted, |
| | | 4598 | | currencyCode: item.currencyCode || item.CurrencyCode || 'RUB' |
| | | 4599 | | }; |
| | | 4600 | | }) |
| | | 4601 | | .filter(item => item && item.priceDecimal !== undefined); |
| | | 4602 | | |
| | | 4603 | | orderDataCache[orderId].shateMPriceData = { |
| | | 4604 | | items: items, |
| | | 4605 | | empty: items.length === 0, |
| | | 4606 | | lastUpdated: new Date().toISOString() |
| | | 4607 | | }; |
| | | 4608 | | } |
| | | 4609 | | |
| | | 4610 | | function processMotexPriceData(response, orderId) { |
| | | 4611 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4612 | | |
| | | 4613 | | if (response == null) { |
| | | 4614 | | orderDataCache[orderId].motexPriceData = { |
| | | 4615 | | items: [], |
| | | 4616 | | error: 'Запрос цен Motex не выполнен (пустой ответ)', |
| | | 4617 | | lastUpdated: new Date().toISOString() |
| | | 4618 | | }; |
| | | 4619 | | return; |
| | | 4620 | | } |
| | | 4621 | | |
| | | 4622 | | if (!Array.isArray(response) && response.error) { |
| | | 4623 | | orderDataCache[orderId].motexPriceData = { |
| | | 4624 | | items: [], |
| | | 4625 | | error: String(response.error), |
| | | 4626 | | lastUpdated: new Date().toISOString() |
| | | 4627 | | }; |
| | | 4628 | | return; |
| | | 4629 | | } |
| | | 4630 | | |
| | | 4631 | | const items = (Array.isArray(response) ? response : []) |
| | | 4632 | | .map(item => { |
| | | 4633 | | if (!item) return item; |
| | | 4634 | | const priceRaw = item.priceDecimal ?? item.price; |
| | | 4635 | | const priceDecimal = Number.parseFloat(String(priceRaw ?? '').replace(/\s/g, '').replace(',', '.')); |
| | | 4636 | | const costRaw = item.costPrice ?? item.CostPrice ?? priceDecimal; |
| | | 4637 | | const costPrice = Number.parseFloat(String(costRaw ?? '').replace(/\s/g, '').replace(',', '.')); |
| | | 4638 | | return { |
| | | 4639 | | ...item, |
| | | 4640 | | priceDecimal: Number.isFinite(priceDecimal) ? Math.round(priceDecimal) : undefined, |
| | | 4641 | | costPrice: Number.isFinite(costPrice) ? Math.round(costPrice) : undefined, |
| | | 4642 | | costPriceFormatted: item.costPriceFormatted ?? item.CostPriceFormatted, |
| | | 4643 | | currencyCode: item.currencyCode || item.CurrencyCode || 'RUB' |
| | | 4644 | | }; |
| | | 4645 | | }) |
| | | 4646 | | .filter(item => item && item.priceDecimal !== undefined); |
| | | 4647 | | |
| | | 4648 | | orderDataCache[orderId].motexPriceData = { |
| | | 4649 | | items: items, |
| | | 4650 | | empty: items.length === 0, |
| | | 4651 | | lastUpdated: new Date().toISOString() |
| | | 4652 | | }; |
| | | 4653 | | } |
| | | 4654 | | |
| | | 4655 | | function processMlAutoPriceData(response, orderId) { |
| | | 4656 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4657 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4658 | | orderDataCache[orderId].mlAutoPriceData = { items: [], error: response?.error || 'Запрос цен ML-Auto не выпо |
| | | 4659 | | return; |
| | | 4660 | | } |
| | | 4661 | | const sourceItems = Array.isArray(response) ? response : (Array.isArray(response?.items) ? response.items : []); |
| | | 4662 | | const items = sourceItems.map(item => { |
| | | 4663 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4664 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4665 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4666 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4667 | | orderDataCache[orderId].mlAutoPriceData = { items: items, empty: items.length === 0, warning: response?.warning, |
| | | 4668 | | } |
| | | 4669 | | |
| | | 4670 | | function processMlAutoRuPriceData(response, orderId) { |
| | | 4671 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4672 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4673 | | orderDataCache[orderId].mlAutoRuPriceData = { items: [], error: response?.error || 'Запрос цен ML-Auto RU не |
| | | 4674 | | return; |
| | | 4675 | | } |
| | | 4676 | | const sourceItems = Array.isArray(response) ? response : (Array.isArray(response?.items) ? response.items : []); |
| | | 4677 | | const items = sourceItems.map(item => { |
| | | 4678 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4679 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4680 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4681 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4682 | | orderDataCache[orderId].mlAutoRuPriceData = { items: items, empty: items.length === 0, warning: response?.warnin |
| | | 4683 | | } |
| | | 4684 | | |
| | | 4685 | | function processAvdPriceData(response, orderId) { |
| | | 4686 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4687 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4688 | | orderDataCache[orderId].avdPriceData = { items: [], error: response?.error || 'Запрос цен АВД не выполнен', |
| | | 4689 | | return; |
| | | 4690 | | } |
| | | 4691 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4692 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4693 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4694 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4695 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4696 | | orderDataCache[orderId].avdPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date().toISOS |
| | | 4697 | | } |
| | | 4698 | | |
| | | 4699 | | function processBergPriceData(response, orderId) { |
| | | 4700 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4701 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4702 | | orderDataCache[orderId].bergPriceData = { items: [], error: response?.error || 'Запрос цен Berg не выполнен' |
| | | 4703 | | return; |
| | | 4704 | | } |
| | | 4705 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4706 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4707 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4708 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4709 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4710 | | orderDataCache[orderId].bergPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date().toISO |
| | | 4711 | | } |
| | | 4712 | | |
| | | 4713 | | function processJapartsPriceData(response, orderId) { |
| | | 4714 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4715 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4716 | | orderDataCache[orderId].japartsPriceData = { items: [], error: response?.error || 'Запрос цен Japarts не вып |
| | | 4717 | | return; |
| | | 4718 | | } |
| | | 4719 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4720 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4721 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4722 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4723 | | orderDataCache[orderId].japartsPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date().to |
| | | 4724 | | } |
| | | 4725 | | |
| | | 4726 | | function processForumAutoPriceData(response, orderId) { |
| | | 4727 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4728 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4729 | | orderDataCache[orderId].forumAutoPriceData = { items: [], error: response?.error || 'Запрос цен Forum-Auto н |
| | | 4730 | | return; |
| | | 4731 | | } |
| | | 4732 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4733 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4734 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4735 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4736 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4737 | | orderDataCache[orderId].forumAutoPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date(). |
| | | 4738 | | } |
| | | 4739 | | |
| | | 4740 | | function processForumAutoInterpartsPriceData(response, orderId) { |
| | | 4741 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4742 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4743 | | orderDataCache[orderId].forumAutoInterpartsPriceData = { items: [], error: response?.error || 'Запрос цен Fo |
| | | 4744 | | return; |
| | | 4745 | | } |
| | | 4746 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4747 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4748 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4749 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4750 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4751 | | orderDataCache[orderId].forumAutoInterpartsPriceData = { items: items, empty: items.length === 0, lastUpdated: n |
| | | 4752 | | } |
| | | 4753 | | |
| | | 4754 | | function processForumAutoPiterPriceData(response, orderId) { |
| | | 4755 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4756 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4757 | | orderDataCache[orderId].forumAutoPiterPriceData = { items: [], error: response?.error || 'Запрос цен Forum-A |
| | | 4758 | | return; |
| | | 4759 | | } |
| | | 4760 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4761 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4762 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4763 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4764 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4765 | | orderDataCache[orderId].forumAutoPiterPriceData = { items: items, empty: items.length === 0, lastUpdated: new Da |
| | | 4766 | | } |
| | | 4767 | | |
| | | 4768 | | function processForumAutoRostovPriceData(response, orderId) { |
| | | 4769 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4770 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4771 | | orderDataCache[orderId].forumAutoRostovPriceData = { items: [], error: response?.error || 'Запрос цен Forum- |
| | | 4772 | | return; |
| | | 4773 | | } |
| | | 4774 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4775 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4776 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4777 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4778 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4779 | | orderDataCache[orderId].forumAutoRostovPriceData = { items: items, empty: items.length === 0, lastUpdated: new D |
| | | 4780 | | } |
| | | 4781 | | |
| | | 4782 | | function processForumAutoIstraPriceData(response, orderId) { |
| | | 4783 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4784 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4785 | | orderDataCache[orderId].forumAutoIstraPriceData = { items: [], error: response?.error || 'Запрос цен Forum-A |
| | | 4786 | | return; |
| | | 4787 | | } |
| | | 4788 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4789 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4790 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4791 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4792 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4793 | | orderDataCache[orderId].forumAutoIstraPriceData = { items: items, empty: items.length === 0, lastUpdated: new Da |
| | | 4794 | | } |
| | | 4795 | | |
| | | 4796 | | function processFavoritPartsPriceData(response, orderId) { |
| | | 4797 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4798 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4799 | | orderDataCache[orderId].favoritPartsPriceData = { items: [], error: response?.error || 'Запрос цен FavoritPa |
| | | 4800 | | return; |
| | | 4801 | | } |
| | | 4802 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4803 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4804 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4805 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4806 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4807 | | orderDataCache[orderId].favoritPartsPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date |
| | | 4808 | | } |
| | | 4809 | | |
| | | 4810 | | function processFavoritPartsIstraPriceData(response, orderId) { |
| | | 4811 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4812 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4813 | | orderDataCache[orderId].favoritPartsIstraPriceData = { items: [], error: response?.error || 'Запрос цен Фаво |
| | | 4814 | | return; |
| | | 4815 | | } |
| | | 4816 | | const items = (Array.isArray(response) ? response : []).map(item => ({ ...item, priceDecimal: Number(item.price) |
| | | 4817 | | orderDataCache[orderId].favoritPartsIstraPriceData = { items: items, empty: items.length === 0, lastUpdated: new |
| | | 4818 | | } |
| | | 4819 | | |
| | | 4820 | | function processFavoritPartsRostovPriceData(response, orderId) { |
| | | 4821 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4822 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4823 | | orderDataCache[orderId].favoritPartsRostovPriceData = { items: [], error: response?.error || 'Запрос цен Фав |
| | | 4824 | | return; |
| | | 4825 | | } |
| | | 4826 | | const items = (Array.isArray(response) ? response : []).map(item => ({ ...item, priceDecimal: Number(item.price) |
| | | 4827 | | orderDataCache[orderId].favoritPartsRostovPriceData = { items: items, empty: items.length === 0, lastUpdated: ne |
| | | 4828 | | } |
| | | 4829 | | |
| | | 4830 | | function processProfitLigaPriceData(response, orderId) { |
| | | 4831 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4832 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4833 | | orderDataCache[orderId].profitLigaPriceData = { items: [], error: response?.error || 'Запрос цен Профит-Лиги |
| | | 4834 | | return; |
| | | 4835 | | } |
| | | 4836 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4837 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4838 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4839 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4840 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4841 | | orderDataCache[orderId].profitLigaPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date() |
| | | 4842 | | } |
| | | 4843 | | |
| | | 4844 | | function processNikeiPriceData(response, orderId) { |
| | | 4845 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4846 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4847 | | orderDataCache[orderId].nikeiPriceData = { items: [], error: response?.error || 'Запрос цен Nikei не выполне |
| | | 4848 | | return; |
| | | 4849 | | } |
| | | 4850 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4851 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4852 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4853 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4854 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4855 | | orderDataCache[orderId].nikeiPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date().toIS |
| | | 4856 | | } |
| | | 4857 | | |
| | | 4858 | | function processMikadoMskHod20PriceData(response, orderId) { |
| | | 4859 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4860 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4861 | | orderDataCache[orderId].mikadoMskHod20PriceData = { items: [], error: response?.error || 'Запрос цен Микадо |
| | | 4862 | | return; |
| | | 4863 | | } |
| | | 4864 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4865 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4866 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4867 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4868 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4869 | | orderDataCache[orderId].mikadoMskHod20PriceData = { items: items, empty: items.length === 0, lastUpdated: new Da |
| | | 4870 | | } |
| | | 4871 | | |
| | | 4872 | | function processZZapMoscowPriceData(response, orderId) { |
| | | 4873 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4874 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4875 | | orderDataCache[orderId].zzapMoscowPriceData = { items: [], error: response?.error || 'Запрос цен ZZap не вып |
| | | 4876 | | return; |
| | | 4877 | | } |
| | | 4878 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4879 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4880 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4881 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4882 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4883 | | orderDataCache[orderId].zzapMoscowPriceData = { items: items, empty: items.length === 0, lastUpdated: new Date() |
| | | 4884 | | } |
| | | 4885 | | |
| | | 4886 | | function processMoskvorechieIstraPriceData(response, orderId) { |
| | | 4887 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4888 | | if (response == null || (!Array.isArray(response) && response.error)) { |
| | | 4889 | | orderDataCache[orderId].moskvorechieIstraPriceData = { items: [], error: response?.error || 'Запрос цен Моск |
| | | 4890 | | return; |
| | | 4891 | | } |
| | | 4892 | | const items = (Array.isArray(response) ? response : []).map(item => { |
| | | 4893 | | const price = Number.parseFloat(String(item.priceDecimal ?? item.price ?? '').replace(/\s/g, '').replace(',' |
| | | 4894 | | const cost = Number.parseFloat(String(item.costPrice ?? item.CostPrice ?? price).replace(/\s/g, '').replace( |
| | | 4895 | | return { ...item, priceDecimal: Number.isFinite(price) ? Math.round(price) : undefined, costPrice: Number.is |
| | | 4896 | | }).filter(item => item && item.priceDecimal !== undefined); |
| | | 4897 | | orderDataCache[orderId].moskvorechieIstraPriceData = { items: items, empty: items.length === 0, lastUpdated: new |
| | | 4898 | | } |
| | | 4899 | | |
| | | 4900 | | function processYumboData(yumboResponse, orderId) { |
| | | 4901 | | const yumboData = Array.isArray(yumboResponse) ? yumboResponse : []; |
| | | 4902 | | |
| | | 4903 | | const preferred_yumbo = document.querySelector(`.preferred-yumbo[data-order-id="${orderId}"]`); |
| | | 4904 | | if (preferred_yumbo) { |
| | | 4905 | | preferred_yumbo.style.display = yumboData.length > 0 ? 'inline' : 'none'; |
| | | 4906 | | } |
| | | 4907 | | |
| | | 4908 | | if (!orderDataCache[orderId]) orderDataCache[orderId] = {}; |
| | | 4909 | | orderDataCache[orderId].yumboData = { |
| | | 4910 | | yumboData |
| | | 4911 | | }; |
| | | 4912 | | } |
| | | 4913 | | |
| | | 4914 | | |
| | | 4915 | | function openImage(imageSrc) { |
| | | 4916 | | const modalImage = document.getElementById('modalImage'); |
| | | 4917 | | if (modalImage) { |
| | | 4918 | | modalImage.src = imageSrc; |
| | | 4919 | | modalImage.style.width = '530px'; // Увеличиваем в 2 раза (265 * 2) |
| | | 4920 | | modalImage.style.height = '530px'; |
| | | 4921 | | const modal = new bootstrap.Modal(document.getElementById('imageModal')); |
| | | 4922 | | modal.show(); |
| | | 4923 | | } else { |
| | | 4924 | | console.error("Элемент modalImage не найден."); |
| | | 4925 | | } |
| | | 4926 | | } |
| | | 4927 | | |
| | | 4928 | | function formatPrice(price) { |
| | | 4929 | | if (price == null) return ''; |
| | | 4930 | | // Remove decimals by converting the price to an integer |
| | | 4931 | | let formattedPrice = Math.floor(price).toLocaleString('ru-RU'); |
| | | 4932 | | // Replace commas with spaces for the thousands separator |
| | | 4933 | | return formattedPrice.replace(",", " "); |
| | | 4934 | | } |
| | | 4935 | | |
| | | 4936 | | function handleRowClick(index) { |
| | | 4937 | | const supplier = response.suppliersDetailedInformation[index]; |
| | | 4938 | | alert('Вы выбрали поставщика: ' + supplier.supplierName); |
| | | 4939 | | // Здесь можно выполнить любую другую функцию по вашему требованию |
| | | 4940 | | } |
| | | 4941 | | |
| | | 4942 | | // Функция для выделения строки и конкретного элемента |
| | | 4943 | | function highlightRow(index, element) { |
| | | 4944 | | const table = element.closest('table'); // Найти таблицу, в которой находится элемент |
| | | 4945 | | const orderId = element.getAttribute('data-order-id'); // Получить data-order-id текущего элемента |
| | | 4946 | | |
| | | 4947 | | // Найти все элементы с данным индексом и data-order-id внутри одной таблицы |
| | | 4948 | | const elements = table.querySelectorAll(`[data-index="${index}"][data-order-id="${orderId}"]`); |
| | | 4949 | | |
| | | 4950 | | // Добавить класс 'highlighted' для всех найденных элементов |
| | | 4951 | | elements.forEach(el => { |
| | | 4952 | | el.classList.add('highlighted'); |
| | | 4953 | | }); |
| | | 4954 | | |
| | | 4955 | | // Выделяем текущий элемент отдельно |
| | | 4956 | | element.classList.add('highlighted-item'); |
| | | 4957 | | } |
| | | 4958 | | |
| | | 4959 | | function setWight(element) { |
| | | 4960 | | const orderId = element.getAttribute('data-order-id'); |
| | | 4961 | | const blockValue = element.textContent.trim(); |
| | | 4962 | | |
| | | 4963 | | var productWeight = document.getElementById(`ProductWeight_${orderId}`); |
| | | 4964 | | const productWeightValue = parseFloat(blockValue); |
| | | 4965 | | if (!isNaN(productWeightValue)) { |
| | | 4966 | | productWeight.value = productWeightValue; |
| | | 4967 | | } |
| | | 4968 | | |
| | | 4969 | | var event = new Event('input', {bubbles: true, cancelable: true}); |
| | | 4970 | | productWeight.dispatchEvent(event); |
| | | 4971 | | } |
| | | 4972 | | |
| | | 4973 | | function setSupplierPrice(element) { |
| | | 4974 | | const orderId = element.getAttribute('data-order-id'); |
| | | 4975 | | const blockValue = element.textContent.trim(); |
| | | 4976 | | |
| | | 4977 | | var purchasePrice = document.getElementById(`PurchasePrice_${orderId}`); |
| | | 4978 | | const purchasePriceValue = parseFloat(blockValue.replace(/[^0-9.-]+/g, '')); |
| | | 4979 | | if (!isNaN(purchasePriceValue)) { |
| | | 4980 | | purchasePrice.value = purchasePriceValue; |
| | | 4981 | | } |
| | | 4982 | | |
| | | 4983 | | var event = new Event('input', {bubbles: true, cancelable: true}); |
| | | 4984 | | purchasePrice.dispatchEvent(event); |
| | | 4985 | | } |
| | | 4986 | | |
| | | 4987 | | // Функция для снятия выделения |
| | | 4988 | | function removeHighlight(index, element) { |
| | | 4989 | | const table = element.closest('table'); // Найти таблицу, в которой находится элемент |
| | | 4990 | | const orderId = element.getAttribute('data-order-id'); |
| | | 4991 | | |
| | | 4992 | | // Найти все элементы с данным индексом и data-order-id внутри одной таблицы |
| | | 4993 | | const elements = table.querySelectorAll(`[data-index="${index}"][data-order-id="${orderId}"]`); |
| | | 4994 | | |
| | | 4995 | | // Удалить класс 'highlighted' у всех найденных элементов |
| | | 4996 | | elements.forEach(el => { |
| | | 4997 | | el.classList.remove('highlighted', 'highlighted-item'); |
| | | 4998 | | }); |
| | | 4999 | | } |
| | | 5000 | | |
| | | 5001 | | </script> |
| | | 5002 | | |
| | | 5003 | | <!-- Модальное окно Bootstrap --> |
| | | 5004 | | <div class="modal fade" id="substituteModal" tabindex="-1" aria-labelledby="substituteModalLabel" aria-hidden="true"> |
| | | 5005 | | <div class="modal-dialog modal-lg"> |
| | | 5006 | | <div class="modal-content"> |
| | | 5007 | | <div class="modal-header"> |
| | | 5008 | | <h5 class="modal-title" id="substituteModalLabel">Применяемость</h5> |
| | | 5009 | | <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> |
| | | 5010 | | </div> |
| | | 5011 | | <div class="modal-body"> |
| | | 5012 | | <!-- Контейнер для динамического вывода таблицы --> |
| | | 5013 | | <div id="substituteTableContainer"> |
| | | 5014 | | <!-- Данные будут добавлены через JavaScript --> |
| | | 5015 | | </div> |
| | | 5016 | | </div> |
| | | 5017 | | <div class="modal-footer"> |
| | | 5018 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button> |
| | | 5019 | | </div> |
| | | 5020 | | </div> |
| | | 5021 | | </div> |
| | | 5022 | | </div> |
| | | 5023 | | |
| | | 5024 | | <script> |
| | | 5025 | | const params = new URLSearchParams(window.location.search); |
| | | 5026 | | const ids = params.get("ids"); |
| | | 5027 | | console.log(ids); |
| | | 5028 | | |
| | | 5029 | | $(document).ready(function () { |
| | | 5030 | | // Кэш для данных |
| | | 5031 | | const substituteCache = new Map(); |
| | | 5032 | | |
| | | 5033 | | // Обработчик клика по ссылке |
| | | 5034 | | $(document).on("click", ".getSubstituteLink", function (e) { |
| | | 5035 | | e.preventDefault(); // Предотвращаем переход по ссылке |
| | | 5036 | | |
| | | 5037 | | const orderId = $(this).data("id"); // Получаем ID заказа из data-атрибута |
| | | 5038 | | const linkElement = $(this); // Сохраняем ссылку для управления |
| | | 5039 | | |
| | | 5040 | | if (!orderId) { |
| | | 5041 | | alert("ID заказа не указан."); |
| | | 5042 | | return; |
| | | 5043 | | } |
| | | 5044 | | |
| | | 5045 | | // Деактивируем ссылку |
| | | 5046 | | linkElement.addClass("disabled").prop("disabled", true); |
| | | 5047 | | |
| | | 5048 | | // Проверяем наличие данных в кэше |
| | | 5049 | | if (substituteCache.has(orderId)) { |
| | | 5050 | | const cachedData = substituteCache.get(orderId); |
| | | 5051 | | displaySubstituteData(cachedData); |
| | | 5052 | | // Активируем ссылку обратно |
| | | 5053 | | linkElement.removeClass("disabled").prop("disabled", false); |
| | | 5054 | | return; |
| | | 5055 | | } |
| | | 5056 | | |
| | | 5057 | | // Используем единый overlay, чтобы не смешивать старые и новые лоадеры |
| | | 5058 | | const loader = document.getElementById('txSubmitOverlay'); |
| | | 5059 | | if (loader) loader.style.display = 'flex'; |
| | | 5060 | | |
| | | 5061 | | $.ajax({ |
| | | 5062 | | url: `/Studio2/Orders/OrderSubstitute`, // Укажите имя контроллера |
| | | 5063 | | type: "GET", |
| | | 5064 | | data: {orderId: orderId}, // Передаем orderId в качестве параметра |
| | | 5065 | | success: function (response) { |
| | | 5066 | | if (response && response.substituteSchema && response.substituteSchema.length > 0) { |
| | | 5067 | | substituteCache.set(orderId, response); // Сохраняем данные в кэш |
| | | 5068 | | displaySubstituteData(response); |
| | | 5069 | | } else { |
| | | 5070 | | showToast("Применяемость не найдена") |
| | | 5071 | | } |
| | | 5072 | | }, |
| | | 5073 | | error: function (xhr, status, error) { |
| | | 5074 | | console.error("Ошибка:", error); |
| | | 5075 | | showFriendlyRequestError(xhr, status, error, "Ошибка при получении данных о заменах"); |
| | | 5076 | | }, |
| | | 5077 | | complete: function () { |
| | | 5078 | | if (loader) loader.style.display = 'none'; |
| | | 5079 | | linkElement.removeClass("disabled").prop("disabled", false); |
| | | 5080 | | } |
| | | 5081 | | }); |
| | | 5082 | | }); |
| | | 5083 | | |
| | | 5084 | | // Функция отображения данных |
| | | 5085 | | function displaySubstituteData(data) { |
| | | 5086 | | const modal = new bootstrap.Modal($('#substituteModal')); |
| | | 5087 | | modal.show(); |
| | | 5088 | | |
| | | 5089 | | // Очищаем контейнер перед добавлением новых данных |
| | | 5090 | | $('#substituteTableContainer').empty(); |
| | | 5091 | | |
| | | 5092 | | let htmlContent = `<table class="table table-bordered"> |
| | | 5093 | | <thead> |
| | | 5094 | | <tr> |
| | | 5095 | | <th>Тип</th> |
| | | 5096 | | <th>Kатегории</th> |
| | | 5097 | | <th>Полное название модификации</th> |
| | | 5098 | | </tr> |
| | | 5099 | | </thead> |
| | | 5100 | | <tbody>`; |
| | | 5101 | | |
| | | 5102 | | data.substituteSchema.forEach(substitute => { |
| | | 5103 | | htmlContent += `<tr> |
| | | 5104 | | <td>${substitute.type || ''}</td> |
| | | 5105 | | <td>${substitute.name || ''}</td> |
| | | 5106 | | <td>${substitute.modification?.description || ''} ${substitute.modification?.constructionInterval || |
| | | 5107 | | </tr>`; |
| | | 5108 | | |
| | | 5109 | | if (Array.isArray(substitute.attributes) && substitute.attributes.length > 0) { |
| | | 5110 | | const uniqueId = 'attributes-' + Math.random().toString(36).substr(2, 9); // Генерация уникального I |
| | | 5111 | | |
| | | 5112 | | htmlContent += `<tr id="${uniqueId}"> |
| | | 5113 | | <td colspan="3"> |
| | | 5114 | | <table class="table table-sm table-striped"> |
| | | 5115 | | <thead> |
| | | 5116 | | <tr> |
| | | 5117 | | <th></th> |
| | | 5118 | | <th></th> |
| | | 5119 | | </tr> |
| | | 5120 | | </thead> |
| | | 5121 | | <tbody>`; |
| | | 5122 | | |
| | | 5123 | | // Добавляем первые три атрибута |
| | | 5124 | | substitute.attributes.slice(0, 3).forEach(attribute => { |
| | | 5125 | | htmlContent += `<tr> |
| | | 5126 | | <td>${attribute.title || ''}</td> |
| | | 5127 | | <td>${attribute.value || ''}</td> |
| | | 5128 | | </tr>`; |
| | | 5129 | | }); |
| | | 5130 | | |
| | | 5131 | | // Добавляем скрытые атрибуты (начально скрыты) |
| | | 5132 | | if (substitute.attributes.length > 3) { |
| | | 5133 | | htmlContent += `<tr class="additional-attributes" id="additional-${uniqueId}" style="display:non |
| | | 5134 | | <td colspan="2"> |
| | | 5135 | | <table class="table table-sm table-striped">`; |
| | | 5136 | | |
| | | 5137 | | substitute.attributes.slice(3).forEach(attribute => { |
| | | 5138 | | htmlContent += `<tr> |
| | | 5139 | | <td>${attribute.title || ''}</td> |
| | | 5140 | | <td>${attribute.value || ''}</td> |
| | | 5141 | | </tr>`; |
| | | 5142 | | }); |
| | | 5143 | | |
| | | 5144 | | htmlContent += `</table></td></tr>`; |
| | | 5145 | | } |
| | | 5146 | | |
| | | 5147 | | // Кнопка для отображения скрытых атрибутов |
| | | 5148 | | if (substitute.attributes.length > 3) { |
| | | 5149 | | htmlContent += `<tr> |
| | | 5150 | | <td colspan="3"> |
| | | 5151 | | <button class="btn btn-link" id="toggle-btn-${uniqueId}" onclick="toggleAttributes(' |
| | | 5152 | | </td> |
| | | 5153 | | </tr>`; |
| | | 5154 | | } |
| | | 5155 | | |
| | | 5156 | | htmlContent += `</tbody></table> |
| | | 5157 | | </td> |
| | | 5158 | | </tr>`; |
| | | 5159 | | } |
| | | 5160 | | }); |
| | | 5161 | | |
| | | 5162 | | htmlContent += `</tbody></table>`; |
| | | 5163 | | |
| | | 5164 | | $('#substituteTableContainer').html(htmlContent); |
| | | 5165 | | } |
| | | 5166 | | }); |
| | | 5167 | | |
| | | 5168 | | function toggleAttributes(uniqueId) { |
| | | 5169 | | const additionalAttributes = document.querySelector(`#additional-${uniqueId}`); |
| | | 5170 | | const button = document.querySelector(`#toggle-btn-${uniqueId}`); |
| | | 5171 | | |
| | | 5172 | | // Переключаем видимость скрытых атрибутов |
| | | 5173 | | if (additionalAttributes) { |
| | | 5174 | | additionalAttributes.style.display = (additionalAttributes.style.display === 'none') ? '' : 'none'; |
| | | 5175 | | } |
| | | 5176 | | |
| | | 5177 | | // Изменяем текст кнопки в зависимости от состояния |
| | | 5178 | | if (button) { |
| | | 5179 | | button.textContent = (button.textContent === 'Все') ? 'Скрыть' : 'Все'; |
| | | 5180 | | } |
| | | 5181 | | } |
| | | 5182 | | |
| | | 5183 | | function showToast(message) { |
| | | 5184 | | const toastBody = document.getElementById('flashMessageText'); |
| | | 5185 | | toastBody.textContent = message; |
| | | 5186 | | const toast = new bootstrap.Toast(document.getElementById('flashMessage')); |
| | | 5187 | | toast.show(); |
| | | 5188 | | } |
| | | 5189 | | |
| | | 5190 | | // Корзина живёт только в рамках текущей загрузки страницы (сворачивание/разворачивание строки — ок, |
| | | 5191 | | // перезагрузка страницы или переход на другую — состояние обнуляется). sessionStorage для этого не годится, |
| | | 5192 | | // он переживает reload — поэтому простое in-memory хранилище с тем же API. |
| | | 5193 | | const inMemoryCartStorage = (() => { |
| | | 5194 | | const store = {}; |
| | | 5195 | | return { |
| | | 5196 | | getItem: key => Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null, |
| | | 5197 | | setItem: (key, value) => { store[key] = value; }, |
| | | 5198 | | removeItem: key => { delete store[key]; } |
| | | 5199 | | }; |
| | | 5200 | | })(); |
| | | 5201 | | |
| | | 5202 | | function cartIconSvg(checked) { |
| | | 5203 | | return checked |
| | | 5204 | | ? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cart-chec |
| | | 5205 | | <path d="M11.354 6.354a.5 .5 0 0 0-.708-.708L8 8.293 6.854 7.146a.5 .5 0 1 0-.708.708l1.5 1.5a.5 .5 0 |
| | | 5206 | | <path d="M.5 1a.5 .5 0 0 0 0 1h1.11l.401 1.607 1.498 7.985A.5 .5 0 0 0 4 12h1a2 2 0 1 0 0 4 |
| | | 5207 | | 2 2 0 0 0 0-4h7a2 2 0 1 0 0 4 2 2 0 0 0 0-4h1a.5 .5 0 0 0 .491-.408l1.5-8A.5 .5 0 0 0 |
| | | 5208 | | 14.5 3H2.89l-.405-1.621A.5 .5 0 0 0 2 1zm3.915 10L3.102 4h10.796l-1.313 7zM6 |
| | | 5209 | | 14a1 1 0 1 1-2 0 1 1 0 0 1 2 0m7 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/> |
| | | 5210 | | </svg>` |
| | | 5211 | | : `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-cart" vie |
| | | 5212 | | <path d="M0 1.5A.5 .5 0 0 1 .5 1H2a.5 .5 0 0 1 .485.379L2.89 3H14.5a.5 .5 0 0 1 |
| | | 5213 | | .491.592l-1.5 8A.5 .5 0 0 1 13 12H4a.5 .5 0 0 1-.491-.408L2.01 3.607 |
| | | 5214 | | 1.61 2H.5a.5 .5 0 0 1-.5-.5M3.102 4l1.313 7h8.17l1.313-7zM5 |
| | | 5215 | | 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4m7 0a2 2 0 1 0 0 4 |
| | | 5216 | | 2 2 0 0 0 0-4m-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2m7 |
| | | 5217 | | 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/> |
| | | 5218 | | </svg>`; |
| | | 5219 | | } |
| | | 5220 | | |
| | | 5221 | | /** Единая точка визуального состояния кнопки корзины — иконка + зелёный цвет при добавлении. */ |
| | | 5222 | | function setCartButtonState($btn, checked) { |
| | | 5223 | | $btn.find('svg').replaceWith(cartIconSvg(checked)); |
| | | 5224 | | $btn.toggleClass('checked', checked); |
| | | 5225 | | $btn.toggleClass('text-success', checked); |
| | | 5226 | | } |
| | | 5227 | | |
| | | 5228 | | /** Подсветка строки предложения зелёным — привязана к тому же состоянию "в корзине", что и кнопка. */ |
| | | 5229 | | function highlightRow(orderId, rowKey, tableRow) { |
| | | 5230 | | window.highlightedItem = window.highlightedItem || {}; |
| | | 5231 | | const previousRowKey = window.highlightedItem[orderId] || null; |
| | | 5232 | | if (previousRowKey && previousRowKey !== rowKey) { |
| | | 5233 | | const prevRow = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${previousRowKey}"]`); |
| | | 5234 | | if (prevRow) prevRow.classList.remove('tx-trade-row--selected'); |
| | | 5235 | | } |
| | | 5236 | | if (tableRow) tableRow.classList.add('tx-trade-row--selected'); |
| | | 5237 | | window.highlightedItem[orderId] = rowKey; |
| | | 5238 | | } |
| | | 5239 | | |
| | | 5240 | | function clearRowHighlight(orderId) { |
| | | 5241 | | window.highlightedItem = window.highlightedItem || {}; |
| | | 5242 | | const rowKey = window.highlightedItem[orderId]; |
| | | 5243 | | if (rowKey) { |
| | | 5244 | | const row = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${rowKey}"]`); |
| | | 5245 | | if (row) row.classList.remove('tx-trade-row--selected'); |
| | | 5246 | | } |
| | | 5247 | | delete window.highlightedItem[orderId]; |
| | | 5248 | | } |
| | | 5249 | | |
| | | 5250 | | /** Подставляет цену в поле заказа без переключения подсветки — подсветкой управляет вызывающий код (единый источник |
| | | 5251 | | function applyPurchasePriceOnly(orderId, price, index, itemId, rowKey) { |
| | | 5252 | | try { |
| | | 5253 | | const sourceMeta = resolveSourceMeta(orderId, index, itemId, rowKey); |
| | | 5254 | | const selectedSupplier = trySetSupplierFromSource(orderId, sourceMeta.preferredSupplierId); |
| | | 5255 | | const supplierCurrency = selectedSupplier?.dataset.currencyCode || ''; |
| | | 5256 | | const canUseSourcePrice = sourceMeta.sourcePrice |
| | | 5257 | | && sourceMeta.sourceCurrencyCode |
| | | 5258 | | && supplierCurrency.toUpperCase() === sourceMeta.sourceCurrencyCode.toUpperCase(); |
| | | 5259 | | const targetRate = apiSupplierCurrencyRates[supplierCurrency.toUpperCase()]; |
| | | 5260 | | const effectivePrice = canUseSourcePrice |
| | | 5261 | | ? sourceMeta.sourcePrice |
| | | 5262 | | : selectedSupplier && Number.isFinite(targetRate) && targetRate > 0 |
| | | 5263 | | ? (Number.parseFloat(String(price).replace(',', '.')) / targetRate).toFixed(4) |
| | | 5264 | | : price; |
| | | 5265 | | |
| | | 5266 | | const input = document.querySelector(`#PurchasePrice_${orderId}`); |
| | | 5267 | | if (input) { |
| | | 5268 | | const formattedPrice = effectivePrice.toString().replace(',', '.'); |
| | | 5269 | | input.value = formattedPrice; |
| | | 5270 | | input.dispatchEvent(new Event('input', { bubbles: true })); |
| | | 5271 | | } |
| | | 5272 | | updateAllPricesForOrder(orderId); |
| | | 5273 | | } catch (error) { |
| | | 5274 | | console.error('Error in applyPurchasePriceOnly:', error); |
| | | 5275 | | } |
| | | 5276 | | } |
| | | 5277 | | |
| | | 5278 | | $(document).on('click', '.stok-order', function () { |
| | | 5279 | | const $button = $(this); |
| | | 5280 | | const indexRow = $button.data('index-row'); // получаем индекс строки |
| | | 5281 | | |
| | | 5282 | | const orderId = $button.data('order-id'); |
| | | 5283 | | const stokId = $button.data('stok-id'); |
| | | 5284 | | const supplierUrl = $button.data('supplier-url'); |
| | | 5285 | | const supplierTitle = $button.data('supplier-title') || ''; |
| | | 5286 | | const itemHash = $button.data('item-hash') || ''; |
| | | 5287 | | const minQuantity = $button.data('min-quantity'); |
| | | 5288 | | const rest = $button.data('rest'); |
| | | 5289 | | const deliveryDays = $button.data('delivery-days'); |
| | | 5290 | | const deliveryDaysMin = $button.data('delivery-days-min'); |
| | | 5291 | | const deliveryDaysMax = $button.data('delivery-days-max'); |
| | | 5292 | | const direction = $button.data('direction') || ''; |
| | | 5293 | | const providerKey = $button.data('provider-key') || ''; |
| | | 5294 | | |
| | | 5295 | | const $priceElement = $(`.price[data-order-id="${orderId}"][data-index-row="${indexRow}"]`); |
| | | 5296 | | const rowMeta = (typeof getOrderRowMeta === 'function') |
| | | 5297 | | ? getOrderRowMeta(orderId) |
| | | 5298 | | : { article: '', productName: '', producer: '', shipmentNumber: '', clientName: '' }; |
| | | 5299 | | |
| | | 5300 | | var article = rowMeta.article || ''; |
| | | 5301 | | var quantity = getSupplierCartQuantity(orderId); |
| | | 5302 | | const minimumQuantity = Number.parseFloat(String(minQuantity ?? 1).replace(',', '.')) || 1; |
| | | 5303 | | if (providerKey === 'MlAuto' && quantity < minimumQuantity) { |
| | | 5304 | | alert(`ML-Auto принимает эту позицию от ${minimumQuantity} шт., а в заказе указано ${quantity} шт. Увеличьте |
| | | 5305 | | return false; |
| | | 5306 | | } |
| | | 5307 | | selectSupplierByUrl(orderId, supplierUrl); |
| | | 5308 | | var produser = supplierTitle || '-'; |
| | | 5309 | | var itemName = rowMeta.productName || ''; |
| | | 5310 | | let manufacturer = rowMeta.producer || ''; |
| | | 5311 | | let shipmentNumber = rowMeta.shipmentNumber || ''; |
| | | 5312 | | let clientName = rowMeta.clientName || ''; |
| | | 5313 | | let priceText = $priceElement.text().trim(); // например, "4.72 EUR" |
| | | 5314 | | let priceValue = null; |
| | | 5315 | | let currencyCode = null; |
| | | 5316 | | const sourcePrice = $priceElement.data('source-price'); |
| | | 5317 | | const sourceCurrencyCode = $priceElement.data('source-currency-code') || ''; |
| | | 5318 | | |
| | | 5319 | | if (priceText && priceText !== '-') { |
| | | 5320 | | const parts = priceText.split(' '); |
| | | 5321 | | priceValue = parseFloat(parts[0].replace(',', '.')); // числовая часть |
| | | 5322 | | currencyCode = parts[1]; // валютный код |
| | | 5323 | | } |
| | | 5324 | | |
| | | 5325 | | console.log("Цена:", priceValue); |
| | | 5326 | | console.log("Валюта:", currencyCode); |
| | | 5327 | | |
| | | 5328 | | const productImage = (typeof getOrderProductImage === 'function') ? getOrderProductImage(orderId) : null; |
| | | 5329 | | |
| | | 5330 | | const item = { |
| | | 5331 | | orderId, |
| | | 5332 | | stokId, |
| | | 5333 | | produser, |
| | | 5334 | | priceValue, |
| | | 5335 | | currencyCode, |
| | | 5336 | | sourcePrice, |
| | | 5337 | | sourceCurrencyCode, |
| | | 5338 | | manufacturer, |
| | | 5339 | | itemName, |
| | | 5340 | | quantity, |
| | | 5341 | | article, |
| | | 5342 | | indexRow, |
| | | 5343 | | siteUrl: supplierUrl || '', |
| | | 5344 | | shipmentNumber, |
| | | 5345 | | clientName, |
| | | 5346 | | itemHash, |
| | | 5347 | | minQuantity: minimumQuantity, |
| | | 5348 | | rest: rest || '', |
| | | 5349 | | deliveryDays: deliveryDays || '', |
| | | 5350 | | deliveryDaysMin: deliveryDaysMin ?? null, |
| | | 5351 | | deliveryDaysMax: deliveryDaysMax ?? null, |
| | | 5352 | | direction, |
| | | 5353 | | productImage, |
| | | 5354 | | providerKey, |
| | | 5355 | | artId: $button.data('art-id') || '', |
| | | 5356 | | offerCode: $button.data('offer-code') || article, |
| | | 5357 | | offerBrand: $button.data('offer-brand') || manufacturer, |
| | | 5358 | | keyzak: $button.data('keyzak') || '', |
| | | 5359 | | warehouseName: $button.data('warehouse-name') || '', |
| | | 5360 | | partnerWarehouseCode: $button.data('partner-warehouse-code') || '', |
| | | 5361 | | profitLigaArticleId: $button.data('profit-liga-article-id') || '', |
| | | 5362 | | profitLigaWarehouseId: $button.data('profit-liga-warehouse-id') || '', |
| | | 5363 | | profitLigaProductCode: $button.data('profit-liga-product-code') || '', |
| | | 5364 | | bergResourceId: $button.data('berg-resource-id') || '', |
| | | 5365 | | bergWarehouseId: $button.data('berg-warehouse-id') || '', |
| | | 5366 | | directionCode: $button.data('location-code') || '', |
| | | 5367 | | deliveryAtUtc: $button.data('delivery-at-utc') || '', |
| | | 5368 | | // Снимок связи «склад отгрузки → адрес Armtek» на момент проценки: сервер перерезолвит её сам |
| | | 5369 | | // при постановке в очередь, здесь значения нужны только чтобы показать их в корзине. |
| | | 5370 | | deliveryLinked: $button.data('delivery-linked') === true || $button.data('delivery-linked') === 'true', |
| | | 5371 | | buyerKunnr: $button.data('buyer-kunnr') || '', |
| | | 5372 | | buyerName: $button.data('buyer-name') || '', |
| | | 5373 | | deliveryAddress: $button.data('delivery-address') || '', |
| | | 5374 | | deliverySettingsUrl: $button.data('delivery-settings-url') || '', |
| | | 5375 | | zzapCodeDocB: String($button.data('zzap-code-doc-b') ?? ''), |
| | | 5376 | | zzapSellerUserKey: $button.data('zzap-seller-user-key') || '', |
| | | 5377 | | zzapSellerName: $button.data('zzap-seller-name') || '', |
| | | 5378 | | zzapSellerLocation: $button.data('zzap-seller-location') || '', |
| | | 5379 | | zzapCodeRegion: $button.data('zzap-code-region') || '', |
| | | 5380 | | zzapMinSumOrder: $button.data('zzap-min-sum-order') || '', |
| | | 5381 | | zzapPack: $button.data('zzap-pack') || '', |
| | | 5382 | | zzapQtyMax: $button.data('zzap-qty-max') || '' |
| | | 5383 | | }; |
| | | 5384 | | |
| | | 5385 | | // Получаем текущий список из sessionStorage |
| | | 5386 | | let list = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 5387 | | |
| | | 5388 | | // Проверяем, выбрана ли уже эта кнопка |
| | | 5389 | | const isAlreadySelected = list.some(x => x.orderId === orderId && x.stokId === stokId); |
| | | 5390 | | |
| | | 5391 | | const rowKey = `${indexRow}__${stokId || ''}`; |
| | | 5392 | | let tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${rowKey}"]`); |
| | | 5393 | | if (!tableRow) tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-item-id="${stokId}"]`); |
| | | 5394 | | |
| | | 5395 | | if (!isAlreadySelected) { |
| | | 5396 | | // Удаляем все элементы с тем же orderId — на заказ можно выбрать только одно предложение |
| | | 5397 | | list = list.filter(x => x.orderId !== orderId); |
| | | 5398 | | |
| | | 5399 | | // Снимаем выделение и иконку с других кнопок с тем же orderId |
| | | 5400 | | $(`.stok-order[data-order-id="${orderId}"]`).each(function () { |
| | | 5401 | | const $btn = $(this); |
| | | 5402 | | if ($btn.hasClass('checked')) setCartButtonState($btn, false); |
| | | 5403 | | }); |
| | | 5404 | | |
| | | 5405 | | list.push(item); |
| | | 5406 | | setCartButtonState($button, true); |
| | | 5407 | | highlightRow(orderId, rowKey, tableRow); |
| | | 5408 | | |
| | | 5409 | | const priceDecimal = $priceElement.data('price'); |
| | | 5410 | | if (priceDecimal !== undefined && priceDecimal !== null) { |
| | | 5411 | | applyPurchasePriceOnly(orderId, priceDecimal, indexRow, stokId, rowKey); |
| | | 5412 | | } |
| | | 5413 | | } else { |
| | | 5414 | | // Удаляем текущий элемент (отмена выбора) |
| | | 5415 | | list = list.filter(x => !(x.orderId === orderId && x.stokId === stokId)); |
| | | 5416 | | |
| | | 5417 | | setCartButtonState($button, false); |
| | | 5418 | | clearRowHighlight(orderId); |
| | | 5419 | | } |
| | | 5420 | | inMemoryCartStorage.setItem('selectedStockList' + ids, JSON.stringify(list)); |
| | | 5421 | | updateCartBadge(); |
| | | 5422 | | document.dispatchEvent(new CustomEvent('tx-cart-changed')); |
| | | 5423 | | }); |
| | | 5424 | | |
| | | 5425 | | function updateCartBadge() { |
| | | 5426 | | const badge = document.getElementById('cartBadge'); |
| | | 5427 | | const list = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 5428 | | updateOrderCartIndicators(list); |
| | | 5429 | | if (!badge) return; |
| | | 5430 | | if (list.length > 0) { |
| | | 5431 | | badge.innerHTML = `<span class="tx-cart-badge-count">${list.length}</span><span class="visually-hidden">това |
| | | 5432 | | badge.classList.remove('d-none'); |
| | | 5433 | | } else { |
| | | 5434 | | badge.innerHTML = '<span class="visually-hidden">товаров в корзине</span>'; |
| | | 5435 | | badge.classList.add('d-none'); |
| | | 5436 | | } |
| | | 5437 | | if (list.length > lastCartBadgeCount) { |
| | | 5438 | | badge.classList.remove('tx-cart-badge--bump'); |
| | | 5439 | | void badge.offsetWidth; |
| | | 5440 | | badge.classList.add('tx-cart-badge--bump'); |
| | | 5441 | | } |
| | | 5442 | | lastCartBadgeCount = list.length; |
| | | 5443 | | } |
| | | 5444 | | |
| | | 5445 | | // Основная функция для выбора поставщика по URL |
| | | 5446 | | function selectSupplierByUrl(orderId, supplierUrl) { |
| | | 5447 | | // Находим select по orderId |
| | | 5448 | | const $select = $(`#SupplierId_${orderId}`); |
| | | 5449 | | |
| | | 5450 | | if (!$select.length) { |
| | | 5451 | | console.error(`Не найден select для orderId: ${orderId}`); |
| | | 5452 | | return false; |
| | | 5453 | | } |
| | | 5454 | | |
| | | 5455 | | // Ищем точное совпадение по URL |
| | | 5456 | | const $exactMatch = $select.find(`option[data-site="${supplierUrl}"]`); |
| | | 5457 | | |
| | | 5458 | | if ($exactMatch.length) { |
| | | 5459 | | $select |
| | | 5460 | | .removeClass('highlight-error') |
| | | 5461 | | .val($exactMatch.val()) |
| | | 5462 | | .trigger('change'); |
| | | 5463 | | console.log(`Выбран поставщик: ${$exactMatch.text()}`); |
| | | 5464 | | return true; |
| | | 5465 | | } |
| | | 5466 | | |
| | | 5467 | | // Если точного совпадения нет, ищем без учета регистра |
| | | 5468 | | const $options = $select.find('option[data-site]'); |
| | | 5469 | | let foundMatch = null; |
| | | 5470 | | |
| | | 5471 | | $options.each(function () { |
| | | 5472 | | const optionUrl = $(this).data('site') || ''; |
| | | 5473 | | if (optionUrl.trim().toLowerCase() === supplierUrl.trim().toLowerCase()) { |
| | | 5474 | | foundMatch = $(this); |
| | | 5475 | | return false; // Прерываем цикл |
| | | 5476 | | } |
| | | 5477 | | }); |
| | | 5478 | | |
| | | 5479 | | if (foundMatch) { |
| | | 5480 | | // Удаляем класс ошибки и устанавливаем найденного поставщика |
| | | 5481 | | $select.removeClass('highlight-error') |
| | | 5482 | | .val(foundMatch.val()) |
| | | 5483 | | .trigger('change'); |
| | | 5484 | | return true; |
| | | 5485 | | } |
| | | 5486 | | |
| | | 5487 | | console.warn(`Не найден поставщик с URL: ${supplierUrl}`); |
| | | 5488 | | return false; |
| | | 5489 | | } |
| | | 5490 | | |
| | | 5491 | | |
| | | 5492 | | function txGetCartItems() { |
| | | 5493 | | try { |
| | | 5494 | | const parsed = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 5495 | | return Array.isArray(parsed) ? parsed : []; |
| | | 5496 | | } catch { |
| | | 5497 | | return []; |
| | | 5498 | | } |
| | | 5499 | | } |
| | | 5500 | | |
| | | 5501 | | // Артикул с суффиксом вида ARTICLE=BRAND[4] продаётся упаковкой из 4 шт. |
| | | 5502 | | // В корзину поставщика и её итоговую сумму должно попадать физическое количество, |
| | | 5503 | | // а не число упаковок из системного заказа. |
| | | 5504 | | function getSupplierCartQuantity(orderId) { |
| | | 5505 | | const orderRow = document.querySelector(`tr[data-order-id="${orderId}"]`); |
| | | 5506 | | const multiplier = Number.parseFloat(String(orderRow?.dataset.packMultiplier ?? '1').replace(',', '.')) || 1; |
| | | 5507 | | const quantityInput = document.querySelector(`.quantity-input-field[data-order-id="${orderId}"]`); |
| | | 5508 | | const orderQuantity = Number.parseFloat(String(quantityInput?.value ?? '').replace(',', '.')) || 0; |
| | | 5509 | | const quantity = orderQuantity * multiplier; |
| | | 5510 | | return quantity > 0 ? quantity : 1; |
| | | 5511 | | } |
| | | 5512 | | |
| | | 5513 | | // Количество в строке транзакции можно изменить уже после выбора предложения. |
| | | 5514 | | // Перед показом и отправкой корзины обновляем сохранённый снимок, чтобы не заказать |
| | | 5515 | | // старое количество. |
| | | 5516 | | function synchronizeSupplierCartQuantities(items) { |
| | | 5517 | | return items.map(item => ({ ...item, quantity: getSupplierCartQuantity(item.orderId) })); |
| | | 5518 | | } |
| | | 5519 | | |
| | | 5520 | | function txClearCartQueue() { |
| | | 5521 | | inMemoryCartStorage.removeItem('selectedStockList' + ids); |
| | | 5522 | | } |
| | | 5523 | | |
| | | 5524 | | // ВАЖНО: обработчик обязан оставаться идемпотентным (только сериализация, без побочных эффектов). |
| | | 5525 | | // Он не вызывает preventDefault и срабатывает на каждом submit-событии, в том числе на попытках, |
| | | 5526 | | // которые страница потом прерывает на валидации, подтверждении корзины или ошибке. Страница с |
| | | 5527 | | // подтверждением вызывает его ещё раз напрямую — состав корзины к тому моменту мог измениться. |
| | | 5528 | | // Раньше здесь же чистилась корзина, из-за чего после неудачной отправки она пропадала без объяснений. |
| | | 5529 | | function txSerializeCartQueues() { |
| | | 5530 | | if (window.txSupplierCheckoutMode === 'none') { |
| | | 5531 | | document.querySelectorAll('#createOrderForm input[id$="BasketQueueJson"], #createOrderForm input[id$="Checko |
| | | 5532 | | .forEach(field => { field.value = '[]'; }); |
| | | 5533 | | const shateMOptions = document.getElementById('shateMCheckoutOptionsJson'); |
| | | 5534 | | if (shateMOptions) shateMOptions.value = ''; |
| | | 5535 | | return; |
| | | 5536 | | } |
| | | 5537 | | const listJson = inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'; |
| | | 5538 | | try { |
| | | 5539 | | const parsedList = synchronizeSupplierCartQuantities(JSON.parse(listJson)); |
| | | 5540 | | const readIntOrNull = value => Number.isFinite(Number(value)) && String(value ?? '').trim() !== '' ? Number( |
| | | 5541 | | const avdQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5542 | | .filter(x => x && x.providerKey === 'Avd' && x.itemHash) |
| | | 5543 | | .map(x => ({ |
| | | 5544 | | orderId: Number(x.orderId), |
| | | 5545 | | itemHash: x.itemHash, |
| | | 5546 | | quantity: Number(x.quantity) > 0 ? Number(x.quantity) : (Number(x.minQuantity) || 1), |
| | | 5547 | | article: x.article || '', |
| | | 5548 | | productName: x.itemName || '', |
| | | 5549 | | shipmentNumber: x.shipmentNumber || '', |
| | | 5550 | | clientName: x.clientName || '', |
| | | 5551 | | price: Number.isFinite(Number(x.priceValue)) ? Number(x.priceValue) : null, |
| | | 5552 | | deliveryDaysMin: readIntOrNull(x.deliveryDaysMin), |
| | | 5553 | | deliveryDaysMax: readIntOrNull(x.deliveryDaysMax), |
| | | 5554 | | direction: x.direction || '' |
| | | 5555 | | })); |
| | | 5556 | | const directCheckout = window.txSupplierCheckoutMode === 'direct'; |
| | | 5557 | | const hiddenField = document.getElementById('avdBasketQueueJson'); |
| | | 5558 | | if (hiddenField) hiddenField.value = directCheckout ? '[]' : JSON.stringify(avdQueue); |
| | | 5559 | | const avdCheckoutHiddenField = document.getElementById('avdCheckoutQueueJson'); |
| | | 5560 | | if (avdCheckoutHiddenField) avdCheckoutHiddenField.value = directCheckout ? JSON.stringify(avdQueue) : '[]'; |
| | | 5561 | | // Japarts: JSON.stringify превращает NaN в null, а null в orderId/quantity роняет разбор очереди |
| | | 5562 | | // на сервере целиком. Поэтому нормализуем числа здесь и отправляем только полные позиции. |
| | | 5563 | | const japartsQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5564 | | .filter(x => x && x.providerKey === 'Japarts') |
| | | 5565 | | .map(x => { |
| | | 5566 | | const quantity = Number(x.quantity) > 0 ? Number(x.quantity) : (Number(x.minQuantity) > 0 ? Number(x |
| | | 5567 | | const price = Number.isFinite(Number(x.priceValue)) ? Number(x.priceValue) : null; |
| | | 5568 | | return { |
| | | 5569 | | orderId: readIntOrNull(x.orderId), |
| | | 5570 | | priceId: String(x.itemHash || x.itemId || '').trim(), |
| | | 5571 | | quantity: quantity, |
| | | 5572 | | price: price, |
| | | 5573 | | shipmentNumber: x.shipmentNumber || '', |
| | | 5574 | | article: x.article || '', |
| | | 5575 | | productName: x.itemName || '', |
| | | 5576 | | clientName: x.clientName || '', |
| | | 5577 | | deliveryDaysMin: readIntOrNull(x.deliveryDaysMin), |
| | | 5578 | | deliveryDaysMax: readIntOrNull(x.deliveryDaysMax), |
| | | 5579 | | direction: x.direction || '' |
| | | 5580 | | }; |
| | | 5581 | | }) |
| | | 5582 | | .filter(x => Number.isInteger(x.orderId) && x.orderId > 0 && x.priceId.length > 0); |
| | | 5583 | | const japartsHiddenField = document.getElementById('japartsBasketQueueJson'); |
| | | 5584 | | if (japartsHiddenField) japartsHiddenField.value = directCheckout ? '[]' : JSON.stringify(japartsQueue); |
| | | 5585 | | const japartsCheckoutHiddenField = document.getElementById('japartsCheckoutQueueJson'); |
| | | 5586 | | if (japartsCheckoutHiddenField) japartsCheckoutHiddenField.value = directCheckout ? JSON.stringify(japartsQu |
| | | 5587 | | // Berg адресует предложение парой resource_id + warehouse_id; itemId вида |
| | | 5588 | | // «resource|warehouse|price|days» остаётся запасным источником на стороне сервера. |
| | | 5589 | | const bergQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5590 | | .filter(x => x && x.providerKey === 'Berg') |
| | | 5591 | | .map(x => { |
| | | 5592 | | const quantity = Number(x.quantity) > 0 ? Number(x.quantity) : (Number(x.minQuantity) > 0 ? Number(x |
| | | 5593 | | const price = Number.isFinite(Number(x.priceValue)) ? Number(x.priceValue) : null; |
| | | 5594 | | return { |
| | | 5595 | | orderId: readIntOrNull(x.orderId), |
| | | 5596 | | resourceId: readIntOrNull(x.bergResourceId), |
| | | 5597 | | warehouseId: readIntOrNull(x.bergWarehouseId), |
| | | 5598 | | itemId: String(x.stokId || x.itemId || '').trim(), |
| | | 5599 | | quantity: quantity, |
| | | 5600 | | price: price, |
| | | 5601 | | minQuantity: readIntOrNull(x.minQuantity), |
| | | 5602 | | shipmentNumber: x.shipmentNumber || '', |
| | | 5603 | | article: x.offerCode || x.article || '', |
| | | 5604 | | brand: x.offerBrand || x.manufacturer || '', |
| | | 5605 | | productName: x.itemName || '', |
| | | 5606 | | clientName: x.clientName || '', |
| | | 5607 | | deliveryDaysMin: readIntOrNull(x.deliveryDaysMin), |
| | | 5608 | | deliveryDaysMax: readIntOrNull(x.deliveryDaysMax), |
| | | 5609 | | direction: x.direction || '' |
| | | 5610 | | }; |
| | | 5611 | | }) |
| | | 5612 | | .filter(x => Number.isInteger(x.orderId) && x.orderId > 0 && (x.itemId.length > 0 || (x.resourceId > 0 & |
| | | 5613 | | const bergHiddenField = document.getElementById('bergBasketQueueJson'); |
| | | 5614 | | if (bergHiddenField) bergHiddenField.value = directCheckout ? '[]' : JSON.stringify(bergQueue); |
| | | 5615 | | const bergCheckoutHiddenField = document.getElementById('bergCheckoutQueueJson'); |
| | | 5616 | | if (bergCheckoutHiddenField) bergCheckoutHiddenField.value = directCheckout ? JSON.stringify(bergQueue) : '[ |
| | | 5617 | | const normalizeDeliveryAtUtc = value => { |
| | | 5618 | | const raw = String(value || '').trim(); |
| | | 5619 | | if (!raw) return null; |
| | | 5620 | | const compact = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(raw); |
| | | 5621 | | if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}T${compact[4]}:${compact[5]}:${compact[6]} |
| | | 5622 | | const date = new Date(raw); |
| | | 5623 | | return Number.isNaN(date.getTime()) ? null : date.toISOString(); |
| | | 5624 | | }; |
| | | 5625 | | const armtekQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5626 | | .filter(x => x && (x.providerKey === 'Armtek' || x.providerKey === 'ArmtekBy')) |
| | | 5627 | | .map(x => ({ orderId: Number(x.orderId), providerKey: x.providerKey, artId: x.artId || '', pin: x.offerC |
| | | 5628 | | const armtekCheckoutMode = directCheckout || window.txArmtekCheckoutMode === 'direct'; |
| | | 5629 | | const armtekBasketHiddenField = document.getElementById('armtekBasketQueueJson'); |
| | | 5630 | | if (armtekBasketHiddenField) armtekBasketHiddenField.value = armtekCheckoutMode ? '[]' : JSON.stringify(armt |
| | | 5631 | | const armtekHiddenField = document.getElementById('armtekCheckoutQueueJson'); |
| | | 5632 | | if (armtekHiddenField) armtekHiddenField.value = armtekCheckoutMode ? JSON.stringify(armtekQueue) : '[]'; |
| | | 5633 | | const mlAutoQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5634 | | .filter(x => x && x.providerKey === 'MlAuto') |
| | | 5635 | | .map(x => ({ orderId: Number(x.orderId), pin: x.offerCode || '', brand: x.offerBrand || '', storageCode: |
| | | 5636 | | const mlAutoHiddenField = document.getElementById('mlAutoBasketQueueJson'); |
| | | 5637 | | if (mlAutoHiddenField) mlAutoHiddenField.value = directCheckout ? '[]' : JSON.stringify(mlAutoQueue); |
| | | 5638 | | const mlAutoCheckoutHiddenField = document.getElementById('mlAutoCheckoutQueueJson'); |
| | | 5639 | | if (mlAutoCheckoutHiddenField) mlAutoCheckoutHiddenField.value = directCheckout ? JSON.stringify(mlAutoQueue |
| | | 5640 | | const mlAutoRuQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5641 | | .filter(x => x && x.providerKey === 'MlAutoRu') |
| | | 5642 | | .map(x => ({ orderId: Number(x.orderId), pin: x.offerCode || '', brand: x.offerBrand || '', storageCode: |
| | | 5643 | | const mlAutoRuHiddenField = document.getElementById('mlAutoRuBasketQueueJson'); |
| | | 5644 | | if (mlAutoRuHiddenField) mlAutoRuHiddenField.value = directCheckout ? '[]' : JSON.stringify(mlAutoRuQueue); |
| | | 5645 | | const mlAutoRuCheckoutHiddenField = document.getElementById('mlAutoRuCheckoutQueueJson'); |
| | | 5646 | | if (mlAutoRuCheckoutHiddenField) mlAutoRuCheckoutHiddenField.value = directCheckout ? JSON.stringify(mlAutoR |
| | | 5647 | | const forumAutoQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5648 | | .filter(x => x && (x.providerKey === 'ForumAuto' || x.providerKey === 'ForumAutoInterparts' || x.provide |
| | | 5649 | | .map(x => ({ orderId: Number(x.orderId), providerKey: x.providerKey, tid: x.stokId || '', article: x.off |
| | | 5650 | | const forumAutoBasketHiddenField = document.getElementById('forumAutoBasketQueueJson'); |
| | | 5651 | | if (forumAutoBasketHiddenField) forumAutoBasketHiddenField.value = directCheckout ? '[]' : JSON.stringify(fo |
| | | 5652 | | const forumAutoCheckoutHiddenField = document.getElementById('forumAutoCheckoutQueueJson'); |
| | | 5653 | | if (forumAutoCheckoutHiddenField) forumAutoCheckoutHiddenField.value = directCheckout ? JSON.stringify(forum |
| | | 5654 | | const favoritPartsQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5655 | | .filter(x => x && x.providerKey === 'FavoritParts') |
| | | 5656 | | .map(x => ({ orderId: Number(x.orderId), goodsId: x.stokId || '', warehouseGroupId: x.partnerWarehouseCo |
| | | 5657 | | const favoritPartsBasketHiddenField = document.getElementById('favoritPartsBasketQueueJson'); |
| | | 5658 | | if (favoritPartsBasketHiddenField) favoritPartsBasketHiddenField.value = directCheckout ? '[]' : JSON.string |
| | | 5659 | | const favoritPartsCheckoutHiddenField = document.getElementById('favoritPartsCheckoutQueueJson'); |
| | | 5660 | | if (favoritPartsCheckoutHiddenField) favoritPartsCheckoutHiddenField.value = directCheckout ? JSON.stringify |
| | | 5661 | | const favoritPartsIstraQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5662 | | .filter(x => x && x.providerKey === 'FavoritPartsIstra') |
| | | 5663 | | .map(x => ({ orderId: Number(x.orderId), goodsId: x.stokId || '', warehouseGroupId: x.partnerWarehouseCo |
| | | 5664 | | const favoritPartsIstraBasketHiddenField = document.getElementById('favoritPartsIstraBasketQueueJson'); |
| | | 5665 | | if (favoritPartsIstraBasketHiddenField) favoritPartsIstraBasketHiddenField.value = directCheckout ? '[]' : J |
| | | 5666 | | const favoritPartsIstraCheckoutHiddenField = document.getElementById('favoritPartsIstraCheckoutQueueJson'); |
| | | 5667 | | if (favoritPartsIstraCheckoutHiddenField) favoritPartsIstraCheckoutHiddenField.value = directCheckout ? JSON |
| | | 5668 | | const favoritPartsRostovQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5669 | | .filter(x => x && x.providerKey === 'FavoritPartsRostov') |
| | | 5670 | | .map(x => ({ orderId: Number(x.orderId), goodsId: x.stokId || '', warehouseGroupId: x.partnerWarehouseCo |
| | | 5671 | | const favoritPartsRostovBasketHiddenField = document.getElementById('favoritPartsRostovBasketQueueJson'); |
| | | 5672 | | if (favoritPartsRostovBasketHiddenField) favoritPartsRostovBasketHiddenField.value = directCheckout ? '[]' : |
| | | 5673 | | const favoritPartsRostovCheckoutHiddenField = document.getElementById('favoritPartsRostovCheckoutQueueJson') |
| | | 5674 | | if (favoritPartsRostovCheckoutHiddenField) favoritPartsRostovCheckoutHiddenField.value = directCheckout ? JS |
| | | 5675 | | const moskvorechieQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5676 | | .filter(x => x && x.providerKey === 'MoskvorechieIstra') |
| | | 5677 | | .map(x => ({ orderId: Number(x.orderId), offerId: x.stokId || '', article: x.offerCode || '', brand: x.o |
| | | 5678 | | const moskvorechieHiddenField = document.getElementById('moskvorechieBasketQueueJson'); |
| | | 5679 | | if (moskvorechieHiddenField) moskvorechieHiddenField.value = directCheckout ? '[]' : JSON.stringify(moskvore |
| | | 5680 | | const moskvorechieCheckoutHiddenField = document.getElementById('moskvorechieCheckoutQueueJson'); |
| | | 5681 | | if (moskvorechieCheckoutHiddenField) moskvorechieCheckoutHiddenField.value = directCheckout ? JSON.stringify |
| | | 5682 | | const profitLigaQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5683 | | .filter(x => x && x.providerKey === 'ProfitLiga') |
| | | 5684 | | .map(x => ({ orderId: Number(x.orderId), articleId: x.profitLigaArticleId || '', warehouseId: x.profitLi |
| | | 5685 | | const profitLigaBasketField = document.getElementById('profitLigaBasketQueueJson'); |
| | | 5686 | | if (profitLigaBasketField) profitLigaBasketField.value = directCheckout ? '[]' : JSON.stringify(profitLigaQu |
| | | 5687 | | const profitLigaCheckoutField = document.getElementById('profitLigaCheckoutQueueJson'); |
| | | 5688 | | if (profitLigaCheckoutField) profitLigaCheckoutField.value = directCheckout ? JSON.stringify(profitLigaQueue |
| | | 5689 | | const mikadoQueue = (Array.isArray(parsedList) ? parsedList : []).filter(x => x && x.providerKey === 'Mikado |
| | | 5690 | | const mikadoBasketField = document.getElementById('mikadoBasketQueueJson'); |
| | | 5691 | | if (mikadoBasketField) mikadoBasketField.value = directCheckout ? '[]' : JSON.stringify(mikadoQueue); |
| | | 5692 | | // ZZap: code_doc_b — int64, в JSON передаётся строкой, иначе JS округлит идентификатор. |
| | | 5693 | | const zzapQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5694 | | .filter(x => x && x.providerKey === 'ZZapMoscow') |
| | | 5695 | | .map(x => ({ |
| | | 5696 | | orderId: Number(x.orderId), |
| | | 5697 | | codeDocB: String(x.zzapCodeDocB || x.stokId || ''), |
| | | 5698 | | article: x.offerCode || x.article || '', |
| | | 5699 | | brand: x.offerBrand || x.manufacturer || '', |
| | | 5700 | | productName: x.itemName || '', |
| | | 5701 | | sellerName: x.zzapSellerName || '', |
| | | 5702 | | sellerUserKey: x.zzapSellerUserKey || '', |
| | | 5703 | | sellerLocation: x.zzapSellerLocation || '', |
| | | 5704 | | quantity: Number(x.quantity) > 0 ? Number(x.quantity) : (Number(x.minQuantity) || 1), |
| | | 5705 | | minimumQuantity: Number(x.zzapPack) || Number(x.minQuantity) || 1, |
| | | 5706 | | sourcePrice: Number.isFinite(Number(x.sourcePrice)) ? Number(x.sourcePrice) : null, |
| | | 5707 | | deliveryDays: readIntOrNull(x.deliveryDaysMin) || 0, |
| | | 5708 | | codeRegion: readIntOrNull(x.zzapCodeRegion), |
| | | 5709 | | minSumOrder: Number.isFinite(Number(x.zzapMinSumOrder)) ? Number(x.zzapMinSumOrder) : null, |
| | | 5710 | | qtyMax: readIntOrNull(x.zzapQtyMax), |
| | | 5711 | | direction: x.direction || '', |
| | | 5712 | | deliveryDaysMin: readIntOrNull(x.deliveryDaysMin), |
| | | 5713 | | deliveryDaysMax: readIntOrNull(x.deliveryDaysMax), |
| | | 5714 | | shipmentNumber: x.shipmentNumber || '', |
| | | 5715 | | clientName: x.clientName || '' |
| | | 5716 | | })) |
| | | 5717 | | .filter(x => Number.isInteger(x.orderId) && x.orderId > 0 && x.codeDocB.length > 0); |
| | | 5718 | | const zzapBasketField = document.getElementById('zzapMoscowBasketQueueJson'); |
| | | 5719 | | if (zzapBasketField) zzapBasketField.value = directCheckout ? '[]' : JSON.stringify(zzapQueue); |
| | | 5720 | | const zzapCheckoutField = document.getElementById('zzapMoscowCheckoutQueueJson'); |
| | | 5721 | | if (zzapCheckoutField) zzapCheckoutField.value = directCheckout ? JSON.stringify(zzapQueue) : '[]'; |
| | | 5722 | | const motexQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5723 | | .filter(x => x && x.providerKey === 'Motex') |
| | | 5724 | | .map(x => ({ orderId: Number(x.orderId), article: x.offerCode || '', brand: x.offerBrand || '', productN |
| | | 5725 | | const motexBasketField = document.getElementById('motexBasketQueueJson'); |
| | | 5726 | | if (motexBasketField) motexBasketField.value = directCheckout ? '[]' : JSON.stringify(motexQueue); |
| | | 5727 | | const motexCheckoutField = document.getElementById('motexCheckoutQueueJson'); |
| | | 5728 | | if (motexCheckoutField) motexCheckoutField.value = directCheckout ? JSON.stringify(motexQueue) : '[]'; |
| | | 5729 | | const shateMQueue = (Array.isArray(parsedList) ? parsedList : []) |
| | | 5730 | | .filter(x => x && x.providerKey === 'ShateM') |
| | | 5731 | | .map(x => ({ orderId: Number(x.orderId), article: x.offerCode || '', brand: x.offerBrand || '', productN |
| | | 5732 | | const shateMBasketField = document.getElementById('shateMBasketQueueJson'); |
| | | 5733 | | if (shateMBasketField) shateMBasketField.value = directCheckout ? '[]' : JSON.stringify(shateMQueue); |
| | | 5734 | | const shateMCheckoutField = document.getElementById('shateMCheckoutQueueJson'); |
| | | 5735 | | if (shateMCheckoutField) shateMCheckoutField.value = directCheckout ? JSON.stringify(shateMQueue) : '[]'; |
| | | 5736 | | } catch (err) { |
| | | 5737 | | console.warn('Ошибка при чтении selectedStockList из sessionStorage', err); |
| | | 5738 | | } |
| | | 5739 | | } |
| | | 5740 | | |
| | | 5741 | | function saveMikadoCartOption(event) { |
| | | 5742 | | const input = event.target.closest?.('[data-mikado-option]'); |
| | | 5743 | | if (!input) return; |
| | | 5744 | | const list = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 5745 | | const item = list.find(x => Number(x.orderId) === Number(input.dataset.mikadoOrder) && x.providerKey === 'Mikado |
| | | 5746 | | if (!item) return; |
| | | 5747 | | item[input.dataset.mikadoOption] = Math.max(0, Number(input.value) || 0); |
| | | 5748 | | inMemoryCartStorage.setItem('selectedStockList' + ids, JSON.stringify(list)); |
| | | 5749 | | } |
| | | 5750 | | document.addEventListener('change', saveMikadoCartOption); |
| | | 5751 | | |
| | | 5752 | | const mikadoExpressOptionsCache = new Map(); |
| | | 5753 | | async function loadMikadoExpressOptions() { |
| | | 5754 | | const selects = Array.from(document.querySelectorAll('[data-mikado-express-select]')); |
| | | 5755 | | await Promise.all(selects.map(async select => { |
| | | 5756 | | const orderCode = select.dataset.mikadoOrderCode || ''; |
| | | 5757 | | const stockId = Number(select.dataset.mikadoStockId) || 0; |
| | | 5758 | | const cacheKey = `${orderCode}|${stockId}`; |
| | | 5759 | | if (!orderCode || stockId <= 0) return; |
| | | 5760 | | try { |
| | | 5761 | | let options = mikadoExpressOptionsCache.get(cacheKey); |
| | | 5762 | | if (!options) { |
| | 0 | 5763 | | const response = await fetch('@Url.Action("GetExpressOptions", "MikadoMskHod20Price")?orderCode=' + |
| | | 5764 | | if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| | | 5765 | | options = await response.json(); |
| | | 5766 | | mikadoExpressOptionsCache.set(cacheKey, options); |
| | | 5767 | | } |
| | | 5768 | | const currentValue = String(select.value || '0'); |
| | | 5769 | | select.replaceChildren(new Option('Не выбрана', '0')); |
| | | 5770 | | options.forEach(option => select.add(new Option(option.label, String(option.id)))); |
| | | 5771 | | select.value = Array.from(select.options).some(option => option.value === currentValue) ? currentValue : |
| | | 5772 | | select.disabled = options.length === 0; |
| | | 5773 | | if (options.length === 0) select.options[0].text = 'Недоступна для этой позиции'; |
| | | 5774 | | } catch (error) { |
| | | 5775 | | select.replaceChildren(new Option('Не удалось загрузить варианты', '0')); |
| | | 5776 | | select.disabled = true; |
| | | 5777 | | console.debug('Микадо: не удалось загрузить экспресс-доставку', error); |
| | | 5778 | | } |
| | | 5779 | | })); |
| | | 5780 | | } |
| | | 5781 | | |
| | | 5782 | | /* Фоновые загрузки (проценка, остатки, история) занимают соединения к серверу — по HTTP/1.1 |
| | | 5783 | | их всего шесть на хост. Пока очередь прогрева не разошлась, POST проведения стоит в ней же |
| | | 5784 | | и ждёт свободный слот: пользователь видит «Ждём ответ сервера» на минуту и дольше. |
| | | 5785 | | После нажатия «Сохранить» фоновые данные уже не нужны — обрываем их и освобождаем канал. */ |
| | | 5786 | | const txBackgroundRequests = new Set(); |
| | | 5787 | | |
| | | 5788 | | if (window.jQuery && !window.jQuery.__txBackgroundTrackerInstalled) { |
| | | 5789 | | window.jQuery.ajaxPrefilter(function (options, originalOptions, jqXHR) { |
| | | 5790 | | const url = options.url || ''; |
| | | 5791 | | if ((options.type || 'GET').toUpperCase() !== 'GET') { |
| | | 5792 | | // Момент реальной отправки документа: раньше обрывать нельзя — форма может |
| | | 5793 | | // не пройти валидацию, и прогрев остальных строк ещё понадобится. |
| | | 5794 | | if (/Transaction\/|\/Percentage\//i.test(url)) window.txAbortBackgroundLoads(); |
| | | 5795 | | return; |
| | | 5796 | | } |
| | | 5797 | | if (!/Price\/(Search|Warmup)|onec-lookup|Quotation|GetSupplierQuotationHistorySummary/i.test(url)) return; |
| | | 5798 | | txBackgroundRequests.add(jqXHR); |
| | | 5799 | | jqXHR.always(function () { txBackgroundRequests.delete(jqXHR); }); |
| | | 5800 | | }); |
| | | 5801 | | window.jQuery.__txBackgroundTrackerInstalled = true; |
| | | 5802 | | } |
| | | 5803 | | |
| | | 5804 | | window.txAbortBackgroundLoads = function () { |
| | | 5805 | | txQuotationPrefetch.queue.length = 0; |
| | | 5806 | | txQuotationGate.drain(); |
| | | 5807 | | txBackgroundRequests.forEach(function (jqXHR) { |
| | | 5808 | | try { jqXHR.abort(); } catch (error) { /* уже завершён */ } |
| | | 5809 | | }); |
| | | 5810 | | txBackgroundRequests.clear(); |
| | | 5811 | | if (window.txAbortStockLoads) window.txAbortStockLoads(); |
| | | 5812 | | }; |
| | | 5813 | | |
| | | 5814 | | $('#createOrderForm').on('submit', txSerializeCartQueues); |
| | | 5815 | | window.txSerializeCartQueues = txSerializeCartQueues; |
| | | 5816 | | window.txGetCartItems = txGetCartItems; |
| | | 5817 | | window.txClearCartQueue = txClearCartQueue; |
| | | 5818 | | |
| | | 5819 | | document.getElementById('cartModal')?.addEventListener('shown.bs.modal', function () { |
| | | 5820 | | printGroupedCartToModal(); |
| | | 5821 | | }); |
| | | 5822 | | |
| | | 5823 | | // ── Подтверждение корзины перед проведением ─────────────────────────────── |
| | | 5824 | | // Размещение в API-корзине поставщика необратимо, поэтому перед проведением показываем состав |
| | | 5825 | | // корзины и требуем явного подтверждения. Кнопку добавляем из JS: футер модалки есть во всех |
| | | 5826 | | // транзакционных вьюхах, и так не нужно править каждую. |
| | | 5827 | | let txCartConfirmMode = false; |
| | | 5828 | | |
| | | 5829 | | window.txRequestCartConfirmation = function (onConfirm) { |
| | | 5830 | | const modalElement = document.getElementById('cartModal'); |
| | | 5831 | | if (!modalElement || !window.bootstrap || !bootstrap.Modal) { |
| | | 5832 | | // Fail-open: отсутствие UI подтверждения не должно блокировать проведение документа. |
| | | 5833 | | onConfirm(); |
| | | 5834 | | return; |
| | | 5835 | | } |
| | | 5836 | | |
| | | 5837 | | const footer = modalElement.querySelector('.modal-footer'); |
| | | 5838 | | if (!footer) { onConfirm(); return; } |
| | | 5839 | | const body = modalElement.querySelector('#cartModalBody'); |
| | | 5840 | | if (!body) { onConfirm(); return; } |
| | | 5841 | | |
| | | 5842 | | let confirmButton = footer.querySelector('#txCartConfirmBtn'); |
| | | 5843 | | if (!confirmButton) { |
| | | 5844 | | confirmButton = document.createElement('button'); |
| | | 5845 | | confirmButton.type = 'button'; |
| | | 5846 | | confirmButton.id = 'txCartConfirmBtn'; |
| | | 5847 | | confirmButton.className = 'btn btn-primary'; |
| | | 5848 | | confirmButton.innerHTML = '<i class="fas fa-check me-1"></i>Подтвердить и провести'; |
| | | 5849 | | footer.appendChild(confirmButton); |
| | | 5850 | | } |
| | | 5851 | | |
| | | 5852 | | let addToCartButton = footer.querySelector('#txArmtekCartBtn'); |
| | | 5853 | | if (!addToCartButton) { |
| | | 5854 | | addToCartButton = document.createElement('button'); |
| | | 5855 | | addToCartButton.type = 'button'; |
| | | 5856 | | addToCartButton.id = 'txArmtekCartBtn'; |
| | | 5857 | | addToCartButton.className = 'btn btn-outline-primary'; |
| | | 5858 | | addToCartButton.innerHTML = '<i class="fas fa-cart-plus me-1"></i>В корзину и провести'; |
| | | 5859 | | footer.appendChild(addToCartButton); |
| | | 5860 | | } |
| | | 5861 | | |
| | | 5862 | | let proceedWithoutSupplierButton = footer.querySelector('#txCartProceedWithoutSupplierBtn'); |
| | | 5863 | | if (!proceedWithoutSupplierButton) { |
| | | 5864 | | proceedWithoutSupplierButton = document.createElement('button'); |
| | | 5865 | | proceedWithoutSupplierButton.type = 'button'; |
| | | 5866 | | proceedWithoutSupplierButton.id = 'txCartProceedWithoutSupplierBtn'; |
| | | 5867 | | proceedWithoutSupplierButton.className = 'btn btn-outline-secondary'; |
| | | 5868 | | proceedWithoutSupplierButton.innerHTML = '<i class="fas fa-file-circle-check me-1"></i>Продолжить без заказа |
| | | 5869 | | footer.appendChild(proceedWithoutSupplierButton); |
| | | 5870 | | } |
| | | 5871 | | |
| | | 5872 | | let previewButton = footer.querySelector('#txArmtekPreviewBtn'); |
| | | 5873 | | if (!previewButton) { |
| | | 5874 | | previewButton = document.createElement('button'); |
| | | 5875 | | previewButton.type = 'button'; |
| | | 5876 | | previewButton.id = 'txArmtekPreviewBtn'; |
| | | 5877 | | previewButton.className = 'btn btn-outline-secondary'; |
| | | 5878 | | previewButton.title = 'Показать данные запроса Armtek'; |
| | | 5879 | | previewButton.setAttribute('aria-label', previewButton.title); |
| | | 5880 | | previewButton.innerHTML = '<i class="fas fa-code" aria-hidden="true"></i>'; |
| | | 5881 | | footer.appendChild(previewButton); |
| | | 5882 | | } |
| | | 5883 | | |
| | | 5884 | | let summary = footer.querySelector('.tx-cart-confirm-summary'); |
| | | 5885 | | if (!summary) { |
| | | 5886 | | summary = document.createElement('span'); |
| | | 5887 | | summary.className = 'tx-cart-confirm-summary'; |
| | | 5888 | | footer.prepend(summary); |
| | | 5889 | | } |
| | | 5890 | | |
| | | 5891 | | const renderSummary = () => { |
| | | 5892 | | const items = txGetCartItems(); |
| | | 5893 | | const queueEnabled = txCartQueueEnabled(); |
| | | 5894 | | const queued = queueEnabled |
| | | 5895 | | ? items.filter(x => TX_CART_QUEUE_PROVIDERS.includes(x.providerKey)).length |
| | | 5896 | | : 0; |
| | | 5897 | | const hasArmtek = items.some(txIsArmtekItem); |
| | | 5898 | | const directItems = items.filter(x => x && ['Armtek', 'ArmtekBy', 'ShateM', 'MlAuto', 'MlAutoRu', 'Avd', 'Ja |
| | | 5899 | | const moskvorechieItems = items.filter(x => x && x.providerKey === 'MoskvorechieIstra'); |
| | | 5900 | | const directProviders = [...new Set(directItems.map(x => x.providerKey))].join(', '); |
| | | 5901 | | summary.textContent = directItems.length > 0 |
| | | 5902 | | ? `${directProviders}: выберите «В корзину и провести» или «Оформить с проведением». Позиций: ${directIt |
| | | 5903 | | : moskvorechieItems.length > 0 |
| | | 5904 | | ? `Москворечье (Истра): позиции будут добавлены в системную и API-корзину после проведения. Заказ у пост |
| | | 5905 | | : queued > 0 |
| | | 5906 | | ? `В корзину поставщика уйдёт позиций: ${queued} из ${items.length}.` |
| | | 5907 | | : `Позиций в корзине: ${items.length}. В API-корзину поставщика ничего не уйдёт.`; |
| | | 5908 | | confirmButton.disabled = items.length === 0; |
| | | 5909 | | confirmButton.innerHTML = directItems.length > 0 |
| | | 5910 | | ? '<i class="fas fa-check me-1"></i>Оформить с проведением' |
| | | 5911 | | : moskvorechieItems.length > 0 |
| | | 5912 | | ? '<i class="fas fa-cart-plus me-1"></i>В корзину и провести' |
| | | 5913 | | : '<i class="fas fa-check me-1"></i>Подтвердить и провести'; |
| | | 5914 | | previewButton.hidden = !hasArmtek || !document.getElementById('armtekCheckoutQueueJson'); |
| | | 5915 | | addToCartButton.hidden = directItems.length === 0; |
| | | 5916 | | addToCartButton.disabled = items.length === 0; |
| | | 5917 | | proceedWithoutSupplierButton.hidden = items.length === 0; |
| | | 5918 | | }; |
| | | 5919 | | |
| | | 5920 | | const ensureShateMOptions = async () => { |
| | | 5921 | | const esc = value => String(value ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'). |
| | | 5922 | | const shateMItems = txGetCartItems().filter(x => x && x.providerKey === 'ShateM'); |
| | | 5923 | | if (!shateMItems.length) return true; |
| | | 5924 | | const optionsField = document.getElementById('shateMCheckoutOptionsJson'); |
| | | 5925 | | const form = body.querySelector('#txShateMCheckoutForm'); |
| | | 5926 | | if (!form) { |
| | | 5927 | | const response = await PageLoadingBar.run(() => fetch('/ShateM/CheckoutData', { credentials: 'same-origi |
| | | 5928 | | const payload = await response.json(); |
| | | 5929 | | if (!response.ok) throw new Error(payload.message || 'Не удалось получить договоры Шате‑М.'); |
| | | 5930 | | const agreements = Array.isArray(payload.agreements) ? payload.agreements : []; |
| | | 5931 | | const addresses = Array.isArray(payload.addresses) ? payload.addresses : []; |
| | | 5932 | | const location = shateMItems[0].directionCode || ''; |
| | | 5933 | | body.insertAdjacentHTML('afterbegin', `<div id="txShateMCheckoutForm" class="alert alert-primary"><stron |
| | | 5934 | | const agreement = body.querySelector('#txShateMCheckoutForm [name="agreement"]'); |
| | | 5935 | | const address = body.querySelector('#txShateMCheckoutForm [name="address"]'); |
| | | 5936 | | const date = body.querySelector('#txShateMCheckoutForm [name="date"]'); |
| | | 5937 | | const isWeekend = value => { const day = new Date(value).getDay(); return day === 0 || day === 6; }; |
| | | 5938 | | // Список дат перезагружается под выбранный адрес; предвыбираем ближайший рабочий день. |
| | | 5939 | | const fillDates = async () => { |
| | | 5940 | | if (!address.value) { date.innerHTML = '<option value="">Сначала выберите адрес</option>'; return; } |
| | | 5941 | | date.innerHTML = '<option>Загрузка…</option>'; |
| | | 5942 | | try { |
| | | 5943 | | const datesResponse = await PageLoadingBar.run(() => fetch(`/ShateM/AvailableDates?locationCode= |
| | | 5944 | | const dates = await datesResponse.json(); |
| | | 5945 | | const list = Array.isArray(dates) ? dates : []; |
| | | 5946 | | date.innerHTML = '<option value="">Выберите дату</option>' + list.map(x => `<option value="${esc |
| | | 5947 | | const sorted = [...list].sort((a, b) => new Date(a) - new Date(b)); |
| | | 5948 | | const preferred = sorted.find(x => !isWeekend(x)) ?? sorted[0]; |
| | | 5949 | | if (preferred) date.value = preferred; |
| | | 5950 | | } catch (error) { |
| | | 5951 | | console.warn('[ShateM] Не удалось загрузить даты доставки', error); |
| | | 5952 | | date.innerHTML = '<option value="">Не удалось загрузить даты. Выберите адрес повторно.</option>' |
| | | 5953 | | } |
| | | 5954 | | }; |
| | | 5955 | | address.addEventListener('change', fillDates); |
| | | 5956 | | if (agreement.options.length > 1) agreement.selectedIndex = 1; |
| | | 5957 | | if (address.options.length > 1) address.selectedIndex = 1; |
| | | 5958 | | await fillDates(); |
| | | 5959 | | return false; |
| | | 5960 | | } |
| | | 5961 | | const value = name => form.querySelector(`[name="${name}"]`)?.value || ''; |
| | | 5962 | | const checked = name => !!form.querySelector(`[name="${name}"]`)?.checked; |
| | | 5963 | | if (!value('agreement') || !value('address') || !value('date') || !checked('terms') || !checked('personal')) |
| | | 5964 | | optionsField.value = JSON.stringify({ agreementCode: value('agreement'), deliveryAddressCode: value('address |
| | | 5965 | | return true; |
| | | 5966 | | }; |
| | | 5967 | | |
| | | 5968 | | txCartConfirmMode = true; |
| | | 5969 | | modalElement.querySelector('.modal-content')?.classList.add('tx-cart-modal--confirm'); |
| | | 5970 | | confirmButton.hidden = false; |
| | | 5971 | | summary.hidden = false; |
| | | 5972 | | printGroupedCartToModal(); |
| | | 5973 | | renderSummary(); |
| | | 5974 | | |
| | | 5975 | | let confirmed = false; |
| | | 5976 | | const onCartChanged = () => renderSummary(); |
| | | 5977 | | const onConfirmClick = async () => { |
| | | 5978 | | try { if (!await ensureShateMOptions()) return; } catch (error) { alert(error instanceof Error ? error.messa |
| | | 5979 | | if (txGetCartItems().some(x => x && ['Armtek', 'ArmtekBy', 'ShateM', 'MlAuto', 'MlAutoRu', 'Avd', 'Japarts', |
| | | 5980 | | window.txSupplierCheckoutMode = 'direct'; |
| | | 5981 | | window.txArmtekCheckoutMode = 'direct'; |
| | | 5982 | | } |
| | | 5983 | | confirmed = true; |
| | | 5984 | | bootstrap.Modal.getOrCreateInstance(modalElement).hide(); |
| | | 5985 | | }; |
| | | 5986 | | const onAddToCartClick = async () => { |
| | | 5987 | | try { if (!await ensureShateMOptions()) return; } catch (error) { alert(error instanceof Error ? error.messa |
| | | 5988 | | window.txSupplierCheckoutMode = 'cart'; |
| | | 5989 | | window.txArmtekCheckoutMode = 'cart'; |
| | | 5990 | | confirmed = true; |
| | | 5991 | | bootstrap.Modal.getOrCreateInstance(modalElement).hide(); |
| | | 5992 | | }; |
| | | 5993 | | const onProceedWithoutSupplierClick = () => { |
| | | 5994 | | // Корзина остаётся на странице как снимок выбранных цен, но её позиции |
| | | 5995 | | // не сериализуются ни в одну очередь API-поставщика. |
| | | 5996 | | window.txSupplierCheckoutMode = 'none'; |
| | | 5997 | | window.txArmtekCheckoutMode = 'none'; |
| | | 5998 | | if (window.txSerializeCartQueues) window.txSerializeCartQueues(); |
| | | 5999 | | confirmed = true; |
| | | 6000 | | bootstrap.Modal.getOrCreateInstance(modalElement).hide(); |
| | | 6001 | | }; |
| | | 6002 | | const onPreviewClick = async () => { |
| | | 6003 | | window.txArmtekCheckoutMode = 'direct'; |
| | | 6004 | | if (window.txSerializeCartQueues) window.txSerializeCartQueues(); |
| | | 6005 | | const field = document.getElementById('armtekCheckoutQueueJson'); |
| | | 6006 | | if (!field || !field.value) return; |
| | | 6007 | | const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value; |
| | | 6008 | | try { |
| | | 6009 | | const data = new FormData(); |
| | | 6010 | | data.append('armtekCheckoutQueueJson', field.value); |
| | | 6011 | | if (token) data.append('__RequestVerificationToken', token); |
| | | 6012 | | const response = await PageLoadingBar.run(() => fetch('/Studio2/OrderToSupplierTransaction/PreviewArmtek |
| | | 6013 | | method: 'POST', headers: { RequestVerificationToken: token || '', Accept: 'application/json' }, body |
| | | 6014 | | })); |
| | | 6015 | | const payload = await response.json(); |
| | | 6016 | | if (!response.ok) throw new Error(payload.error || 'Не удалось сформировать запрос Armtek.'); |
| | | 6017 | | const code = document.getElementById('armtekPreviewJson'); |
| | | 6018 | | if (code) code.textContent = JSON.stringify(payload.orders, null, 2); |
| | | 6019 | | const previewModal = document.getElementById('armtekPreviewModal'); |
| | | 6020 | | if (previewModal && window.bootstrap?.Modal) { |
| | | 6021 | | // Bootstrap не управляет вложенными модалками: переносим предпросмотр в body |
| | | 6022 | | // и явно поднимаем его над открытой корзиной. |
| | | 6023 | | if (previewModal.parentElement !== document.body) document.body.appendChild(previewModal); |
| | | 6024 | | previewModal.style.zIndex = '1070'; |
| | | 6025 | | previewModal.addEventListener('shown.bs.modal', () => { |
| | | 6026 | | const backdrops = document.querySelectorAll('.modal-backdrop'); |
| | | 6027 | | backdrops[backdrops.length - 1]?.style.setProperty('z-index', '1065'); |
| | | 6028 | | }, { once: true }); |
| | | 6029 | | bootstrap.Modal.getOrCreateInstance(previewModal).show(); |
| | | 6030 | | } |
| | | 6031 | | } catch (error) { |
| | | 6032 | | alert(error instanceof Error ? error.message : 'Не удалось сформировать запрос Armtek.'); |
| | | 6033 | | } |
| | | 6034 | | }; |
| | | 6035 | | const onHidden = () => { |
| | | 6036 | | confirmButton.removeEventListener('click', onConfirmClick); |
| | | 6037 | | addToCartButton.removeEventListener('click', onAddToCartClick); |
| | | 6038 | | proceedWithoutSupplierButton.removeEventListener('click', onProceedWithoutSupplierClick); |
| | | 6039 | | previewButton.removeEventListener('click', onPreviewClick); |
| | | 6040 | | document.removeEventListener('tx-cart-changed', onCartChanged); |
| | | 6041 | | confirmButton.hidden = true; |
| | | 6042 | | addToCartButton.hidden = true; |
| | | 6043 | | proceedWithoutSupplierButton.hidden = true; |
| | | 6044 | | summary.hidden = true; |
| | | 6045 | | modalElement.querySelector('.modal-content')?.classList.remove('tx-cart-modal--confirm'); |
| | | 6046 | | txCartConfirmMode = false; |
| | | 6047 | | // Продолжаем только после закрытия модалки, иначе backdrop останется поверх страницы. |
| | | 6048 | | if (confirmed) onConfirm(); |
| | | 6049 | | }; |
| | | 6050 | | |
| | | 6051 | | confirmButton.addEventListener('click', onConfirmClick); |
| | | 6052 | | addToCartButton.addEventListener('click', onAddToCartClick); |
| | | 6053 | | proceedWithoutSupplierButton.addEventListener('click', onProceedWithoutSupplierClick); |
| | | 6054 | | previewButton.addEventListener('click', onPreviewClick); |
| | | 6055 | | document.addEventListener('tx-cart-changed', onCartChanged); |
| | | 6056 | | modalElement.addEventListener('hidden.bs.modal', onHidden, { once: true }); |
| | | 6057 | | bootstrap.Modal.getOrCreateInstance(modalElement).show(); |
| | | 6058 | | }; |
| | | 6059 | | |
| | | 6060 | | // Поставщики, у которых есть реальная API-корзина. Очередь уходит на сервер только со страниц, |
| | | 6061 | | // где объявлены скрытые поля — на остальных транзакциях корзина остаётся справочной. |
| | | 6062 | | window.txArmtekCheckoutMode = 'cart'; |
| | | 6063 | | const TX_CART_QUEUE_PROVIDERS = ['Avd', 'Japarts', 'Berg', 'Armtek', 'ArmtekBy', 'ShateM', 'Motex', 'MlAuto', 'MlAut |
| | | 6064 | | const txCartQueueEnabled = () => !!(document.getElementById('avdBasketQueueJson') |
| | | 6065 | | || document.getElementById('japartsBasketQueueJson') || document.getElementById('japartsCheckoutQueueJson') |
| | | 6066 | | || document.getElementById('bergBasketQueueJson') || document.getElementById('bergCheckoutQueueJson') |
| | | 6067 | | || document.getElementById('armtekBasketQueueJson') || document.getElementById('armtekCheckoutQueueJson') || doc |
| | | 6068 | | const txIsArmtekItem = item => item.providerKey === 'Armtek' || item.providerKey === 'ArmtekBy'; |
| | | 6069 | | |
| | | 6070 | | function txFormatTotals(totals) { |
| | | 6071 | | return Object.entries(totals).map(([currency, total]) => `${total.toFixed(2)} ${currency}`).join(', '); |
| | | 6072 | | } |
| | | 6073 | | |
| | | 6074 | | function printGroupedCartToModal() { |
| | | 6075 | | const data = synchronizeSupplierCartQuantities(JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) |
| | | 6076 | | const body = document.getElementById('cartModalBody'); |
| | | 6077 | | const esc = (typeof escapeHtml === 'function') ? escapeHtml : (v => String(v ?? '')); |
| | | 6078 | | const shorten = (value, maxLength) => { |
| | | 6079 | | const text = String(value ?? ''); |
| | | 6080 | | return text.length > maxLength ? `${text.slice(0, maxLength - 1).trimEnd()}…` : text; |
| | | 6081 | | }; |
| | | 6082 | | |
| | | 6083 | | if (data.length === 0) { |
| | | 6084 | | body.innerHTML = ` |
| | | 6085 | | <div class="text-center text-muted py-4"> |
| | | 6086 | | <i class="fas fa-cart-shopping fa-2x mb-3 d-block"></i> |
| | | 6087 | | Корзина пуста |
| | | 6088 | | </div>`; |
| | | 6089 | | return; |
| | | 6090 | | } |
| | | 6091 | | |
| | | 6092 | | // Табличная часть корзины — AG Grid: сортировка/изменение ширины колонок доступны без |
| | | 6093 | | // потери существующего Bootstrap-modal и сценария удаления позиции. |
| | | 6094 | | if (window.agGrid) { |
| | | 6095 | | renderCartAgGrid(data, body, esc); |
| | | 6096 | | return; |
| | | 6097 | | } |
| | | 6098 | | |
| | | 6099 | | const queueEnabled = txCartQueueEnabled(); |
| | | 6100 | | const blocks = { |
| | | 6101 | | api: { |
| | | 6102 | | title: 'Уйдёт в API-корзину поставщика', |
| | | 6103 | | hint: 'АВД, Армтек, Форум-Авто и Фаворит: после успешного проведения документа позиции добавятся в корзи |
| | | 6104 | | modifier: 'tx-cart-block--api', |
| | | 6105 | | groups: {}, |
| | | 6106 | | totals: {}, |
| | | 6107 | | count: 0 |
| | | 6108 | | }, |
| | | 6109 | | doc: { |
| | | 6110 | | title: 'Только в документе', |
| | | 6111 | | hint: queueEnabled |
| | | 6112 | | ? 'Цена и поставщик подставятся в документ. В корзину поставщика эти позиции не уйдут.' |
| | | 6113 | | : 'На этой странице корзина поставщика не отправляется — цена и поставщик только подставятся в докум |
| | | 6114 | | modifier: '', |
| | | 6115 | | groups: {}, |
| | | 6116 | | totals: {}, |
| | | 6117 | | count: 0 |
| | | 6118 | | } |
| | | 6119 | | }; |
| | | 6120 | | const grandTotalSum = {}; |
| | | 6121 | | |
| | | 6122 | | data.forEach(item => { |
| | | 6123 | | const producer = item.produser?.trim() || '-'; |
| | | 6124 | | const currency = item.currencyCode?.trim() || '-'; |
| | | 6125 | | const quantity = item.quantity || 1; |
| | | 6126 | | const price = item.priceValue || 0; |
| | | 6127 | | const sum = quantity * price; |
| | | 6128 | | |
| | | 6129 | | grandTotalSum[currency] = (grandTotalSum[currency] || 0) + sum; |
| | | 6130 | | |
| | | 6131 | | const block = (queueEnabled && TX_CART_QUEUE_PROVIDERS.includes(item.providerKey)) ? blocks.api : blocks.doc |
| | | 6132 | | block.count += 1; |
| | | 6133 | | block.totals[currency] = (block.totals[currency] || 0) + sum; |
| | | 6134 | | if (!block.groups[producer]) { |
| | | 6135 | | block.groups[producer] = { items: [], siteUrl: item.siteUrl || '', totalSum: {} }; |
| | | 6136 | | } |
| | | 6137 | | block.groups[producer].items.push({ ...item, quantity, price, sum, currency }); |
| | | 6138 | | block.groups[producer].totalSum[currency] = (block.groups[producer].totalSum[currency] || 0) + sum; |
| | | 6139 | | }); |
| | | 6140 | | |
| | | 6141 | | const renderGroup = (producer, group) => { |
| | | 6142 | | const logo = (typeof getSupplierLogoMarkup === 'function') |
| | | 6143 | | ? getSupplierLogoMarkup({ siteUrl: group.siteUrl, title: producer }) |
| | | 6144 | | : ''; |
| | | 6145 | | // Колонка «Клиент и адрес» имеет смысл только для Армтека — у остальных она была бы пустой. |
| | | 6146 | | const withDelivery = group.items.some(txIsArmtekItem); |
| | | 6147 | | let group_html = `<div class="tx-cart-group card mb-3">`; |
| | | 6148 | | group_html += `<div class="card-header tx-cart-group-header"><span class="d-inline-flex align-items-center"> |
| | | 6149 | | group_html += `<div class="table-responsive"><table class="table table-sm table-striped table-hover align-mi |
| | | 6150 | | group_html += `<thead class="table-light"><tr> |
| | | 6151 | | <th></th> |
| | | 6152 | | <th>Товар<span class="tx-cart-th-hint">Артикул и производитель</span></th> |
| | | 6153 | | <th>Заказ<span class="tx-cart-th-hint">Отправление и клиент</span></th> |
| | | 6154 | | ${withDelivery ? '<th>Клиент и адрес<span class="tx-cart-th-hint">KUNRG и адрес доставки Armtek</span></ |
| | | 6155 | | <th class="text-end">Наличие</th> |
| | | 6156 | | <th class="text-end">Срок</th> |
| | | 6157 | | <th class="text-end">Кол-во</th> |
| | | 6158 | | <th class="text-end">Цена</th> |
| | | 6159 | | <th class="text-end">Сумма</th> |
| | | 6160 | | <th></th> |
| | | 6161 | | </tr></thead><tbody>`; |
| | | 6162 | | group.items.forEach(item => { |
| | | 6163 | | const hasValidProductImage = typeof item.productImage === 'string' |
| | | 6164 | | && (/^https?:\/\//i.test(item.productImage) || item.productImage.startsWith('/')); |
| | | 6165 | | const img = hasValidProductImage |
| | | 6166 | | ? ('<img ' + 'src="' + esc(item.productImage) + '" alt="" style="width:40px;height:40px;object-fit:c |
| | | 6167 | | : '<div class="d-flex align-items-center justify-content-center text-muted" style="width:40px;height |
| | | 6168 | | const orderLabel = item.shipmentNumber || `№${esc(item.orderId)}`; |
| | | 6169 | | const restNumber = Number.parseFloat(String(item.rest ?? '').replace(',', '.')); |
| | | 6170 | | const hasRestInfo = Number.isFinite(restNumber); |
| | | 6171 | | const notEnoughStock = hasRestInfo && item.quantity > restNumber; |
| | | 6172 | | const tightStock = hasRestInfo && !notEnoughStock && item.quantity === restNumber; |
| | | 6173 | | const minimumQuantity = Number.parseFloat(String(item.minQuantity ?? 1).replace(',', '.')) || 1; |
| | | 6174 | | const mlAutoMinimumNotMet = item.providerKey === 'MlAuto' && item.quantity < minimumQuantity; |
| | | 6175 | | const mikadoExpressId = Math.max(0, Number(item.expressId) || 0); |
| | | 6176 | | const [mikadoOrderCode, mikadoStockId] = String(item.stokId || '').split('|'); |
| | | 6177 | | const mikadoOptions = item.providerKey === 'MikadoMskHod20' |
| | | 6178 | | ? `<div class="tx-cart-mikado-options" role="group" aria-label="Параметры поставки Микадо"> |
| | | 6179 | | <span class="tx-cart-mikado-options__title"><i class="fas fa-truck" aria-hidden="true"></i> |
| | | 6180 | | <span class="tx-cart-mikado-options__field"><span>Тип поставки</span><b>Обычная поставка</b> |
| | | 6181 | | <label class="tx-cart-mikado-options__field"><span>Экспресс-доставка</span><select data-mika |
| | | 6182 | | </div>` : ''; |
| | | 6183 | | const restCell = hasRestInfo |
| | | 6184 | | ? notEnoughStock |
| | | 6185 | | ? `<span class="text-danger fw-semibold">${esc(item.rest)} <i class="fas fa-triangle-exclamation |
| | | 6186 | | : tightStock |
| | | 6187 | | ? `<span class="text-warning fw-semibold">${esc(item.rest)} <i class="fas fa-triangle-exclam |
| | | 6188 | | : `<span>${esc(item.rest)}</span>` |
| | | 6189 | | : '<span class="text-muted">-</span>'; |
| | | 6190 | | const unlinkedArmtek = txIsArmtekItem(item) && !item.deliveryLinked; |
| | | 6191 | | const rowClasses = [ |
| | | 6192 | | notEnoughStock ? 'tx-cart-row--short' : '', |
| | | 6193 | | unlinkedArmtek ? 'tx-cart-row--unlinked' : '', |
| | | 6194 | | mlAutoMinimumNotMet ? 'tx-cart-row--min-quantity' : '' |
| | | 6195 | | ].filter(Boolean).join(' '); |
| | | 6196 | | let deliveryCell = ''; |
| | | 6197 | | if (withDelivery) { |
| | | 6198 | | if (!txIsArmtekItem(item)) { |
| | | 6199 | | deliveryCell = '<td class="tx-cart-cell--delivery text-muted">-</td>'; |
| | | 6200 | | } else if (item.deliveryLinked) { |
| | | 6201 | | const buyerTitle = item.buyerName ? `${item.buyerKunnr} — ${item.buyerName}` : item.buyerKunnr; |
| | | 6202 | | deliveryCell = `<td class="tx-cart-cell--delivery"> |
| | | 6203 | | <div class="tx-cart-truncate tx-cart-order-number" title="${esc(buyerTitle || '-')}">${esc(i |
| | | 6204 | | <div class="tx-cart-truncate tx-cart-meta" title="${esc(item.deliveryAddress || '-')}">${esc |
| | | 6205 | | </td>`; |
| | | 6206 | | } else { |
| | | 6207 | | const settingsLink = item.deliverySettingsUrl |
| | | 6208 | | ? `<a class="tx-cart-delivery-settings" href="${esc(item.deliverySettingsUrl)}" target="_bla |
| | | 6209 | | : ''; |
| | | 6210 | | deliveryCell = `<td class="tx-cart-cell--delivery"> |
| | | 6211 | | <span class="tx-cart-delivery-warning" title="Склад отгрузки заказа не связан с адресом дост |
| | | 6212 | | </td>`; |
| | | 6213 | | } |
| | | 6214 | | } |
| | | 6215 | | group_html += ` |
| | | 6216 | | <tr${rowClasses ? ` class="${rowClasses}"` : ''}> |
| | | 6217 | | <td>${img}</td> |
| | | 6218 | | <td class="tx-cart-cell--product"> |
| | | 6219 | | <div class="tx-cart-truncate tx-cart-product-name" title="${esc(item.itemName || '')}">${esc |
| | | 6220 | | <div class="tx-cart-truncate tx-cart-meta" title="Арт. ${esc(item.article || '-')} · ${esc(i |
| | | 6221 | | </td> |
| | | 6222 | | <td class="tx-cart-cell--order"> |
| | | 6223 | | <div class="tx-cart-truncate tx-cart-order-number" title="${esc(orderLabel)}">${esc(shorten( |
| | | 6224 | | <div class="tx-cart-truncate tx-cart-meta" title="${esc(item.clientName || '-')}">${esc(shor |
| | | 6225 | | </td> |
| | | 6226 | | ${deliveryCell} |
| | | 6227 | | <td class="text-end">${restCell}</td> |
| | | 6228 | | <td class="text-end">${item.deliveryDays ? esc(item.deliveryDays) + ' дн.' : '<span class="text- |
| | | 6229 | | <td class="text-end">${item.quantity}${mlAutoMinimumNotMet ? `<div class="tx-cart-min-quantity-w |
| | | 6230 | | <td class="text-end">${item.price.toFixed(2)} ${esc(item.currency)}</td> |
| | | 6231 | | <td class="text-end fw-semibold">${item.sum.toFixed(2)} ${esc(item.currency)}</td> |
| | | 6232 | | <td class="text-end"> |
| | | 6233 | | <button type="button" class="btn btn-sm btn-outline-danger cart-item-remove tx-cart-remove" |
| | | 6234 | | data-order-id="${esc(item.orderId)}" data-stok-id="${esc(item.stokId)}" title="Удали |
| | | 6235 | | <i class="fas fa-times"></i> |
| | | 6236 | | </button> |
| | | 6237 | | </td> |
| | | 6238 | | </tr>${mikadoOptions ? `<tr class="tx-cart-mikado-options-row"><td colspan="${withDelivery ? 10 : 9} |
| | | 6239 | | }); |
| | | 6240 | | group_html += `</tbody></table></div>`; |
| | | 6241 | | group_html += `<div class="tx-cart-provider-total">Итого по поставщику: <b>${txFormatTotals(group.totalSum)} |
| | | 6242 | | group_html += `</div>`; |
| | | 6243 | | return group_html; |
| | | 6244 | | }; |
| | | 6245 | | |
| | | 6246 | | let html = txCartConfirmMode |
| | | 6247 | | ? '<div class="tx-cart-confirm-notice"><i class="fas fa-circle-info mt-1"></i><span>Проверьте состав корзины |
| | | 6248 | | : ''; |
| | | 6249 | | html += txRenderCartNotices(data); |
| | | 6250 | | [blocks.api, blocks.doc].forEach(block => { |
| | | 6251 | | if (block.count === 0) return; |
| | | 6252 | | html += `<div class="tx-cart-block ${block.modifier}">`; |
| | | 6253 | | html += `<div class="tx-cart-block-header"><span class="tx-cart-block-title">${esc(block.title)}</span><span |
| | | 6254 | | html += `<div class="tx-cart-block-body">`; |
| | | 6255 | | for (const [producer, group] of Object.entries(block.groups)) { |
| | | 6256 | | html += renderGroup(producer, group); |
| | | 6257 | | } |
| | | 6258 | | html += `</div></div>`; |
| | | 6259 | | }); |
| | | 6260 | | html += `<div class="tx-cart-grand-total"><span>Общая сумма</span><b>${txFormatTotals(grandTotalSum)}</b></div>` |
| | | 6261 | | |
| | | 6262 | | body.innerHTML = html; |
| | | 6263 | | queueMicrotask(loadMikadoExpressOptions); |
| | | 6264 | | if (typeof initTooltip === 'function') { |
| | | 6265 | | body.querySelectorAll('[title]').forEach(initTooltip); |
| | | 6266 | | } |
| | | 6267 | | } |
| | | 6268 | | |
| | | 6269 | | let txCartGridApi = null; |
| | | 6270 | | function renderCartAgGrid(data, body, esc) { |
| | | 6271 | | if (txCartGridApi) { txCartGridApi.destroy(); txCartGridApi = null; } |
| | | 6272 | | const money = value => `${Number(value || 0).toFixed(2)}`; |
| | | 6273 | | const rows = data.map(item => ({ ...item, quantity: Number(item.quantity) || 1, price: Number(item.priceValue) | |
| | | 6274 | | sum: (Number(item.quantity) || 1) * (Number(item.priceValue) || 0), currency: item.currencyCode || '-' })); |
| | | 6275 | | const totals = {}; |
| | | 6276 | | rows.forEach(row => totals[row.currency] = (totals[row.currency] || 0) + row.sum); |
| | | 6277 | | body.innerHTML = (txCartConfirmMode ? '<div class="tx-cart-confirm-notice"><i class="fas fa-circle-info mt-1"></ |
| | | 6278 | | txRenderCartNotices(rows) + '<div id="txCartAgGrid" class="ag-theme-quartz tx-cart-ag-grid"></div>'; |
| | | 6279 | | const grid = body.querySelector('#txCartAgGrid'); |
| | | 6280 | | grid.style.height = `${Math.min(560, Math.max(190, rows.length * 52 + 96))}px`; |
| | | 6281 | | txCartGridApi = agGrid.createGrid(grid, { |
| | | 6282 | | rowData: rows, |
| | | 6283 | | getRowId: p => `${p.data.orderId}|${p.data.stokId}`, |
| | | 6284 | | columnDefs: [ |
| | | 6285 | | { headerName: '', width: 58, sortable: false, resizable: false, cellRenderer: p => { |
| | | 6286 | | // Элемент вместо HTML-строки с тегом картинки: такой литерал в inline-скрипте |
| | | 6287 | | // представления однажды ушёл на сервер как относительный URL (#2273). |
| | | 6288 | | const src = p.data.productImage; |
| | | 6289 | | if (!src || !(/^https?:\/\//i.test(src) || src.startsWith('/'))) return '<i class="fas fa-image text |
| | | 6290 | | const img = document.createElement('img'); |
| | | 6291 | | img.className = 'tx-cart-ag-image'; |
| | | 6292 | | img.alt = ''; |
| | | 6293 | | img.src = src; |
| | | 6294 | | return img; |
| | | 6295 | | } }, |
| | | 6296 | | { headerName: 'Товар', flex: 2, minWidth: 190, valueGetter: p => `${p.data.article || ''} ${p.data.manuf |
| | | 6297 | | { headerName: 'Заказ', flex: 1, minWidth: 145, valueGetter: p => `${p.data.shipmentNumber || p.data.orde |
| | | 6298 | | { headerName: 'Поставщик', minWidth: 130, valueGetter: p => p.data.produser || p.data.providerKey || '-' |
| | | 6299 | | { headerName: 'Наличие', width: 95, type: 'rightAligned', valueGetter: p => p.data.rest ?? '', cellRende |
| | | 6300 | | { headerName: 'Срок', width: 85, type: 'rightAligned', valueGetter: p => p.data.deliveryDays ?? '', cell |
| | | 6301 | | { headerName: 'Кол-во', width: 88, type: 'rightAligned', field: 'quantity' }, |
| | | 6302 | | { headerName: 'Цена', width: 125, type: 'rightAligned', valueGetter: p => p.data.price, cellRenderer: p |
| | | 6303 | | { headerName: 'Сумма', width: 135, type: 'rightAligned', valueGetter: p => p.data.sum, cellRenderer: p = |
| | | 6304 | | { headerName: '', width: 54, sortable: false, resizable: false, cellRenderer: p => `<button type="button |
| | | 6305 | | ], |
| | | 6306 | | pinnedBottomRowData: [{ itemName: 'Общая сумма', currency: '', sum: Object.entries(totals).map(([c, v]) => ` |
| | | 6307 | | defaultColDef: { sortable: true, resizable: true, filter: true, suppressHeaderMenuButton: true }, |
| | | 6308 | | suppressCellFocus: true, |
| | | 6309 | | onGridPreDestroyed: () => { txCartGridApi = null; } |
| | | 6310 | | }); |
| | | 6311 | | } |
| | | 6312 | | |
| | | 6313 | | // Предупреждения над блоками: не блокируют отправку, но должны быть замечены до подтверждения. |
| | | 6314 | | function txRenderCartNotices(items) { |
| | | 6315 | | const esc = (typeof escapeHtml === 'function') ? escapeHtml : (v => String(v ?? '')); |
| | | 6316 | | const notices = []; |
| | | 6317 | | const unlinked = items.filter(x => txIsArmtekItem(x) && !x.deliveryLinked).length; |
| | | 6318 | | if (unlinked > 0) { |
| | | 6319 | | notices.push(`Позиций Армтека без связи склада отгрузки с адресом доставки: ${unlinked}. Проценка по ним вып |
| | | 6320 | | } |
| | | 6321 | | const shortStock = items.filter(x => { |
| | | 6322 | | const rest = Number.parseFloat(String(x.rest ?? '').replace(',', '.')); |
| | | 6323 | | return Number.isFinite(rest) && (x.quantity || 1) > rest; |
| | | 6324 | | }).length; |
| | | 6325 | | if (shortStock > 0) { |
| | | 6326 | | notices.push(`Позиций, где заказано больше остатка поставщика: ${shortStock}.`); |
| | | 6327 | | } |
| | | 6328 | | const mlAutoMinimum = items.filter(x => { |
| | | 6329 | | const minimum = Number.parseFloat(String(x.minQuantity ?? 1).replace(',', '.')) || 1; |
| | | 6330 | | return x.providerKey === 'MlAuto' && (x.quantity || 1) < minimum; |
| | | 6331 | | }).length; |
| | | 6332 | | if (mlAutoMinimum > 0) { |
| | | 6333 | | notices.push(`Позиций ML-Auto с количеством меньше минимального: ${mlAutoMinimum}. Они не будут добавлены в |
| | | 6334 | | } |
| | | 6335 | | return notices |
| | | 6336 | | .map(text => `<div class="tx-cart-confirm-notice"><i class="fas fa-triangle-exclamation mt-1"></i><span>${es |
| | | 6337 | | .join(''); |
| | | 6338 | | } |
| | | 6339 | | |
| | | 6340 | | $(document).on('click', '.cart-item-remove', function () { |
| | | 6341 | | const orderId = String($(this).data('order-id')); |
| | | 6342 | | const stokId = String($(this).data('stok-id')); |
| | | 6343 | | removeCartItem(orderId, stokId); |
| | | 6344 | | }); |
| | | 6345 | | |
| | | 6346 | | function removeCartItem(orderId, stokId) { |
| | | 6347 | | let list = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + ids) || '[]'); |
| | | 6348 | | list = list.filter(x => !(String(x.orderId) === orderId && String(x.stokId) === stokId)); |
| | | 6349 | | inMemoryCartStorage.setItem('selectedStockList' + ids, JSON.stringify(list)); |
| | | 6350 | | |
| | | 6351 | | const $btn = $(`.stok-order[data-order-id="${orderId}"][data-stok-id="${stokId}"]`); |
| | | 6352 | | if ($btn.length) setCartButtonState($btn, false); |
| | | 6353 | | clearRowHighlight(orderId); |
| | | 6354 | | |
| | | 6355 | | printGroupedCartToModal(); |
| | | 6356 | | updateCartBadge(); |
| | | 6357 | | document.dispatchEvent(new CustomEvent('tx-cart-changed')); |
| | | 6358 | | } |
| | | 6359 | | |
| | | 6360 | | function trySetSupplierFromSource(orderId, preferredSupplierId) { |
| | | 6361 | | if (!preferredSupplierId) return null; |
| | | 6362 | | const supplierSelect = document.querySelector(`#SupplierId_${orderId}`); |
| | | 6363 | | if (!supplierSelect || !supplierSelect.options || supplierSelect.options.length === 0) return null; |
| | | 6364 | | const options = Array.from(supplierSelect.options); |
| | | 6365 | | const targetOption = options.find(o => String(o.value) === String(preferredSupplierId)); |
| | | 6366 | | if (!targetOption) return null; |
| | | 6367 | | |
| | | 6368 | | if (supplierSelect.value !== targetOption.value) { |
| | | 6369 | | supplierSelect.value = targetOption.value; |
| | | 6370 | | supplierSelect.dispatchEvent(new Event('change', { bubbles: true })); |
| | | 6371 | | } |
| | | 6372 | | return targetOption; |
| | | 6373 | | } |
| | | 6374 | | |
| | | 6375 | | function resolveSourceMeta(orderId, index, itemId, rowKey) { |
| | | 6376 | | const meta = { site: '', currencyCode: '', sourcePrice: '', sourceCurrencyCode: '', preferredSupplierId: '', pre |
| | | 6377 | | let tableRow = null; |
| | | 6378 | | if (rowKey) { |
| | | 6379 | | tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${rowKey}"]`); |
| | | 6380 | | } |
| | | 6381 | | if (!tableRow) { |
| | | 6382 | | tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-item-id="${itemId}"]`); |
| | | 6383 | | } |
| | | 6384 | | if (tableRow) { |
| | | 6385 | | const priceLink = tableRow.querySelector(`.price[data-order-id="${orderId}"][data-index-row="${index}"]`); |
| | | 6386 | | if (priceLink) { |
| | | 6387 | | meta.site = priceLink.getAttribute('data-site') || ''; |
| | | 6388 | | meta.currencyCode = priceLink.getAttribute('data-currency-code') || ''; |
| | | 6389 | | meta.sourcePrice = priceLink.getAttribute('data-source-price') || ''; |
| | | 6390 | | meta.sourceCurrencyCode = priceLink.getAttribute('data-source-currency-code') || ''; |
| | | 6391 | | meta.preferredSupplierId = priceLink.getAttribute('data-preferred-supplier-id') || ''; |
| | | 6392 | | meta.preferredSupplierCurrencyCode = priceLink.getAttribute('data-preferred-supplier-currency-code') || |
| | | 6393 | | } |
| | | 6394 | | if (!meta.site) { |
| | | 6395 | | const cartButton = tableRow.querySelector(`.stok-order[data-order-id="${orderId}"][data-index-row="${ind |
| | | 6396 | | if (cartButton) { |
| | | 6397 | | meta.site = cartButton.getAttribute('data-supplier-url') || ''; |
| | | 6398 | | } |
| | | 6399 | | } |
| | | 6400 | | } |
| | | 6401 | | return meta; |
| | | 6402 | | } |
| | | 6403 | | |
| | | 6404 | | function setPurchasePrice(orderId, price, index, itemId, rowKey) { |
| | | 6405 | | console.log(`Установка цены для orderId ${orderId}, index ${index}: ${price}`); |
| | | 6406 | | try { |
| | | 6407 | | const sourceMeta = resolveSourceMeta(orderId, index, itemId, rowKey); |
| | | 6408 | | const selectedSupplier = trySetSupplierFromSource(orderId, sourceMeta.preferredSupplierId); |
| | | 6409 | | const supplierCurrency = selectedSupplier?.dataset.currencyCode || ''; |
| | | 6410 | | const canUseSourcePrice = sourceMeta.sourcePrice |
| | | 6411 | | && sourceMeta.sourceCurrencyCode |
| | | 6412 | | && supplierCurrency.toUpperCase() === sourceMeta.sourceCurrencyCode.toUpperCase(); |
| | | 6413 | | const targetRate = apiSupplierCurrencyRates[supplierCurrency.toUpperCase()]; |
| | | 6414 | | const effectivePrice = canUseSourcePrice |
| | | 6415 | | ? sourceMeta.sourcePrice |
| | | 6416 | | : selectedSupplier && Number.isFinite(targetRate) && targetRate > 0 |
| | | 6417 | | ? (Number.parseFloat(String(price).replace(',', '.')) / targetRate).toFixed(4) |
| | | 6418 | | : price; |
| | | 6419 | | |
| | | 6420 | | // Установка цены |
| | | 6421 | | const input = document.querySelector(`#PurchasePrice_${orderId}`); |
| | | 6422 | | if (input) { |
| | | 6423 | | const formattedPrice = effectivePrice.toString().replace(',', '.'); |
| | | 6424 | | input.value = formattedPrice; |
| | | 6425 | | const inputEvent = new Event('input', {bubbles: true}); |
| | | 6426 | | input.dispatchEvent(inputEvent); |
| | | 6427 | | } |
| | | 6428 | | updateAllPricesForOrder(orderId) |
| | | 6429 | | |
| | | 6430 | | let tableRow = null; |
| | | 6431 | | if (rowKey) { |
| | | 6432 | | tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${rowKey}"]`); |
| | | 6433 | | } |
| | | 6434 | | if (!tableRow) { |
| | | 6435 | | tableRow = document.querySelector(`tr[data-order-id="${orderId}"][data-item-id="${itemId}"]`); |
| | | 6436 | | } |
| | | 6437 | | // --- Логика кеширования и переключения подсветки --- |
| | | 6438 | | window.highlightedItem = window.highlightedItem || {}; |
| | | 6439 | | const currentRowKey = rowKey || `${index}__${itemId || ''}`; |
| | | 6440 | | const previousRowKey = window.highlightedItem[orderId] || null; |
| | | 6441 | | const isCurrentlyHighlighted = tableRow && tableRow.classList.contains('tx-trade-row--selected'); |
| | | 6442 | | |
| | | 6443 | | // Снимаем подсветку только с ранее выбранной строки (без массового мерцания) |
| | | 6444 | | if (previousRowKey && previousRowKey !== currentRowKey) { |
| | | 6445 | | const prevRow = document.querySelector(`tr[data-order-id="${orderId}"][data-row-key="${previousRowKey}"] |
| | | 6446 | | if (prevRow) prevRow.classList.remove('tx-trade-row--selected'); |
| | | 6447 | | } |
| | | 6448 | | |
| | | 6449 | | // Если строка была подсвечена, то снимаем подсветку и очищаем кеш. |
| | | 6450 | | if (isCurrentlyHighlighted) { |
| | | 6451 | | if (tableRow) tableRow.classList.remove('tx-trade-row--selected'); |
| | | 6452 | | delete window.highlightedItem[orderId]; |
| | | 6453 | | return; |
| | | 6454 | | } |
| | | 6455 | | |
| | | 6456 | | // Если не была, то подсвечиваем ее и сохраняем itemId в кеш |
| | | 6457 | | if (tableRow) { |
| | | 6458 | | tableRow.classList.add('tx-trade-row--selected'); |
| | | 6459 | | window.highlightedItem[orderId] = currentRowKey; |
| | | 6460 | | } |
| | | 6461 | | |
| | | 6462 | | const clickedButton = document.querySelector(` |
| | | 6463 | | .stok-order[data-order-id="${orderId}"][data-index-row="${index}"], |
| | | 6464 | | .price[data-order-id="${orderId}"][data-index-row="${index}"] |
| | | 6465 | | `); |
| | | 6466 | | if (clickedButton) { |
| | | 6467 | | clickedButton.classList.add('highlight-button'); |
| | | 6468 | | setTimeout(() => { |
| | | 6469 | | clickedButton.classList.remove('highlight-button'); |
| | | 6470 | | }, 1000); |
| | | 6471 | | } |
| | | 6472 | | |
| | | 6473 | | } catch (error) { |
| | | 6474 | | console.error('Error in setPurchasePrice:', error); |
| | | 6475 | | } |
| | | 6476 | | } |
| | | 6477 | | |
| | | 6478 | | document.addEventListener("DOMContentLoaded", function () { |
| | | 6479 | | const chatToggle = document.getElementById("chatToggle"); |
| | | 6480 | | |
| | | 6481 | | // Заменяем атрибуты |
| | | 6482 | | chatToggle.setAttribute("data-bs-toggle", "modal"); |
| | | 6483 | | chatToggle.setAttribute("data-bs-target", "#cartModal"); |
| | | 6484 | | chatToggle.setAttribute("aria-expanded", "false"); |
| | | 6485 | | chatToggle.setAttribute("title", "Открыть корзину"); |
| | | 6486 | | chatToggle.classList.add("shadow-none"); |
| | | 6487 | | |
| | | 6488 | | // Очищаем старое содержимое |
| | | 6489 | | chatToggle.innerHTML = ` |
| | | 6490 | | <svg xmlns="http://www.w3.org/2000/svg" |
| | | 6491 | | width="20" |
| | | 6492 | | height="20" |
| | | 6493 | | fill="currentColor" |
| | | 6494 | | class="bi bi-cart" |
| | | 6495 | | viewBox="0 0 16 16"> |
| | | 6496 | | <path d="M0 1.5A.5 .5 0 0 1 .5 1H2a.5 .5 0 0 1 .485.379L2.89 3H14.5a.5 .5 0 0 1 .491.592l-1.5 8A.5 .5 0 |
| | | 6497 | | </svg> |
| | | 6498 | | <span id="cartBadge" class="d-none badge rounded-pill bg-danger position-absolute top-0 start-100 translate- |
| | | 6499 | | style="font-size:.65rem;line-height:1;min-width:1.15rem;height:1.15rem;display:flex;align-items:center |
| | | 6500 | | <span class="visually-hidden">товаров в корзине</span> |
| | | 6501 | | </span> |
| | | 6502 | | `; |
| | | 6503 | | updateCartBadge(); |
| | | 6504 | | }); |
| | | 6505 | | document.addEventListener('click', function (e) { |
| | | 6506 | | if (e.target.classList.contains('weight-value')) { |
| | | 6507 | | let weight = e.target.getAttribute('data-weight') || ''; |
| | | 6508 | | const orderId = e.target.getAttribute('data-order-id'); |
| | | 6509 | | const weightInput = document.querySelector(`#ProductWeight_${orderId}`); |
| | | 6510 | | |
| | | 6511 | | if (weightInput) { |
| | | 6512 | | // Очистка строки: убираем "кг" и пробелы |
| | | 6513 | | weight = weight.replace(/кг/gi, '').trim(); |
| | | 6514 | | |
| | | 6515 | | // Заменяем запятые на точки |
| | | 6516 | | weight = weight.replace(/,/g, '.'); |
| | | 6517 | | |
| | | 6518 | | // Берём первое число из строки (если несколько значений через запятую/пробел) |
| | | 6519 | | const match = weight.match(/[\d.]+/g); |
| | | 6520 | | const firstWeight = match ? match[0] : null; |
| | | 6521 | | |
| | | 6522 | | if (firstWeight) { |
| | | 6523 | | // Форматируем вес (parseFloat убирает лишние нули) |
| | | 6524 | | const formattedWeight = parseFloat(firstWeight).toString(); |
| | | 6525 | | |
| | | 6526 | | // Устанавливаем значение |
| | | 6527 | | weightInput.value = formattedWeight; |
| | | 6528 | | |
| | | 6529 | | // Триггерим события |
| | | 6530 | | const eventsToTrigger = ['input', 'change', 'blur']; |
| | | 6531 | | eventsToTrigger.forEach(eventType => { |
| | | 6532 | | const event = new Event(eventType, {bubbles: true, cancelable: true}); |
| | | 6533 | | weightInput.dispatchEvent(event); |
| | | 6534 | | }); |
| | | 6535 | | |
| | | 6536 | | // Для jQuery (если используется) |
| | | 6537 | | if (typeof jQuery !== 'undefined') { |
| | | 6538 | | $(weightInput).trigger('input').trigger('change'); |
| | | 6539 | | } |
| | | 6540 | | |
| | | 6541 | | // Подсвечиваем поле на 1 секунду |
| | | 6542 | | weightInput.style.backgroundColor = '#e6ffe6'; |
| | | 6543 | | setTimeout(() => { |
| | | 6544 | | weightInput.style.backgroundColor = ''; |
| | | 6545 | | }, 1000); |
| | | 6546 | | |
| | | 6547 | | // Фокусируем поле ввода |
| | | 6548 | | weightInput.focus(); |
| | | 6549 | | } else { |
| | | 6550 | | console.warn(`Не удалось распарсить вес: "${weight}"`); |
| | | 6551 | | } |
| | | 6552 | | |
| | | 6553 | | sortAndRerenderTable(orderId, 'costPrice', 'number'); |
| | | 6554 | | } else { |
| | | 6555 | | console.warn(`Не найдено поле ввода веса для orderId: ${orderId}`); |
| | | 6556 | | } |
| | | 6557 | | } |
| | | 6558 | | }); |
| | | 6559 | | |
| | | 6560 | | const rates = { |
| | | 6561 | | 'RUB': 1, |
| | 0 | 6562 | | 'USD': parseFloat('@Model.RateUSD'.replace(',', '.')), |
| | 0 | 6563 | | 'EUR': parseFloat('@Model.RateEUR'.replace(',', '.')), |
| | 0 | 6564 | | 'BYN': parseFloat('@Model.RateBYN'.replace(',', '.')) |
| | | 6565 | | }; |
| | | 6566 | | |
| | | 6567 | | function updatePriceDisplay(orderId, indexRow) { |
| | | 6568 | | const $priceLink = $(`.price-link[data-order-id="${orderId}"][data-index-row="${indexRow}"]`); |
| | | 6569 | | const $priceBlock = $(`.cost-price-row[data-order-id="${orderId}"][data-index-row="${indexRow}"]`); |
| | | 6570 | | const $weightInput = $(`#ProductWeight_${orderId}`); |
| | | 6571 | | const $supplierSelect = $(`#SupplierId_${orderId}`); |
| | | 6572 | | const selectedSupplier = $supplierSelect.find('option:selected'); |
| | | 6573 | | |
| | | 6574 | | |
| | | 6575 | | const weight = parseFloat(($weightInput.val() || '0').replace(',', '.')) || 0; |
| | | 6576 | | |
| | | 6577 | | const priceSite = $priceLink.data('site'); |
| | | 6578 | | const supplierOptionBySite = $supplierSelect.find(`option[data-site="${priceSite}"]`); |
| | | 6579 | | const weightFactor = parseFloat((supplierOptionBySite.data('weight-factor') || '0').toString().replace(',', '.') |
| | | 6580 | | const costFactor = parseFloat((supplierOptionBySite.data('cost-factor') || '1').toString().replace(',', '.')) || |
| | | 6581 | | const weightCurrency = supplierOptionBySite.data('weight-currency-code') || 'RUB'; |
| | | 6582 | | |
| | | 6583 | | const rawPrice = parseFloat(($priceLink.data('price') || '0').toString().replace(',', '.')) || 0; |
| | | 6584 | | const priceCurrency = $priceLink.data('currency-code') || 'RUB'; |
| | | 6585 | | |
| | | 6586 | | const ratePrice = rates[priceCurrency] || 1; |
| | | 6587 | | const rateWeight = rates[weightCurrency] || 1; |
| | | 6588 | | |
| | | 6589 | | const pricePerKgRUB = weightFactor * rateWeight; |
| | | 6590 | | const totalWeightCost = weight * pricePerKgRUB; |
| | | 6591 | | |
| | | 6592 | | const baseCostAttr = $priceBlock.attr('data-base-cost-rub'); |
| | | 6593 | | let baseCostRub = NaN; |
| | | 6594 | | if (baseCostAttr !== undefined && baseCostAttr !== null && String(baseCostAttr).trim() !== '') { |
| | | 6595 | | baseCostRub = parseFloat(String(baseCostAttr).replace(/\s/g, '').replace(',', '.')); |
| | | 6596 | | } |
| | | 6597 | | |
| | | 6598 | | let purchasePriceRUB; |
| | | 6599 | | let title; |
| | | 6600 | | if (!isNaN(baseCostRub) && baseCostRub >= 0) { |
| | | 6601 | | purchasePriceRUB = baseCostRub * costFactor; |
| | | 6602 | | title = `${weight.toFixed(0)} (Вес) * ${pricePerKgRUB.toFixed(4)} ` + |
| | | 6603 | | `(Цена за кг. RUB = ${weightFactor.toFixed(2)} * ${rateWeight.toFixed(4)}) + ` + |
| | | 6604 | | `${baseCostRub.toFixed(0)} ₽ (вход, API) * ${costFactor.toFixed(4)} (Коэфф.) = ` + |
| | | 6605 | | `${purchasePriceRUB.toFixed(13)} (Закупка RUB) = ${Math.round(totalWeightCost + purchasePriceRUB)} RUB`; |
| | | 6606 | | } else { |
| | | 6607 | | purchasePriceRUB = rawPrice * costFactor * ratePrice; |
| | | 6608 | | title = `${weight.toFixed(0)} (Вес) * ${pricePerKgRUB.toFixed(4)} ` + |
| | | 6609 | | `(Цена за кг. RUB = ${weightFactor.toFixed(2)} * ${rateWeight.toFixed(4)}) + ` + |
| | | 6610 | | `${rawPrice.toFixed(2)} ${priceCurrency} * ${ratePrice.toFixed(4)} * ${costFactor.toFixed(4)} (Коэфф.) = |
| | | 6611 | | `${purchasePriceRUB.toFixed(13)} (Цена закупки RUB) = ${Math.round(totalWeightCost + purchasePriceRUB)} |
| | | 6612 | | } |
| | | 6613 | | |
| | | 6614 | | const totalRUB = Math.round(totalWeightCost + purchasePriceRUB); |
| | | 6615 | | |
| | | 6616 | | const formattedTotalRUB = totalRUB.toLocaleString('ru-RU'); |
| | | 6617 | | const displayText = `${formattedTotalRUB} ₽`; |
| | | 6618 | | |
| | | 6619 | | $priceBlock.text(displayText); |
| | | 6620 | | $priceBlock.attr('data-value', totalRUB); |
| | | 6621 | | |
| | | 6622 | | if (window.tradesofDataCache && window.tradesofDataCache[orderId] && window.tradesofDataCache[orderId][indexRow] |
| | | 6623 | | window.tradesofDataCache[orderId][indexRow].costPrice = totalRUB; |
| | | 6624 | | window.tradesofDataCache[orderId][indexRow].costPriceFormatted = displayText; |
| | | 6625 | | } |
| | | 6626 | | |
| | | 6627 | | // Пересчёт затирает подсказку ячейки, поэтому расшифровку API (исходная цена × курс) дописываем заново. |
| | | 6628 | | const priceDetails = String(window.tradesofDataCache?.[orderId]?.[indexRow]?.priceDescription || '').trim(); |
| | | 6629 | | $priceBlock.attr('title', priceDetails |
| | | 6630 | | ? `${title}\n\nДетали расчёта:\n${priceDetails.replace(/\s*;\s*/g, '\n')}` |
| | | 6631 | | : title); |
| | | 6632 | | |
| | | 6633 | | // Себестоимость строки изменилась — пересчитываем прибыль и наценку предложения |
| | | 6634 | | if (typeof window.updateOfferMetricsRow === 'function') { |
| | | 6635 | | window.updateOfferMetricsRow(orderId, indexRow); |
| | | 6636 | | } |
| | | 6637 | | } |
| | | 6638 | | |
| | | 6639 | | function updateAllPricesForOrder(orderId) { |
| | | 6640 | | $(`.price-link[data-order-id="${orderId}"]`).each(function () { |
| | | 6641 | | const indexRow = $(this).data('index-row'); |
| | | 6642 | | updatePriceDisplay(orderId, indexRow); |
| | | 6643 | | }); |
| | | 6644 | | } |
| | | 6645 | | |
| | | 6646 | | $('.product-weight-field').on('input', function () { |
| | | 6647 | | const orderId = $(this).attr('id').split('_')[1]; |
| | | 6648 | | updateAllPricesForOrder(orderId); |
| | | 6649 | | sortAndRerenderTable(orderId, 'costPrice', 'number', 'asc') |
| | | 6650 | | }); |
| | | 6651 | | |
| | | 6652 | | $('.supplier-select').on('change', function () { |
| | | 6653 | | const orderId = $(this).attr('id').split('_')[1]; |
| | | 6654 | | updateAllPricesForOrder(orderId); |
| | | 6655 | | }); |
| | | 6656 | | </script> |
| | | 6657 | | <script> |
| | | 6658 | | let regionsLoaded = false; |
| | | 6659 | | |
| | | 6660 | | $('#regionSelect').one('focus', function () { |
| | | 6661 | | if (regionsLoaded) return; |
| | | 6662 | | |
| | | 6663 | | $.ajax({ |
| | | 6664 | | url: '/Tradesoft/GetRegions', |
| | | 6665 | | type: 'GET' |
| | | 6666 | | }).done(function (response) { |
| | | 6667 | | if (response && response.error === "") { |
| | | 6668 | | const regions = response.table; |
| | | 6669 | | const select = $('#regionSelect'); |
| | | 6670 | | |
| | | 6671 | | // Удаляем все, кроме первой опции (по умолчанию) |
| | | 6672 | | select.find('option').not(':first').remove(); |
| | | 6673 | | |
| | | 6674 | | regions.forEach(region => { |
| | | 6675 | | if (region.code_region !== 1) { // 1 уже добавлен |
| | | 6676 | | const option = $('<option>', { |
| | | 6677 | | value: region.code_region, |
| | | 6678 | | text: region.class_region |
| | | 6679 | | }); |
| | | 6680 | | select.append(option); |
| | | 6681 | | } |
| | | 6682 | | }); |
| | | 6683 | | |
| | | 6684 | | regionsLoaded = true; |
| | | 6685 | | } else { |
| | | 6686 | | console.error('Ошибка получения регионов:', response.error || 'Неизвестная ошибка'); |
| | | 6687 | | } |
| | | 6688 | | }).fail(function (error) { |
| | | 6689 | | console.error('Ошибка при запросе регионов:', error); |
| | | 6690 | | }); |
| | | 6691 | | }); |
| | | 6692 | | document.getElementById("createOrderForm")?.addEventListener("keydown", function (event) { |
| | | 6693 | | if (event.key === "Enter") { |
| | | 6694 | | event.preventDefault(); // отменяет стандартное действие |
| | | 6695 | | return false; // дополнительная защита |
| | | 6696 | | } |
| | | 6697 | | }); |
| | | 6698 | | </script> |
| | | 6699 | | |
| | | 6700 | | <script> |
| | | 6701 | | function toggleComment(e) { |
| | | 6702 | | e.preventDefault(); |
| | | 6703 | | const container = document.getElementById('commentContainer'); |
| | | 6704 | | const toggle = e.currentTarget; |
| | | 6705 | | if (!container || !toggle) return; |
| | | 6706 | | |
| | | 6707 | | const isVisible = !container.hidden; |
| | | 6708 | | container.hidden = isVisible; |
| | | 6709 | | toggle.setAttribute('aria-expanded', (!isVisible).toString()); |
| | | 6710 | | const label = toggle.querySelector('.tx-page-comment-label'); |
| | | 6711 | | if (label) label.lastChild.textContent = isVisible |
| | | 6712 | | ? ' Добавить комментарий' |
| | | 6713 | | : ' Скрыть комментарий'; |
| | | 6714 | | |
| | | 6715 | | const icon = toggle.querySelector('.fa-chevron-down, .fa-chevron-up'); |
| | | 6716 | | if (icon) { |
| | | 6717 | | icon.className = 'fas ' + (isVisible ? 'fa-chevron-down' : 'fa-chevron-up'); |
| | | 6718 | | } |
| | | 6719 | | |
| | | 6720 | | if (!isVisible) { |
| | | 6721 | | container.querySelector('#commentField')?.focus(); |
| | | 6722 | | } |
| | | 6723 | | } |
| | | 6724 | | </script> |
| | | 6725 | | |
| | | 6726 | | @* Подсказки обслуживает единый движок js/tooltip.js: он работает через |
| | | 6727 | | делегирование, поэтому подхватывает и строки, дорисованные скриптом, |
| | | 6728 | | в отличие от разовой инициализации Bootstrap на document.ready. *@ |
| | | 6729 | | |
| | | 6730 | | <script> |
| | | 6731 | | $(document).ready(function () { |
| | | 6732 | | var deletedOrders = []; |
| | | 6733 | | |
| | | 6734 | | function getActiveOrderIds() { |
| | | 6735 | | return $('#shipmentTable tbody tr[id^="orderRow_"]') |
| | | 6736 | | .map(function () { |
| | | 6737 | | return this.id.replace('orderRow_', ''); |
| | | 6738 | | }) |
| | | 6739 | | .get() |
| | | 6740 | | .filter(Boolean); |
| | | 6741 | | } |
| | | 6742 | | |
| | | 6743 | | function updatePrintLinks() { |
| | | 6744 | | const activeIds = getActiveOrderIds(); |
| | | 6745 | | const deletedIds = deletedOrders.join(','); |
| | | 6746 | | const idsValue = activeIds.join(','); |
| | | 6747 | | |
| | | 6748 | | const excelBtn = document.getElementById('printTransactionExcelBtn'); |
| | | 6749 | | const pdfBtn = document.getElementById('printTransactionPdfBtn'); |
| | | 6750 | | |
| | | 6751 | | [excelBtn, pdfBtn].forEach(btn => { |
| | | 6752 | | if (!btn) return; |
| | | 6753 | | if (btn.id === 'printTransactionExcelBtn') { |
| | | 6754 | | const baseUrl = btn.dataset.baseUrl || '/Studio2/Transaction/StartTransactionExcelTask'; |
| | | 6755 | | const params = new URLSearchParams(); |
| | | 6756 | | if (idsValue) params.set('ids', idsValue); |
| | | 6757 | | if (deletedIds) params.set('deletedIds', deletedIds); |
| | | 6758 | | params.set('tableMode', 'true'); |
| | | 6759 | | const query = params.toString(); |
| | | 6760 | | btn.setAttribute('href', query ? `${baseUrl}?${query}` : baseUrl); |
| | | 6761 | | } else { |
| | | 6762 | | const baseUrl = btn.dataset.baseUrl || btn.getAttribute('href').split('?')[0]; |
| | | 6763 | | const params = new URLSearchParams(); |
| | | 6764 | | if (idsValue) params.set('ids', idsValue); |
| | | 6765 | | if (deletedIds) params.set('deletedIds', deletedIds); |
| | | 6766 | | const query = params.toString(); |
| | | 6767 | | btn.setAttribute('href', query ? `${baseUrl}?${query}` : baseUrl); |
| | | 6768 | | } |
| | | 6769 | | btn.classList.toggle('disabled', btn.id === 'printTransactionExcelBtn' ? !idsValue : !idsValue); |
| | | 6770 | | btn.setAttribute('aria-disabled', !idsValue ? 'true' : 'false'); |
| | | 6771 | | if (!idsValue) { |
| | | 6772 | | btn.setAttribute('title', 'Нет строк для печати'); |
| | | 6773 | | } |
| | | 6774 | | }); |
| | | 6775 | | } |
| | | 6776 | | |
| | | 6777 | | updatePrintLinks(); |
| | | 6778 | | |
| | | 6779 | | document.addEventListener('click', async function (e) { |
| | | 6780 | | const btn = e.target.closest('.st2-transaction-excel-task-btn, .st2-transaction-excel-export'); |
| | | 6781 | | if (!btn || btn.classList.contains('disabled')) return; |
| | | 6782 | | e.preventDefault(); |
| | | 6783 | | if (!window.St2ChannelTask) return; |
| | | 6784 | | let startUrl = btn.getAttribute('href'); |
| | | 6785 | | if (btn.classList.contains('st2-transaction-excel-export')) { |
| | | 6786 | | const txId = btn.getAttribute('data-transaction-id'); |
| | | 6787 | | if (!txId) return; |
| | | 6788 | | startUrl = `/Studio2/Transaction/StartTransactionExcelTask?transactionId=${encodeURIComponent(txId)}&tab |
| | | 6789 | | } |
| | | 6790 | | if (!startUrl || startUrl === '#') return; |
| | | 6791 | | try { |
| | | 6792 | | await St2ChannelTask.run({ |
| | | 6793 | | startUrl, |
| | | 6794 | | startMethod: 'GET', |
| | | 6795 | | statusUrl: taskId => St2ChannelTask.channelStatusUrl(taskId), |
| | | 6796 | | downloadUrl: taskId => St2ChannelTask.channelDownloadUrl(taskId), |
| | | 6797 | | modalId: 'st2ChannelTaskModal', |
| | | 6798 | | triggerBtn: btn, |
| | | 6799 | | loadingHtml: '<span class="spinner-border spinner-border-sm"></span>', |
| | | 6800 | | resultKind: 'File' |
| | | 6801 | | }); |
| | | 6802 | | } catch (_) { } |
| | | 6803 | | }); |
| | | 6804 | | |
| | | 6805 | | $(document).on('click', '.delete-order', function () { |
| | | 6806 | | var orderId = $(this).data('order-id'); |
| | | 6807 | | |
| | | 6808 | | // Удаляем основную строку |
| | | 6809 | | $('#orderRow_' + orderId).remove(); |
| | | 6810 | | |
| | | 6811 | | // Удаляем строку с информацией |
| | | 6812 | | $('.info-row[data-order-id="' + orderId + '"]').remove(); |
| | | 6813 | | |
| | | 6814 | | // Добавляем ID удалённого заказа в массив |
| | | 6815 | | if (!deletedOrders.includes(orderId)) { |
| | | 6816 | | deletedOrders.push(orderId); |
| | | 6817 | | } |
| | | 6818 | | $('#deletedOrders').val(deletedOrders.join(',')); |
| | | 6819 | | |
| | | 6820 | | // Обновление нумерации строк, если необходимо |
| | | 6821 | | updateRowNumbers(); |
| | | 6822 | | |
| | | 6823 | | // Перерасчет стоимости и других данных |
| | | 6824 | | calculateSumAndDisplay(); |
| | | 6825 | | calculateTotalPurchasePriceRub(); |
| | | 6826 | | let list = JSON.parse(inMemoryCartStorage.getItem('selectedStockList' + params.get("ids")) || '[]'); |
| | | 6827 | | list = list.filter(item => String(item.orderId) !== String(orderId)); |
| | | 6828 | | inMemoryCartStorage.setItem('selectedStockList' + params.get("ids"), JSON.stringify(list)); |
| | | 6829 | | |
| | | 6830 | | // Обновление списка поставщиков |
| | | 6831 | | window.updateSupplierList(); |
| | | 6832 | | |
| | | 6833 | | // Синхронизируем ссылки печати после удаления строк |
| | | 6834 | | updatePrintLinks(); |
| | | 6835 | | |
| | | 6836 | | }); |
| | | 6837 | | |
| | | 6838 | | function updateRowNumbers() { |
| | | 6839 | | $('#shipmentTable tbody tr').each(function (index) { |
| | | 6840 | | $(this).find('.row-number').text(index + 1); |
| | | 6841 | | }); |
| | | 6842 | | } |
| | | 6843 | | }); |
| | | 6844 | | |
| | | 6845 | | |
| | | 6846 | | // При загрузке страницы создаём tooltip через JS (так можно управлять содержимым) |
| | | 6847 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 6848 | | const el = document.getElementById('thArticle2'); |
| | | 6849 | | if (!el) return; // шапки таблицы нет, когда показана панель ошибки |
| | | 6850 | | const tooltip = new bootstrap.Tooltip(el, { |
| | | 6851 | | trigger: 'manual', |
| | | 6852 | | title: 'Нажмите, чтобы скопировать' |
| | | 6853 | | }); |
| | | 6854 | | el._tooltipInstance = tooltip; // сохраняем ссылку для дальнейшего использования |
| | | 6855 | | }); |
| | | 6856 | | |
| | | 6857 | | // Функция для показа уведомлений поверх всех окон |
| | | 6858 | | function showNotification(message, type = 'info') { |
| | | 6859 | | // Создаем контейнер для уведомлений если его нет |
| | | 6860 | | let notificationContainer = document.getElementById('notification-container'); |
| | | 6861 | | if (!notificationContainer) { |
| | | 6862 | | notificationContainer = document.createElement('div'); |
| | | 6863 | | notificationContainer.id = 'notification-container'; |
| | | 6864 | | notificationContainer.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 99999; display: fle |
| | | 6865 | | document.body.appendChild(notificationContainer); |
| | | 6866 | | } |
| | | 6867 | | |
| | | 6868 | | // Создаем уведомление |
| | | 6869 | | const toast = document.createElement('div'); |
| | | 6870 | | toast.className = `alert alert-${type} alert-dismissible fade show`; |
| | | 6871 | | toast.style.cssText = 'min-width: 300px; max-width: 400px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);'; |
| | | 6872 | | toast.innerHTML = ` |
| | | 6873 | | ${message} |
| | | 6874 | | <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
| | | 6875 | | `; |
| | | 6876 | | |
| | | 6877 | | // Добавляем уведомление в контейнер |
| | | 6878 | | notificationContainer.appendChild(toast); |
| | | 6879 | | |
| | | 6880 | | // Инициализируем Bootstrap alert |
| | | 6881 | | const bsAlert = new bootstrap.Alert(toast); |
| | | 6882 | | |
| | | 6883 | | // Автоматическое скрытие через 5 секунд |
| | | 6884 | | setTimeout(() => { |
| | | 6885 | | if (toast.parentNode) { |
| | | 6886 | | bsAlert.close(); |
| | | 6887 | | } |
| | | 6888 | | }, 5000); |
| | | 6889 | | |
| | | 6890 | | // Удаляем элемент из DOM после скрытия |
| | | 6891 | | toast.addEventListener('closed.bs.alert', function () { |
| | | 6892 | | if (toast.parentNode) { |
| | | 6893 | | toast.parentNode.removeChild(toast); |
| | | 6894 | | } |
| | | 6895 | | }); |
| | | 6896 | | } |
| | | 6897 | | |
| | | 6898 | | |
| | | 6899 | | // class Combobox вынесен в /js/shared/combobox.js (подключён в _Layout). |
| | | 6900 | | |
| | | 6901 | | // Обработка изменения количества |
| | | 6902 | | document.addEventListener('DOMContentLoaded', function () { |
| | 0 | 6903 | | const quantityZeroStatusName = '@(ViewData["QuantityZeroStatusName"] ?? "К отмене")'; |
| | | 6904 | | |
| | | 6905 | | // Функция для обновления отображения разницы количества |
| | | 6906 | | function updateQuantityDiff(inputField) { |
| | | 6907 | | const orderId = inputField.dataset.orderId; |
| | | 6908 | | const originalQuantity = parseInt(inputField.dataset.originalQuantity) || 0; |
| | | 6909 | | const currentQuantity = parseInt(inputField.value) || 0; |
| | | 6910 | | |
| | | 6911 | | // Находим элемент для отображения разницы |
| | | 6912 | | const diffDisplay = document.querySelector(`.quantity-diff-display[data-order-id="${orderId}"]`); |
| | | 6913 | | |
| | | 6914 | | if (diffDisplay) { |
| | | 6915 | | const diff = currentQuantity - originalQuantity; |
| | | 6916 | | |
| | | 6917 | | if (diff === 0) { |
| | | 6918 | | // Если разницы нет, оставляем пустым |
| | | 6919 | | diffDisplay.textContent = ''; |
| | | 6920 | | } else if (diff > 0) { |
| | | 6921 | | // Показываем положительное отклонение |
| | | 6922 | | diffDisplay.textContent = `+${diff}`; |
| | | 6923 | | diffDisplay.style.color = '#28a745'; // Зеленый для увеличения |
| | | 6924 | | } else { |
| | | 6925 | | // Показываем отрицательное отклонение |
| | | 6926 | | diffDisplay.textContent = `${diff}`; |
| | | 6927 | | diffDisplay.style.color = '#dc3545'; // Красный для уменьшения |
| | | 6928 | | } |
| | | 6929 | | } |
| | | 6930 | | |
| | | 6931 | | // Обновляем цвет текста в поле ввода (без заливки ячейки) |
| | | 6932 | | inputField.classList.remove('qty-value--multi', 'qty-value--zero'); |
| | | 6933 | | if (currentQuantity === 0 && originalQuantity > 0) { |
| | | 6934 | | inputField.classList.add('qty-value--zero'); |
| | | 6935 | | } else if (currentQuantity > 1) { |
| | | 6936 | | inputField.classList.add('qty-value--multi'); |
| | | 6937 | | } |
| | | 6938 | | inputField.style.color = ''; |
| | | 6939 | | inputField.style.fontWeight = ''; |
| | | 6940 | | inputField.style.backgroundColor = ''; |
| | | 6941 | | } |
| | | 6942 | | |
| | | 6943 | | function notifyQuantityChange(inputField) { |
| | | 6944 | | const originalQuantity = parseInt(inputField.dataset.originalQuantity) || 0; |
| | | 6945 | | const currentQuantity = parseInt(inputField.value) || 0; |
| | | 6946 | | |
| | | 6947 | | if (currentQuantity === originalQuantity) return; |
| | | 6948 | | |
| | | 6949 | | if (currentQuantity === 0 && originalQuantity > 0) { |
| | | 6950 | | showNotification( |
| | | 6951 | | `<div style="font-size:0.9rem;"><b>Внимание:</b> количество установлено в <b>0</b>. ` + |
| | | 6952 | | `При сохранении этот заказ будет переведён в статус <b>«${quantityZeroStatusName}»</b>.</div>`, |
| | | 6953 | | 'warning' |
| | | 6954 | | ); |
| | | 6955 | | return; |
| | | 6956 | | } |
| | | 6957 | | |
| | | 6958 | | const diff = originalQuantity - currentQuantity; |
| | | 6959 | | if (diff > 0) { |
| | | 6960 | | showNotification( |
| | | 6961 | | `<div style="font-size:0.9rem;">При сохранении будет создан новый заказ с количеством <b>${diff}</b> |
| | | 6962 | | `и исходным статусом. Текущий заказ будет изменён на количество <b>${currentQuantity}</b>.</div>`, |
| | | 6963 | | 'info' |
| | | 6964 | | ); |
| | | 6965 | | } |
| | | 6966 | | } |
| | | 6967 | | |
| | | 6968 | | // Обработка всех полей ввода количества |
| | | 6969 | | document.querySelectorAll('.quantity-input-field').forEach(inputField => { |
| | | 6970 | | // Инициализация при загрузке |
| | | 6971 | | updateQuantityDiff(inputField); |
| | | 6972 | | |
| | | 6973 | | // Обработка изменения значения |
| | | 6974 | | inputField.addEventListener('input', function () { |
| | | 6975 | | const originalQuantity = parseInt(this.dataset.originalQuantity, 10); |
| | | 6976 | | let currentQuantity = parseInt(this.value, 10); |
| | | 6977 | | currentQuantity = isNaN(currentQuantity) ? 0 : currentQuantity; |
| | | 6978 | | |
| | | 6979 | | if (!isNaN(originalQuantity) && currentQuantity > originalQuantity) { |
| | | 6980 | | this.value = String(originalQuantity); |
| | | 6981 | | currentQuantity = originalQuantity; |
| | | 6982 | | } |
| | | 6983 | | if (currentQuantity < 0) { |
| | | 6984 | | this.value = '0'; |
| | | 6985 | | currentQuantity = 0; |
| | | 6986 | | } |
| | | 6987 | | |
| | | 6988 | | updateQuantityDiff(this); |
| | | 6989 | | }); |
| | | 6990 | | |
| | | 6991 | | // Обработка изменения (при потере фокуса) |
| | | 6992 | | inputField.addEventListener('change', function () { |
| | | 6993 | | const originalQuantity = parseInt(this.dataset.originalQuantity) || 0; |
| | | 6994 | | let currentQuantity = parseInt(this.value); |
| | | 6995 | | currentQuantity = isNaN(currentQuantity) ? 0 : currentQuantity; |
| | | 6996 | | |
| | | 6997 | | // Валидация: не позволяем вводить значение больше оригинального |
| | | 6998 | | if (currentQuantity > originalQuantity) { |
| | | 6999 | | this.value = originalQuantity; |
| | | 7000 | | updateQuantityDiff(this); |
| | | 7001 | | currentQuantity = originalQuantity; |
| | | 7002 | | } |
| | | 7003 | | |
| | | 7004 | | // Не позволяем вводить значение меньше 0 |
| | | 7005 | | if (currentQuantity < 0) { |
| | | 7006 | | this.value = 0; |
| | | 7007 | | updateQuantityDiff(this); |
| | | 7008 | | currentQuantity = 0; |
| | | 7009 | | } |
| | | 7010 | | |
| | | 7011 | | // Уведомление при любом изменении |
| | | 7012 | | notifyQuantityChange(this); |
| | | 7013 | | }); |
| | | 7014 | | }); |
| | | 7015 | | |
| | | 7016 | | // Инициализация tooltips для полей количества |
| | | 7017 | | const tooltipTriggerList = document.querySelectorAll('.quantity-input-field[title]'); |
| | | 7018 | | tooltipTriggerList.forEach(el => { |
| | | 7019 | | new bootstrap.Tooltip(el, { |
| | | 7020 | | placement: 'top', |
| | | 7021 | | trigger: 'hover focus' |
| | | 7022 | | }); |
| | | 7023 | | }); |
| | | 7024 | | }); |
| | | 7025 | | </script> |
| | | 7026 | | |
| | | 7027 | | <script> |
| | | 7028 | | // Обработчик для сканера штрих-кодов с поиском в таблице |
| | | 7029 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 7030 | | // Создаем скрытое поле для сканера, если его еще нет |
| | | 7031 | | let barcodeInput = document.getElementById('barcode'); |
| | | 7032 | | if (!barcodeInput) { |
| | | 7033 | | barcodeInput = document.createElement('input'); |
| | | 7034 | | barcodeInput.type = 'text'; |
| | | 7035 | | barcodeInput.id = 'barcode'; |
| | | 7036 | | barcodeInput.name = 'barcode'; |
| | | 7037 | | barcodeInput.style.cssText = 'position: fixed; left: -9999px; top: 0; width: 1px; height: 1px; opacity: 0;'; |
| | | 7038 | | barcodeInput.setAttribute('autocomplete', 'off'); |
| | | 7039 | | barcodeInput.setAttribute('tabindex', '-1'); |
| | | 7040 | | document.body.appendChild(barcodeInput); |
| | | 7041 | | } |
| | | 7042 | | |
| | | 7043 | | // Переменная для хранения предыдущего выделения |
| | | 7044 | | let previousHighlightedRow = null; |
| | | 7045 | | let highlightTimeout = null; |
| | | 7046 | | |
| | | 7047 | | // Функция для добавления иконки сканирования рядом с кодом |
| | | 7048 | | function addScanIcon(row, scanCode) { |
| | | 7049 | | // Удаляем предыдущую иконку, если она есть |
| | | 7050 | | const existingIcon = row.querySelector('.scan-success-icon'); |
| | | 7051 | | if (existingIcon) { |
| | | 7052 | | existingIcon.remove(); |
| | | 7053 | | } |
| | | 7054 | | |
| | | 7055 | | // Ищем ячейку с номером отправления или артикулом |
| | | 7056 | | let shipmentNumberCell = null; |
| | | 7057 | | const allCells = row.querySelectorAll('td'); |
| | | 7058 | | |
| | | 7059 | | for (let cell of allCells) { |
| | | 7060 | | // Проверяем наличие скрытого поля с ShipmentNumber |
| | | 7061 | | const shipmentInput = cell.querySelector('input[name*="ShipmentNumber"]'); |
| | | 7062 | | if (shipmentInput) { |
| | | 7063 | | shipmentNumberCell = cell; |
| | | 7064 | | break; |
| | | 7065 | | } |
| | | 7066 | | // Или проверяем текстовое содержимое |
| | | 7067 | | const cellText = cell.textContent || cell.innerText; |
| | | 7068 | | if (cellText.includes(scanCode)) { |
| | | 7069 | | shipmentNumberCell = cell; |
| | | 7070 | | break; |
| | | 7071 | | } |
| | | 7072 | | } |
| | | 7073 | | |
| | | 7074 | | if (!shipmentNumberCell) { |
| | | 7075 | | return; |
| | | 7076 | | } |
| | | 7077 | | |
| | | 7078 | | // Создаем контейнер для иконки |
| | | 7079 | | const iconContainer = document.createElement('span'); |
| | | 7080 | | iconContainer.className = 'scan-success-icon'; |
| | | 7081 | | iconContainer.style.cssText = 'display: inline-flex; align-items: center; margin-left: 8px; vertical-align: |
| | | 7082 | | iconContainer.title = 'Отсканировано: ' + scanCode; |
| | | 7083 | | |
| | | 7084 | | // Создаем SVG иконку штрих-кода |
| | | 7085 | | const iconSvg = ` |
| | | 7086 | | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" |
| | | 7087 | | viewBox="0 0 16 16" style="color: #28a745;"> |
| | | 7088 | | <path d="M2 2h2v12H2V2zm3 0h1v12H5V2zm2 0h2v12H7V2zm3 0h1v12h-1V2zm2 0h1v12h-1V2zm2 0h1v12h-1V2z"/> |
| | | 7089 | | <path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm1 0v12a1 1 0 0 0 1 |
| | | 7090 | | </svg> |
| | | 7091 | | `; |
| | | 7092 | | |
| | | 7093 | | iconContainer.innerHTML = iconSvg; |
| | | 7094 | | |
| | | 7095 | | // Ищем место для вставки иконки (рядом с номером отправления) |
| | | 7096 | | // Сначала ищем span с row-number |
| | | 7097 | | let targetElement = null; |
| | | 7098 | | const spansWithRowNumber = shipmentNumberCell.querySelectorAll('span'); |
| | | 7099 | | for (let span of spansWithRowNumber) { |
| | | 7100 | | if (span.querySelector('b.row-number')) { |
| | | 7101 | | targetElement = span; |
| | | 7102 | | break; |
| | | 7103 | | } |
| | | 7104 | | } |
| | | 7105 | | |
| | | 7106 | | // Если не нашли, ищем любой span или div |
| | | 7107 | | if (!targetElement) { |
| | | 7108 | | targetElement = shipmentNumberCell.querySelector('span') || |
| | | 7109 | | shipmentNumberCell.querySelector('div'); |
| | | 7110 | | } |
| | | 7111 | | |
| | | 7112 | | if (targetElement) { |
| | | 7113 | | // Добавляем иконку в контейнер с номером отправления |
| | | 7114 | | targetElement.appendChild(iconContainer); |
| | | 7115 | | } else { |
| | | 7116 | | // Если не нашли подходящий контейнер, добавляем в начало ячейки |
| | | 7117 | | shipmentNumberCell.insertBefore(iconContainer, shipmentNumberCell.firstChild); |
| | | 7118 | | } |
| | | 7119 | | |
| | | 7120 | | // Добавляем анимацию появления |
| | | 7121 | | iconContainer.style.opacity = '0'; |
| | | 7122 | | iconContainer.style.transform = 'scale(0.8)'; |
| | | 7123 | | iconContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; |
| | | 7124 | | |
| | | 7125 | | setTimeout(() => { |
| | | 7126 | | iconContainer.style.opacity = '1'; |
| | | 7127 | | iconContainer.style.transform = 'scale(1)'; |
| | | 7128 | | }, 10); |
| | | 7129 | | } |
| | | 7130 | | |
| | | 7131 | | // Функция для снятия выделения со всех строк |
| | | 7132 | | function removeAllHighlights() { |
| | | 7133 | | if (previousHighlightedRow) { |
| | | 7134 | | previousHighlightedRow.style.border = ''; |
| | | 7135 | | previousHighlightedRow.style.boxShadow = ''; |
| | | 7136 | | previousHighlightedRow.style.transition = ''; |
| | | 7137 | | } |
| | | 7138 | | // Убираем выделение со всех строк |
| | | 7139 | | const table = document.getElementById('shipmentTable'); |
| | | 7140 | | if (table) { |
| | | 7141 | | table.querySelectorAll('tbody tr').forEach(row => { |
| | | 7142 | | row.style.border = ''; |
| | | 7143 | | row.style.boxShadow = ''; |
| | | 7144 | | row.style.transition = ''; |
| | | 7145 | | }); |
| | | 7146 | | } |
| | | 7147 | | // Очищаем таймер, если он был установлен |
| | | 7148 | | if (highlightTimeout) { |
| | | 7149 | | clearTimeout(highlightTimeout); |
| | | 7150 | | highlightTimeout = null; |
| | | 7151 | | } |
| | | 7152 | | } |
| | | 7153 | | |
| | | 7154 | | // Функция для поиска и выделения строки |
| | | 7155 | | function findAndHighlightRow(searchCode) { |
| | | 7156 | | if (!searchCode || searchCode.trim() === '') { |
| | | 7157 | | return false; |
| | | 7158 | | } |
| | | 7159 | | |
| | | 7160 | | const table = document.getElementById('shipmentTable'); |
| | | 7161 | | if (!table) { |
| | | 7162 | | console.warn('Таблица shipmentTable не найдена'); |
| | | 7163 | | if (typeof showNotification === 'function') { |
| | | 7164 | | showNotification('Таблица не найдена', 'danger'); |
| | | 7165 | | } |
| | | 7166 | | return false; |
| | | 7167 | | } |
| | | 7168 | | |
| | | 7169 | | const rows = table.querySelectorAll('tbody tr[id^="orderRow_"]'); |
| | | 7170 | | let foundRow = null; |
| | | 7171 | | |
| | | 7172 | | // Ищем строку по номеру отправления или артикулу |
| | | 7173 | | for (let row of rows) { |
| | | 7174 | | // Получаем все текстовое содержимое строки |
| | | 7175 | | const rowText = row.textContent || row.innerText; |
| | | 7176 | | |
| | | 7177 | | // Ищем скрытое поле с ShipmentNumber |
| | | 7178 | | const shipmentNumberInput = row.querySelector('input[name*="ShipmentNumber"]'); |
| | | 7179 | | const shipmentNumber = shipmentNumberInput ? shipmentNumberInput.value : ''; |
| | | 7180 | | |
| | | 7181 | | // Ищем артикул в тексте строки (может быть в разных местах) |
| | | 7182 | | const articleCells = row.querySelectorAll('[id*="Article"], [id*="thArticle"]'); |
| | | 7183 | | let articleFound = false; |
| | | 7184 | | articleCells.forEach(cell => { |
| | | 7185 | | const cellText = cell.textContent || cell.innerText; |
| | | 7186 | | if (cellText.includes(searchCode)) { |
| | | 7187 | | articleFound = true; |
| | | 7188 | | } |
| | | 7189 | | }); |
| | | 7190 | | |
| | | 7191 | | // Проверяем совпадение по номеру отправления или артикулу |
| | | 7192 | | if ((shipmentNumber && shipmentNumber.includes(searchCode)) || |
| | | 7193 | | (rowText.includes(searchCode) && (articleFound || rowText.indexOf(searchCode) !== -1))) { |
| | | 7194 | | foundRow = row; |
| | | 7195 | | break; |
| | | 7196 | | } |
| | | 7197 | | } |
| | | 7198 | | |
| | | 7199 | | if (foundRow) { |
| | | 7200 | | // Снимаем предыдущее выделение |
| | | 7201 | | removeAllHighlights(); |
| | | 7202 | | |
| | | 7203 | | // Выделяем найденную строку обводкой |
| | | 7204 | | foundRow.style.border = '3px solid #0d6efd'; |
| | | 7205 | | foundRow.style.boxShadow = '0 0 10px rgba(13, 110, 253, 0.5)'; |
| | | 7206 | | foundRow.style.transition = 'border 0.3s ease, box-shadow 0.3s ease'; |
| | | 7207 | | previousHighlightedRow = foundRow; |
| | | 7208 | | |
| | | 7209 | | // Добавляем иконку сканирования рядом с кодом |
| | | 7210 | | addScanIcon(foundRow, searchCode); |
| | | 7211 | | |
| | | 7212 | | // Прокручиваем к найденной строке |
| | | 7213 | | foundRow.scrollIntoView({ |
| | | 7214 | | behavior: 'smooth', |
| | | 7215 | | block: 'center' |
| | | 7216 | | }); |
| | | 7217 | | |
| | | 7218 | | // Показываем уведомление об успешном поиске |
| | | 7219 | | if (typeof showNotification === 'function') { |
| | | 7220 | | showNotification('Заказ найден: ' + searchCode, 'success'); |
| | | 7221 | | } |
| | | 7222 | | |
| | | 7223 | | // Автоматически снимаем выделение через 5 секунд |
| | | 7224 | | highlightTimeout = setTimeout(() => { |
| | | 7225 | | if (foundRow) { |
| | | 7226 | | foundRow.style.border = ''; |
| | | 7227 | | foundRow.style.boxShadow = ''; |
| | | 7228 | | foundRow.style.transition = ''; |
| | | 7229 | | previousHighlightedRow = null; |
| | | 7230 | | } |
| | | 7231 | | }, 5000); |
| | | 7232 | | |
| | | 7233 | | return true; |
| | | 7234 | | } else { |
| | | 7235 | | // Если не найдено, показываем уведомление |
| | | 7236 | | console.warn('Строка с кодом "' + searchCode + '" не найдена'); |
| | | 7237 | | if (typeof showNotification === 'function') { |
| | | 7238 | | showNotification('Заказ с кодом "' + searchCode + '" не найден', 'warning'); |
| | | 7239 | | } |
| | | 7240 | | return false; |
| | | 7241 | | } |
| | | 7242 | | } |
| | | 7243 | | |
| | | 7244 | | // Обработчик события keydown для сканера |
| | | 7245 | | barcodeInput.addEventListener('keydown', function (e) { |
| | | 7246 | | if (e.key === 'Enter') { |
| | | 7247 | | e.preventDefault(); |
| | | 7248 | | const code = barcodeInput.value.trim(); |
| | | 7249 | | |
| | | 7250 | | if (code) { |
| | | 7251 | | // Ищем и выделяем строку |
| | | 7252 | | findAndHighlightRow(code); |
| | | 7253 | | |
| | | 7254 | | // Очищаем поле сканера |
| | | 7255 | | barcodeInput.value = ''; |
| | | 7256 | | } |
| | | 7257 | | } |
| | | 7258 | | }); |
| | | 7259 | | |
| | | 7260 | | // Автоматически устанавливаем фокус на поле сканера при загрузке страницы |
| | | 7261 | | // Используем setTimeout чтобы избежать прокрутки страницы |
| | | 7262 | | setTimeout(function () { |
| | | 7263 | | if (document.activeElement !== barcodeInput) { |
| | | 7264 | | barcodeInput.focus(); |
| | | 7265 | | } |
| | | 7266 | | }, 100); |
| | | 7267 | | |
| | | 7268 | | // Устанавливаем фокус на поле сканера при клике в любом месте страницы |
| | | 7269 | | // (если клик не по интерактивному элементу) |
| | | 7270 | | // Переменная для отслеживания выделения текста |
| | | 7271 | | let isSelectingText = false; |
| | | 7272 | | let mouseDownTime = 0; |
| | | 7273 | | |
| | | 7274 | | document.addEventListener('mousedown', function (e) { |
| | | 7275 | | mouseDownTime = Date.now(); |
| | | 7276 | | isSelectingText = false; |
| | | 7277 | | }); |
| | | 7278 | | |
| | | 7279 | | document.addEventListener('mousemove', function (e) { |
| | | 7280 | | if (mouseDownTime > 0 && Date.now() - mouseDownTime > 10) { |
| | | 7281 | | isSelectingText = true; |
| | | 7282 | | } |
| | | 7283 | | }); |
| | | 7284 | | |
| | | 7285 | | document.addEventListener('mouseup', function (e) { |
| | | 7286 | | setTimeout(function () { |
| | | 7287 | | isSelectingText = false; |
| | | 7288 | | mouseDownTime = 0; |
| | | 7289 | | }, 100); |
| | | 7290 | | }); |
| | | 7291 | | |
| | | 7292 | | document.addEventListener('click', function (e) { |
| | | 7293 | | // Проверяем, есть ли выделенный текст |
| | | 7294 | | const selection = window.getSelection(); |
| | | 7295 | | const hasSelection = selection && selection.toString().trim().length > 0; |
| | | 7296 | | |
| | | 7297 | | // Если есть выделенный текст или происходит выделение, не устанавливаем фокус |
| | | 7298 | | if (hasSelection || isSelectingText) { |
| | | 7299 | | return; |
| | | 7300 | | } |
| | | 7301 | | |
| | | 7302 | | // Если клик не по интерактивному элементу |
| | | 7303 | | if (e.target.tagName !== 'INPUT' && |
| | | 7304 | | e.target.tagName !== 'TEXTAREA' && |
| | | 7305 | | e.target.tagName !== 'SELECT' && |
| | | 7306 | | e.target.tagName !== 'BUTTON' && |
| | | 7307 | | e.target.tagName !== 'A' && |
| | | 7308 | | e.target.tagName !== 'TD' && |
| | | 7309 | | e.target.tagName !== 'TH' && |
| | | 7310 | | e.target.tagName !== 'SPAN' && |
| | | 7311 | | !e.target.closest('.dropdown-menu') && |
| | | 7312 | | !e.target.closest('.modal') && |
| | | 7313 | | !e.target.closest('.btn') && |
| | | 7314 | | !e.target.closest('.form-control') && |
| | | 7315 | | !e.target.closest('table')) { |
| | | 7316 | | // Устанавливаем фокус на поле сканера с небольшой задержкой |
| | | 7317 | | setTimeout(function () { |
| | | 7318 | | // Повторно проверяем выделение перед установкой фокуса |
| | | 7319 | | const currentSelection = window.getSelection(); |
| | | 7320 | | if (!currentSelection || !currentSelection.toString().trim()) { |
| | | 7321 | | barcodeInput.focus(); |
| | | 7322 | | } |
| | | 7323 | | }, 100); |
| | | 7324 | | } |
| | | 7325 | | }); |
| | | 7326 | | |
| | | 7327 | | // Обработка клавиши Escape для снятия выделения |
| | | 7328 | | document.addEventListener('keydown', function (e) { |
| | | 7329 | | if (e.key === 'Escape') { |
| | | 7330 | | removeAllHighlights(); |
| | | 7331 | | } |
| | | 7332 | | }); |
| | | 7333 | | }); |
| | | 7334 | | </script> |
| | | 7335 | | |
| | | 7336 | | <script> |
| | | 7337 | | (function () { |
| | | 7338 | | function initTransactionCustomSelects() { |
| | | 7339 | | if (typeof window.initCustomSelects !== 'function') return; |
| | | 7340 | | window.initCustomSelects('#shipmentTable .custom-select-init'); |
| | | 7341 | | } |
| | | 7342 | | if (document.readyState === 'loading') { |
| | | 7343 | | document.addEventListener('DOMContentLoaded', initTransactionCustomSelects); |
| | | 7344 | | } else { |
| | | 7345 | | initTransactionCustomSelects(); |
| | | 7346 | | } |
| | | 7347 | | })(); |
| | | 7348 | | </script> |