Создан переиспользуемый компонент поиска товаров

- product_search_picker.html - универсальный шаблон компонента поиска и выбора товаров
- product-search-picker.js - JavaScript модуль с поддержкой фильтрации по категориям, тегам, наличию
- product-search-picker.css - стили для компонента
- Поддержка одиночного и множественного выбора товаров
- Фильтрация по категориям, тегам и наличию на складе
- Отображение фото товара в результатах поиска
- Адаптивный интерфейс с прокруткой для больших списков
- API для программного управления (init, search, clearSelection и др.)
- Возможность кастомизации через параметры (заголовок, высота, текст кнопок)
This commit is contained in:
2025-12-10 23:36:13 +03:00
parent 96e04ca4b7
commit ccab09fb40
3 changed files with 1099 additions and 0 deletions

View File

@@ -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:
<script>
ProductSearchPicker.init('#writeoff-products', {
onAddSelected: function(products, instance) {
// products = [{id, text, sku, price, in_stock, photo_url}, ...]
products.forEach(addToForm);
instance.clearSelection();
}
});
</script>
{% endcomment %}
{% load static %}
<div class="product-search-picker"
id="{{ container_id|default:'product-search-picker' }}"
data-api-url="{{ api_url|default:'/products/api/search-products-variants/' }}"
data-multi-select="{{ multi_select|default:'true' }}"
data-max-selection="{{ max_selection|default:'' }}"
data-exclude-kits="true">
<div class="card shadow-sm">
<!-- Заголовок -->
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
<strong>
<i class="bi bi-box-seam text-primary"></i>
{{ title|default:'Выбор товаров' }}
</strong>
<div class="d-flex gap-2 align-items-center flex-wrap">
<!-- Поиск -->
<div class="input-group" style="width: 250px;">
<input type="text"
class="form-control form-control-sm product-picker-search"
placeholder="Поиск по названию, артикулу...">
<button class="btn btn-outline-secondary btn-sm product-picker-search-clear"
type="button" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
{% if show_view_toggle|default:True %}
<!-- Переключатель вида -->
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary product-picker-view"
data-view="grid" title="Карточки">
<i class="bi bi-grid-3x3-gap"></i>
</button>
<button class="btn btn-outline-secondary product-picker-view"
data-view="list" title="Список">
<i class="bi bi-list-ul"></i>
</button>
</div>
{% endif %}
</div>
</div>
{% if show_filters|default:True %}
<!-- Фильтры -->
<div class="card-body border-bottom py-2 product-picker-filters">
<div class="row g-2 align-items-center">
{% if categories %}
<!-- Фильтр по категории -->
<div class="col-auto">
<select class="form-select form-select-sm product-picker-category">
<option value="">Все категории</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if tags %}
<!-- Фильтр по тегам -->
<div class="col-auto">
<select class="form-select form-select-sm product-picker-tag">
<option value="">Все теги</option>
{% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<!-- Фильтр по наличию -->
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input product-picker-in-stock"
type="checkbox"
id="{{ container_id|default:'product-search-picker' }}-in-stock"
{% if filter_in_stock_only|default:False %}checked{% endif %}>
<label class="form-check-label small"
for="{{ container_id|default:'product-search-picker' }}-in-stock">
Только в наличии
</label>
</div>
</div>
<!-- Счетчик выбранных -->
<div class="col-auto ms-auto">
<span class="badge bg-primary product-picker-selected-count">0</span>
<span class="text-muted small">выбрано</span>
</div>
</div>
</div>
{% endif %}
<!-- Контент: сетка/список товаров -->
<div class="card-body product-picker-content" style="max-height: {{ content_height|default:'400px' }}; overflow-y: auto;">
<!-- Индикатор загрузки -->
<div class="product-picker-loading text-center py-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<!-- Сетка товаров -->
<div class="row g-2 product-picker-grid" data-view="{{ initial_view|default:'list' }}">
<!-- Товары загружаются через AJAX -->
</div>
<!-- Пустой результат -->
<div class="product-picker-empty text-center py-4 text-muted" style="display: none;">
<i class="bi bi-search fs-1 opacity-25"></i>
<p class="mb-0 mt-2">Товары не найдены</p>
</div>
</div>
<!-- Футер с кнопками действий -->
<div class="card-footer bg-white py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
{% if show_select_all|default:True %}
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm product-picker-select-all">
<i class="bi bi-check2-all"></i> Выбрать все
</button>
<button class="btn btn-outline-secondary btn-sm product-picker-clear-selection">
<i class="bi bi-x-lg"></i> Сбросить
</button>
</div>
{% else %}
<div></div>
{% endif %}
{% if show_add_button|default:True %}
<button class="btn btn-primary btn-sm product-picker-add-selected" disabled>
<i class="bi bi-plus-circle"></i>
{{ add_button_text|default:'Добавить выбранные' }}
</button>
{% endif %}
</div>
</div>
</div>