Papology
Papology
🍟 Papas
🥤 Bebidas
➕ Adiciones
📦 Pedidos
Cargando…
Sin pedidos hoy todavía 🍟
Pedido #001
🛒
Toca un producto
para agregarlo
🏍️ Domiciliarios
Selecciona un domiciliario para gestionar sus pedidos

➕ Agregar domiciliario

📦 Órdenes del Día

Actualiza el estado a medida que se preparan los pedidos

Sin pedidos hoy todavía 🍟
`; const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;border:none;'; document.body.appendChild(iframe); const doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); iframe.contentWindow.focus(); setTimeout(() => { iframe.contentWindow.print(); setTimeout(() => { if (iframe.parentNode) iframe.parentNode.removeChild(iframe); }, 4000); }, 600); } // ── Listener: códigos de descuento ─────────────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/discountCodes'), (snap) => { discountCodes = snap.val() || {}; console.log('🏷️ POS — Códigos de descuento:', Object.keys(discountCodes).length); }, (err) => console.warn('⚠️ POS — Error descuentos:', err.message)); } // ── Listener: promociones activas ───────────────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/promotions'), (snap) => { promotions = snap.val() || {}; console.log('🎉 POS — Promociones cargadas:', Object.keys(promotions).length); }, (err) => console.warn('⚠️ POS — Error promociones:', err.message)); } // ── Contador global atómico ─────────────────────────────────────────────── window.getNextOrderIdFirebase = async function() { const today = getBusinessDay(); let orderId = null; if (_db) { try { await runTransaction(ref(_db, 'tenants/' + window._tenantSlug + '/settings'), (current) => { if (!current) current = {}; if (current.dailyResetDate !== today) { current.orderCount = 1; current.dailyResetDate = today; } else { current.orderCount = (current.orderCount || 0) + 1; } orderId = 'P' + String(current.orderCount).padStart(3, '0'); return current; }); } catch(e) { console.warn('⚠️ Firebase transaction falló:', e.message); } } if (!orderId) { const hoy = getBusinessDayStr(); const stored = localStorage.getItem('pos_' + (window._tenantSlug || 'x') + '_ventas_' + hoy); const count = stored ? JSON.parse(stored).length + 1 : 1; orderId = 'L' + String(count).padStart(3, '0'); } return orderId; }; // ── Descontar inventario en Firebase ───────────────────────────────────── async function _descontarInventarioFb(venta) { if (!_db) return; const recetasKeys = Object.keys(_recetasFb); if (recetasKeys.length === 0) return; // no hay recetas configuradas const { push: fbPush, set: fbSet, ref: fbRef, runTransaction } = await import('https://www.gstatic.com/firebasejs/10.12.0/firebase-database.js'); const today = (typeof getBusinessDay === 'function') ? getBusinessDay() : new Date().toISOString().split('T')[0]; const alertas = []; // ingredientes que cruzaron su nivel de alerta en esta venta for (const item of venta.items) { const pKey = item.nombre.toUpperCase().replace(/\s+/g, '_').replace(/[^A-Z0-9_]/g, ''); const receta = _recetasFb[pKey]; if (!receta || !receta.porciones) continue; for (const porcion of receta.porciones) { const ingId = porcion.ingredienteId; const qty = porcion.qty * (item.qty || 1); const ing = _invFb[ingId]; if (!ing) continue; // Deducción atómica del stock const txn = await runTransaction(fbRef(_db, `tenants/${window._tenantSlug}/inventario/${ingId}/stock`), (current) => { return Math.max(0, (current || 0) - qty); }); const nuevoStock = txn?.snapshot?.val(); // Registrar movimiento const movRef = fbPush(fbRef(_db, 'tenants/' + window._tenantSlug + '/inv_movimientos')); await fbSet(movRef, { ts: Date.now(), ingredienteId: ingId, name: ing.name, emoji: ing.emoji || '', unit: ing.unit || '', tipo: 'salida', qty: qty, nota: `POS ${venta.num} x${item.qty || 1} ${item.nombre}` }); // ── Alerta en tiempo real: si quedó en/bajo el nivel de alerta ── const alertAt = Number(ing.alertAt) || 0; if (typeof nuevoStock === 'number' && alertAt > 0 && nuevoStock <= alertAt && ing.alertedDia !== today) { alertas.push({ name: ing.name, stock: nuevoStock, unit: ing.unit || '', emoji: ing.emoji || '', estado: nuevoStock <= 0 ? 'agotado' : 'bajo' }); await fbSet(fbRef(_db, `tenants/${window._tenantSlug}/inventario/${ingId}/alertedDia`), today); _invFb[ingId].alertedDia = today; // evitar reenvíos en la misma sesión } } } // Enviar alerta consolidada al flujo n8n (notifica a Alejandra y Andres) if (alertas.length) { fetch('https://n8n.adsgadgets.com/webhook/papology-inventario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Papology-Secret': 'papology-inventario-2024' }, body: JSON.stringify({ tipo: 'alerta', ingredientes: alertas }) }).catch(() => {}); } } // Exponer al scope global (cobrar() está en script no-module) window._descontarInventarioFb = _descontarInventarioFb; // ── Guardar pedido en Firebase ──────────────────────────────────────────── window.saveOrderToFirebase = async function(venta) { if (!_db) return; const today = getBusinessDay(); const di = venta.domicilioInfo; // Resolver nombre y teléfono según tipo de pedido const customerName = (di?.nombre) || (venta.mesaNombre) || (venta.llevarNombre) || 'POS'; const customerPhone = (di?.tel) || (venta.mesaTel) || (venta.llevarTel) || ''; // Capturar el cajero activo (usuario logueado en el POS) // Necesario para auditoría: saber quién cobró cada pedido. let _cajeroPOS = ''; try { const _sess = JSON.parse(sessionStorage.getItem(SESSION_KEY) || '{}'); _cajeroPOS = _sess.name || _sess.username || _sess.user || ''; } catch(e) { _cajeroPOS = ''; } const orderData = { id: venta.num, timestamp: venta.ts, customerName: customerName || (venta.tipo === 'domicilio' ? 'Domicilio' : 'POS'), phone: customerPhone, orderType: venta.tipo, address: di?.dir || '', commune: di?.comuna || '', neighborhood: di?.barrio || '', deliveryFee: di?.valorDomicilio || 0, items: venta.items.map(i => ({ name: i.nombre, qty: i.qty, price: i.precio })), subtotal: venta.subtotal || 0, discountCode: venta.discountCode || null, discountPct: venta.discountPct || 0, discountAmt: venta.discountAmt || 0, promoId: descuentoActivo?.promoId || null, total: venta.total, paymentType: venta.pago, status: 'recibido', source: 'pos', cajero: _cajeroPOS, // ← auditoría: quién cobró observations: venta.obs || '', notaNombre: venta.notaNombre || '', notaMotivo: venta.notaMotivo || '' }; // 1. Guardar pedido const newRef = push(ref(_db, `tenants/${window._tenantSlug}/orders/${today}`)); await set(newRef, orderData); console.log('✅ POS — Pedido guardado en Firebase:', venta.num); // 1b. Si el pago fue transferencia, guardar también el comprobante (origen POS) if (venta.pago === 'transferencia') { const ti = venta.transferInfo || {}; const compRef = push(ref(_db, `tenants/${window._tenantSlug}/comprobantes_wa/${today}`)); set(compRef, { orderId: venta.num, customerName: orderData.customerName, phone: customerPhone || '', monto: venta.total, comprobante: ti.num || '—', fecha: ti.date || '', hora: ti.time || venta.hora || '', plataforma: 'Transferencia', destinatario: 'Papology', origen: 'pos', timestamp: venta.ts || Date.now() }).catch(e => console.warn('⚠️ comprobante POS:', e.message)); } // 2. Guardar/actualizar perfil del cliente (no bloqueante) if (customerPhone) { syncClienteProfile(_db, { customerName, customerPhone, venta, orderData }).catch(() => {}); } }; // ── Sincronizar perfil del cliente en Firebase + GHL (vía n8n) ─────────── // URL del webhook n8n — reemplazar con tu URL real después de importar el workflow const N8N_CLIENT_SYNC_URL = 'https://TU_N8N_URL/webhook/papology-pos-client-sync'; async function syncClienteProfile(_db, { customerName, customerPhone, venta, orderData }) { const phoneKey = customerPhone.replace(/\D/g, ''); if (!phoneKey || phoneKey.length < 7) return; // 1. Guardar perfil en Firebase (siempre funciona, sin CORS) const contactRef = ref(_db, `tenants/${window._tenantSlug}/contacts/${phoneKey}`); const snap = await get(contactRef); const prev = snap.val() || {}; const historial = prev.historial || []; historial.unshift({ id: orderData.id, fecha: new Date().toLocaleDateString('es-CO'), hora: new Date().toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit'}), items: orderData.items.map(i => `${i.qty}x ${i.name}`), total: orderData.total, tipo: orderData.orderType, pago: orderData.paymentType }); if (historial.length > 10) historial.length = 10; await set(contactRef, { nombre: customerName || prev.nombre || '', telefono: customerPhone, primerPedido: prev.primerPedido || Date.now(), ultimoPedido: Date.now(), totalPedidos: (prev.totalPedidos || 0) + 1, ultimoItems: orderData.items.map(i => ({ name: i.name, qty: i.qty })), ultimoTotal: orderData.total, ultimoTipo: orderData.orderType, historial }); console.log('👤 Perfil Firebase actualizado:', phoneKey); // 2. Sincronizar a GHL vía n8n (servidor, sin CORS) if (N8N_CLIENT_SYNC_URL.includes('TU_N8N_URL')) { console.log('ℹ️ N8N_CLIENT_SYNC_URL no configurada — solo Firebase guardado'); return; } try { await fetch(N8N_CLIENT_SYNC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre: customerName, telefono: customerPhone, orderId: orderData.id, items: orderData.items, total: orderData.total, tipo: orderData.orderType, pago: orderData.paymentType, fecha: new Date().toLocaleDateString('es-CO'), hora: new Date().toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit'}) }) }); console.log('✅ Sync GHL enviado a n8n para pedido:', orderData.id); } catch(e) { console.log('ℹ️ n8n sync omitido:', e.message); } } // ── Actualizar estado de pedido en Firebase ──────────────────────────────── window.updateOrderStatus = async function(firebaseKey, firebaseDate, newStatus) { if (!_db) { if (window.showToast) showToast('⚠️ Sin conexión Firebase'); return; } try { await update(ref(_db, `tenants/${window._tenantSlug}/orders/${firebaseDate}/${firebaseKey}`), { status: newStatus }); if (window.showToast) showToast('✅ ' + newStatus.charAt(0).toUpperCase() + newStatus.slice(1)); } catch(e) { console.warn('❌ Error actualizando estado:', e.message); if (window.showToast) showToast('⚠️ Error al actualizar estado'); } }; // ── Reset completo del día ──────────────────────────────────────────────── // ── Cambiar la propia contraseña (verifica la actual antes) ── window.cambiarClaveUsuario = async function(username, actual, nueva) { if (!_db) return { ok: false, error: 'Sin conexión a Firebase' }; try { const snap = await get(ref(_db, `tenants/${window._tenantSlug}/pos-users/${username}`)); if (!snap.exists()) return { ok: false, error: 'Usuario no encontrado' }; const user = snap.val(); if (String(user.password) !== String(actual)) return { ok: false, error: 'La contraseña actual no es correcta' }; await update(ref(_db, `tenants/${window._tenantSlug}/pos-users/${username}`), { password: String(nueva) }); console.log('🔑 Contraseña actualizada para', username); return { ok: true }; } catch(e) { return { ok: false, error: e.message }; } }; // ── Control de cierre único por jornada ── window.cierreYaImpresoHoy = async function() { if (!_db) return false; try { const today = (typeof getBusinessDay === 'function') ? getBusinessDay() : new Date().toISOString().split('T')[0]; const snap = await get(ref(_db, `tenants/${window._tenantSlug}/cierres/${today}/done`)); return snap.val() === true; } catch(e) { return false; } }; window.marcarCierreImpreso = async function(usuario) { if (!_db) return; try { const today = (typeof getBusinessDay === 'function') ? getBusinessDay() : new Date().toISOString().split('T')[0]; await set(ref(_db, `tenants/${window._tenantSlug}/cierres/${today}`), { done: true, ts: Date.now(), usuario: usuario || '' }); } catch(e) { console.warn('⚠️ marcar cierre:', e.message); } }; // Registrar gasto a proveedor en Firebase (lo lee el CPanel → Gastos) window.guardarGastoProveedorFirebase = async function(gasto) { if (!_db) throw new Error('Sin conexión a Firebase'); const r = push(ref(_db, 'tenants/' + window._tenantSlug + '/gastos')); await set(r, gasto); console.log('💸 Gasto proveedor guardado en Firebase:', gasto.proveedor); }; window.reiniciarDiaFirebase = async function() { if (!_db) { showToast('⚠️ Sin conexión Firebase'); return; } const today = getBusinessDay(); try { // 1. Borrar todos los pedidos de hoy en Firebase await remove(ref(_db, `tenants/${window._tenantSlug}/orders/${today}`)); // 2. Resetear el contador de pedidos del día await runTransaction(ref(_db, 'tenants/' + window._tenantSlug + '/settings'), (current) => { if (!current) current = {}; current.orderCount = 0; current.dailyResetDate = today; return current; }); // 3. Limpiar la cola del Display (TV) → vuelve al mensaje de bienvenida await remove(ref(_db, 'tenants/' + window._tenantSlug + '/display/queue')); // 4. Permitir un nuevo cierre de caja para la jornada reiniciada await remove(ref(_db, `tenants/${window._tenantSlug}/cierres/${today}`)); console.log('✅ Firebase reiniciado: pedidos + contador + Display + cierre'); } catch(e) { console.warn('⚠️ Error reiniciando Firebase:', e.message); throw e; // lo maneja reiniciarDia() } }; // ── Cargar PINs operativos desde Firebase ──────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/pos-config/pines'), (snap) => { const p = snap.val(); if (p) { if (p.reiniciar) PIN_REINICIAR = String(p.reiniciar); if (p.eliminar) PIN_ELIMINAR = String(p.eliminar); console.log('✅ PINs cargados desde Firebase'); } }, { onlyOnce: false }); } // ── Imágenes de productos en tiempo real ───────────────────────────────── // Cuando se actualiza tenants/{slug}/pos-config/imagenes/{CLAVE} // el POS reemplaza la imagen del producto automáticamente sin recargar. if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/pos-config/imagenes'), (snap) => { const data = snap.val(); if (!data) return; // Reemplazar URLs en PRODUCTOS.papas con lo que venga de Firebase let cambios = 0; PRODUCTOS.papas.forEach(p => { // Clave normalizada: nombre en mayúsculas, espacios → _ const key = p.nombre.toUpperCase().replace(/\s+/g, '_').replace(/[^A-Z0-9_]/g, ''); if (data[key]) { p.img = data[key]; cambios++; } }); if (cambios > 0) { renderCatalogo(); console.log(`🖼️ POS — ${cambios} imagen(es) actualizadas desde Firebase`); } }, (err) => { console.warn('⚠️ POS — No se pudieron cargar imágenes:', err.message); }); } // ── Inventario desde Firebase (fuente única de verdad) ─────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/inventario'), (snap) => { const data = snap.val(); _invFb = data || {}; console.log('📦 POS — Inventario sincronizado:', Object.keys(_invFb).length, 'items'); }); } // ── Recetas desde Firebase ──────────────────────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/recetas'), (snap) => { const data = snap.val(); _recetasFb = data || {}; console.log('📋 POS — Recetas sincronizadas:', Object.keys(_recetasFb).length); }); } // ── Precios de productos desde Firebase ────────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/pos-config/precios'), (snap) => { const data = snap.val(); if (!data) return; let cambios = 0; ['papas','bebidas','adiciones'].forEach(cat => { PRODUCTOS[cat].forEach(p => { const key = p.nombre.toUpperCase().replace(/\s+/g,'_').replace(/[^A-Z0-9_]/g,''); if (data[key] !== undefined) { p.precio = data[key]; cambios++; } }); }); if (cambios > 0) { renderCatalogo(); if (document.getElementById('cpanel-contenido')?.classList.contains('active')) renderContenidoCpanel(); console.log(`💰 POS — ${cambios} precio(s) actualizados desde Firebase`); } }); } // ── Productos extra desde Firebase ─────────────────────────────────────── if (_db) { onValue(ref(_db, 'tenants/' + window._tenantSlug + '/pos-config/productos_extra'), (snap) => { const data = snap.val(); // Limpiar productos extra previos (tienen _fbKey) ['papas','bebidas','adiciones'].forEach(cat => { PRODUCTOS[cat] = PRODUCTOS[cat].filter(p => !p._fbKey); }); if (data) { Object.entries(data).forEach(([key, p]) => { if (p.cat && PRODUCTOS[p.cat]) { PRODUCTOS[p.cat].push({ nombre:p.nombre, precio:p.precio, emoji:p.emoji, _fbKey:key }); } }); } renderCatalogo(); if (document.getElementById('cpanel-contenido')?.classList.contains('active')) renderContenidoCpanel(); console.log('📦 POS — Productos extra sincronizados desde Firebase'); }); // ── CPanel "Contenido" — overrides de imagen/precio/ocultos ────────────── // El CPanel escribe en tenants/{slug}/catalogo/{imagenes,precios,eliminados}. // El POS aplica esos overrides sobre los productos hardcoded en cada cambio. // Helper: normaliza el nombre a la clave que usa el CPanel. function _catalogoNormKey(s) { return (s || '').toUpperCase() .normalize('NFD').replace(/\p{Mn}/gu, '') .replace(/\s+/g, '_').replace(/[^A-Z0-9_]/g, ''); } // Guardamos los originales para poder revertir si se borra un override if (!window._PRODUCTOS_BACKUP) { window._PRODUCTOS_BACKUP = JSON.parse(JSON.stringify(PRODUCTOS)); } window._eliminadosCatalogo = window._eliminadosCatalogo || {}; function _applyCatalogoOverrides(cfg) { const imagenes = (cfg && cfg.imagenes) || {}; const precios = (cfg && cfg.precios) || {}; const eliminados = (cfg && cfg.eliminados) || {}; window._eliminadosCatalogo = eliminados; let cambios = 0; ['papas','bebidas','adiciones'].forEach(cat => { const orig = window._PRODUCTOS_BACKUP[cat] || []; PRODUCTOS[cat].forEach(p => { if (p._cpKey || p._fbKey) { // Extras: solo aplicar la marca de eliminado por la clave normalizada const key = _catalogoNormKey(p.nombre); p._hidden = !!eliminados[key]; return; } const key = _catalogoNormKey(p.nombre); const origP = orig.find(x => _catalogoNormKey(x.nombre) === key); // Imagen if (imagenes[key]) { if (p.img !== imagenes[key]) { p.img = imagenes[key]; cambios++; } } else if (origP && p.img !== origP.img) { p.img = origP.img; cambios++; } // Precio if (typeof precios[key] === 'number') { if (p.precio !== precios[key]) { p.precio = precios[key]; cambios++; } } else if (origP && p.precio !== origP.precio) { p.precio = origP.precio; cambios++; } // Ocultar (marcar — el filtro real lo aplica el wrap de renderCatalogo) p._hidden = !!eliminados[key]; }); }); renderCatalogo(document.getElementById('search-input')?.value?.toLowerCase() || ''); console.log(`🗂️ POS — Catálogo sincronizado con CPanel (${cambios} cambios img/precio, ${Object.keys(eliminados).length} ocultos)`); } onValue(ref(_db, 'tenants/' + window._tenantSlug + '/catalogo'), (snap) => { const cfg = snap.val() || {}; _applyCatalogoOverrides(cfg); }, (err) => console.warn('⚠️ POS — catalogo CPanel:', err.message)); // Wrap de renderCatalogo para ocultar (sin mutar) los productos con _hidden if (typeof renderCatalogo === 'function' && !window._renderCatalogoWrapped) { const _origRenderCatalogo = renderCatalogo; // Reemplazamos por una versión que oculta antes de pintar y restaura después window.renderCatalogo = function(filtro = '') { const snap = {}; ['papas','bebidas','adiciones'].forEach(cat => { if (!PRODUCTOS[cat]) return; snap[cat] = PRODUCTOS[cat]; PRODUCTOS[cat] = PRODUCTOS[cat].filter(p => !p._hidden); }); try { return _origRenderCatalogo(filtro); } finally { // Restaurar referencia original para que otros consumidores vean los reales Object.keys(snap).forEach(cat => { PRODUCTOS[cat] = snap[cat]; }); } }; window._renderCatalogoWrapped = true; } // ── Productos creados desde el CPanel (Gestión de Contenido) ───────────── // El CPanel guarda en catalogo/extra_productos/{categoria}; el POS los mezcla aquí. onValue(ref(_db, 'tenants/' + window._tenantSlug + '/catalogo/extra_productos'), (snap) => { const data = snap.val(); // Limpiar los del CPanel previos (marcados con _cpKey) ['papas','bebidas','adiciones'].forEach(cat => { PRODUCTOS[cat] = PRODUCTOS[cat].filter(p => !p._cpKey); }); if (data) { ['papas','bebidas','adiciones'].forEach(cat => { const catObj = data[cat]; if (catObj && typeof catObj === 'object') { Object.entries(catObj).forEach(([key, p]) => { if (!p || !p.nombre) return; const item = { nombre: p.nombre, precio: p.precio || 0, emoji: p.emoji || '🍽️', _cpKey: cat + '/' + key }; if (p.img) item.img = p.img; PRODUCTOS[cat].push(item); }); } }); } renderCatalogo(); console.log('📦 POS — Productos del CPanel sincronizados'); }, (err) => console.warn('⚠️ POS — extra_productos CPanel:', err.message)); }