diff --git a/myproject/inventory/templates/inventory/writeoff_document/detail.html b/myproject/inventory/templates/inventory/writeoff_document/detail.html index eba43b1..165e4fa 100644 --- a/myproject/inventory/templates/inventory/writeoff_document/detail.html +++ b/myproject/inventory/templates/inventory/writeoff_document/detail.html @@ -107,7 +107,7 @@
- {% include 'products/components/product_search_picker.html' with container_id='writeoff-document-picker' title='Найти товар для списания' warehouse_id=document.warehouse.id filter_in_stock_only=True categories=categories tags=tags add_button_text='Выбрать товар' multi_select=False show_select_all=False content_height='250px' %} + {% include 'products/components/product_search_picker.html' with container_id='writeoff-document-picker' title='Найти товар для списания' warehouse_id=document.warehouse.id filter_in_stock_only=True categories=categories tags=tags add_button_text='Выбрать товар' content_height='250px' %}
@@ -291,9 +291,9 @@ document.addEventListener('DOMContentLoaded', function() { // Инициализация компонента поиска товаров const picker = ProductSearchPicker.init('#writeoff-document-picker', { - onAddSelected: function(products, instance) { - if (products.length > 0) { - selectProduct(products[0]); + onAddSelected: function(product, instance) { + if (product) { + selectProduct(product); instance.clearSelection(); } } diff --git a/myproject/products/static/products/js/product-search-picker.js b/myproject/products/static/products/js/product-search-picker.js index b64dbcf..375e928 100644 --- a/myproject/products/static/products/js/product-search-picker.js +++ b/myproject/products/static/products/js/product-search-picker.js @@ -1,19 +1,18 @@ /** - * ProductSearchPicker - переиспользуемый модуль для поиска и выбора товаров + * ProductSearchPicker - переиспользуемый модуль для поиска и выбора товара + * Только single-select режим. * * API: * - ProductSearchPicker.init(containerSelector, options) - инициализация - * - instance.getSelected() - получить выбранные товары (массив объектов) - * - instance.getSelectedIds() - получить ID выбранных товаров (массив строк) + * - instance.getSelected() - получить выбранный товар (объект или null) * - instance.clearSelection() - сбросить выбор - * - instance.setSelection(productIds) - установить выбор программно * - instance.refresh() - обновить список товаров * - instance.destroy() - уничтожить экземпляр * * Пример использования: * const picker = ProductSearchPicker.init('#my-picker', { - * onAddSelected: function(products, instance) { - * console.log('Добавлены товары:', products); + * onAddSelected: function(product, instance) { + * console.log('Выбран товар:', product); * } * }); */ @@ -39,23 +38,20 @@ // Настройки по умолчанию 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) => {} + onSelect: null, // (product, instance) => {} + onDeselect: null, // (instance) => {} + onAddSelected: null // (product, instance) => {} }, options || {}); // Состояние this.state = { products: [], - selected: {}, // {productId: productData} + selected: null, // Единичный выбранный товар (объект или null) currentPage: 1, hasMore: false, isLoading: false, @@ -94,9 +90,6 @@ 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') }; @@ -210,25 +203,11 @@ }); } - // Выбрать все - 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); + if (self.state.selected && self.options.onAddSelected) { + self.options.onAddSelected(self.state.selected, self); } }); } @@ -348,7 +327,7 @@ products.forEach(function(product) { // Извлекаем ID (убираем префикс "product_") var productId = String(product.id).replace('product_', ''); - var isSelected = self.state.selected.hasOwnProperty(productId); + var isSelected = self.state.selected && self.state.selected.id === productId; var html = self.state.currentView === 'grid' ? self._renderGridCard(product, productId, isSelected) @@ -446,7 +425,7 @@ }; /** - * Переключение выбора товара + * Переключение выбора товара (single-select) */ ProductSearchPicker.prototype._toggleProduct = function(productId) { var self = this; @@ -457,47 +436,40 @@ var p = this.state.products[i]; if (String(p.id).replace('product_', '') === productId) { product = p; + product.id = productId; // Сохраняем очищенный ID break; } } if (!product) return; - var isSelected = this.state.selected.hasOwnProperty(productId); + var isSelected = this.state.selected && this.state.selected.id === productId; if (isSelected) { // Снять выбор - delete this.state.selected[productId]; + var oldProductId = this.state.selected.id; + this.state.selected = null; + this._updateProductUI(oldProductId); if (this.options.onDeselect) { - this.options.onDeselect([product], this); + this.options.onDeselect(this); } } else { - // Проверка множественного выбора - if (!this.options.multiSelect) { - this.state.selected = {}; + // Сначала снимаем выбор со старого товара + if (this.state.selected) { + var oldProductId = this.state.selected.id; + this._updateProductUI(oldProductId); } - - // Проверка лимита - if (this.options.maxSelection && - Object.keys(this.state.selected).length >= this.options.maxSelection) { - return; - } - - // Добавить в выбор - this.state.selected[productId] = product; + + // Выбираем новый товар + this.state.selected = product; + this._updateProductUI(productId); if (this.options.onSelect) { - this.options.onSelect([product], this); + this.options.onSelect(product, this); } } // Обновить UI - this._updateProductUI(productId); this._updateSelectionUI(); - - // Общий callback - if (this.options.onSelectionChange) { - this.options.onSelectionChange(this.getSelected(), this); - } }; /** @@ -507,7 +479,7 @@ var card = this.elements.grid.querySelector('[data-product-id="' + productId + '"]'); if (!card) return; - var isSelected = this.state.selected.hasOwnProperty(productId); + var isSelected = this.state.selected && this.state.selected.id === productId; if (isSelected) { card.classList.add('selected'); @@ -545,52 +517,16 @@ }; /** - * Обновление UI выбора (счетчик, кнопки) + * Обновление 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; + this.elements.addSelectedBtn.disabled = !this.state.selected; } }; - /** - * Выбрать все видимые товары - */ - 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() { @@ -625,68 +561,25 @@ // ========== ПУБЛИЧНОЕ API ========== /** - * Получить массив выбранных товаров + * Получить выбранный товар (объект или null) */ 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); + return 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); + if (this.state.selected) { + var productId = this.state.selected.id; + this.state.selected = null; + this._updateProductUI(productId); } - }; - - /** - * Установить выбор программно - */ - 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() { diff --git a/myproject/products/templates/products/components/product_search_picker.html b/myproject/products/templates/products/components/product_search_picker.html index 5756c91..7079542 100644 --- a/myproject/products/templates/products/components/product_search_picker.html +++ b/myproject/products/templates/products/components/product_search_picker.html @@ -1,18 +1,15 @@ {% comment %} -Переиспользуемый компонент поиска и выбора штучных товаров (Product). +Переиспользуемый компонент поиска и выбора товара (Product). +Только единичный выбор (single-select). Параметры (Django template variables): - container_id: уникальный ID контейнера (default: 'product-search-picker') -- title: заголовок компонента (default: 'Выбор товаров') +- 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: 'Добавить выбранные') +- add_button_text: текст кнопки добавления (default: 'Выбрать товар') - initial_view: начальный вид 'grid' или 'list' (default: 'list') -- multi_select: множественный выбор (default: True) -- max_selection: максимальное количество выбранных товаров (default: null) - filter_in_stock_only: показывать только товары в наличии (default: False) - warehouse_id: ID склада для фильтрации товаров (default: None) - categories: список категорий для фильтра (queryset или list) @@ -22,7 +19,7 @@ Пример использования: {% include 'products/components/product_search_picker.html' with container_id='writeoff-products' - title='Выберите товары для списания' + title='Выберите товар для списания' show_filters=True filter_in_stock_only=True categories=categories @@ -31,9 +28,9 @@ После включения инициализируйте JS: