From: fomichev Date: Fri, 17 Apr 2026 10:48:07 +0000 (+0300) Subject: Фикс по фильтры X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=89f14219c1867f7a506f0a6681fd74de0496f10f;p=erp24_rep%2Fyii-erp24%2F.git Фикс по фильтры --- diff --git a/erp24/views/buyer-reference/index.php b/erp24/views/buyer-reference/index.php index 6e6368c8..af720d5e 100644 --- a/erp24/views/buyer-reference/index.php +++ b/erp24/views/buyer-reference/index.php @@ -168,4 +168,5 @@ JS; $this->registerJs($js); $this->registerJsFile('/js/product-mapping/index.js', ['position' => \yii\web\View::POS_END]); +$this->registerJs('if (typeof window.pmSetup === "function") { window.pmSetup(); }'); ?> diff --git a/erp24/views/product-mapping/index.php b/erp24/views/product-mapping/index.php index 71c58820..275eca69 100644 --- a/erp24/views/product-mapping/index.php +++ b/erp24/views/product-mapping/index.php @@ -109,7 +109,7 @@ use yii\widgets\LinkPager; registerJs('if (typeof window.pmInit === "function") { window.pmInit(' . \yii\helpers\Json::encode([ +$this->registerJs('if (typeof window.pmReinit === "function") { window.pmReinit(' . \yii\helpers\Json::encode([ 'urls' => [ 'createForm' => Url::to(['/product-mapping/create-form']), 'updateForm' => Url::to(['/product-mapping/update-form']), diff --git a/erp24/web/js/product-mapping/index.js b/erp24/web/js/product-mapping/index.js index 304c87fe..83feb61e 100644 --- a/erp24/web/js/product-mapping/index.js +++ b/erp24/web/js/product-mapping/index.js @@ -1,17 +1,29 @@ /* global bootstrap, yii */ -window.pmInit = function (cfg) { - if (!cfg) return; - // Снимаем все предыдущие обработчики этого модуля с document, - // чтобы предотвратить накопление при каждом reloadList(). - var NS = '.pmapping'; - $(document).off(NS); +/** + * Модуль маппинга товаров. + * + * Архитектура: + * pmSetup() — регистрирует обработчики НА DOCUMENT один раз при загрузке страницы. + * pmReinit() — вызывается каждый раз при (пере)загрузке контента вкладки: + * обновляет cfg и пересоздаёт Bootstrap Modal. + * + * Разделение позволяет не накапливать обработчики при каждом reloadList(). + */ +(function () { + // Единое состояние модуля — живёт пока жива страница + var state = { + cfg: null, // URL-ы эндпоинтов + modal: null, // Bootstrap Modal instance + editingId: null, + cascadeXhr: null, // in-flight cascade AJAX + activeXhr: null, // in-flight reloadList AJAX + searchTimer: null + }; - var pmModal = new bootstrap.Modal(document.getElementById('pm-modal')); - var editingId = null; - var searchTimer = null; - var cascadeXhr = null; - var loaderHtml = '
'; + /* ─────────────────────────────────────────────── + Helpers + ─────────────────────────────────────────────── */ function collectFilters() { var params = {}; @@ -20,232 +32,265 @@ window.pmInit = function (cfg) { var name = $el.attr('name'); if (!name) return; if ($el.is(':checkbox')) { - if ($el.is(':checked')) { - params[name] = '1'; - } + if ($el.is(':checked')) params[name] = '1'; } else { var v = $el.val(); - if (v !== '' && v !== null) { - params[name] = v; - } + if (v !== '' && v !== null) params[name] = v; } }); - params['per-page'] = $('#pm-per-page').val(); + params['per-page'] = $('#pm-per-page').val() || 50; return params; } function reloadList(extraParams) { - // Отменяем незавершённый cascade AJAX — сервер вернёт актуальные данные - if (cascadeXhr) { - cascadeXhr.abort(); - cascadeXhr = null; - } + if (!state.cfg) return; + + // Отменяем предыдущие AJAX-запросы чтобы избежать stale-ответов + if (state.cascadeXhr) { state.cascadeXhr.abort(); state.cascadeXhr = null; } + if (state.activeXhr) { state.activeXhr.abort(); state.activeXhr = null; } + var params = collectFilters(); - if (extraParams) { - $.extend(params, extraParams); - } + if (extraParams) $.extend(params, extraParams); + var $tab = $('#tab-mappings'); $tab.html('
Загрузка...
'); - $.get(cfg.urls.index, params, function (html) { + + state.activeXhr = $.get(state.cfg.urls.index, params, function (html) { + state.activeXhr = null; $tab.html(html); + }).fail(function (xhr, status) { + if (status === 'abort') return; + state.activeXhr = null; + var code = xhr.status || 0; + var msg = code >= 500 ? 'Ошибка сервера (' + code + ').' + : code === 403 ? 'Нет прав доступа (403).' + : code === 404 ? 'Страница не найдена (404).' + : 'Ошибка загрузки (' + (code || status) + ').'; + $tab.html( + '
' + + '' + msg + + ' ' + + '
' + ); }); } - function reloadAnalytics() { - var params = collectFilters(); - delete params['per-page']; - $.get(cfg.urls.analytics, params, function (data) { - $.each(data, function (field, value) { - $('[data-analytics-field="' + field + '"]').text(value); - }); - }, 'json'); + function getModal() { + // Всегда берём актуальный экземпляр Bootstrap Modal + return state.modal; } - // ========= ФИЛЬТРЫ ========= - - $(document).on('change' + NS, '.pm-filter', function () { - var name = $(this).attr('name'); - - if (name === 'category') { - var category = $(this).val(); - $('#pm-filter-subcategory').html('').prop('disabled', !category); - $('#pm-filter-species').html('').prop('disabled', true); - if (category) { - // Показываем подкатегории быстро, до полного reload списка - cascadeXhr = $.get(cfg.urls.cascade, {category: category}, function (data) { - cascadeXhr = null; - var html = ''; - $.each(data.subcategories, function (_, sub) { - html += ''; - }); - $('#pm-filter-subcategory').html(html).prop('disabled', false); - }, 'json'); + /* ─────────────────────────────────────────────── + pmSetup — вызывается ОДИН РАЗ при загрузке страницы + ─────────────────────────────────────────────── */ + + window.pmSetup = function () { + var NS = '.pmapping'; + $(document).off(NS); // на случай повторного вызова + + // ── Фильтры ────────────────────────────── + $(document).on('change' + NS, '.pm-filter', function () { + var name = $(this).attr('name'); + + if (name === 'category') { + var category = $(this).val(); + $('#pm-filter-subcategory').html('').prop('disabled', !category); + $('#pm-filter-species').html('').prop('disabled', true); + if (category && state.cfg) { + state.cascadeXhr = $.get(state.cfg.urls.cascade, {category: category}, function (data) { + state.cascadeXhr = null; + var html = ''; + $.each(data.subcategories || [], function (_, sub) { + html += ''; + }); + $('#pm-filter-subcategory').html(html).prop('disabled', false); + }, 'json').fail(function (x, s) { if (s !== 'abort') state.cascadeXhr = null; }); + } + reloadList({page: 1}); + return; } - reloadList({page: 1}); - return; - } - if (name === 'subcategory') { - var cat = $('#pm-filter-category').val(); - var sub = $(this).val(); - $('#pm-filter-species').html('').prop('disabled', !sub); - if (cat && sub) { - cascadeXhr = $.get(cfg.urls.cascade, {category: cat, subcategory: sub}, function (data) { - cascadeXhr = null; - var html = ''; - $.each(data.species, function (_, sp) { - html += ''; - }); - $('#pm-filter-species').html(html).prop('disabled', false); - }, 'json'); + if (name === 'subcategory') { + var cat = $('#pm-filter-category').val(); + var sub = $(this).val(); + $('#pm-filter-species').html('').prop('disabled', !sub); + if (cat && sub && state.cfg) { + state.cascadeXhr = $.get(state.cfg.urls.cascade, {category: cat, subcategory: sub}, function (data) { + state.cascadeXhr = null; + var html = ''; + $.each(data.species || [], function (_, sp) { + html += ''; + }); + $('#pm-filter-species').html(html).prop('disabled', false); + }, 'json').fail(function (x, s) { if (s !== 'abort') state.cascadeXhr = null; }); + } + reloadList({page: 1}); + return; } + + if (name === 'search') return; + reloadList({page: 1}); - return; - } + }); - if (name === 'search') return; + // Поиск с debounce + $(document).on('input' + NS, 'input[name="search"].pm-filter', function () { + clearTimeout(state.searchTimer); + state.searchTimer = setTimeout(function () { reloadList({page: 1}); }, 400); + }); - reloadList({page: 1}); - }); + // Сброс фильтров + $(document).on('click' + NS, '#pm-filter-reset', function () { + $('.pm-filter').each(function () { + var $el = $(this); + if ($el.is(':checkbox')) $el.prop('checked', false); + else if ($el.is('select')) $el.val(''); + else $el.val(''); + }); + reloadList({page: 1}); + }); - // Поиск — с debounce 400мс - $(document).on('input' + NS, 'input[name="search"].pm-filter', function () { - clearTimeout(searchTimer); - searchTimer = setTimeout(function () { + // Смена per-page (делегируем на document — элемент пересоздаётся при reload) + $(document).on('change' + NS, '#pm-per-page', function () { reloadList({page: 1}); - }, 400); - }); + }); - // Сброс фильтров - $(document).on('click' + NS, '#pm-filter-reset', function () { - $('.pm-filter').each(function () { - var $el = $(this); - if ($el.is(':checkbox')) { - $el.prop('checked', false); - } else if ($el.is('select')) { - $el.val(''); - } else { - $el.val(''); - } + // Экспорт + $(document).on('click' + NS, '#pm-export-btn', function () { + if (!state.cfg) return; + var params = collectFilters(); + delete params['per-page']; + var qs = $.param(params); + window.location.href = state.cfg.urls.export + (qs ? '?' + qs : ''); }); - reloadList({page: 1}); - }); - // Смена per-page - $('#pm-per-page').on('change', function () { - reloadList({page: 1}); - }); + // Пагинация + $(document).on('click' + NS, '.pm-pager a', function (e) { + e.preventDefault(); + var href = $(this).attr('href') || ''; + var m = href.match(/[?&]page=(\d+)/); + reloadList({page: m ? m[1] : 1}); + }); - // Экспорт в .xlsx — редирект с текущими фильтрами - $(document).on('click' + NS, '#pm-export-btn', function () { - var params = collectFilters(); - delete params['per-page']; - var qs = $.param(params); - window.location.href = cfg.urls.export + (qs ? '?' + qs : ''); - }); - - // Пагинация — перехват ссылок - $(document).on('click' + NS, '.pm-pager a', function (e) { - e.preventDefault(); - var href = $(this).attr('href'); - var pageMatch = href.match(/[?&]page=(\d+)/); - var page = pageMatch ? pageMatch[1] : 1; - reloadList({page: page}); - }); - - // ========= МОДАЛКА МАППИНГА ========= - - $(document).on('click' + NS, '.btn-pm-add', function () { - var $btn = $(this); - if ($btn.prop('disabled')) return; - $btn.prop('disabled', true); - editingId = null; - var guid = $btn.data('product-guid'); - $('#pm-modal-title').text('Добавить поставщика'); - $('#pm-modal-body').html(loaderHtml); - pmModal.show(); - $.get(cfg.urls.createForm, {product_guid: guid}, function (html) { - $('#pm-modal-body').html(html); - }).always(function () { - $btn.prop('disabled', false); + // Повтор при ошибке загрузки + $(document).on('click' + NS, '.js-pm-retry', function () { + reloadList({page: 1}); }); - }); - - $(document).on('click' + NS, '.btn-pm-edit', function (e) { - e.preventDefault(); - var $btn = $(this); - if ($btn.prop('disabled')) return; - $btn.prop('disabled', true); - editingId = $btn.data('id'); - $('#pm-modal-title').text('Редактировать маппинг'); - $('#pm-modal-body').html(loaderHtml); - pmModal.show(); - $.get(cfg.urls.updateForm, {id: editingId}, function (html) { - $('#pm-modal-body').html(html); - }).always(function () { - $btn.prop('disabled', false); + + // ── Модалка ─────────────────────────────── + $(document).on('click' + NS, '.btn-pm-add', function () { + if (!state.cfg) return; + var $btn = $(this); + if ($btn.prop('disabled')) return; + $btn.prop('disabled', true); + state.editingId = null; + + var modal = getModal(); + if (!modal) { $btn.prop('disabled', false); return; } + + $('#pm-modal-title').text('Добавить поставщика'); + $('#pm-modal-body').html('
'); + modal.show(); + + $.get(state.cfg.urls.createForm, {product_guid: $btn.data('product-guid')}, function (html) { + $('#pm-modal-body').html(html); + }).always(function () { $btn.prop('disabled', false); }); }); - }); - - $(document).on('click' + NS, '#btn-mapping-save', function () { - var $form = $('#mapping-form'); - var url = editingId ? cfg.urls.update + '?id=' + editingId : cfg.urls.create; - - $form.find('.is-invalid').removeClass('is-invalid'); - $form.find('.invalid-feedback').remove(); - - $.ajax({ - url: url, - type: 'POST', - data: $form.serialize(), - dataType: 'json', - success: function (resp) { - if (resp.success) { - pmModal.hide(); - reloadList(); - } else if (resp.errors) { - $.each(resp.errors, function (field, messages) { - var $input = $form.find('[name="ProductMapping[' + field + ']"]'); - if (!$input.length) { - $input = $form.find('[name="ProductMapping[' + field + '][]"]'); - } - $input.addClass('is-invalid'); - $input.after('
' + messages[0] + '
'); - }); - } - }, - error: function () { alert('Ошибка сервера'); } + + $(document).on('click' + NS, '.btn-pm-edit', function (e) { + e.preventDefault(); + if (!state.cfg) return; + var $btn = $(this); + if ($btn.prop('disabled')) return; + $btn.prop('disabled', true); + state.editingId = $btn.data('id'); + + var modal = getModal(); + if (!modal) { $btn.prop('disabled', false); return; } + + $('#pm-modal-title').text('Редактировать маппинг'); + $('#pm-modal-body').html('
'); + modal.show(); + + $.get(state.cfg.urls.updateForm, {id: state.editingId}, function (html) { + $('#pm-modal-body').html(html); + }).always(function () { $btn.prop('disabled', false); }); }); - }); - $(document).on('click' + NS, '.btn-pm-delete', function (e) { - e.preventDefault(); - var id = $(this).data('id'); - var name = $(this).data('name'); + $(document).on('click' + NS, '#btn-mapping-save', function () { + if (!state.cfg) return; + var $form = $('#mapping-form'); + var url = state.editingId + ? state.cfg.urls.update + '?id=' + state.editingId + : state.cfg.urls.create; - if (!confirm('Удалить маппинг на поставщика "' + name + '"?')) { - return; - } + $form.find('.is-invalid').removeClass('is-invalid'); + $form.find('.invalid-feedback').remove(); - $.ajax({ - url: cfg.urls.delete + '?id=' + id, - type: 'POST', - data: {_csrf: yii.getCsrfToken()}, - dataType: 'json', - success: function (resp) { - if (resp.success) { - reloadList(); - } else { - alert(resp.message || 'Ошибка удаления'); - } - }, - error: function () { alert('Ошибка сервера'); } + $.ajax({ + url: url, + type: 'POST', + data: $form.serialize(), + dataType: 'json', + success: function (resp) { + if (resp.success) { + var modal = getModal(); + if (modal) modal.hide(); + reloadList(); + } else if (resp.errors) { + $.each(resp.errors, function (field, messages) { + var $input = $form.find('[name="ProductMapping[' + field + ']"]'); + if (!$input.length) $input = $form.find('[name="ProductMapping[' + field + '][]"]'); + $input.addClass('is-invalid').after('
' + messages[0] + '
'); + }); + } + }, + error: function () { alert('Ошибка сервера'); } + }); + }); + + $(document).on('click' + NS, '.btn-pm-delete', function (e) { + e.preventDefault(); + if (!state.cfg) return; + var id = $(this).data('id'); + var name = $(this).data('name'); + if (!confirm('Удалить маппинг на поставщика "' + name + '"?')) return; + + $.ajax({ + url: state.cfg.urls.delete + '?id=' + id, + type: 'POST', + data: {_csrf: yii.getCsrfToken()}, + dataType: 'json', + success: function (resp) { + if (resp.success) reloadList(); + else alert(resp.message || 'Ошибка удаления'); + }, + error: function () { alert('Ошибка сервера'); } + }); + }); + + // Очистка ошибок при вводе в форме модалки + $(document).on('input change' + NS, '#mapping-form input, #mapping-form select', function () { + $(this).removeClass('is-invalid').next('.invalid-feedback').remove(); }); - }); - - // Очистка ошибок при вводе - $(document).on('input change' + NS, '#mapping-form input, #mapping-form select', function () { - $(this).removeClass('is-invalid'); - $(this).next('.invalid-feedback').remove(); - }); -}; + }; + + /* ─────────────────────────────────────────────── + pmReinit — вызывается при каждой (пере)загрузке контента вкладки + ─────────────────────────────────────────────── */ + + window.pmReinit = function (cfg) { + state.cfg = cfg; + + // Корректно уничтожаем старый Modal, чтобы Bootstrap не плодил глобальные слушатели + if (state.modal) { + try { state.modal.dispose(); } catch (e) {} + state.modal = null; + } + + var el = document.getElementById('pm-modal'); + if (el) { + state.modal = new bootstrap.Modal(el); + } + }; +}());