diff --git a/myproject/products/static/products/css/product-search-picker.css b/myproject/products/static/products/css/product-search-picker.css new file mode 100644 index 0000000..465abd5 --- /dev/null +++ b/myproject/products/static/products/css/product-search-picker.css @@ -0,0 +1,170 @@ +/** + * ProductSearchPicker - стили компонента поиска и выбора товаров + */ + +.product-search-picker .product-picker-item { + transition: all 0.15s ease; + border: 2px solid transparent; +} + +.product-search-picker .product-picker-item:hover { + border-color: #dee2e6; + background-color: #f8f9fa; +} + +.product-search-picker .product-picker-item.selected { + border-color: #198754 !important; + background-color: #d1e7dd !important; +} + +.product-search-picker .product-picker-item.selected .card { + border-color: #198754; +} + +/* Grid view - карточки */ +.product-search-picker .product-picker-grid[data-view="grid"] { + display: flex; + flex-wrap: wrap; +} + +.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item .card { + transition: transform 0.15s, box-shadow 0.15s; +} + +.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item:hover .card { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item.selected .card { + box-shadow: 0 0 0 2px #198754; +} + +/* List view - список */ +.product-search-picker .product-picker-grid[data-view="list"] { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.product-search-picker .product-picker-grid[data-view="list"] .col-6, +.product-search-picker .product-picker-grid[data-view="list"] .col-lg-4, +.product-search-picker .product-picker-grid[data-view="list"] .col-xl-3 { + flex: 0 0 100%; + max-width: 100%; + padding: 0; +} + +/* Loading spinner */ +.product-search-picker .product-picker-loading { + min-height: 100px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Контент - scrollable */ +.product-search-picker .product-picker-content { + position: relative; + min-height: 150px; +} + +/* Фильтры */ +.product-search-picker .product-picker-filters { + background-color: #f8f9fa; +} + +/* Счетчик выбранных */ +.product-search-picker .product-picker-selected-count { + min-width: 24px; + text-align: center; +} + +/* Иконка выбора в list view */ +.product-search-picker .product-picker-grid[data-view="list"] .bi-circle, +.product-search-picker .product-picker-grid[data-view="list"] .bi-check-circle-fill { + font-size: 1.2rem; + flex-shrink: 0; +} + +/* Иконка выбора в grid view */ +.product-search-picker .product-picker-grid[data-view="grid"] .check-icon { + position: absolute; + top: 0.5rem; + right: 0.5rem; + font-size: 1.5rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + z-index: 1; +} + +/* Пустой результат */ +.product-search-picker .product-picker-empty { + min-height: 150px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* Кнопка очистки поиска */ +.product-search-picker .product-picker-search-clear { + border-left: 0; +} + +/* Hover эффект для кнопок действий */ +.product-search-picker .card-footer .btn { + transition: all 0.15s; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .product-search-picker .product-picker-grid[data-view="grid"] .col-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .product-search-picker .card-header .d-flex { + flex-direction: column; + align-items: stretch !important; + gap: 0.5rem; + } + + .product-search-picker .card-header .input-group { + width: 100% !important; + } + + .product-search-picker .card-footer { + flex-direction: column; + gap: 0.5rem; + } + + .product-search-picker .card-footer > div { + width: 100%; + display: flex; + gap: 0.5rem; + } + + .product-search-picker .card-footer .btn { + flex: 1; + } +} + +@media (max-width: 576px) { + .product-search-picker .product-picker-grid[data-view="grid"] .col-6 { + flex: 0 0 100%; + max-width: 100%; + } + + .product-search-picker .product-picker-filters .row { + flex-direction: column; + } + + .product-search-picker .product-picker-filters .col-auto { + width: 100%; + } + + .product-search-picker .product-picker-filters .col-auto.ms-auto { + margin-left: 0 !important; + margin-top: 0.5rem; + } +} diff --git a/myproject/products/static/products/js/product-search-picker.js b/myproject/products/static/products/js/product-search-picker.js new file mode 100644 index 0000000..fefc274 --- /dev/null +++ b/myproject/products/static/products/js/product-search-picker.js @@ -0,0 +1,747 @@ +/** + * ProductSearchPicker - переиспользуемый модуль для поиска и выбора товаров + * + * API: + * - ProductSearchPicker.init(containerSelector, options) - инициализация + * - instance.getSelected() - получить выбранные товары (массив объектов) + * - instance.getSelectedIds() - получить ID выбранных товаров (массив строк) + * - instance.clearSelection() - сбросить выбор + * - instance.setSelection(productIds) - установить выбор программно + * - instance.refresh() - обновить список товаров + * - instance.destroy() - уничтожить экземпляр + * + * Пример использования: + * const picker = ProductSearchPicker.init('#my-picker', { + * onAddSelected: function(products, instance) { + * console.log('Добавлены товары:', products); + * } + * }); + */ +(function(window) { + 'use strict'; + + // Приватное хранилище экземпляров + var instances = {}; + + /** + * Конструктор компонента + */ + function ProductSearchPicker(container, options) { + this.container = typeof container === 'string' + ? document.querySelector(container) + : container; + + if (!this.container) { + console.error('ProductSearchPicker: container not found'); + return; + } + + // Настройки по умолчанию + this.options = Object.assign({ + apiUrl: this.container.dataset.apiUrl || '/products/api/search-products-variants/', + multiSelect: this.container.dataset.multiSelect !== 'false', + maxSelection: parseInt(this.container.dataset.maxSelection) || null, + excludeKits: this.container.dataset.excludeKits !== 'false', + debounceDelay: 300, + pageSize: 30, + + // Callbacks + onSelect: null, // (products, instance) => {} + onDeselect: null, // (products, instance) => {} + onSelectionChange: null, // (selectedProducts, instance) => {} + onAddSelected: null // (selectedProducts, instance) => {} + }, options || {}); + + // Состояние + this.state = { + products: [], + selected: {}, // {productId: productData} + currentPage: 1, + hasMore: false, + isLoading: false, + currentView: 'list', + filters: { + search: '', + category: '', + tag: '', + inStock: false + } + }; + + // Кэшируем DOM элементы + this._cacheElements(); + + // Привязываем обработчики + this._bindEvents(); + + // Начальная загрузка + this._loadProducts(); + } + + /** + * Кэширование DOM элементов + */ + ProductSearchPicker.prototype._cacheElements = function() { + var c = this.container; + this.elements = { + searchInput: c.querySelector('.product-picker-search'), + searchClear: c.querySelector('.product-picker-search-clear'), + categorySelect: c.querySelector('.product-picker-category'), + tagSelect: c.querySelector('.product-picker-tag'), + inStockCheckbox: c.querySelector('.product-picker-in-stock'), + grid: c.querySelector('.product-picker-grid'), + loading: c.querySelector('.product-picker-loading'), + empty: c.querySelector('.product-picker-empty'), + content: c.querySelector('.product-picker-content'), + selectedCount: c.querySelector('.product-picker-selected-count'), + selectAllBtn: c.querySelector('.product-picker-select-all'), + clearSelectionBtn: c.querySelector('.product-picker-clear-selection'), + addSelectedBtn: c.querySelector('.product-picker-add-selected'), + viewButtons: c.querySelectorAll('.product-picker-view') + }; + + // Начальный вид из data-атрибута + if (this.elements.grid) { + var initialView = this.elements.grid.dataset.view || 'list'; + this.state.currentView = initialView; + this._updateViewButtons(); + } + }; + + /** + * Привязка обработчиков событий + */ + ProductSearchPicker.prototype._bindEvents = function() { + var self = this; + var debounceTimer; + + // Поиск с debounce + if (this.elements.searchInput) { + this.elements.searchInput.addEventListener('input', function() { + clearTimeout(debounceTimer); + var value = this.value.trim(); + + // Показать/скрыть кнопку очистки + if (self.elements.searchClear) { + self.elements.searchClear.style.display = value ? 'block' : 'none'; + } + + debounceTimer = setTimeout(function() { + self.state.filters.search = value; + self.state.currentPage = 1; + self._loadProducts(); + }, self.options.debounceDelay); + }); + + // Поиск по Enter + this.elements.searchInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + clearTimeout(debounceTimer); + self.state.filters.search = this.value.trim(); + self.state.currentPage = 1; + self._loadProducts(); + } + }); + } + + // Кнопка очистки поиска + if (this.elements.searchClear) { + this.elements.searchClear.addEventListener('click', function() { + self.elements.searchInput.value = ''; + this.style.display = 'none'; + self.state.filters.search = ''; + self.state.currentPage = 1; + self._loadProducts(); + }); + } + + // Фильтр по категории + if (this.elements.categorySelect) { + this.elements.categorySelect.addEventListener('change', function() { + self.state.filters.category = this.value; + self.state.currentPage = 1; + self._loadProducts(); + }); + } + + // Фильтр по тегу + if (this.elements.tagSelect) { + this.elements.tagSelect.addEventListener('change', function() { + self.state.filters.tag = this.value; + self.state.currentPage = 1; + self._loadProducts(); + }); + } + + // Фильтр по наличию + if (this.elements.inStockCheckbox) { + this.elements.inStockCheckbox.addEventListener('change', function() { + self.state.filters.inStock = this.checked; + self.state.currentPage = 1; + self._loadProducts(); + }); + // Инициализация из checkbox + this.state.filters.inStock = this.elements.inStockCheckbox.checked; + } + + // Переключатель вида + this.elements.viewButtons.forEach(function(btn) { + btn.addEventListener('click', function() { + var view = this.dataset.view; + self._setView(view); + }); + }); + + // Клик по товару (делегирование) + if (this.elements.grid) { + this.elements.grid.addEventListener('click', function(e) { + var productCard = e.target.closest('.product-picker-item'); + if (productCard) { + var productId = productCard.dataset.productId; + self._toggleProduct(productId); + } + }); + } + + // Выбрать все + if (this.elements.selectAllBtn) { + this.elements.selectAllBtn.addEventListener('click', function() { + self._selectAll(); + }); + } + + // Сбросить выбор + if (this.elements.clearSelectionBtn) { + this.elements.clearSelectionBtn.addEventListener('click', function() { + self.clearSelection(); + }); + } + + // Добавить выбранные + if (this.elements.addSelectedBtn) { + this.elements.addSelectedBtn.addEventListener('click', function() { + if (self.options.onAddSelected) { + self.options.onAddSelected(self.getSelected(), self); + } + }); + } + + // Infinite scroll + if (this.elements.content) { + this.elements.content.addEventListener('scroll', function() { + if (self.state.hasMore && !self.state.isLoading) { + var scrollTop = this.scrollTop; + var scrollHeight = this.scrollHeight; + var clientHeight = this.clientHeight; + + if (scrollTop + clientHeight >= scrollHeight - 100) { + self.state.currentPage++; + self._loadProducts(true); // append mode + } + } + }); + } + }; + + /** + * Загрузка товаров через AJAX + */ + ProductSearchPicker.prototype._loadProducts = function(append) { + var self = this; + + if (this.state.isLoading) return; + this.state.isLoading = true; + + // Показать индикатор загрузки + if (!append) { + if (this.elements.loading) this.elements.loading.style.display = 'flex'; + if (this.elements.grid) this.elements.grid.innerHTML = ''; + if (this.elements.empty) this.elements.empty.style.display = 'none'; + } + + // Формируем параметры запроса + var params = new URLSearchParams({ + type: 'product', // Только штучные товары + page: this.state.currentPage + }); + + if (this.state.filters.search) { + params.append('q', this.state.filters.search); + } + if (this.state.filters.category) { + params.append('category', this.state.filters.category); + } + if (this.state.filters.tag) { + params.append('tag', this.state.filters.tag); + } + if (this.state.filters.inStock) { + params.append('in_stock', 'true'); + } + + fetch(this.options.apiUrl + '?' + params.toString()) + .then(function(response) { + return response.json(); + }) + .then(function(data) { + self.state.isLoading = false; + if (self.elements.loading) self.elements.loading.style.display = 'none'; + + // Извлекаем товары (учитываем группировку в API) + var products = []; + if (Array.isArray(data.results)) { + data.results.forEach(function(item) { + if (item.children) { + // Группированные результаты + item.children.forEach(function(p) { + if (p.type === 'product') { + products.push(p); + } + }); + } else if (item.type === 'product') { + products.push(item); + } + }); + } + + if (append) { + self.state.products = self.state.products.concat(products); + } else { + self.state.products = products; + } + + self.state.hasMore = data.pagination && data.pagination.more; + + // Рендеринг + self._renderProducts(products, append); + + // Показать "пусто" если нет результатов + if (self.state.products.length === 0) { + if (self.elements.empty) self.elements.empty.style.display = 'flex'; + } else { + if (self.elements.empty) self.elements.empty.style.display = 'none'; + } + }) + .catch(function(error) { + console.error('ProductSearchPicker: fetch error', error); + self.state.isLoading = false; + if (self.elements.loading) self.elements.loading.style.display = 'none'; + }); + }; + + /** + * Рендеринг товаров + */ + ProductSearchPicker.prototype._renderProducts = function(products, append) { + var self = this; + var fragment = document.createDocumentFragment(); + + products.forEach(function(product) { + // Извлекаем ID (убираем префикс "product_") + var productId = String(product.id).replace('product_', ''); + var isSelected = self.state.selected.hasOwnProperty(productId); + + var html = self.state.currentView === 'grid' + ? self._renderGridCard(product, productId, isSelected) + : self._renderListItem(product, productId, isSelected); + + var temp = document.createElement('div'); + temp.innerHTML = html; + fragment.appendChild(temp.firstElementChild); + }); + + if (append) { + this.elements.grid.appendChild(fragment); + } else { + this.elements.grid.innerHTML = ''; + this.elements.grid.appendChild(fragment); + } + }; + + /** + * Рендеринг карточки товара (grid view) + */ + ProductSearchPicker.prototype._renderGridCard = function(product, productId, isSelected) { + var inStockBadge = product.in_stock + ? 'В наличии' + : 'Нет'; + + var selectedClass = isSelected ? 'selected' : ''; + var checkIcon = isSelected + ? '' + : ''; + + var photoHtml = product.photo_url + ? '' + (product.text || '') + '' + : '
'; + + var name = product.text || product.name || ''; + var price = product.actual_price || product.price || '0'; + + return '
' + + '
' + + '
' + + photoHtml + + checkIcon + + '
' + + '
' + + '
' + name + '
' + + '
' + + '' + price + ' руб.' + + inStockBadge + + '
' + + '' + (product.sku || '') + '' + + '
' + + '
' + + '
'; + }; + + /** + * Рендеринг строки товара (list view) + */ + ProductSearchPicker.prototype._renderListItem = function(product, productId, isSelected) { + var inStockBadge = product.in_stock + ? 'В наличии' + : 'Нет'; + + var selectedClass = isSelected ? 'selected' : ''; + var checkIcon = isSelected + ? '' + : ''; + + var photoHtml = product.photo_url + ? '' + : '
'; + + var name = product.text || product.name || ''; + var price = product.actual_price || product.price || '0'; + + return '
' + + '
' + + checkIcon + + '
' + + photoHtml + + '
' + + '
' + + '
' + name + '
' + + '' + (product.sku || '') + '' + + '
' + + '
' + + '
' + price + ' руб.
' + + inStockBadge + + '
' + + '
' + + '
'; + }; + + /** + * Переключение выбора товара + */ + ProductSearchPicker.prototype._toggleProduct = function(productId) { + var self = this; + var product = null; + + // Находим товар в списке + for (var i = 0; i < this.state.products.length; i++) { + var p = this.state.products[i]; + if (String(p.id).replace('product_', '') === productId) { + product = p; + break; + } + } + + if (!product) return; + + var isSelected = this.state.selected.hasOwnProperty(productId); + + if (isSelected) { + // Снять выбор + delete this.state.selected[productId]; + if (this.options.onDeselect) { + this.options.onDeselect([product], this); + } + } else { + // Проверка множественного выбора + if (!this.options.multiSelect) { + this.state.selected = {}; + } + + // Проверка лимита + if (this.options.maxSelection && + Object.keys(this.state.selected).length >= this.options.maxSelection) { + return; + } + + // Добавить в выбор + this.state.selected[productId] = product; + if (this.options.onSelect) { + this.options.onSelect([product], this); + } + } + + // Обновить UI + this._updateProductUI(productId); + this._updateSelectionUI(); + + // Общий callback + if (this.options.onSelectionChange) { + this.options.onSelectionChange(this.getSelected(), this); + } + }; + + /** + * Обновление UI товара + */ + ProductSearchPicker.prototype._updateProductUI = function(productId) { + var card = this.elements.grid.querySelector('[data-product-id="' + productId + '"]'); + if (!card) return; + + var isSelected = this.state.selected.hasOwnProperty(productId); + + if (isSelected) { + card.classList.add('selected'); + // Обновить иконку + var icon = card.querySelector('.bi-circle'); + if (icon) { + icon.classList.remove('bi-circle', 'text-muted'); + icon.classList.add('bi-check-circle-fill', 'text-success'); + } + // Добавить иконку в grid view если её нет + if (this.state.currentView === 'grid') { + var posRel = card.querySelector('.position-relative'); + if (posRel && !posRel.querySelector('.check-icon')) { + var checkEl = document.createElement('i'); + checkEl.className = 'bi bi-check-circle-fill text-success check-icon'; + posRel.appendChild(checkEl); + } + } + } else { + card.classList.remove('selected'); + var iconCheck = card.querySelector('.bi-check-circle-fill'); + if (iconCheck) { + // В list view меняем иконку + if (this.state.currentView === 'list') { + iconCheck.classList.remove('bi-check-circle-fill', 'text-success'); + iconCheck.classList.add('bi-circle', 'text-muted'); + } else { + // В grid view удаляем иконку + if (iconCheck.classList.contains('check-icon')) { + iconCheck.remove(); + } + } + } + } + }; + + /** + * Обновление UI выбора (счетчик, кнопки) + */ + ProductSearchPicker.prototype._updateSelectionUI = function() { + var count = Object.keys(this.state.selected).length; + + // Счетчик + if (this.elements.selectedCount) { + this.elements.selectedCount.textContent = count; + } + + // Кнопка "Добавить" + if (this.elements.addSelectedBtn) { + this.elements.addSelectedBtn.disabled = count === 0; + } + }; + + /** + * Выбрать все видимые товары + */ + ProductSearchPicker.prototype._selectAll = function() { + var self = this; + var currentCount = Object.keys(this.state.selected).length; + var maxToSelect = this.options.maxSelection + ? this.options.maxSelection - currentCount + : Infinity; + + var added = 0; + this.state.products.forEach(function(product) { + if (added >= maxToSelect) return; + + var productId = String(product.id).replace('product_', ''); + if (!self.state.selected.hasOwnProperty(productId)) { + self.state.selected[productId] = product; + self._updateProductUI(productId); + added++; + } + }); + + this._updateSelectionUI(); + + if (this.options.onSelectionChange) { + this.options.onSelectionChange(this.getSelected(), this); + } + }; + + /** + * Обновление кнопок переключателя вида + */ + ProductSearchPicker.prototype._updateViewButtons = function() { + var self = this; + this.elements.viewButtons.forEach(function(btn) { + if (btn.dataset.view === self.state.currentView) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + }; + + /** + * Переключение вида (grid/list) + */ + ProductSearchPicker.prototype._setView = function(view) { + this.state.currentView = view; + + // Обновить data-атрибут + if (this.elements.grid) { + this.elements.grid.dataset.view = view; + } + + // Обновить кнопки + this._updateViewButtons(); + + // Перерисовать + this._renderProducts(this.state.products, false); + }; + + // ========== ПУБЛИЧНОЕ API ========== + + /** + * Получить массив выбранных товаров + */ + ProductSearchPicker.prototype.getSelected = function() { + var result = []; + for (var key in this.state.selected) { + if (this.state.selected.hasOwnProperty(key)) { + result.push(this.state.selected[key]); + } + } + return result; + }; + + /** + * Получить массив ID выбранных товаров + */ + ProductSearchPicker.prototype.getSelectedIds = function() { + return Object.keys(this.state.selected); + }; + + /** + * Сбросить выбор + */ + ProductSearchPicker.prototype.clearSelection = function() { + var self = this; + var ids = Object.keys(this.state.selected); + this.state.selected = {}; + + ids.forEach(function(productId) { + self._updateProductUI(productId); + }); + + this._updateSelectionUI(); + + if (this.options.onSelectionChange) { + this.options.onSelectionChange([], this); + } + }; + + /** + * Установить выбор программно + */ + ProductSearchPicker.prototype.setSelection = function(productIds) { + var self = this; + this.clearSelection(); + + productIds.forEach(function(id) { + var productId = String(id).replace('product_', ''); + // Ищем товар в загруженном списке + for (var i = 0; i < self.state.products.length; i++) { + var p = self.state.products[i]; + if (String(p.id).replace('product_', '') === productId) { + self.state.selected[productId] = p; + self._updateProductUI(productId); + break; + } + } + }); + + this._updateSelectionUI(); + }; + + /** + * Обновить список товаров + */ + ProductSearchPicker.prototype.refresh = function() { + this.state.currentPage = 1; + this._loadProducts(); + }; + + /** + * Уничтожить экземпляр + */ + ProductSearchPicker.prototype.destroy = function() { + // Очистить состояние + this.state.products = []; + this.state.selected = {}; + + // Удалить из хранилища + if (this.container.id && instances[this.container.id]) { + delete instances[this.container.id]; + } + }; + + // ========== СТАТИЧЕСКИЕ МЕТОДЫ ========== + + /** + * Инициализация компонента + * @param {string|Element} container - селектор или DOM элемент + * @param {Object} options - настройки + * @returns {ProductSearchPicker} + */ + ProductSearchPicker.init = function(container, options) { + var element = typeof container === 'string' + ? document.querySelector(container) + : container; + + if (!element) { + console.error('ProductSearchPicker.init: container not found'); + return null; + } + + // Проверяем, не инициализирован ли уже + if (element.id && instances[element.id]) { + return instances[element.id]; + } + + var instance = new ProductSearchPicker(element, options); + + if (element.id) { + instances[element.id] = instance; + } + + return instance; + }; + + /** + * Получить экземпляр по контейнеру + */ + ProductSearchPicker.getInstance = function(container) { + var element = typeof container === 'string' + ? document.querySelector(container) + : container; + return element && element.id ? instances[element.id] : null; + }; + + // Экспорт в глобальную область + window.ProductSearchPicker = ProductSearchPicker; + +})(window); diff --git a/myproject/products/templates/products/components/product_search_picker.html b/myproject/products/templates/products/components/product_search_picker.html new file mode 100644 index 0000000..6a3c096 --- /dev/null +++ b/myproject/products/templates/products/components/product_search_picker.html @@ -0,0 +1,182 @@ +{% comment %} +Переиспользуемый компонент поиска и выбора штучных товаров (Product). + +Параметры (Django template variables): +- container_id: уникальный ID контейнера (default: 'product-search-picker') +- title: заголовок компонента (default: 'Выбор товаров') +- api_url: URL для AJAX поиска (default: '/products/api/search-products-variants/') +- show_filters: показывать фильтры (default: True) +- show_view_toggle: показывать переключатель вида (default: True) +- show_select_all: показывать кнопку "Выбрать все" (default: True) +- show_add_button: показывать кнопку "Добавить выбранные" (default: True) +- add_button_text: текст кнопки добавления (default: 'Добавить выбранные') +- initial_view: начальный вид 'grid' или 'list' (default: 'list') +- multi_select: множественный выбор (default: True) +- max_selection: максимальное количество выбранных товаров (default: null) +- filter_in_stock_only: показывать только товары в наличии (default: False) +- categories: список категорий для фильтра (queryset или list) +- tags: список тегов для фильтра (queryset или list) +- content_height: высота контейнера с товарами (default: '400px') + +Пример использования: +{% include 'products/components/product_search_picker.html' with + container_id='writeoff-products' + title='Выберите товары для списания' + show_filters=True + filter_in_stock_only=True + categories=categories +%} + +После включения инициализируйте JS: + +{% endcomment %} + +{% load static %} + +
+ +
+ +
+ + + {{ title|default:'Выбор товаров' }} + + +
+ +
+ + +
+ + {% if show_view_toggle|default:True %} + +
+ + +
+ {% endif %} +
+
+ + {% if show_filters|default:True %} + +
+
+ {% if categories %} + +
+ +
+ {% endif %} + + {% if tags %} + +
+ +
+ {% endif %} + + +
+
+ + +
+
+ + +
+ 0 + выбрано +
+
+
+ {% endif %} + + +
+ + + + +
+ +
+ + + +
+ + + +
+