Server-side search in POS: instant search by name and SKU with debounce 300ms

This commit is contained in:
2025-11-17 16:10:23 +03:00
parent 42c6e086da
commit c06e569cbd
2 changed files with 62 additions and 19 deletions

View File

@@ -12,6 +12,8 @@ const cart = new Map();
let currentPage = 1; let currentPage = 1;
let hasMoreItems = false; let hasMoreItems = false;
let isLoadingItems = false; let isLoadingItems = false;
let currentSearchQuery = ''; // Текущий поисковый запрос
let searchDebounceTimer = null;
// Переменные для режима редактирования // Переменные для режима редактирования
let isEditMode = false; let isEditMode = false;
@@ -60,6 +62,8 @@ function renderCategories() {
allCard.onclick = async () => { allCard.onclick = async () => {
currentCategoryId = null; currentCategoryId = null;
isShowcaseView = false; isShowcaseView = false;
currentSearchQuery = ''; // Сбрасываем поиск
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
renderCategories(); renderCategories();
await loadItems(); // Загрузка через API await loadItems(); // Загрузка через API
}; };
@@ -83,6 +87,8 @@ function renderCategories() {
card.onclick = async () => { card.onclick = async () => {
currentCategoryId = cat.id; currentCategoryId = cat.id;
isShowcaseView = false; isShowcaseView = false;
currentSearchQuery = ''; // Сбрасываем поиск
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
renderCategories(); renderCategories();
await loadItems(); // Загрузка через API await loadItems(); // Загрузка через API
}; };
@@ -104,27 +110,26 @@ function renderCategories() {
function renderProducts() { function renderProducts() {
const grid = document.getElementById('productGrid'); const grid = document.getElementById('productGrid');
grid.innerHTML = ''; grid.innerHTML = '';
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
let filtered; let filtered;
// Если выбран режим витрины - показываем витринные комплекты // Если выбран режим витрины - показываем витринные комплекты
if (isShowcaseView) { if (isShowcaseView) {
filtered = showcaseKits; filtered = showcaseKits;
// Для витрины — клиентская фильтрация по поиску
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
if (searchTerm) {
filtered = filtered.filter(item => {
const name = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return name.includes(searchTerm) || sku.includes(searchTerm);
});
}
} else { } else {
// Обычный режим - ITEMS уже отфильтрованы по категории на сервере // Обычный режим - ITEMS уже отфильтрованы на сервере (категория + поиск)
filtered = ITEMS; filtered = ITEMS;
} }
// Поиск — по названию или артикулу, по любой части, без регистра
if (searchTerm) {
const term = searchTerm.trim();
filtered = filtered.filter(item => {
const name = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return name.includes(term) || sku.includes(term);
});
}
filtered.forEach(item => { filtered.forEach(item => {
const col = document.createElement('div'); const col = document.createElement('div');
@@ -272,6 +277,11 @@ async function loadItems(append = false) {
params.append('category_id', currentCategoryId); params.append('category_id', currentCategoryId);
} }
// Добавляем поисковый запрос, если есть
if (currentSearchQuery) {
params.append('query', currentSearchQuery);
}
const response = await fetch(`/pos/api/items/?${params}`); const response = await fetch(`/pos/api/items/?${params}`);
const data = await response.json(); const data = await response.json();
@@ -932,11 +942,6 @@ document.getElementById('scheduleLater').onclick = async () => {
alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.'); alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.');
}; };
// Search functionality - клиентская фильтрация
document.getElementById('searchInput').addEventListener('input', () => {
renderProducts(); // Просто перерисовываем с фильтрацией
});
// Customer selection // Customer selection
document.getElementById('customerSelectBtn').addEventListener('click', () => { document.getElementById('customerSelectBtn').addEventListener('click', () => {
alert('Функция выбора клиента будет реализована позже'); alert('Функция выбора клиента будет реализована позже');
@@ -1005,6 +1010,29 @@ function getCsrfToken() {
return cookieValue; return cookieValue;
} }
// Обработчик поиска с debounce
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
// Отменяем предыдущий таймер
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer);
}
// Для витрины — мгновенная клиентская фильтрация
if (isShowcaseView) {
renderProducts();
return;
}
// Для обычных товаров/комплектов — серверный поиск с debounce 300мс
searchDebounceTimer = setTimeout(async () => {
currentSearchQuery = query;
await loadItems(); // Перезагрузка с серверным поиском
}, 300);
});
// Инициализация // Инициализация
renderCategories(); renderCategories();
renderProducts(); // Сначала пустая сетка renderProducts(); // Сначала пустая сетка

View File

@@ -266,15 +266,17 @@ def get_showcase_kits_api(request):
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def get_items_api(request): def get_items_api(request):
""" """
API endpoint для получения товаров и комплектов с пагинацией. API endpoint для получения товаров и комплектов с пагинацией и поиском.
Параметры: Параметры:
- category_id: ID категории (опционально, для фильтрации) - category_id: ID категории (опционально, для фильтрации)
- query: поисковый запрос по name или sku (опционально)
- page: номер страницы (по умолчанию 1) - page: номер страницы (по умолчанию 1)
- page_size: размер страницы (по умолчанию 60) - page_size: размер страницы (по умолчанию 60)
Сортировка по умолчанию: по свободному остатку (available - reserved) DESC Сортировка по умолчанию: по свободному остатку (available - reserved) DESC
""" """
from products.models import ProductPhoto, ProductKitPhoto from products.models import ProductPhoto, ProductKitPhoto
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
# Получаем текущий склад # Получаем текущий склад
current_warehouse = get_pos_warehouse(request) current_warehouse = get_pos_warehouse(request)
@@ -284,8 +286,9 @@ def get_items_api(request):
'error': 'Нет активного склада' 'error': 'Нет активного склада'
}, status=400) }, status=400)
# Параметры пагинации # Параметры пагинации и поиска
category_id = request.GET.get('category_id') category_id = request.GET.get('category_id')
search_query = request.GET.get('query', '').strip()
page = int(request.GET.get('page', 1)) page = int(request.GET.get('page', 1))
page_size = int(request.GET.get('page_size', 60)) page_size = int(request.GET.get('page_size', 60))
@@ -328,6 +331,12 @@ def get_items_api(request):
if category_id: if category_id:
products_qs = products_qs.filter(categories__id=category_id) products_qs = products_qs.filter(categories__id=category_id)
# Фильтруем по поисковому запросу (name или sku)
if search_query:
products_qs = products_qs.filter(
Q(name__icontains=search_query) | Q(sku__icontains=search_query)
)
# Сериализуем товары # Сериализуем товары
products = [] products = []
for p in products_qs: for p in products_qs:
@@ -373,6 +382,12 @@ def get_items_api(request):
if category_id: if category_id:
kits_qs = kits_qs.filter(categories__id=category_id) kits_qs = kits_qs.filter(categories__id=category_id)
# Фильтруем комплекты по поисковому запросу (name или sku)
if search_query:
kits_qs = kits_qs.filter(
Q(name__icontains=search_query) | Q(sku__icontains=search_query)
)
# Сериализуем комплекты # Сериализуем комплекты
kits = [] kits = []
for k in kits_qs: for k in kits_qs: