// ========== 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)'; 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 amount = prompt('Ingrese el monto a cobrar:'); if (!amount || isNaN(parseFloat(amount))) return; api.req('POST', `/api/restaurant/tables/${tableId}/collect`, {amount: parseFloat(amount)}, (err, data) => { if (err) { alert('Error: ' + err); return; } alert('Pago registrado correctamente'); pollCashierStatus(); } ); } 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; } } // ── Admin Restaurante ────────────────────────────────────────────────────── function loadAdminRestaurantDashboard() { const token = sessionStorage.getItem('pos_token'); if (!token) return; try { // Cargar KPIs api.req('/sales?period=today').then(r => { const sales = r.data || []; const totalSales = sales.reduce((s, v) => s + (v.total || 0), 0); const orderCount = sales.length; const topDish = sales.flatMap(v => (v.items || [])).reduce((m, i) => { m[i.name] = (m[i.name] || 0) + i.qty; return m; }, {}); const topDishName = Object.entries(topDish).sort((a,b) => b[1] - a[1])[0]?.[0] || '—'; document.getElementById('ar-kpi-sales').textContent = 'RD$' + fmt(totalSales); document.getElementById('ar-kpi-orders').textContent = orderCount; document.getElementById('ar-kpi-top-dish').textContent = topDishName; }).catch(console.warn); // Cargar datos de mesas, meseros, reservas, usuarios renderAdminMesas(); loadAdminRestWaiters(); loadAdminRestReservations(); loadAdminRestUsers(); } catch(e) { console.error('Error loading restaurant dashboard:', e); } } function adminRestTab(btn, section) { document.querySelectorAll('.admin-rest-section').forEach(s => s.style.display = 'none'); document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); const sectionId = 'admin-rest-' + section; const sectionEl = document.getElementById(sectionId); if (sectionEl) { sectionEl.style.display = 'block'; if (section === 'kpis') loadAdminRestaurantDashboard(); if (section === 'mesas') renderAdminMesas(); if (section === 'meseros') loadAdminRestWaiters(); if (section === 'reservas') loadAdminRestReservations(); if (section === 'usuarios') loadAdminRestUsers(); } btn.classList.add('active'); } let _adminMesasData = []; let _currentMesaZone = 'all'; function renderAdminMesas() { const token = sessionStorage.getItem('pos_token'); const grid = document.getElementById('ar-mesas-grid'); grid.innerHTML = '
Cargando mesas...
'; fetch('/waiter-api.php?action=dashboard', { headers: { 'Authorization': 'Bearer ' + token } }) .then(r => r.json()) .then(data => { _adminMesasData = data.tables || []; const occupied = _adminMesasData.filter(t => t.status !== 'libre').length; const kpi = document.getElementById('ar-kpi-tables'); if (kpi) kpi.textContent = occupied + ' / ' + _adminMesasData.length; renderMesaGrid(); }) .catch(e => { grid.innerHTML = '
Error: ' + e.message + '
'; }); } function renderMesaGrid() { const grid = document.getElementById('ar-mesas-grid'); const mesas = _currentMesaZone === 'all' ? _adminMesasData : _adminMesasData.filter(t => t.zone === _currentMesaZone); if (!mesas.length) { grid.innerHTML = '
Sin mesas en esta zona
'; return; } grid.innerHTML = mesas.map(t => { const color = t.status === 'libre' ? '#4ade80' : t.status === 'ocupada' ? '#ef4444' : '#f59e0b'; const consumption = parseFloat(t.consumption || 0).toFixed(2); const num = t.name.replace(/[^0-9]/g, '') || t.id; return `
${num}
${t.zone || ''}
${t.status_text || (t.status==='libre'?'Disponible':'Ocupada')}
${t.waiter_name ? `
👤 ${t.waiter_name}
` : ''} ${parseFloat(t.consumption||0)>0 ? `
RD$${consumption}
` : ''}
`; }).join(''); } async function openMesaProfile(tableId) { const token = sessionStorage.getItem('pos_token'); showModal('
Cargando...
'); try { const r = await fetch('/api/restaurant.php?action=table-invoice&id=' + tableId, { headers: { 'Authorization': 'Bearer ' + token } }); const data = await r.json(); const mesa = data.table || {}; const orders = data.orders || []; const total = parseFloat(data.total || 0).toFixed(2); const statusOpts = ['libre','ocupada','reservada'].map(s => `` ).join(''); let itemsHtml = ''; orders.forEach(ord => { (ord.items||[]).forEach(item => { itemsHtml += `
${item.product_name} ×${item.qty||item.quantity} ${item.notes?`
${item.notes}
`:''}
RD$${parseFloat(item.unit_price||0).toFixed(2)}
`; }); }); const inner = document.querySelector('#modal-overlay > div > div') || document.querySelector('#modal-overlay .modal-inner'); if (inner) inner.innerHTML = `

