feat: Добавить мобильную адаптацию для POS-терминала

- Добавить фиксированную панель корзины внизу экрана на мобильных
  - Отображение количества товаров и суммы
  - Кнопки "Продать" и "Очистить" всегда доступны
  - Тап на панель открывает корзину как overlay

- Фиксировать поиск и категории сверху на мобильных
  - Поиск всегда виден при скролле
  - Категории в collapsible-блоке (сворачиваются)
  - Категории в 3 колонки на мобильных

- Улучшить поиск по токенам (разбивает фразу на слова)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 23:03:40 +03:00
parent 3095e01659
commit c77fcaf669
4 changed files with 454 additions and 12 deletions

View File

@@ -787,10 +787,12 @@ function renderProducts() {
// Для витрины — клиентская фильтрация по поиску
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
if (searchTerm) {
const tokens = searchTerm.split(/\s+/).filter(t => t.length > 0);
filtered = filtered.filter(item => {
const name = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return name.includes(searchTerm) || sku.includes(searchTerm);
// Каждый токен должен совпадать хотя бы с одним словом в name или sku
return tokens.every(token => name.includes(token) || sku.includes(token));
});
}
} else {
@@ -1261,6 +1263,7 @@ function renderCart() {
list.innerHTML = '<p class="text-muted text-center py-4 small">Корзина пуста</p>';
document.getElementById('cartTotal').textContent = '0.00';
updateShowcaseButtonState(); // Обновляем состояние кнопки
updateMobileCartBar(); // Обновляем мобильный бар даже когда корзина пуста
return;
}
@@ -1420,9 +1423,59 @@ function renderCart() {
});
document.getElementById('cartTotal').textContent = formatMoney(total);
// Обновляем состояние кнопки "НА ВИТРИНУ"
updateShowcaseButtonState();
// Обновляем мобильный бар корзины
updateMobileCartBar();
}
/**
* Склонение слов в зависимости от числа
* @param {number} number - число
* @param {string} one - форма для 1 (товар)
* @param {string} two - форма для 2-4 (товара)
* @param {string} five - форма для 5+ (товаров)
*/
function getNoun(number, one, two, five) {
const n = Math.abs(number);
const n10 = n % 10;
const n100 = n % 100;
if (n100 >= 11 && n100 <= 19) {
return five;
}
if (n10 === 1) {
return one;
}
if (n10 >= 2 && n10 <= 4) {
return two;
}
return five;
}
/**
* Обновляет мобильный бар корзины
*/
function updateMobileCartBar() {
const countEl = document.querySelector('.mobile-cart-count');
const totalEl = document.querySelector('.mobile-cart-total');
if (!countEl || !totalEl) return;
let count = 0;
let total = 0;
cart.forEach((item) => {
count += item.qty;
total += item.qty * item.price;
});
// Округляем количество до целого для отображения
const displayCount = Math.round(count);
countEl.textContent = `${displayCount} ${getNoun(displayCount, 'товар', 'товара', 'товаров')}`;
totalEl.textContent = formatMoney(total);
}
async function removeFromCart(cartKey) {
@@ -3260,6 +3313,85 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('confirmAddUnitToCart').addEventListener('click', () => {
addToCartFromModal();
});
// ===== МОБИЛЬНАЯ КОРЗИНА =====
// Тап на бар — открываем корзину
const mobileCartSummary = document.getElementById('mobileCartSummary');
if (mobileCartSummary) {
mobileCartSummary.addEventListener('click', () => {
const overlay = document.getElementById('mobileCartOverlay');
const body = document.getElementById('mobileCartBody');
// Копируем содержимое корзины
if (body && overlay) {
const cartList = document.getElementById('cartList');
body.innerHTML = cartList ? cartList.innerHTML : '<p class="text-muted text-center py-4">Корзина пуста</p>';
overlay.classList.add('active');
}
});
}
// Кнопка закрытия мобильной корзины
const mobileCartClose = document.getElementById('mobileCartClose');
if (mobileCartClose) {
mobileCartClose.addEventListener('click', () => {
const overlay = document.getElementById('mobileCartOverlay');
if (overlay) {
overlay.classList.remove('active');
}
});
}
// Закрытие по клику на фон
const mobileCartOverlay = document.getElementById('mobileCartOverlay');
if (mobileCartOverlay) {
mobileCartOverlay.addEventListener('click', (e) => {
if (e.target.id === 'mobileCartOverlay') {
e.target.classList.remove('active');
}
});
}
// Мобильная кнопка "Продать"
const mobileCheckoutBtn = document.getElementById('mobileCheckoutBtn');
if (mobileCheckoutBtn) {
mobileCheckoutBtn.addEventListener('click', () => {
const checkoutBtn = document.getElementById('checkoutNow');
if (checkoutBtn) {
checkoutBtn.click();
}
});
}
// Мобильная кнопка "Очистить"
const mobileClearCartBtn = document.getElementById('mobileClearCartBtn');
if (mobileClearCartBtn) {
mobileClearCartBtn.addEventListener('click', () => {
const clearBtn = document.getElementById('clearCart');
if (clearBtn) {
clearBtn.click();
}
});
}
// ===== СВОРАЧИВАНИЕ КАТЕГОРИЙ НА МОБИЛЬНЫХ =====
const categoriesToggle = document.getElementById('categoriesToggle');
const categoriesContent = document.getElementById('categoriesContent');
if (categoriesToggle && categoriesContent) {
categoriesToggle.addEventListener('click', () => {
categoriesToggle.classList.toggle('collapsed');
categoriesContent.classList.toggle('collapsed');
});
// Автоматически сворачиваем категории на мобильных при загрузке
if (window.innerWidth <= 767) {
categoriesToggle.classList.add('collapsed');
categoriesContent.classList.add('collapsed');
}
}
});
// Смена склада