/**
* ============================================
* 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
? `
`
: `
`;
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);
}
});