${mesa.name||'Mesa '+tableId}

${mesa.zone||''} · Cap. ${mesa.capacity||'?'} · Mesero: ${data.waiter_name||'Sin asignar'}
Consumo:
${itemsHtml||'
Sin órdenes activas
'}
Total RD$${total}
${parseFloat(total)>0?``:''}
`; } catch(e) { showToast('Error: ' + e.message, 'error'); closeModal(); } } async function saveMesaStatus(tableId) { const sel = document.getElementById('mesa-status-sel-' + tableId); if (!sel) return; const status = sel.value; const token = sessionStorage.getItem('pos_token'); try { await fetch('/api/restaurant/tables/' + tableId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); showToast('Estado actualizado'); closeModal(); renderAdminMesas(); } catch(e) { showToast('Error al guardar', 'error'); } } async function closeMesaAndCharge(tableId) { const token = sessionStorage.getItem('pos_token'); const methods = { '1':'cash', '2':'card', '3':'transfer' }; const choice = prompt('Método de pago:\n1) Efectivo\n2) Tarjeta\n3) Transferencia', '1'); const paymentMethod = methods[choice?.trim()] || 'cash'; try { const r = await fetch('/api/restaurant.php?action=table-invoice&id=' + tableId, { headers: { 'Authorization': 'Bearer ' + token } }); const data = await r.json(); const orderId = data.order?.id || data.orders?.[0]?.id; if (!orderId) { showToast('Sin orden activa para cobrar', 'error'); return; } const closeR = await fetch('/api/restaurant/orders/' + orderId + '/close', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ payment_method: paymentMethod }) }); const closeData = await closeR.json(); if (!closeR.ok) throw new Error(closeData.error || 'Error cerrando orden'); // Marcar mesa como libre después de cobrar await fetch('/api/restaurant/tables/' + tableId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'libre' }) }); showToast('✅ Cuenta cerrada — mesa disponible'); closeModal(); renderAdminMesas(); } catch(e) { showToast('Error al cobrar: ' + e.message, 'error'); } } function filterMesaZone(zone, btn) { _currentMesaZone = zone; document.querySelectorAll('#ar-zone-tabs .tab-btn').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); renderMesaGrid(); } function showMesaQR(tableId, uuid) { if (!uuid) { showToast('Mesa sin código QR (sin UUID)', 'error'); return; } const qrUrl = window.location.origin + '/qr-menu.html?table=' + uuid; showModal(`

Mesa ${tableId}

${qrUrl}
`); setTimeout(() => { const el = document.getElementById('qr-div-' + tableId); if (el && typeof QRCode !== 'undefined') { new QRCode(el, { text: qrUrl, width: 180, height: 180, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H }); } }, 80); } function printMesaQR(tableId, uuid) { const url = window.location.origin + '/qr-menu.html?table=' + uuid; const w = window.open('', '_blank'); w.document.write(`Mesa ${tableId}