/** * ============================================ * TABLET MODE - PDV CATÁLOGO * Gerencia categorias, grid de produtos e * carrinho drawer. Reutiliza CaixaState e * funções do caixa.js principal. * ============================================ */ function getOptimizedImage(photoPath) { return `https://maispdv.com/image/optimize?path=${encodeURIComponent(photoPath)}`; } const TabletMode = { categories: [], products: [], // produtos da categoria atual filteredProducts: [], selectedCategory: 'all', selectedProduct: null, cartOpen: false, // ======================================== // INIT // ======================================== init() { this.loadCategories(); this.syncCartUI(); // Observer: quando caixa.js atualizar o state, refletir no tablet this._observeCartChanges(); }, // ======================================== // CATEGORIAS // ======================================== async loadCategories() { try { const r = await fetch('/caixa/tablet/categorias', { headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }); const data = await r.json(); if (!data.success) return; this.categories = data.categories; this.renderCategories(); // Auto-seleciona "Todos" e carrega produtos this.selectCategory('all'); } catch (e) { console.error('Erro ao carregar categorias:', e); } }, renderCategories() { const bar = document.getElementById('tabletCategoriesBar'); if (!bar) return; // Mantém o "Todos" e adiciona as categorias let html = ``; this.categories.forEach(cat => { html += ``; }); // "Sem categoria" se houver const uncategorized = this.categories.find(c => c.id === 0); // Já incluído na query do backend se existir bar.innerHTML = html; }, async selectCategory(categoryId, btnEl) { this.selectedCategory = categoryId; // Atualiza chips ativos document.querySelectorAll('.tablet-cat-chip').forEach(c => c.classList.remove('active')); if (btnEl) { btnEl.classList.add('active'); } else { const chip = document.querySelector(`.tablet-cat-chip[data-category="${categoryId}"]`); if (chip) chip.classList.add('active'); } // Limpa filtro de busca const searchInput = document.getElementById('tabletSearchInput'); if (searchInput) searchInput.value = ''; await this.loadProducts(categoryId); }, // ======================================== // PRODUTOS // ======================================== async loadProducts(categoryId) { const grid = document.getElementById('tabletProductsGrid'); const empty = document.getElementById('tabletEmpty'); const loading = document.getElementById('tabletLoading'); if (grid) grid.innerHTML = ''; if (empty) empty.style.display = 'none'; if (loading) loading.style.display = 'flex'; try { const url = categoryId === 'all' ? '/caixa/tablet/produtos' : `/caixa/tablet/produtos?category_id=${categoryId}`; const r = await fetch(url, { headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }); const data = await r.json(); if (loading) loading.style.display = 'none'; if (!data.success || !data.products.length) { this.products = []; this.filteredProducts = []; if (empty) { empty.style.display = 'flex'; empty.querySelector('p').textContent = 'Nenhum produto encontrado nesta categoria'; } return; } this.products = data.products; this.filteredProducts = [...this.products]; this.renderProducts(); } catch (e) { console.error('Erro ao carregar produtos:', e); if (loading) loading.style.display = 'none'; if (empty) { empty.style.display = 'flex'; empty.querySelector('p').textContent = 'Erro ao carregar produtos'; } } }, renderProducts() { const grid = document.getElementById('tabletProductsGrid'); const empty = document.getElementById('tabletEmpty'); if (!grid) return; if (!this.filteredProducts.length) { grid.innerHTML = ''; if (empty) { empty.style.display = 'flex'; empty.querySelector('p').textContent = 'Nenhum produto encontrado'; } return; } if (empty) empty.style.display = 'none'; grid.innerHTML = this.filteredProducts.map(p => { const inCart = CaixaState.items.find(i => i.product_id == p.id); const inCartClass = inCart ? 'in-cart' : ''; const qtyAttr = inCart ? `data-qty="${inCart.quantity}"` : ''; const outClass = (p.is_stock_controlled && p.stock <= 0) ? 'out-of-stock' : ''; const imgHtml = p.photo ? `${this._escHtml(p.name)}` : `
`; let stockHtml = ''; if (p.is_stock_controlled) { const stockClass = p.stock <= 0 ? 'out' : (p.stock <= 5 ? 'low' : ''); stockHtml = `
${p.stock <= 0 ? 'Sem estoque' : p.stock + ' ' + (p.unit_type_short || 'UN')}
`; } return `
${imgHtml}
${this._escHtml(p.name)}
R$ ${p.price_formatted}
${stockHtml}
`; }).join(''); }, filterProducts() { const query = (document.getElementById('tabletSearchInput')?.value || '').trim().toLowerCase(); if (!query) { this.filteredProducts = [...this.products]; } else { this.filteredProducts = this.products.filter(p => { return p.name.toLowerCase().includes(query) || (p.barcode && p.barcode.toLowerCase().includes(query)) || String(p.id).includes(query); }); } this.renderProducts(); }, // ======================================== // ADICIONAR PRODUTO (via modal de qtd) // ======================================== openProduct(productId) { const product = this.products.find(p => p.id === productId); if (!product) return; // Se sem estoque e controlado, não abre if (product.is_stock_controlled && product.stock <= 0) return; this.selectedProduct = product; // Preenche modal const nameEl = document.getElementById('tabletQtyProductName'); const priceEl = document.getElementById('tabletQtyProductPrice'); const imgEl = document.getElementById('tabletQtyProductImg'); const qtyEl = document.getElementById('tabletQtyInput'); if (nameEl) nameEl.textContent = product.name; if (priceEl) priceEl.textContent = 'R$ ' + product.price_formatted; if (qtyEl) { qtyEl.value = product.is_fractional ? '0.100' : '1'; qtyEl.step = product.is_fractional ? '0.001' : '1'; qtyEl.min = product.is_fractional ? '0.001' : '1'; } if (imgEl) { imgEl.innerHTML = product.photo ? `` : ``; } // Abre modal const modal = new bootstrap.Modal(document.getElementById('tabletQtyModal')); modal.show(); // Focus no input de quantidade setTimeout(() => { if (qtyEl) { qtyEl.select(); qtyEl.focus(); } }, 300); }, adjustQty(delta) { const input = document.getElementById('tabletQtyInput'); if (!input) return; let val = parseFloat(input.value) || 1; const step = this.selectedProduct?.is_fractional ? 0.1 : 1; val = Math.max(step, val + (delta * step)); input.value = this.selectedProduct?.is_fractional ? val.toFixed(3) : Math.round(val); }, async confirmAddProduct() { if (!this.selectedProduct) return; const qty = parseFloat(document.getElementById('tabletQtyInput')?.value) || 1; // Fecha o modal customQuantity = qty; await Cart.addProduct(this.selectedProduct.id, qty); customQuantity = 1; // Toast de confirmação this.showToast(`${this.selectedProduct.name} adicionado!`); // Atualiza visual do grid this.syncCartUI(); }, // ======================================== // CARRINHO DRAWER // ======================================== toggleCart() { this.cartOpen = !this.cartOpen; const overlay = document.getElementById('tabletCartOverlay'); const drawer = document.getElementById('tabletCartDrawer'); if (this.cartOpen) { overlay?.classList.add('show'); drawer?.classList.add('show'); this.renderCartDrawer(); } else { overlay?.classList.remove('show'); drawer?.classList.remove('show'); } }, renderCartDrawer() { const container = document.getElementById('tabletCartItems'); const emptyEl = document.getElementById('tabletCartEmpty'); const totalEl = document.getElementById('tabletCartTotal'); if (!container) return; const items = CaixaState.items; if (!items.length) { if (emptyEl) emptyEl.style.display = 'block'; container.querySelectorAll('.tablet-cart-item').forEach(el => el.remove()); if (totalEl) totalEl.textContent = 'R$ 0,00'; return; } if (emptyEl) emptyEl.style.display = 'none'; let html = ''; items.forEach((item, idx) => { const imgHtml = item.photo ? `` : `
`; html += `
${imgHtml}
${this._escHtml(item.name)}
${Utils.formatMoney(item.price)} x ${item.quantity}
${item.quantity}
`; }); // Remove itens antigos e insere novos (preserva o empty div) container.querySelectorAll('.tablet-cart-item').forEach(el => el.remove()); container.insertAdjacentHTML('beforeend', html); if (totalEl) totalEl.textContent = Utils.formatMoney(CaixaState.total); }, async changeItemQty(itemId, delta) { const item = CaixaState.items.find(i => i.id === itemId); if (!item) return; const newQty = item.quantity + delta; if (newQty <= 0) { return this.removeItem(itemId); } await Cart.updateItem(itemId, newQty, item.price); this.syncCartUI(); }, async removeItem(itemId) { await Cart.removeItem(itemId); this.syncCartUI(); }, // ======================================== // SYNC UI // ======================================== syncCartUI() { const items = CaixaState.items; const count = items.reduce((s, i) => s + i.quantity, 0); const total = CaixaState.total; // Badge do carrinho const badge = document.getElementById('tabletCartBadge'); if (badge) { badge.textContent = count; badge.setAttribute('data-count', count); badge.style.display = count > 0 ? 'flex' : 'none'; } // Footer const footerCount = document.getElementById('tabletFooterCount'); const footerTotal = document.getElementById('tabletFooterTotal'); if (footerCount) footerCount.textContent = `${items.length} ${items.length === 1 ? 'item' : 'itens'}`; if (footerTotal) footerTotal.textContent = Utils.formatMoney(total); // Atualiza cards no grid que estão no carrinho document.querySelectorAll('.tablet-product-card').forEach(card => { const pid = parseInt(card.dataset.productId); const inCart = items.find(i => i.product_id == pid); if (inCart) { card.classList.add('in-cart'); card.setAttribute('data-qty', inCart.quantity); } else { card.classList.remove('in-cart'); card.removeAttribute('data-qty'); } }); // Se drawer aberto, atualiza if (this.cartOpen) { this.renderCartDrawer(); } }, // ======================================== // OBSERVER DE MUDANÇAS NO CAIXA STATE // ======================================== _observeCartChanges() { // Polling simples a cada 500ms pra sincronizar // (caixa.js não tem events, então observamos) let lastItemsJson = ''; setInterval(() => { const currentJson = JSON.stringify(CaixaState.items) + CaixaState.total; if (currentJson !== lastItemsJson) { lastItemsJson = currentJson; this.syncCartUI(); } }, 500); }, // ======================================== // TOAST // ======================================== showToast(msg) { let toast = document.querySelector('.tablet-toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'tablet-toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2000); }, // ======================================== // UTILS // ======================================== _escHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } }; // Handler para barcode scanner no modo tablet function handleTabletBarcodeScanned(barcode) { if (!barcode) return; // Busca o produto pelo código escaneado const input = document.getElementById('tabletSearchInput'); if (input) input.value = barcode; TabletMode.filterProducts(); // Se encontrar exatamente 1, abre direto if (TabletMode.filteredProducts.length === 1) { TabletMode.openProduct(TabletMode.filteredProducts[0].id); } } // Init quando o DOM estiver pronto document.addEventListener('DOMContentLoaded', () => { if (window.PDV_LAYOUT === 'tablet') { // Aguarda o caixa.js carregar setTimeout(() => TabletMode.init(), 300); } });