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

@@ -576,3 +576,272 @@ body {
}
/* На мобильных - такой же вид с текстом */
/* ============================================================
МОБИЛЬНАЯ ПЛАВАЮЩАЯ КОРЗИНА
============================================================ */
/* Фиксированный бар внизу экрана */
.mobile-cart-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 2px solid #dee2e6;
padding: 0.75rem;
display: none; /* скрываем на десктопе */
justify-content: space-between;
align-items: center;
gap: 0.75rem;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
/* Сводка по корзине (кликабельная) */
.mobile-cart-summary {
flex: 1;
display: flex;
flex-direction: column;
cursor: pointer;
padding: 0.25rem;
border-radius: 8px;
transition: background-color 0.2s;
}
.mobile-cart-summary:hover {
background-color: #f8f9fa;
}
.mobile-cart-summary:active {
background-color: #e9ecef;
}
/* Количество товаров */
.mobile-cart-count {
font-size: 0.8rem;
color: #6c757d;
font-weight: 500;
}
/* Итоговая сумма */
.mobile-cart-total {
font-size: 1.35rem;
font-weight: 700;
color: #198754;
line-height: 1.2;
}
/* Кнопки действий в мобильном баре */
.mobile-cart-actions {
display: flex;
gap: 0.5rem;
}
.mobile-cart-actions .btn {
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
white-space: nowrap;
}
.mobile-cart-actions .btn span {
display: inline;
}
/* Overlay корзины (фон затемнения) */
.mobile-cart-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1100;
display: none;
}
.mobile-cart-overlay.active {
display: flex;
align-items: flex-end;
}
/* Контент корзины в overlay */
.mobile-cart-content {
background: white;
width: 100%;
max-height: 75vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
border-radius: 16px 16px 0 0;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
/* Заголовок мобильной корзины */
.mobile-cart-header {
padding: 1rem;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
background: #f8f9fa;
border-radius: 16px 16px 0 0;
}
.mobile-cart-header h6 {
font-weight: 600;
}
/* Тело корзины с прокруткой */
.mobile-cart-body {
padding: 1rem;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Копия стилей корзины для мобильного view */
.mobile-cart-body .cart-item {
padding: 0.5rem 0;
}
.mobile-cart-body .item-name-price .fw-semibold {
font-size: 0.95rem;
}
/* Показываем мобильный бар только на маленьких экранах */
@media (max-width: 767.98px) {
.mobile-cart-bar {
display: flex;
}
/* Скрываем десктопную правую панель на мобильных */
.right-panel-fixed {
display: none !important;
}
/* Добавляем отступы чтобы контент не перекрывался барами */
.pos-container {
padding-bottom: 80px;
padding-top: 70px; /* место для фиксированного поиска */
}
/* Фиксируем основной контейнер для работы с позиционированием */
.pos-main-container {
overflow: visible; /* меняем для работы fixed внутри */
}
/* Создаём фиксированный бар поиска на мобильных */
.pos-container > .row {
position: relative;
}
/* Левая колонка с товарами */
.pos-container > .row > .col-12:first-child {
position: relative;
}
/* Блок поиска выносим в фиксированный бар */
.pos-container > .row > .col-12:first-child > div:first-child,
.pos-container > .row > .col-12:first-child > .mb-3:first-child {
position: fixed;
top: 56px; /* сразу под navbar */
left: 0;
right: 0;
z-index: 900;
background: white;
padding: 0.75rem;
margin: 0;
border-bottom: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
/* Категории тоже фиксируем под поиском */
.pos-container > .row > .col-12:first-child > div:nth-child(2) {
position: fixed;
top: 115px; /* поиск (56+~55) + категории */
left: 0;
right: 0;
z-index: 899;
background: white;
padding: 0.5rem 0.75rem;
margin: 0;
border-bottom: 1px solid #e9ecef;
}
/* Сворачивание категорий на мобильных */
.categories-wrapper {
margin-bottom: 0.5rem !important;
}
/* Кнопка переключения категорий */
.categories-toggle {
cursor: pointer;
color: #495057;
font-size: 0.9rem;
}
.categories-toggle:hover {
color: #212529;
}
/* Иконка chevron с поворотом */
.categories-toggle .bi-chevron-down {
transition: transform 0.2s ease;
}
.categories-toggle.collapsed .bi-chevron-down {
transform: rotate(-90deg);
}
/* Содержимое категорий */
.categories-content {
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
-webkit-overflow-scrolling: touch;
}
.categories-content.collapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
}
/* Меньшие карточки категорий на мобильных */
.category-card {
min-height: 32px !important;
padding: 0.25rem 0.5rem !important;
}
.category-card .card-body {
padding: 0.25rem !important;
}
.category-name {
font-size: 0.75rem !important;
}
/* Уменьшаем gap между категориями */
#categoryGrid {
gap: 0.5rem !important;
}
/* Категории в 3 колонки на мобильных */
#categoryGrid > div {
flex: 0 0 calc(33.333% - 0.33rem);
max-width: calc(33.333% - 0.33rem);
}
/* Прокручиваемая область товаров смещаем вниз */
.products-scrollable {
margin-top: 90px; /* учитываем поиск и категории */
}
}

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');
}
}
});
// Смена склада