]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Фикс по фильтры
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 17 Apr 2026 10:48:07 +0000 (13:48 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 17 Apr 2026 10:48:07 +0000 (13:48 +0300)
erp24/views/buyer-reference/index.php
erp24/views/product-mapping/index.php
erp24/web/js/product-mapping/index.js

index 6e6368c8c52805eb068c945250ab08bb0aaf5abf..af720d5edae2fdabbb419679c152e524952cd52a 100644 (file)
@@ -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(); }');
 ?>
index 71c58820ac4d7b13391ef21b6c8c0d563f957213..275eca692b4750febf91f4a175aa4b48adf115c3 100644 (file)
@@ -109,7 +109,7 @@ use yii\widgets\LinkPager;
 </div>
 
 <?php
-$this->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']),
index 304c87fea8bfe17e42626314706ffcbdbe9de416..83feb61e931e095b64eb750fc3ea84e8cc9e495e 100644 (file)
@@ -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 = '<div class="text-center p-4"><i class="fa fa-spinner fa-spin fa-2x text-secondary"></i></div>';
+    /* ───────────────────────────────────────────────
+       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('<div class="text-muted p-4 text-center"><i class="fa fa-spinner fa-spin me-2"></i>Загрузка...</div>');
-        $.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(
+                '<div class="alert alert-danger m-3">' +
+                '<i class="fa fa-exclamation-triangle me-2"></i>' + msg +
+                ' <button type="button" class="btn btn-sm btn-outline-danger ms-2 js-pm-retry">Повторить</button>' +
+                '</div>'
+            );
         });
     }
 
-    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('<option value="">Все</option>').prop('disabled', !category);
-            $('#pm-filter-species').html('<option value="">Все</option>').prop('disabled', true);
-            if (category) {
-                // Показываем подкатегории быстро, до полного reload списка
-                cascadeXhr = $.get(cfg.urls.cascade, {category: category}, function (data) {
-                    cascadeXhr = null;
-                    var html = '<option value="">Все</option>';
-                    $.each(data.subcategories, function (_, sub) {
-                        html += '<option value="' + sub + '">' + sub + '</option>';
-                    });
-                    $('#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('<option value="">Все</option>').prop('disabled', !category);
+                $('#pm-filter-species').html('<option value="">Все</option>').prop('disabled', true);
+                if (category && state.cfg) {
+                    state.cascadeXhr = $.get(state.cfg.urls.cascade, {category: category}, function (data) {
+                        state.cascadeXhr = null;
+                        var html = '<option value="">Все</option>';
+                        $.each(data.subcategories || [], function (_, sub) {
+                            html += '<option value="' + sub + '">' + sub + '</option>';
+                        });
+                        $('#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('<option value="">Все</option>').prop('disabled', !sub);
-            if (cat && sub) {
-                cascadeXhr = $.get(cfg.urls.cascade, {category: cat, subcategory: sub}, function (data) {
-                    cascadeXhr = null;
-                    var html = '<option value="">Все</option>';
-                    $.each(data.species, function (_, sp) {
-                        html += '<option value="' + sp + '">' + sp + '</option>';
-                    });
-                    $('#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('<option value="">Все</option>').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 = '<option value="">Все</option>';
+                        $.each(data.species || [], function (_, sp) {
+                            html += '<option value="' + sp + '">' + sp + '</option>';
+                        });
+                        $('#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('<div class="text-center p-4"><i class="fa fa-spinner fa-spin fa-2x text-secondary"></i></div>');
+            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('<div class="invalid-feedback">' + messages[0] + '</div>');
-                    });
-                }
-            },
-            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('<div class="text-center p-4"><i class="fa fa-spinner fa-spin fa-2x text-secondary"></i></div>');
+            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('<div class="invalid-feedback">' + messages[0] + '</div>');
+                        });
+                    }
+                },
+                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);
+        }
+    };
+}());