< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_Shared__TransactionScript
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/Shared/_TransactionScript.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 79
Coverable lines: 79
Total lines: 7348
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 14
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%210140%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/Shared/_TransactionScript.cshtml

#LineLine coverage
 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) => {
 016        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') {
 01205            window.rateUSD = @Model.RateUSD.ToString().Replace(',', '.');
 01206            window.rateEUR = @Model.RateEUR.ToString().Replace(',', '.');
 01207            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
 02025        var rateUSD = @Model.RateUSD.ToString().Replace(',', '.');
 02026        var rateEUR = @Model.RateEUR.ToString().Replace(',', '.');
 02027        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') {
 02554            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();
 02856    const armtekQuotationEnabled = @Json.Serialize(Model.ArmtekQuotationEnabled);
 02857    const armtekByQuotationEnabled = @Json.Serialize(Model.ArmtekByQuotationEnabled);
 02858    const shateMQuotationEnabled = @Json.Serialize(Model.ShateMQuotationEnabled);
 02859    const avdQuotationEnabled = @Json.Serialize(Model.AvdQuotationEnabled);
 02860    const bergQuotationEnabled = @Json.Serialize(Model.BergQuotationEnabled);
 02861    const motexQuotationEnabled = @Json.Serialize(Model.MotexQuotationEnabled);
 02862    const mlAutoQuotationEnabled = @Json.Serialize(Model.MlAutoQuotationEnabled);
 02863    const mlAutoRuQuotationEnabled = @Json.Serialize(Model.MlAutoRuQuotationEnabled);
 02864    const forumAutoQuotationEnabled = @Json.Serialize(Model.ForumAutoQuotationEnabled);
 02865    const forumAutoInterpartsQuotationEnabled = @Json.Serialize(Model.ForumAutoInterpartsQuotationEnabled);
 02866    const forumAutoPiterQuotationEnabled = @Json.Serialize(Model.ForumAutoPiterQuotationEnabled);
 02867    const forumAutoRostovQuotationEnabled = @Json.Serialize(Model.ForumAutoRostovQuotationEnabled);
 02868    const forumAutoIstraQuotationEnabled = @Json.Serialize(Model.ForumAutoIstraQuotationEnabled);
 02869    const forumAutoDisplayName = @Json.Serialize(Model.ForumAutoDisplayName);
 02870    const forumAutoInterpartsDisplayName = @Json.Serialize(Model.ForumAutoInterpartsDisplayName);
 02871    const forumAutoPiterDisplayName = @Json.Serialize(Model.ForumAutoPiterDisplayName);
 02872    const forumAutoRostovDisplayName = @Json.Serialize(Model.ForumAutoRostovDisplayName);
 02873    const forumAutoIstraDisplayName = @Json.Serialize(Model.ForumAutoIstraDisplayName);
 02874    const favoritPartsQuotationEnabled = @Json.Serialize(Model.FavoritPartsQuotationEnabled);
 02875    const favoritPartsIstraQuotationEnabled = @Json.Serialize(Model.FavoritPartsIstraQuotationEnabled);
 02876    const favoritPartsRostovQuotationEnabled = @Json.Serialize(Model.FavoritPartsRostovQuotationEnabled);
 02877    const moskvorechieIstraQuotationEnabled = @Json.Serialize(Model.MoskvorechieIstraQuotationEnabled);
 02878    const profitLigaQuotationEnabled = @Json.Serialize(Model.ProfitLigaQuotationEnabled);
 02879    const nikeiQuotationEnabled = @Json.Serialize(Model.NikeiQuotationEnabled);
 02880    const mikadoMskHod20QuotationEnabled = @Json.Serialize(Model.MikadoMskHod20QuotationEnabled);
 02881    const zzapMoscowQuotationEnabled = @Json.Serialize(Model.ZZapMoscowQuotationEnabled);
 02882    const japartsQuotationEnabled = @Json.Serialize(Model.JapartsQuotationEnabled);
 2883    const apiSupplierCurrencyRates = {
 2884        RUB: 1,
 02885        USD: parseFloat('@Model.RateUSD'.replace(',', '.')),
 02886        EUR: parseFloat('@Model.RateEUR'.replace(',', '.')),
 02887        BYN: parseFloat('@Model.RateBYN'.replace(',', '.'))
 2888    };
 02889    const apiSupplierAllowedWarehouses = @Json.Serialize(Model.ApiSupplierAllowedWarehouses ?? new Dictionary<string, Li
 2890    const studioWarehouseNameToId = {
 02891        @if (Model.Warehouses != null)
 2892        {
 02893            @foreach (var w in Model.Warehouses)
 2894            {
 02895                if (!string.IsNullOrWhiteSpace(w.Name))
 2896                {
 02897                    @:"@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',
 02937            endpoint: '@Url.Action("Search", "ArmtekPrice")',
 02938            warmupEndpoint: '@Url.Action("Warmup", "ArmtekPrice")',
 2939            isEnabled: () => armtekQuotationEnabled,
 2940            process: processArmtekPriceData
 2941        },
 2942        {
 2943            providerKey: 'ArmtekBy',
 2944            displayName: 'Армтек BY',
 2945            cacheKey: 'armtekByPriceData',
 02946            endpoint: '@Url.Action("Search", "ArmtekByPrice")',
 02947            warmupEndpoint: '@Url.Action("Warmup", "ArmtekByPrice")',
 2948            isEnabled: () => armtekByQuotationEnabled,
 2949            process: processArmtekByPriceData
 2950        },
 2951        {
 2952            providerKey: 'ShateM',
 2953            displayName: 'Шате‑М',
 2954            cacheKey: 'shateMPriceData',
 02955            endpoint: '@Url.Action("Search", "ShateMPrice")',
 2956            isEnabled: () => shateMQuotationEnabled,
 2957            process: processShateMPriceData
 2958        },
 2959        {
 2960            providerKey: 'Avd',
 2961            displayName: 'АВД',
 2962            cacheKey: 'avdPriceData',
 02963            endpoint: '@Url.Action("Search", "AvdPrice")',
 2964            isEnabled: () => avdQuotationEnabled,
 2965            process: processAvdPriceData
 2966        },
 2967        {
 2968            providerKey: 'Berg',
 2969            displayName: 'Берг',
 2970            cacheKey: 'bergPriceData',
 02971            endpoint: '@Url.Action("Search", "BergPrice")',
 2972            isEnabled: () => bergQuotationEnabled,
 2973            process: processBergPriceData
 2974        },
 2975        {
 2976            providerKey: 'Japarts',
 2977            displayName: 'Japarts',
 2978            cacheKey: 'japartsPriceData',
 02979            endpoint: '@Url.Action("Search", "JapartsPrice")',
 2980            isEnabled: () => japartsQuotationEnabled,
 2981            process: processJapartsPriceData
 2982        },
 2983        {
 2984            providerKey: 'Motex',
 2985            displayName: 'МоТехС',
 2986            cacheKey: 'motexPriceData',
 02987            endpoint: '@Url.Action("Search", "MotexPrice")',
 2988            isEnabled: () => motexQuotationEnabled,
 2989            process: processMotexPriceData
 2990        },
 2991        {
 2992            providerKey: 'MlAuto',
 2993            displayName: 'ML-Auto BY',
 2994            cacheKey: 'mlAutoPriceData',
 02995            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',
 03004            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',
 03013            endpoint: '@Url.Action("Search", "ForumAutoPrice")',
 3014            isEnabled: () => forumAutoQuotationEnabled,
 3015            process: processForumAutoPriceData
 3016        },
 3017        {
 3018            providerKey: 'ForumAutoInterparts',
 3019            displayName: forumAutoInterpartsDisplayName,
 3020            cacheKey: 'forumAutoInterpartsPriceData',
 03021            endpoint: '@Url.Action("Search", "ForumAutoInterpartsPrice")',
 3022            isEnabled: () => forumAutoInterpartsQuotationEnabled,
 3023            process: processForumAutoInterpartsPriceData
 3024        },
 3025        {
 3026            providerKey: 'ForumAutoPiter',
 3027            displayName: forumAutoPiterDisplayName,
 3028            cacheKey: 'forumAutoPiterPriceData',
 03029            endpoint: '@Url.Action("Search", "ForumAutoPiterPrice")',
 3030            isEnabled: () => forumAutoPiterQuotationEnabled,
 3031            process: processForumAutoPiterPriceData
 3032        },
 3033        {
 3034            providerKey: 'ForumAutoRostov',
 3035            displayName: forumAutoRostovDisplayName,
 3036            cacheKey: 'forumAutoRostovPriceData',
 03037            endpoint: '@Url.Action("Search", "ForumAutoRostovPrice")',
 3038            isEnabled: () => forumAutoRostovQuotationEnabled,
 3039            process: processForumAutoRostovPriceData
 3040        },
 3041        {
 3042            providerKey: 'ForumAutoIstra',
 3043            displayName: forumAutoIstraDisplayName,
 3044            cacheKey: 'forumAutoIstraPriceData',
 03045            endpoint: '@Url.Action("Search", "ForumAutoIstraPrice")',
 3046            isEnabled: () => forumAutoIstraQuotationEnabled,
 3047            process: processForumAutoIstraPriceData
 3048        },
 3049        {
 3050            providerKey: 'FavoritParts',
 3051            displayName: 'Фаворит Союз',
 3052            cacheKey: 'favoritPartsPriceData',
 03053            endpoint: '@Url.Action("Search", "FavoritPartsPrice")',
 3054            isEnabled: () => favoritPartsQuotationEnabled,
 3055            process: processFavoritPartsPriceData
 3056        },
 3057        {
 3058            providerKey: 'FavoritPartsIstra',
 3059            displayName: 'Фаворит Истра',
 3060            cacheKey: 'favoritPartsIstraPriceData',
 03061            endpoint: '@Url.Action("Search", "FavoritPartsIstraPrice")',
 3062            isEnabled: () => favoritPartsIstraQuotationEnabled,
 3063            process: processFavoritPartsIstraPriceData
 3064        },
 3065        {
 3066            providerKey: 'FavoritPartsRostov',
 3067            displayName: 'Фаворит Ростов',
 3068            cacheKey: 'favoritPartsRostovPriceData',
 03069            endpoint: '@Url.Action("Search", "FavoritPartsRostovPrice")',
 3070            isEnabled: () => favoritPartsRostovQuotationEnabled,
 3071            process: processFavoritPartsRostovPriceData
 3072        },
 3073        {
 3074            providerKey: 'MoskvorechieIstra',
 3075            displayName: 'Москворечье (Истра)',
 3076            cacheKey: 'moskvorechieIstraPriceData',
 03077            endpoint: '@Url.Action("Search", "MoskvorechieIstraPrice")',
 3078            isEnabled: () => moskvorechieIstraQuotationEnabled,
 3079            process: processMoskvorechieIstraPriceData
 3080        },
 3081        {
 3082            providerKey: 'ProfitLiga',
 3083            displayName: 'Профит-Лига',
 3084            cacheKey: 'profitLigaPriceData',
 03085            endpoint: '@Url.Action("Search", "ProfitLigaPrice")',
 3086            isEnabled: () => profitLigaQuotationEnabled,
 3087            process: processProfitLigaPriceData
 3088        },
 3089        {
 3090            providerKey: 'Nikei',
 3091            displayName: 'Nikei',
 3092            cacheKey: 'nikeiPriceData',
 03093            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',
 03102            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',
 03111            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 {
 03702            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,
 03984                    'USD': parseFloat('@Model.RateUSD'.replace(',', '.')),
 03985                    'EUR': parseFloat('@Model.RateEUR'.replace(',', '.')),
 03986                    '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({
 04107            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({
 04126            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({
 04152            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) {
 05763                    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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').
 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,
 06562        'USD': parseFloat('@Model.RateUSD'.replace(',', '.')),
 06563        'EUR': parseFloat('@Model.RateEUR'.replace(',', '.')),
 06564        '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 () {
 06903        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>

Methods/Properties

ExecuteAsync()