< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_Shared__TransactionTable
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/Shared/_TransactionTable.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 15
Coverable lines: 15
Total lines: 779
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 50
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%2756520%
Col()0%620%

File(s)

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

#LineLine coverage
 1@using OzonDomains.Models
 2@using OzonOrdersWeb.ViewModels.OrderViewModels
 3@model MultiplayEditOrderViewModel
 4@{
 05    var fieldMap = Model.OrderListFieldReadAccess;
 06    bool Col(string fieldName) => fieldMap == null || fieldMap.GetValueOrDefault(fieldName, true);
 7}
 8
 9<link rel="stylesheet" href="~/css/Studio2/Orders/productImageCell.css" asp-append-version="true" />
 10<link rel="stylesheet" href="~/css/Studio2/Orders/TransactionsTable.css" asp-append-version="true" />
 11@* Жёсткая сетка строки заказа. Грузится после TransactionsTable.css — тот задаёт
 12   полосам 1fr 1fr, из-за чего высота полосы зависела от соседних колонок. *@
 13<link rel="stylesheet" href="~/css/Studio2/Orders/transaction-row-grid.css" asp-append-version="true" />
 14<link rel="stylesheet" href="~/css/Studio2/Labels/labelPrintProgress.css" asp-append-version="true" />
 15
 16<style>
 17    .tx-loading-dots span { animation: tx-blink 1.4s infinite; opacity: 0; }
 18    .tx-loading-dots span:nth-child(1) { animation-delay: 0s; }
 19    .tx-loading-dots span:nth-child(2) { animation-delay: 0.2s; }
 20    .tx-loading-dots span:nth-child(3) { animation-delay: 0.4s; }
 21    @@keyframes tx-blink { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } }
 22</style>
 23
 24<script>
 25    function txLoadingDotsHtml() {
 26        return '<span class="tx-loading-dots text-muted" style="font-size:1.4em;letter-spacing:2px"><span>.</span><span>
 27    }
 28
 29    async function waitForElementById(id, maxMs = 5000) {
 30        const start = Date.now();
 31        while (Date.now() - start < maxMs) {
 32            const el = document.getElementById(id);
 33            if (el) return el;
 34            await new Promise(r => setTimeout(r, 50));
 35        }
 36        return document.getElementById(id);
 37    }
 38
 39    function normalizeWarehouseStockKey(name) {
 40        return (name || '').trim().toUpperCase();
 41    }
 42
 43    function getWarehouseStockQty(data, warehouseKey) {
 44        const map = {
 45            M2: data.m2Qty,
 46            M3: data.m3Qty,
 47            B1: data.b1Qty,
 48            B2: data.b2Qty
 49        };
 50        return Object.prototype.hasOwnProperty.call(map, warehouseKey) ? map[warehouseKey] : null;
 51    }
 52
 53    function txEscapeHtml(text) {
 54        return String(text)
 55            .replace(/&/g, '&amp;')
 56            .replace(/</g, '&lt;')
 57            .replace(/>/g, '&gt;')
 58            .replace(/"/g, '&quot;');
 59    }
 60
 61    // Строки вида «M2: 5» — единый источник и для разметки, и для подсказки.
 62    function warehouseStockLines(data, warehouseName) {
 63        const fmtQty = n => (n == null ? '0' : n.toLocaleString('ru-RU', { maximumFractionDigits: 3 }));
 64        const warehouseKey = normalizeWarehouseStockKey(warehouseName);
 65
 66        if (warehouseKey && ['M2', 'M3', 'B1', 'B2'].includes(warehouseKey)) {
 67            return [warehouseKey + ': ' + fmtQty(getWarehouseStockQty(data, warehouseKey))];
 68        }
 69
 70        const lines = [];
 71        if (data.m2Qty) lines.push('M2: ' + fmtQty(data.m2Qty));
 72        if (data.m3Qty) lines.push('M3: ' + fmtQty(data.m3Qty));
 73        if (data.b1Qty) lines.push('B1: ' + fmtQty(data.b1Qty));
 74        if (data.b2Qty) lines.push('B2: ' + fmtQty(data.b2Qty));
 75        return lines;
 76    }
 77
 78    function oneCStockLines(data) {
 79        const fmtQty = n => n.toLocaleString('ru-RU', { maximumFractionDigits: 3 });
 80        const stocks = data.stocks || [];
 81        return stocks.map(s => s.warehouse + ': ' + fmtQty(s.quantity));
 82    }
 83
 84    // Учётная цена 1С: по складу строки, иначе средняя по всем складам (из ответа сервера).
 85    function oneCUnitCost(data, warehouseName) {
 86        const stocks = data.stocks || [];
 87        if (warehouseName) {
 88            const match = stocks.find(s => s.warehouse?.toLowerCase() === warehouseName.toLowerCase());
 89            if (match && match.unitCost != null) return match.unitCost;
 90        }
 91        return data.unitCost != null ? data.unitCost : null;
 92    }
 93
 94    // Имя склада из строки «M3: 2» — по нему клик подставляет засыл в строку заказа.
 95    function txStoreLineWarehouse(line) {
 96        const idx = String(line || '').lastIndexOf(':');
 97        return idx > 0 ? line.slice(0, idx).trim() : '';
 98    }
 99
 100    function txStockLinesToHtml(lines) {
 101        const items = lines.length > 0 ? lines : ['0 на складах'];
 102        return items.map(l => {
 103            const warehouse = txStoreLineWarehouse(l);
 104            const attr = warehouse ? ' data-warehouse="' + txEscapeHtml(warehouse) + '"' : '';
 105            return '<span class="tx-store-item"' + attr + '>' + txEscapeHtml(l) + '</span>';
 106        }).join('');
 107    }
 108
 109    function txZasylInputForOrder(orderId) {
 110        return orderId ? document.getElementById('orderSupplierMap_' + orderId) : null;
 111    }
 112
 113    /* Склад в остатках кликабелен только там, где есть куда его подставить: на страницах без
 114       колонки склада (и без поля «Засыл») остаётся обычная ссылка на карту движений. */
 115    function txMarkPickableStores(container, orderId) {
 116        if (!container || !txZasylInputForOrder(orderId)) return;
 117        container.querySelectorAll('.tx-store-item[data-warehouse]').forEach(item => {
 118            item.classList.add('tx-store-item--pickable');
 119            item.title = 'Подставить «' + item.dataset.warehouse + '» в засыл';
 120        });
 121    }
 122
 123    // Текст подсказки: обычный текст с переносами, без разметки.
 124    function txStockLinesToTitle(lines, header) {
 125        if (lines.length === 0) return header + ': нет остатков';
 126        return header + ':\n' + lines.join('\n');
 127    }
 128
 129    /* Остатки 1С грузятся по строке — параллельно с проценкой, как только страница отрисована.
 130       Одинаковые артикулы в документе спрашиваем один раз, а активные запросы держим в реестре,
 131       чтобы оборвать их при отправке формы: иначе они занимают соединения, нужные POST-у. */
 132    const txOneCStock = {
 133        cache: new Map(),
 134        inFlight: new Set(),
 135
 136        async get(article) {
 137            const key = String(article || '').trim();
 138            if (!key) return { found: false };
 139            if (this.cache.has(key)) return this.cache.get(key);
 140
 141            const ctrl = new AbortController();
 142            this.inFlight.add(ctrl);
 143            const timeoutId = setTimeout(() => ctrl.abort(), 15000);
 144            try {
 145                const response = await fetch('/Studio2/WarehouseStock/onec-lookup?article=' + encodeURIComponent(key), {
 146                    signal: ctrl.signal,
 147                    credentials: 'same-origin'
 148                });
 149                if (!response.ok) return { found: false };
 150                const data = await response.json();
 151                this.cache.set(key, data);
 152                return data;
 153            } catch (error) {
 154                // Неудачу не кэшируем: строку нужно уметь перезапросить.
 155                return { found: false };
 156            } finally {
 157                clearTimeout(timeoutId);
 158                this.inFlight.delete(ctrl);
 159            }
 160        },
 161
 162        forget(article) {
 163            this.cache.delete(String(article || '').trim());
 164        },
 165
 166        // Форму отправляют — фоновые остатки больше не нужны, освобождаем соединения.
 167        abortAll() {
 168            this.inFlight.forEach(ctrl => { try { ctrl.abort(); } catch (error) { /* уже завершён */ } });
 169            this.inFlight.clear();
 170        }
 171    };
 172
 173    window.txAbortStockLoads = () => txOneCStock.abortAll();
 174
 175    async function loadWarehouseStock(article, manufacturer, containerId, priceInputId, fillPrice, warehouseName, stockS
 176        let container = null;
 177        let requestId = null;
 178        try {
 179            container = containerId ? await waitForElementById(containerId) : null;
 180            // Один контейнер может быть запрошен повторно после изменения строки.
 181            // Ответ старого запроса не должен стирать результат нового.
 182            if (container) {
 183                requestId = String((Number(container.dataset.stockRequestId || '0') + 1));
 184                container.dataset.stockRequestId = requestId;
 185            }
 186            if (container) container.innerHTML = '<div class="tx-bitrix-stores-line">' + txLoadingDotsHtml() + '</div>';
 187
 188            let data;
 189            if (stockSource === 'onec') {
 190                data = await txOneCStock.get(article);
 191            } else {
 192                const ctrl = new AbortController();
 193                const timeoutId = setTimeout(() => ctrl.abort(), 10000);
 194                const params = new URLSearchParams();
 195                if (article) params.set('article', article);
 196                if (manufacturer) params.set('manufacturer', manufacturer);
 197
 198                const response = await fetch('/Studio2/WarehouseStock/lookup?' + params.toString(), {
 199                    signal: ctrl.signal,
 200                    credentials: 'same-origin'
 201                });
 202                clearTimeout(timeoutId);
 203
 204                if (!response.ok) {
 205                    if (container) container.innerHTML = '';
 206                    return;
 207                }
 208
 209                data = await response.json();
 210            }
 211
 212            if (container && container.dataset.stockRequestId !== requestId) return;
 213
 214            if (!data.found) {
 215                if (container) container.innerHTML = '';
 216                return;
 217            }
 218
 219            const containerOrderId = containerId ? containerId.replace('bitrixStores_', '') : '';
 220            if (container) {
 221                if (stockSource === 'onec') {
 222                    const body = txStockLinesToHtml(oneCStockLines(data));
 223                    const title = txStockLinesToTitle(oneCStockLines(data, null), 'Остатки 1С по складам');
 224                    const mmUrl = '/Studio2/WarehouseStock/movement-map?article=' + encodeURIComponent(article || '');
 225                    container.innerHTML = '<div class="tx-bitrix-stores-line" title="' + txEscapeHtml(title) + '"><a hre
 226                } else {
 227                    const wsUrl = '/Studio2/WarehouseStock?article=' + encodeURIComponent(article || '') +
 228                        '&manufacturer=' + encodeURIComponent(manufacturer || '') +
 229                        '#ws-row-' + data.id;
 230                    const linkHtml = ' <a href="' + wsUrl + '" target="_blank" class="tx-bitrix-stores-link" title="Стро
 231                    const body = txStockLinesToHtml(warehouseStockLines(data, warehouseName));
 232                    const title = txStockLinesToTitle(warehouseStockLines(data, null), 'Остатки по складам');
 233                    container.innerHTML = '<div class="tx-bitrix-stores-line" title="' + txEscapeHtml(title) + '">' + bo
 234                }
 235                txMarkPickableStores(container, containerOrderId);
 236            }
 237
 238            if (fillPrice && priceInputId) {
 239                const priceInput = await waitForElementById(priceInputId);
 240                // Заполняем только пустое поле: введённое пользователем значение важнее.
 241                if (priceInput && !priceInput.value.trim()) {
 242                    const isOneC = stockSource === 'onec';
 243                    const rawPrice = isOneC
 244                        ? oneCUnitCost(data, warehouseName)
 245                        : (priceInput.getAttribute('data-warehouse-c0') || data.c0);
 246
 247                    if (rawPrice != null && String(rawPrice).trim()) {
 248                        priceInput.value = typeof rawPrice === 'number'
 249                            ? rawPrice.toString()
 250                            : String(rawPrice).replace(',', '.');
 251                        priceInput.dispatchEvent(new Event('input', { bubbles: true }));
 252                        priceInput.dispatchEvent(new Event('change', { bubbles: true }));
 253                        priceInput.title = isOneC
 254                            ? 'Поле заполнено автоматически: учётная цена 1С'
 255                            : 'Поле заполнено автоматически из WarehouseStock (C0)';
 256                        priceInput.classList.add('highlight-warning');
 257                        setTimeout(() => priceInput.classList.remove('highlight-warning'), 3000);
 258                    }
 259                }
 260            }
 261        } catch (error) {
 262            // Убираем анимацию загрузки, иначе точки висят вечно (таймаут/сетевая ошибка).
 263            if (container && container.dataset.stockRequestId === requestId) container.innerHTML = '';
 264        }
 265    }
 266
 267    function initWarehouseStockAutoLoad() {
 268        document.querySelectorAll('[data-warehouse-stock-load]').forEach(function (el) {
 269            // Источники цены разные: C0 из WarehouseStock и учётная цена 1С — флаги тоже разные.
 270            const fillPrice = el.dataset.stockSource === 'onec'
 271                ? window.fillPriceFromOneCStock === true
 272                : window.fillPriceFromWarehouseStock === true;
 273            loadWarehouseStock(
 274                el.dataset.article || '',
 275                el.dataset.manufacturer || '',
 276                el.dataset.containerId || '',
 277                el.dataset.priceInputId || '',
 278                fillPrice,
 279                el.dataset.warehouse || '',
 280                el.dataset.stockSource || ''
 281            );
 282        });
 283    }
 284
 285    function applyPresetWarehouseC0() {
 286        if (window.fillPriceFromWarehouseStock !== true) return;
 287        document.querySelectorAll('.purchase-price-field').forEach(function (input) {
 288            if (input.value && input.value.trim()) return;
 289            const preset = input.getAttribute('data-warehouse-c0');
 290            if (!preset) return;
 291            input.value = preset.replace(',', '.');
 292            input.dispatchEvent(new Event('input', { bubbles: true }));
 293            input.dispatchEvent(new Event('change', { bubbles: true }));
 294        });
 295    }
 296
 297    function scheduleWarehouseStockAutoLoad() {
 298        applyPresetWarehouseC0();
 299        initWarehouseStockAutoLoad();
 300    }
 301
 302    /* Клик по складу в остатках подставляет его в «Засыл» строки — этим же полем задаётся склад
 303       списания в 1С, поэтому шлём input и change: пересчёт склада и проверка остатков слушают их.
 304       Карта движений остаётся доступной по Ctrl/⌘-клику и средней кнопкой, как у любой ссылки. */
 305    document.addEventListener('click', function (event) {
 306        const item = event.target.closest && event.target.closest('.tx-store-item[data-warehouse]');
 307        if (!item) return;
 308        if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
 309
 310        const orderId = item.closest('tr[data-order-id]')?.dataset.orderId
 311            || item.closest('[id^="bitrixStores_"]')?.id.replace('bitrixStores_', '');
 312        const zasylInput = txZasylInputForOrder(orderId);
 313        if (!zasylInput) return;
 314
 315        event.preventDefault();
 316        event.stopPropagation();
 317
 318        zasylInput.value = item.dataset.warehouse;
 319        zasylInput.dispatchEvent(new Event('input', { bubbles: true }));
 320        zasylInput.dispatchEvent(new Event('change', { bubbles: true }));
 321
 322        zasylInput.classList.add('highlight-warning');
 323        setTimeout(() => zasylInput.classList.remove('highlight-warning'), 1500);
 324    });
 325
 326    // Обработчик клика на стрелки количества
 327    document.addEventListener('DOMContentLoaded', function () {
 328        scheduleWarehouseStockAutoLoad();
 329        // Инициализация скрытых полей значениями из отображаемых span
 330        document.querySelectorAll('.quantity-display').forEach(function (span) {
 331            const orderId = span.getAttribute('data-order-id');
 332            const hiddenInput = document.querySelector(`input.quantity-hidden-input[data-order-id="${orderId}"]`);
 333            if (hiddenInput && !hiddenInput.value) {
 334                hiddenInput.value = span.textContent.trim();
 335            }
 336        });
 337
 338        // Обработчики для кнопок-стрелок
 339        document.querySelectorAll('.quantity-arrow-btn').forEach(function (btn) {
 340            btn.addEventListener('click', function () {
 341                const orderId = this.getAttribute('data-order-id');
 342                const action = this.getAttribute('data-action');
 343                const originalQuantity = parseFloat(this.getAttribute('data-original-quantity')) || 0;
 344
 345                // Находим скрытое поле и отображаемое значение
 346                const hiddenInput = document.querySelector(`input.quantity-hidden-input[data-order-id="${orderId}"]`);
 347                const displaySpan = document.querySelector(`span.quantity-display[data-order-id="${orderId}"]`);
 348
 349                if (!hiddenInput || !displaySpan) return;
 350
 351                let currentValue = parseFloat(hiddenInput.value) || originalQuantity;
 352
 353                // Изменяем значение
 354                if (action === 'increment') {
 355                    currentValue = Math.min(currentValue + 1, originalQuantity);
 356                } else if (action === 'decrement') {
 357                    currentValue = Math.max(currentValue - 1, 0);
 358                }
 359
 360                // Обновляем значения
 361                hiddenInput.value = currentValue;
 362                displaySpan.textContent = currentValue;
 363
 364                // Визуальная индикация изменения
 365                const originalQty = parseFloat(this.getAttribute('data-original-quantity')) || 0;
 366                if (currentValue !== originalQuantity) {
 367                    displaySpan.classList.add('quantity-changed');
 368                    displaySpan.classList.remove('qty-value--multi', 'qty-value--zero');
 369                    displaySpan.style.color = '';
 370                    displaySpan.style.fontWeight = '';
 371                } else {
 372                    displaySpan.classList.remove('quantity-changed');
 373                    displaySpan.classList.remove('qty-value--multi', 'qty-value--zero');
 374                    displaySpan.style.color = '';
 375                    displaySpan.style.fontWeight = '';
 376                    if (currentValue === 0 && originalQty > 0) {
 377                        displaySpan.classList.add('qty-value--zero');
 378                    } else if (currentValue > 1) {
 379                        displaySpan.classList.add('qty-value--multi');
 380                    }
 381                }
 382
 383                // Показываем сообщение об изменении
 384                if (currentValue !== originalQuantity) {
 385                    showQuantityChangeMessage(hiddenInput, currentValue, originalQuantity);
 386                }
 387            });
 388        });
 389
 390        // Инициализация tooltip для кнопок-стрелок
 391        var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
 392        var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
 393            return new bootstrap.Tooltip(tooltipTriggerEl);
 394        });
 395    });
 396
 397    // Функция для показа сообщения об изменении количества
 398    function showQuantityChangeMessage(input, currentValue, originalQuantity) {
 399        const difference = originalQuantity - currentValue;
 400        if (difference > 0) {
 401            // Показываем уведомление
 402            showNotification(
 403                `При сохранении будет создан новый заказ с количеством ${difference} и исходным статусом. Текущий заказ 
 404                'info'
 405            );
 406        }
 407    }
 408</script>
 409
 410<script>
 411
 412    // Функция для показа уведомлений
 413    function showNotification(message, type = 'info') {
 414        // Создаем контейнер для уведомлений если его нет
 415        let notificationContainer = document.getElementById('notification-container');
 416        if (!notificationContainer) {
 417            notificationContainer = document.createElement('div');
 418            notificationContainer.id = 'notification-container';
 419            notificationContainer.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 99999; display: fle
 420            document.body.appendChild(notificationContainer);
 421        }
 422
 423        // Создаем уведомление
 424        const toast = document.createElement('div');
 425        toast.className = `alert alert-${type} alert-dismissible fade show`;
 426        toast.style.cssText = 'min-width: 300px; max-width: 500px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);';
 427        toast.innerHTML = `
 428            <div style="font-size: 0.9rem;">${message}</div>
 429            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 430        `;
 431
 432        // Добавляем уведомление в контейнер
 433        notificationContainer.appendChild(toast);
 434
 435        // Инициализируем Bootstrap alert
 436        const bsAlert = new bootstrap.Alert(toast);
 437
 438        // Автоматическое скрытие через 8 секунд
 439        setTimeout(() => {
 440            if (toast.parentNode) {
 441                bsAlert.close();
 442            }
 443        }, 8000);
 444
 445        // Удаляем элемент из DOM после скрытия
 446        toast.addEventListener('closed.bs.alert', function () {
 447            if (toast.parentNode) {
 448                toast.parentNode.removeChild(toast);
 449            }
 450        });
 451    }
 452
 453    // Обработка загрузки файлов - автоматическая загрузка при выборе файла
 454    (function () {
 455        function initFileUpload() {
 456            const fileUploadContainer = document.getElementById('fileUploadContainer');
 457            if (!fileUploadContainer) return;
 458
 459            const fileInput = fileUploadContainer.querySelector('#fileInput');
 460            const uploadStatus = fileUploadContainer.querySelector('#uploadStatus');
 461
 462            if (!fileInput) return;
 463
 464            // Обрабатываем изменение файла - автоматическая загрузка
 465            fileInput.addEventListener('change', async function (e) {
 466                // Проверяем, что событие именно от нашего поля
 467                if (e.target !== fileInput) return;
 468
 469                // Останавливаем всплытие, но НЕ блокируем другие обработчики
 470                e.stopPropagation();
 471
 472                const file = fileInput.files[0];
 473                if (!file) {
 474                    return;
 475                }
 476
 477                // Собираем все orderIds из скрытых полей внутри контейнера
 478                const orderIds = Array.from(fileUploadContainer.querySelectorAll('input[name="orderIds"]'))
 479                    .map(input => parseInt(input.value))
 480                    .filter(id => !isNaN(id));
 481
 482                if (orderIds.length === 0) {
 483                    showNotification('Не найдено ни одного заказа для загрузки файла', 'warning');
 484                    fileInput.value = '';
 485                    return;
 486                }
 487
 488                // Создаем FormData
 489                const formData = new FormData();
 490                formData.append('file', file);
 491                orderIds.forEach(id => {
 492                    formData.append('orderIds', id);
 493                });
 494
 495                // Добавляем токен антифоржеризации, если он есть
 496                const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
 497                if (token) {
 498                    formData.append('__RequestVerificationToken', token);
 499                }
 500
 501                // Блокируем поле и показываем статус
 502                fileInput.disabled = true;
 503                uploadStatus.style.display = 'block';
 504                uploadStatus.innerHTML = '<div class="alert alert-info mb-0 p-2" style="font-size: 0.85rem;"><span class
 505
 506                try {
 0507                    const response = await fetch('@Url.Action("UploadFile", "TransactionFiles", new { area = "Studio2" }
 508                        method: 'POST',
 509                        body: formData
 510                    });
 511
 512                    const result = await response.json();
 513
 514                    if (response.ok) {
 515                        const folderPath = result.dropboxFolderPath || '';
 516                        uploadStatus.innerHTML = `<div class="alert alert-success mb-0 p-2" style="font-size: 0.85rem;">
 517                            <i class="fas fa-check-circle me-1"></i>${result.message || 'Файл успешно загружен'}
 518                            ${folderPath ? `<br><small class="text-muted">Путь: ${folderPath}</small>` : ''}
 519                            ${result.path ? `<br><a href="${result.path}" target="_blank" class="alert-link" style="font
 520                        </div>`;
 521                        const notificationMessage = folderPath
 522                            ? `Файл успешно загружен в: ${folderPath}`
 523                            : 'Файл успешно загружен';
 524                        showNotification(notificationMessage, 'success');
 525                        // Очищаем поле после успешной загрузки
 526                        setTimeout(() => {
 527                            fileInput.value = '';
 528                            fileInput.disabled = false;
 529                        }, 2000);
 530                    } else {
 531                        uploadStatus.innerHTML = `<div class="alert alert-danger mb-0 p-2" style="font-size: 0.85rem;">
 532                            <i class="fas fa-exclamation-circle me-1"></i>${result.error || 'Ошибка при загрузке файла'}
 533                        </div>`;
 534                        showNotification(result.error || 'Ошибка при загрузке файла', 'danger');
 535                        fileInput.value = '';
 536                        fileInput.disabled = false;
 537                    }
 538                } catch (error) {
 539                    uploadStatus.innerHTML = `<div class="alert alert-danger mb-0 p-2" style="font-size: 0.85rem;">
 540                        <i class="fas fa-exclamation-circle me-1"></i>Ошибка: ${error.message}
 541                    </div>`;
 542                    showNotification('Ошибка при загрузке файла: ' + error.message, 'danger');
 543                    fileInput.value = '';
 544                    fileInput.disabled = false;
 545                }
 546            });
 547        }
 548
 549        // Инициализация при загрузке DOM
 550        if (document.readyState === 'loading') {
 551            document.addEventListener('DOMContentLoaded', initFileUpload);
 552        } else {
 553            initFileUpload();
 554        }
 555    })();
 556</script>
 557<div class="tx-transaction-page">
 558<div class="table-container table-edge-flush">
 559    <table class="table transaction-table" id="shipmentTable">
 560        <colgroup id="tableColgroup"></colgroup>
 561        <partial name="~/Areas/Studio2/Views/Partials/Transaction/_TransactionTableHead.cshtml" model="Model"/>
 562
 563        <partial name="~/Areas/Studio2/Views/Partials/Transaction/_TransactionTableBody.cshtml" model="Model"/>
 564
 565        <tfoot>
 566        <tr>
 567            <th></th>
 568            <th colspan="10"></th>
 569
 0570            @if (Col("ShipmentTotal"))
 571            {
 572                <th style="white-space: nowrap; text-align: right;">
 573                    <div id="totalShipmentAmount"></div>
 574                </th>
 575            }
 576            else
 577            {
 578                <th></th>
 579            }
 580
 581
 0582            @if (Col("PurchasePrice"))
 583            {
 584                <th colspan="3" style="white-space: nowrap; text-align: left;">
 585                    <div id="totalPurchasePriceRub"></div>
 586                </th>
 587            }
 588            else
 589            {
 590                <th></th>
 591            }
 592        </tr>
 593        </tfoot>
 594        <input type="hidden" id="deletedOrders" name="deletedOrders"/>
 595    </table>
 596</div>
 597</div>
 598
 599<partial name="~/Areas/Studio2/Views/Partials/Transaction/_TransactionCalculationModal.cshtml" />
 600<script src="~/js/Studio2/Orders/Transactions/transaction-calculation-explainer.js" asp-append-version="true"></script>
 601<script src="~/js/Studio2/Orders/Transactions/transaction-header-actions.js" asp-append-version="true"></script>
 602
 603@{
 0604    var orderIds = Model.Orders?.Select(o => o.Id.ToString()).ToList() ?? new List<string>();
 0605    var idsString = string.Join(",", orderIds);
 606}
 0607@if (!string.IsNullOrEmpty(idsString))
 608{
 0609    var excelUrl = Url.Action("StartTransactionExcelTask", "Transaction", new { area = "Studio2", tableMode = true, ids 
 0610    var pdfUrl = Url.Action("PrintTransactionTableToPdf", "Transaction", new { area = "Studio2", ids = idsString });
 611
 612    <script>
 613        // Перемещаем кнопки печати в одну строку с кнопкой "Сохранить"
 614        (function () {
 615            function addPrintButtons() {
 616                // Ищем форму с кнопкой "Сохранить"
 617                const form = document.querySelector('form#createOrderForm') || document.querySelector('form');
 618                if (!form) {
 619                    // Если форма еще не загружена, пробуем еще раз через небольшую задержку
 620                    setTimeout(addPrintButtons, 100);
 621                    return;
 622                }
 623
 624                // Ищем блок с кнопкой "Сохранить" - ищем после partial view
 625                const saveButtonContainer = form.querySelector('.tx-page-footer') || form.querySelector('.form-group.mt-
 626                if (!saveButtonContainer) {
 627                    // Если контейнер не найден, пробуем еще раз
 628                    setTimeout(addPrintButtons, 100);
 629                    return;
 630                }
 631
 632                // Проверяем, не добавлены ли уже кнопки
 633                if (saveButtonContainer.querySelector('#printAllOzonLabelsBtn')) {
 634                    return; // Кнопки уже добавлены
 635                }
 636
 637                // Создаем контейнер для кнопок печати
 0638                const excelUrl = '@Html.Raw(excelUrl)';
 0639                const pdfUrl = '@Html.Raw(pdfUrl)';
 640
 641                const printButtonsHtml = `
 642                    <a href="${excelUrl}"
 643                       id="printTransactionExcelBtn"
 0644                       data-base-url="@Url.Action("StartTransactionExcelTask", "Transaction", new { area = "Studio2" })"
 645                       class="btn btn-outline-secondary tx-print-btn st2-transaction-excel-task-btn">
 646                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-
 647                            <path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.1
 648                            <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0
 649                        </svg>
 650                        Печать Excel
 651                    </a>
 652                    <a href="${pdfUrl}"
 653                       id="printTransactionPdfBtn"
 654                       data-base-url="${pdfUrl.split('?')[0]}"
 655                       class="btn btn-outline-secondary tx-print-btn"
 656                       target="_blank">
 657                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-
 658                            <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0
 659                            <path d="M4.603 14.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.
 660                        </svg>
 661                        Печать PDF
 662                    </a>
 663                    <button type="button"
 664                            id="printAllLabelsBtn"
 665                            class="btn btn-outline-secondary tx-print-btn">
 666                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-
 667                            <path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1zM11
 668                        </svg>
 669                        Этикетки всех
 670                    </button>
 671                    <button type="button"
 672                            id="printAllOzonLabelsBtn"
 673                            class="btn btn-outline-secondary tx-print-btn"
 674                            title="Печать Ozon-этикеток">
 675                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-
 676                            <path d="M2.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1"/>
 677                            <path d="M5 1a2 2 0 0 0-2 2v2H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h6a2 2 0 0 0 
 678                        </svg>
 679                        Этикетки Ozon
 680                    </button>
 681                `;
 682
 683                // Добавляем gap-2 класс если его нет
 684                if (!saveButtonContainer.classList.contains('gap-2')) {
 685                    saveButtonContainer.classList.add('gap-2');
 686                }
 687
 688                // Ищем input-group внутри контейнера
 689                const inputGroup = saveButtonContainer.querySelector('.input-group');
 690                const saveActions = saveButtonContainer.querySelector('.ots-save-actions');
 691                if (inputGroup) {
 692                    // Вставляем кнопки после input-group (справа от кнопки "Сохранить")
 693                    const tempDiv = document.createElement('div');
 694                    tempDiv.innerHTML = printButtonsHtml;
 695                    const buttons = Array.from(tempDiv.children);
 696                    buttons.forEach(btn => {
 697                        if (saveActions) {
 698                            saveButtonContainer.appendChild(btn);
 699                            return;
 700                        }
 701
 702                        // Вставляем после input-group
 703                        if (inputGroup.nextSibling) {
 704                            saveButtonContainer.insertBefore(btn, inputGroup.nextSibling);
 705                        } else {
 706                            saveButtonContainer.appendChild(btn);
 707                        }
 708                    });
 709                } else {
 710                    // Если input-group не найден, ищем кнопку submit
 711                    const saveButton = saveButtonContainer.querySelector('input[type="submit"]');
 712                    if (saveButton) {
 713                        const tempDiv = document.createElement('div');
 714                        tempDiv.innerHTML = printButtonsHtml;
 715                        const buttons = Array.from(tempDiv.children);
 716                        // Вставляем кнопки после кнопки submit
 717                        const submitContainer = saveButton.closest('.input-group') || saveButton.parentElement;
 718                        buttons.forEach(btn => {
 719                            if (submitContainer && submitContainer.nextSibling) {
 720                                saveButtonContainer.insertBefore(btn, submitContainer.nextSibling);
 721                            } else {
 722                                saveButtonContainer.appendChild(btn);
 723                            }
 724                        });
 725                    } else {
 726                        // Если ничего не найдено, просто добавляем кнопки в конец контейнера
 727                        saveButtonContainer.insertAdjacentHTML('beforeend', printButtonsHtml);
 728                    }
 729                }
 730
 731                if (typeof updateOzonLabelsButtonState === 'function') {
 732                    updateOzonLabelsButtonState();
 733                }
 734            }
 735
 736            // Запускаем при загрузке DOM
 737            if (document.readyState === 'loading') {
 738                document.addEventListener('DOMContentLoaded', addPrintButtons);
 739            } else {
 740                // DOM уже загружен
 741                addPrintButtons();
 742            }
 743        })();
 744    </script>
 745}
 746
 0747@await Html.PartialAsync("~/Areas/Studio2/Views/Partials/Transaction/_TransactionCommentField.cshtml")
 748
 749<input type="hidden" name="stockItemsJson" id="stockItemsJson"/>
 750
 751<partial name="~/Areas/Studio2/Views/Partials/Orders/_IndexPrintProgressOverlay.cshtml"/>
 752<partial name="~/Areas/Studio2/Views/Partials/Orders/Modals/_MultipleItemsConfirm.cshtml"/>
 753<partial name="~/Areas/Studio2/Views/Partials/Orders/Modals/_LabelPrintConfirm.cshtml"/>
 754
 755<div id="notification-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1060;"></div>
 756
 757<script src="~/js/Studio2/Labels/printLabels.js"></script>
 758<script src="~/js/Studio2/Labels/ozonLabelPrintCore.js"></script>
 759<script src="~/js/Studio2/Labels/transactionOzonLabelPrint.js"></script>
 760<script src="~/js/Studio2/Orders/generateLabel.js"></script>
 761
 762<!-- Контекстное меню для разделения заказов -->
 763<div id="infoWindow" class="dropdown-menu context-menu" style="display: none;">
 764    <!-- Content will be dynamically added by JavaScript -->
 765</div>
 766
 767@* Обработчик сканера штрих-кодов для транзакций *@
 768<script src="~/js/Studio2/Orders/Transactions/barcodeHandler.js" ></script>
 769
 770@* Разделение заказов *@
 771<script src="~/js/Studio2/Orders/Transactions/splitOrders.js"></script>
 0772@await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_ChannelTaskModal.cshtml")
 773<script src="~/js/st2-channel-task.js" asp-append-version="true"></script>
 774
 775@* Дополнительные расходы *@
 776<script src="~/js/Studio2/Orders/Transactions/expTypeHandler.js"></script>
 777
 778@* Панель "Отменить в Ozon" (CancelledTransaction и другие транзакции со статусом "Отменен") *@
 779<script src="~/js/Studio2/Transactions/ozonCancelPanel.js" asp-append-version="true"></script>

Methods/Properties

ExecuteAsync()
Col()