Server-side search in POS: instant search by name and SKU with debounce 300ms
This commit is contained in:
@@ -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;
|
||||||
} else {
|
|
||||||
// Обычный режим - ITEMS уже отфильтрованы по категории на сервере
|
|
||||||
filtered = ITEMS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Поиск — по названию или артикулу, по любой части, без регистра
|
// Для витрины — клиентская фильтрация по поиску
|
||||||
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.trim();
|
|
||||||
filtered = filtered.filter(item => {
|
filtered = filtered.filter(item => {
|
||||||
const name = (item.name || '').toLowerCase();
|
const name = (item.name || '').toLowerCase();
|
||||||
const sku = (item.sku || '').toLowerCase();
|
const sku = (item.sku || '').toLowerCase();
|
||||||
return name.includes(term) || sku.includes(term);
|
return name.includes(searchTerm) || sku.includes(searchTerm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Обычный режим - ITEMS уже отфильтрованы на сервере (категория + поиск)
|
||||||
|
filtered = ITEMS;
|
||||||
|
}
|
||||||
|
|
||||||
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(); // Сначала пустая сетка
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user