Mesa
Mesero
📋 Items
TOTAL CUENTA RD$0.00
💳 Método de Pago
Summer Beach
Restaurant & Bar
// ========== CASHIER DASHBOARD FUNCTIONS ========== let _cashierPollingInterval = null; let _cashierAutoRefreshInterval = null; function showCashierTab() { const tab = document.getElementById('nav-cashier'); if (!user || user.role !== 'cashier') { if (tab) tab.style.display = 'none'; } else { if (tab) tab.style.display = 'flex'; } } function pollCashierStatus() { if (!user || (user.role !== 'cashier' && user.role !== 'admin')) return; // Obtener totales del día api.req('GET', '/api/sales?date=' + new Date().toISOString().split('T')[0], {}, (err, data) => { if (err) return; let cash = 0, card = 0, transfer = 0, total = 0; (data || []).forEach(sale => { const amount = sale.total || 0; if (sale.payment_method === 'cash') cash += amount; else if (sale.payment_method === 'card') card += amount; else if (sale.payment_method === 'transfer') transfer += amount; total += amount; }); document.getElementById('total-cash').textContent = '$' + cash.toFixed(2); document.getElementById('total-card').textContent = '$' + card.toFixed(2); document.getElementById('total-transfer').textContent = '$' + transfer.toFixed(2); document.getElementById('total-general').textContent = '$' + total.toFixed(2); document.getElementById('expected-amount').textContent = '$' + total.toFixed(2); }); // Mesas pendientes (si es restaurante) loadPendingTables(); // Historial de sesiones loadCashierSessions(); } function loadPendingTables() { api.req('GET', '/api/restaurant/tables?status=open', {}, (err, tables) => { if (err || !tables || tables.length === 0) { document.getElementById('pending-tables-card').style.display = 'none'; return; } document.getElementById('pending-tables-card').style.display = 'block'; const tbody = document.getElementById('pending-tables-tbody'); tbody.innerHTML = ''; tables.forEach(table => { const row = document.createElement('tr'); row.style.borderBottom = '1px solid var(--border)'; row.innerHTML = ` ${table.table_number || 'N/A'} ${table.waiter_name || 'Sin asignar'} $${(table.total || 0).toFixed(2)} ${table.payment_method || 'Pendiente'} `; tbody.appendChild(row); }); }); } function loadCashierSessions() { api.req('GET', '/api/cash-sessions?limit=10', {}, (err, sessions) => { if (err || !sessions) return; const tbody = document.getElementById('sessions-history-tbody'); tbody.innerHTML = ''; sessions.forEach(session => { const date = new Date(session.opened_at); const difference = (session.counted_amount || 0) - (session.total || 0); const differenceClass = difference === 0 ? 'green' : (difference > 0 ? 'blue' : 'red'); const row = document.createElement('tr'); row.style.borderBottom = '1px solid var(--border)'; const colorHex = differenceClass === 'green' ? '10b981' : (differenceClass === 'blue' ? '3b82f6' : 'ef4444'); row.innerHTML = ` ${date.toLocaleString('es-DO')} $${(session.opening_amount || 0).toFixed(2)} $${(session.total || 0).toFixed(2)} $${difference.toFixed(2)} `; tbody.appendChild(row); }); }); } function collectPaymentFromTable(tableId) { const token = sessionStorage.getItem('pos_token'); if (!token) { alert('No hay sesión'); return; } const payMethod = prompt('Método de pago:\ncash\ncard\ntransfer', 'cash'); if (!payMethod) return; fetch(`/waiter-api.php?action=mesa-profile&id=${tableId}`, { headers: {'Authorization': 'Bearer ' + token} }) .then(r => r.json()) .then(data => { const order = (data.orders || [])[0]; if (!order) { alert('❌ Sin orden activa'); return; } fetch(`/api/restaurant/orders/${order.id}/close`, { method: 'POST', headers: {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}, body: JSON.stringify({payment_method: payMethod}) }) .then(r => r.json()) .then(res => { if (res.error) { alert('❌ ' + res.error); } else { alert('✅ Venta #' + res.sale_id + ' creada'); renderAdminMesas(); } }); }); } function showPaymentReceiptModal(data) { const { orderId, saleId, total, paymentMethod, tableId, onClose } = data; // Obtener venta y mostrar recibo del POS fetch(`/api/sales/${saleId}`, { headers: {'Authorization': 'Bearer ' + sessionStorage.getItem('pos_token')} }) .then(r => r.json()) .then(result => { if (result.data) { showReceipt(result.data); setTimeout(() => onClose(), 500); } else { alert('❌ Error obteniendo venta'); onClose(); } }) .catch(err => { console.error('Error:', err); alert('❌ Error: ' + err.message); onClose(); }); } function closeCashierSessionModal() { const countedAmount = parseFloat(document.getElementById('counted-amount').value || 0); const expectedAmount = parseFloat(document.getElementById('expected-amount').textContent.replace('$', '') || 0); const notes = document.getElementById('close-notes').value || ''; if (countedAmount === 0) { alert('Ingrese el monto contado'); return; } if (!confirm(`Cerrar caja con diferencia de $${(countedAmount - expectedAmount).toFixed(2)}?`)) { return; } api.req('POST', '/api/cash-sessions/close', { counted_amount: countedAmount, notes: notes }, (err, data) => { if (err) { alert('Error: ' + err); return; } alert('Caja cerrada correctamente'); document.getElementById('counted-amount').value = ''; document.getElementById('close-notes').value = ''; pollCashierStatus(); } ); } // Evento para calcular diferencia al ingresar monto document.addEventListener('DOMContentLoaded', () => { const countedInput = document.getElementById('counted-amount'); if (countedInput) { countedInput.addEventListener('input', (e) => { const counted = parseFloat(e.target.value || 0); const expected = parseFloat(document.getElementById('expected-amount').textContent.replace('$', '') || 0); const difference = counted - expected; const indicator = document.getElementById('difference-indicator'); document.getElementById('amount-difference').textContent = '$' + difference.toFixed(2); if (difference === 0) { indicator.style.background = 'rgba(16,185,129,0.1)'; indicator.style.color = '#10b981'; indicator.textContent = 'Cuadra perfecto'; } else if (difference > 0) { indicator.style.background = 'rgba(59,130,246,0.1)'; indicator.style.color = '#3b82f6'; indicator.textContent = `Sobrante: $${difference.toFixed(2)}`; } else { indicator.style.background = 'rgba(239,68,68,0.1)'; indicator.style.color = '#ef4444'; indicator.textContent = `Faltante: $${Math.abs(difference).toFixed(2)}`; } }); } }); function startCashierAutoRefresh() { stopCashierAutoRefresh(); _cashierAutoRefreshInterval = setInterval(() => { if (currentViewName === 'cashier') { pollCashierStatus(); } }, 5000); // Actualizar cada 5 segundos } function stopCashierAutoRefresh() { if (_cashierAutoRefreshInterval) { clearInterval(_cashierAutoRefreshInterval); _cashierAutoRefreshInterval = null; } } // AUTO-LOGIN: Si hay sesión guardada, mostrar app automáticamente window.addEventListener('DOMContentLoaded', async () => { console.log('🔍 Verificando sesión guardada...'); // Agregar event listener al botón de login const loginBtn = document.getElementById('login-btn'); if (loginBtn) { loginBtn.addEventListener('click', doLogin); loginBtn.addEventListener('touchend', (e) => { e.preventDefault(); doLogin(); }); } // También agregar Enter en campos de password const passInput = document.getElementById('login-pass'); if (passInput) { passInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') doLogin(); }); } if (token && currentUser) { console.log('✅ Sesión encontrada. Usuario:', currentUser.name); try { await showApp(); console.log('✅ App cargada automáticamente'); } catch(err) { console.error('❌ Error cargando app:', err); // Limpiar sesión y volver al login token = null; currentUser = null; sessionStorage.clear(); document.getElementById('login-screen').style.display = 'flex'; document.getElementById('app').style.display = 'none'; } } else { console.log('ℹ️ Sin sesión. Mostrando login.'); document.getElementById('login-screen').style.display = 'flex'; document.getElementById('app').style.display = 'none'; } }); // ========== RESTAURANT PRODUCTS MODAL ========== function openRestaurantProductModal() { loadRestaurantCategories().then(() => { document.getElementById('rp-name').value = ''; document.getElementById('rp-price').value = ''; document.getElementById('rp-cost').value = ''; document.getElementById('rp-prep-time').value = ''; document.getElementById('rp-image-url').value = ''; document.getElementById('rp-description').value = ''; document.getElementById('rp-portion-type').value = 'porción'; document.getElementById('rp-variants-container').innerHTML = ''; document.getElementById('rp-recipes-container').innerHTML = ''; document.getElementById('modal-restaurant-product').style.display = 'flex'; }); } async function loadRestaurantCategories() { try { const res = await fetch(API_URL + '/categories', { headers: {'Authorization': 'Bearer ' + token} }); if (!res.ok) throw new Error('No se pudieron cargar categorías'); const categories = await res.json(); const select = document.getElementById('rp-category-id'); select.innerHTML = ''; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.id; opt.textContent = cat.name; select.appendChild(opt); }); } catch (e) { showToast('Error cargando categorías: ' + e.message, 'error'); } } function addRestaurantVariantRow() { const container = document.getElementById('rp-variants-container'); const row = document.createElement('div'); row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr auto;gap:10px;margin-bottom:10px;align-items:center;'; row.innerHTML = ` `; container.appendChild(row); } function addRestaurantRecipeRow() { const container = document.getElementById('rp-recipes-container'); const row = document.createElement('div'); row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr auto;gap:8px;margin-bottom:10px;align-items:center;'; row.innerHTML = ` `; container.appendChild(row); } async function saveRestaurantProduct() { const name = document.getElementById('rp-name').value.trim(); const price = parseFloat(document.getElementById('rp-price').value); const cost = parseFloat(document.getElementById('rp-cost').value) || 0; const prepTime = parseInt(document.getElementById('rp-prep-time').value) || 0; const portionType = document.getElementById('rp-portion-type').value; const imageUrl = document.getElementById('rp-image-url').value.trim(); const description = document.getElementById('rp-description').value.trim(); const categoryId = parseInt(document.getElementById('rp-category-id').value); if (!name || !price || !categoryId) { showToast('Completa al menos: nombre, precio y categoría', 'error'); return; } // Recolectar variantes const variants = []; document.querySelectorAll('.variant-name').forEach((el, idx) => { const variantPrice = document.querySelectorAll('.variant-price')[idx]?.value; if (el.value && variantPrice) { variants.push({ variant_name: el.value.trim(), variant_price: parseFloat(variantPrice) }); } }); // Recolectar recetas const recipes = []; document.querySelectorAll('.recipe-ingredient').forEach((el, idx) => { const qty = document.querySelectorAll('.recipe-qty')[idx]?.value; const unit = document.querySelectorAll('.recipe-unit')[idx]?.value; const recCost = document.querySelectorAll('.recipe-cost')[idx]?.value; const portions = document.querySelectorAll('.recipe-portions')[idx]?.value; if (el.value && qty && unit && recCost && portions) { recipes.push({ ingredient_name: el.value.trim(), ingredient_quantity: parseFloat(qty), ingredient_unit: unit.trim(), ingredient_cost: parseFloat(recCost), output_portions: parseInt(portions), output_unit: 'porción' }); } }); const payload = { name: name, price: price, cost: cost, preparation_time: prepTime, portion_type: portionType, image_url: imageUrl || null, long_description: description || null, category_id: categoryId, variants: variants, recipes: recipes }; try { const res = await fetch(API_URL + '/restaurant/products', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify(payload) }); if (res.ok) { const data = await res.json(); showToast('Producto creado exitosamente ✓', 'success'); closeModal('modal-restaurant-product'); } else { const err = await res.json(); showToast('Error: ' + (err.error || 'Error desconocido'), 'error'); } } catch (e) { showToast('Error de conexión: ' + e.message, 'error'); } }