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
+ ? ''
+ : '