LO
La OrejonaCentro Deportivo · Horarios
Agenda pública
Elige fecha y un horario libre para solicitar tu reserva
Libre = disponible · Ocupado = ya reservado
Versión de revisión · La Orejona — Demo en este dispositivo. Los datos no se comparten entre teléfonos todavía (cada quien ve su copia local).
Logo
La Orejona Centro Deportivo
— —
Inicio
Centro Deportivo La Orejona

Ventas de hoy

HoraDetalleMétodoTotal

Caja del turno

La caja está abierta. Las ventas del día se muestran arriba.

Reservas de hoy

HoraRecursoClienteEstadoValor
`; const w = window.open('', '_blank'); w.document.write(html); w.document.close(); } function exportReporteExcel() { const desde = document.getElementById('rep-desde')?.value || formatDate(new Date()); const hasta = document.getElementById('rep-hasta')?.value || desde; const ventasDia = ventasEnRango(desde, hasta).filter(v => !v.credito); const movs = (movimientosCaja||[]).filter(m => m.fecha >= desde && m.fecha <= hasta); let cajaNequi=0, cajaTransf=0, cajaEfectivoIn=0, cajaTarjeta=0; ventasDia.forEach(v => { if (v.pagos) { cajaNequi += Number(v.pagos.nequi)||0; cajaTransf += Number(v.pagos.transferencia)||0; cajaEfectivoIn += Number(v.pagos.efectivo)||0; cajaTarjeta += Number(v.pagos.tarjeta)||0; } }); const gastosEf = movs.filter(m => m.tipo==='gasto' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const gastosNq = movs.filter(m => m.tipo==='gasto' && m.medio==='nequi').reduce((s,m)=>s+m.monto,0); const gastosTr = movs.filter(m => m.tipo==='gasto' && m.medio==='transferencia').reduce((s,m)=>s+m.monto,0); const entradasEf = movs.filter(m => m.tipo==='entrada' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const d0 = new Date(desde+'T12:00:00'); d0.setDate(d0.getDate()-1); const fechaAnt = formatDate(d0); const corteAnt = (cortesCaja||[]).filter(c => c.fecha === fechaAnt).slice(-1)[0]; const corteHasta = (cortesCaja||[]).filter(c => c.fecha === hasta).slice(-1)[0]; let cajaInicio = corteAnt && typeof corteAnt.efectivoEnCaja==='number' ? corteAnt.efectivoEnCaja : (cajaBase||0); let cajaFin = corteHasta && typeof corteHasta.efectivoEnCaja==='number' ? corteHasta.efectivoEnCaja : (cajaInicio + cajaEfectivoIn - gastosEf + entradasEf); const rows = []; rows.push(['REPORTE LA OREJONA', desde, hasta]); rows.push([]); rows.push(['CAJAS']); rows.push(['Concepto','Valor']); rows.push(['Caja efectivo INICIO', cajaInicio]); rows.push(['Caja efectivo FIN', cajaFin]); rows.push(['Ventas efectivo', cajaEfectivoIn]); rows.push(['Gastos efectivo', gastosEf]); rows.push(['Entradas extra efectivo', entradasEf]); rows.push(['Nequi entradas', cajaNequi]); rows.push(['Gastos Nequi', gastosNq]); rows.push(['Nequi neto', cajaNequi - gastosNq]); rows.push(['Transferencias entradas', cajaTransf]); rows.push(['Gastos transferencia', gastosTr]); rows.push(['Transfer neto', cajaTransf - gastosTr]); rows.push(['Tarjeta', cajaTarjeta]); rows.push([]); rows.push(['VENTAS']); rows.push(['Fecha','Hora','Detalle','Metodo','Empleado','Total','Descuento']); ventasDia.forEach(v => rows.push([v.fecha, v.hora, v.detalle, v.metodo, v.empleado, v.total, v.descuento||0])); rows.push([]); rows.push(['GASTOS']); rows.push(['Fecha','Concepto','Medio','Empleado','Monto']); movs.filter(m=>m.tipo==='gasto').forEach(m => rows.push([m.fecha, m.concepto, m.medio||'efectivo', m.empleado||'', m.monto])); const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(';')).join('\r\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob(['\ufeff'+csv],{type:'text/csv;charset=utf-8;'})); a.download = `reporte-orejona-${desde}_${hasta}.csv`; a.click(); toast('Excel descargado (con cajas)'); } function ajustarStock(id) { ingresarMercancia(id); } function ingresarMercancia(id) { const p = PRODUCTS.find(x => x.id === id); if (!p) return; // Si no tenía control de stock (alquileres), al ingresar se activa if (p.stock === null) { if (!confirm(`"${p.name}" no tenía control de inventario.\n\n¿Activar stock y registrar entrada de mercancía?`)) return; p.stock = 0; } const raw = prompt( `INGRESO DE MERCANCÍA\n\n` + `Producto: ${p.name}\n` + `Stock actual: ${p.stock}\n\n` + `¿Cuántas unidades llegaron? (ej. 100)`, '100' ); if (raw === null) return; const n = Number(String(raw).replace(/[^0-9.-]/g, '')); if (isNaN(n) || n <= 0) { alert('Ingresa una cantidad mayor a 0'); return; } const antes = p.stock; p.stock += n; const now = new Date(); kardex.unshift({ id: Date.now(), productId: p.id, name: p.name, delta: n, motivo: 'Ingreso mercancía (compra/llegada)', fecha: formatDate(now), hora: now.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' }), user: currentUser?.name }); saveStore(); renderProductosAdmin(); alert(`Listo\n\n${p.name}\nAntes: ${antes}\nEntraron: +${n}\nAhora: ${p.stock}`); } function editarProducto(id) { const p = PRODUCTS.find(x => x.id === id); if (!p) return; document.getElementById('prod-edit-id').value = id; document.getElementById('prod-nombre').value = p.name; document.getElementById('prod-precio').value = p.price; document.getElementById('prod-cat').value = p.cat; document.getElementById('prod-stock').value = p.stock === null ? '' : p.stock; document.getElementById('prod-minstock').value = p.minStock ?? 5; document.getElementById('prod-costo').value = p.costo ?? ''; document.getElementById('prod-form-title').textContent = 'Editar producto'; document.getElementById('btn-cancel-edit-prod').style.display = 'inline-flex'; window.scrollTo({ top: 0, behavior: 'smooth' }); } function cancelarEditProducto() { document.getElementById('prod-edit-id').value = ''; document.getElementById('prod-nombre').value = ''; document.getElementById('prod-precio').value = '0'; document.getElementById('prod-stock').value = ''; document.getElementById('prod-minstock').value = 5; document.getElementById('prod-costo').value = ''; document.getElementById('prod-form-title').textContent = 'Nuevo producto'; document.getElementById('btn-cancel-edit-prod').style.display = 'none'; } function guardarProducto() { const name = document.getElementById('prod-nombre').value.trim(); const price = Number(document.getElementById('prod-precio').value) || 0; const cat = document.getElementById('prod-cat').value; const stockRaw = document.getElementById('prod-stock').value.trim(); const stock = stockRaw === '' ? null : Number(stockRaw); const minStock = Number(document.getElementById('prod-minstock').value) || 5; const costoRaw = document.getElementById('prod-costo').value.trim(); const costo = costoRaw === '' ? null : Number(costoRaw); const editId = document.getElementById('prod-edit-id').value; if (!name) { alert('Escribe el nombre del producto'); return; } const now = new Date(); const fecha = formatDate(now); const hora = now.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' }); if (editId) { const p = PRODUCTS.find(x => x.id === Number(editId)); if (p) { const prev = p.stock; p.name = name; p.price = price; p.cat = cat; p.stock = stock; p.minStock = minStock; p.costo = costo; if (stock !== null && prev !== stock) { kardex.unshift({ id: Date.now(), productId: p.id, name, delta: (stock||0)-(prev||0), motivo: 'Ajuste al editar producto', fecha, hora, user: currentUser?.name }); } } toast('Producto actualizado'); } else { const id = Date.now(); PRODUCTS.push({ id, name, price, cat, stock, minStock, costo }); if (stock !== null && stock > 0) { kardex.unshift({ id: Date.now()+1, productId: id, name, delta: stock, motivo: 'Stock inicial al crear producto', fecha, hora, user: currentUser?.name }); } toast('Producto guardado'); } saveStore(); cancelarEditProducto(); renderProductosAdmin(); } function eliminarProducto(id) { if (!canDo('delete')) { alert('No tienes permiso para eliminar. Solo el administrador puede eliminar datos.'); return; } const p = PRODUCTS.find(x => x.id === id); if (!confirm(`¿Eliminar el producto "${p?.name || ''}"?\n\nEsta acción no se puede deshacer.`)) return; PRODUCTS = PRODUCTS.filter(p => p.id !== id); audit('eliminar', 'producto', id, { name: p?.name }); saveStore(); renderProductosAdmin(); } // ===================== LISTADOS (CHECKLISTS) ===================== function renderChecklist() { const body = document.getElementById('listas-body'); // Solo admin ve/crea todos; el resto solo los asignados a su nombre let visibles = LISTAS; if (!canDo('all_checklists') && !canDo('edit_lists')) { const nombre = currentUser?.name || ''; visibles = LISTAS.filter(l => !l.asignado || l.asignado === nombre || (l.asignado && l.asignado.includes(nombre.split(' ')[0]))); } // Ocultar formulario de crear si no es admin const formCrear = document.querySelector('#page-checklists .panel:last-of-type'); document.querySelectorAll('#page-checklists .panel').forEach((p, i) => { if (p.querySelector('#lista-nombre')) { p.style.display = canDo('edit_lists') ? '' : 'none'; } }); if (!visibles.length) { body.innerHTML = '
No tienes listados asignados.
'; return; } body.innerHTML = visibles.map(lista => { const hechas = lista.items.filter(i => i.done).length; const total = lista.items.length; const pct = total ? Math.round((hechas / total) * 100) : 0; const itemsHtml = lista.items.map((it, idx) => ` `).join(''); return `
${lista.nombre} ${lista.revisado ? 'Revisado' : ''}
👤 ${lista.asignado || 'Sin asignar'} · ${hechas}/${total} completadas (${pct}%)${lista.revisado && lista.revisadoPor ? ' · Revisado por ' + lista.revisadoPor : ''}
${canDo('edit_lists') ? ` ` : ''}
${itemsHtml}
`; }).join(''); const hist = document.getElementById('checklist-historial'); if (hist) { hist.innerHTML = checklistHistorial.length ? checklistHistorial.slice(0, 30).map(h => ` ${esc(h.fecha)} ${esc(h.nombre)} ${esc(h.revisadoPor)} ${h.items} `).join('') : 'Sin revisiones aún'; } } function aplicarPlantillaChecklist() { const t = document.getElementById('lista-plantilla').value; const plantillas = { apertura: { nombre: 'Apertura del día', items: 'Revisar estado de grama canchas\nEncender luces de canchas\nLimpiar baños\nRevisar stock cafetería\nVerificar red y pantalla gigante' }, cierre: { nombre: 'Cierre nocturno', items: 'Cierre de caja del día\nApagar luces de canchas\nRevisar zona BBQ (gas cerrado)\nSeguridad pista RC\nCerrar portones' }, finde: { nombre: 'Fin de semana', items: 'Revisar stock extra bebidas\nPreparar canchas\nRevisar baños cada 2h\nControl de filas recepción\nVerificar extintores a la vista' } }; const p = plantillas[t]; if (!p) return; document.getElementById('lista-nombre').value = p.nombre; document.getElementById('lista-items').value = p.items; } function crearLista() { if (!canDo('edit_lists')) { alert('Solo el administrador puede crear listados'); return; } const nombre = document.getElementById('lista-nombre').value.trim(); const asignado = document.getElementById('lista-asignado').value; const raw = document.getElementById('lista-items').value.trim(); if (!nombre) { alert('Escribe el nombre del listado'); return; } if (!raw) { alert('Agrega al menos un ítem (uno por línea)'); return; } const items = raw.split('\n').map(t => t.trim()).filter(Boolean).map(text => ({ text, done: false })); if (!items.length) { alert('Agrega al menos un ítem válido'); return; } LISTAS.unshift({ id: Date.now(), nombre, asignado, items, creadoEn: new Date().toISOString() }); document.getElementById('lista-nombre').value = ''; document.getElementById('lista-asignado').value = ''; document.getElementById('lista-items').value = ''; document.getElementById('lista-plantilla').value = ''; saveStore(); renderChecklist(); toast('Listado creado'); } function toggleItemLista(listaId, idx) { const lista = LISTAS.find(l => l.id === listaId); if (lista && lista.items[idx]) { lista.items[idx].done = !lista.items[idx].done; saveStore(); renderChecklist(); } } function reasignarLista(listaId, usuario) { if (!canDo('edit_lists')) { alert('Solo el administrador puede reasignar listados'); renderChecklist(); return; } const lista = LISTAS.find(l => l.id === listaId); if (lista) { lista.asignado = usuario; saveStore(); renderChecklist(); } } function completarRevision(listaId) { const lista = LISTAS.find(l => l.id === listaId); if (!lista) return; const pendientes = lista.items.filter(i => !i.done); let observacion = ''; if (pendientes.length > 0) { observacion = prompt( `Hay ${pendientes.length} ítem(s) sin completar:\n` + pendientes.map(i => '• ' + i.text).join('\n') + `\n\nEscribe la observación (por qué no se hicieron). El informe se enviará igual:`, '' ); if (observacion === null) return; if (!observacion.trim()) { alert('Si quedan ítems sin hacer, la observación es obligatoria'); return; } } else { if (!confirm(`¿Completar revisión de "${lista.nombre}" y enviar informe por WhatsApp?`)) return; } lista.revisado = true; lista.revisadoPor = currentUser?.name || '—'; lista.revisadoEn = new Date().toLocaleString('es-CO'); lista.observacion = observacion.trim(); checklistHistorial.unshift({ id: Date.now(), fecha: formatDate(new Date()), nombre: lista.nombre, revisadoPor: lista.revisadoPor, revisadoEn: lista.revisadoEn, items: lista.items.length, hechos: lista.items.filter(i => i.done).length, observacion: lista.observacion || '' }); if (checklistHistorial.length > 100) checklistHistorial.length = 100; saveStore(); renderChecklist(); const lineas = [ `*Checklist revisado — La Orejona*`, ``, `📋 *${lista.nombre}*`, `👤 Asignado: ${lista.asignado || 'Sin asignar'}`, `✓ Revisado por: ${lista.revisadoPor}`, `🕒 ${lista.revisadoEn}`, ``, `*Ítems:*`, ...lista.items.map(i => (i.done ? '[HECHO] ' : '[NO HECHO] ') + i.text), `` ]; if (observacion.trim()) { lineas.push(`*Observación (no completados):*`); lineas.push(observacion.trim()); } else { lineas.push(`_Todos los puntos completados._`); } window._waChecklistMsg = lineas.join('\n'); // Botones directos (más fiable que prompt en móvil) const el = document.getElementById('wa-checklist-bar'); if (el) { el.style.display = 'block'; el.innerHTML = `
Checklist listo — enviar WhatsApp a:
`; } else { elegirDestinoWhatsApp(window._waChecklistMsg, 'WhatsApp checklist'); } toast('Elige un teléfono para enviar'); } function reiniciarLista(listaId) { const lista = LISTAS.find(l => l.id === listaId); if (!lista) return; if (!confirm(`¿Reiniciar el listado "${lista.nombre}" (marcar todo como pendiente)?`)) return; lista.items.forEach(i => i.done = false); lista.revisado = false; lista.revisadoPor = null; lista.revisadoEn = null; saveStore(); renderChecklist(); } function eliminarLista(listaId) { if (!canDo('edit_lists')) { alert('Solo el administrador puede eliminar listados'); return; } const lista = LISTAS.find(l => l.id === listaId); if (!lista) return; if (!confirm(`¿Eliminar el listado completo "${lista.nombre}"?\n\nEsta acción no se puede deshacer.`)) return; LISTAS = LISTAS.filter(l => l.id !== listaId); saveStore(); renderChecklist(); } // ===================== PUBLIC ===================== // ===================== REPORTES ===================== function ventasEnRango(desde, hasta) { return ventas.filter(v => v.fecha >= desde && v.fecha <= hasta && !v.anulada); } function reservasEnRango(desde, hasta) { return reservas.filter(r => r.fecha >= desde && r.fecha <= hasta && r.estado !== 'cancelada'); } function textoReportePeriodo() { const desde = document.getElementById('rep-desde')?.value || formatDate(new Date()); const hasta = document.getElementById('rep-hasta')?.value || desde; const ventasDia = ventasEnRango(desde, hasta).filter(v => !v.credito && !v.anulada); const total = ventasDia.reduce((s,v) => s + v.total, 0); let nq=0, tr=0, ef=0, tar=0; ventasDia.forEach(v => { if (v.pagos) { nq += Number(v.pagos.nequi)||0; tr += Number(v.pagos.transferencia)||0; ef += Number(v.pagos.efectivo)||0; tar += Number(v.pagos.tarjeta)||0; } else if (v.metodo === 'nequi') nq += v.total; else if (v.metodo === 'transferencia') tr += v.total; else if (v.metodo === 'efectivo') ef += v.total; }); const movs = (movimientosCaja||[]).filter(m => m.fecha >= desde && m.fecha <= hasta); const gastosEf = movs.filter(m => m.tipo==='gasto' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const gastosNq = movs.filter(m => m.tipo==='gasto' && m.medio==='nequi').reduce((s,m)=>s+m.monto,0); const entradasEf = movs.filter(m => m.tipo==='entrada' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const d0 = new Date(desde+'T12:00:00'); d0.setDate(d0.getDate()-1); const fechaAnt = formatDate(d0); const corteAnt = (cortesCaja||[]).filter(c => c.fecha === fechaAnt).slice(-1)[0]; const corteHasta = (cortesCaja||[]).filter(c => c.fecha === hasta).slice(-1)[0]; let cajaAnterior = corteAnt && typeof corteAnt.efectivoEnCaja==='number' ? corteAnt.efectivoEnCaja : (cajaBase||0); let cajaFinalEf = corteHasta && typeof corteHasta.efectivoEnCaja==='number' ? corteHasta.efectivoEnCaja : (cajaAnterior + ef - gastosEf + entradasEf); let cajaFinalNq = nq - gastosNq; // Categorías: cancha, bbq, tienda, comida (nequi/efectivo) const cats = { canchas: { ef:0, nq:0 }, bbq: { ef:0, nq:0 }, tienda: { ef:0, nq:0 }, comida: { ef:0, nq:0 }, rc: { ef:0, nq:0 }, otros: { ef:0, nq:0 } }; ventasDia.forEach(v => { const pagos = v.pagos || {}; const payEf = Number(pagos.efectivo)||0; const payNq = Number(pagos.nequi)||0; const payTot = payEf + payNq + (Number(pagos.transferencia)||0) + (Number(pagos.tarjeta)||0) || v.total || 1; const items = v.items || [{ cat:'otros', total: v.total }]; const itemsSum = items.reduce((s,it)=>s+(it.total||0),0) || v.total || 1; items.forEach(it => { let key = 'otros'; const cat = (it.cat||'').toLowerCase(); const name = String(it.name||'').toLowerCase(); if (cat === 'comida') key = 'comida'; else if (cat === 'tienda') key = 'tienda'; else if (cat === 'rc' || name.includes('rc')) key = 'rc'; else if (name.includes('bbq') || name.includes('asado')) key = 'bbq'; else if (cat === 'cancha' || name.includes('cancha') || name.includes('alquiler')) key = 'canchas'; const share = (it.total||0) / itemsSum; cats[key].ef += payEf * share; cats[key].nq += payNq * share; if (!v.pagos) { if (v.metodo === 'nequi') cats[key].nq += (it.total||0); else if (v.metodo === 'efectivo') cats[key].ef += (it.total||0); } }); }); const fmt = (n) => formatMoney(Math.round(n||0)); return [ '*Reporte La Orejona*', 'Fecha: ' + desde + (desde!==hasta ? ' a ' + hasta : ''), 'Usuario: ' + (currentUser?.name || '—'), '', 'Caja anterior (efectivo): ' + fmt(cajaAnterior), 'Venta total: ' + fmt(total), 'Total Efectivo: ' + fmt(ef), 'Total Nequi: ' + fmt(nq), 'Gastos efectivo: ' + fmt(gastosEf), 'Gastos Nequi: ' + fmt(gastosNq), '', '*Ventas por categoría*', 'Canchas — Efectivo: ' + fmt(cats.canchas.ef) + ' | Nequi: ' + fmt(cats.canchas.nq), 'BBQ — Efectivo: ' + fmt(cats.bbq.ef) + ' | Nequi: ' + fmt(cats.bbq.nq), 'Tienda — Efectivo: ' + fmt(cats.tienda.ef) + ' | Nequi: ' + fmt(cats.tienda.nq), 'Comida — Efectivo: ' + fmt(cats.comida.ef) + ' | Nequi: ' + fmt(cats.comida.nq), 'RC — Efectivo: ' + fmt(cats.rc.ef) + ' | Nequi: ' + fmt(cats.rc.nq), '', 'Caja final Nequi: ' + fmt(cajaFinalNq), 'Caja final efectivo: ' + fmt(cajaFinalEf) ].join('\n'); } function enviarReporteWhatsApp(num) { abrirWhatsApp(num, textoReportePeriodo()); toast('Abriendo WhatsApp…'); } function renderReportes() { const today = formatDate(new Date()); let desde = document.getElementById('rep-desde')?.value || today; let hasta = document.getElementById('rep-hasta')?.value || today; if (document.getElementById('rep-desde') && !document.getElementById('rep-desde').value) document.getElementById('rep-desde').value = desde; if (document.getElementById('rep-hasta') && !document.getElementById('rep-hasta').value) document.getElementById('rep-hasta').value = hasta; if (document.getElementById('rep-fecha')) document.getElementById('rep-fecha').value = hasta; if (desde > hasta) { const t = desde; desde = hasta; hasta = t; } // Solo cobros reales (no créditos pendientes) const ventasDia = ventasEnRango(desde, hasta).filter(v => !v.credito); const creditos = ventasEnRango(desde, hasta).filter(v => v.credito); const totalVentas = ventasDia.reduce((s,v) => s + v.total, 0); const hoyRes = reservasEnRango(desde, hasta); const ingresosRes = hoyRes.filter(r=>r.estado==='pagada').reduce((s,r) => s + (r.valor||0), 0); const ticketProm = ventasDia.length ? Math.round(totalVentas / ventasDia.length) : 0; // Cajas por medio en el período let cajaNequi = 0, cajaTransf = 0, cajaEfectivoIn = 0, cajaTarjeta = 0; ventasDia.forEach(v => { if (v.pagos) { cajaNequi += Number(v.pagos.nequi)||0; cajaTransf += Number(v.pagos.transferencia)||0; cajaEfectivoIn += Number(v.pagos.efectivo)||0; cajaTarjeta += Number(v.pagos.tarjeta)||0; } else if (v.metodo === 'nequi') cajaNequi += v.total; else if (v.metodo === 'transferencia') cajaTransf += v.total; else if (v.metodo === 'efectivo') cajaEfectivoIn += v.total; }); const movs = (movimientosCaja||[]).filter(m => m.fecha >= desde && m.fecha <= hasta); const gastosEf = movs.filter(m => m.tipo==='gasto' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const gastosNq = movs.filter(m => m.tipo==='gasto' && m.medio==='nequi').reduce((s,m)=>s+m.monto,0); const gastosTr = movs.filter(m => m.tipo==='gasto' && m.medio==='transferencia').reduce((s,m)=>s+m.monto,0); const entradasEf = movs.filter(m => m.tipo==='entrada' && (!m.medio||m.medio==='efectivo')).reduce((s,m)=>s+m.monto,0); const gastosTot = movs.filter(m => m.tipo==='gasto').reduce((s,m)=>s+m.monto,0); // Caja inicio = saldo heredado al empezar el día "desde" // Preferir corte del día anterior; si no, reconstruir function saldoEfectivoAlCierreDe(fecha) { const corte = (cortesCaja||[]).filter(c => c.fecha === fecha).slice(-1)[0]; if (corte && typeof corte.efectivoEnCaja === 'number') return corte.efectivoEnCaja; return null; } // Día anterior a desde const d0 = new Date(desde+'T12:00:00'); d0.setDate(d0.getDate()-1); const fechaAnt = formatDate(d0); let cajaInicio = saldoEfectivoAlCierreDe(fechaAnt); if (cajaInicio === null) { // Si el rango incluye hoy y no hay cortes, usar base actual menos movimientos posteriores a desde-1 cajaInicio = typeof cajaBase === 'number' ? cajaBase : 0; } const corteHasta = saldoEfectivoAlCierreDe(hasta); let cajaFin = corteHasta; if (cajaFin === null) { // reconstruir: inicio + efectivo ventas - gastos ef + entradas ef del rango cajaFin = cajaInicio + cajaEfectivoIn - gastosEf + entradasEf; } const nequiNeto = cajaNequi - gastosNq; const transfNeto = cajaTransf - gastosTr; document.getElementById('rep-cards').innerHTML = `
Ventas cobradas
${formatMoney(totalVentas)}
Nº cobros
${ventasDia.length}
Créditos (no caja)
${formatMoney(creditos.reduce((s,v)=>s+v.total,0))}
Gastos período
${formatMoney(gastosTot)}
Caja efectivo INICIO (${esc(desde)})
${formatMoney(cajaInicio)}
Caja efectivo FIN (${esc(hasta)})
${formatMoney(cajaFin)}
Caja Nequi (período)
${formatMoney(nequiNeto)}
Caja Transferencias
${formatMoney(transfNeto)}
`; // Bloque gastos en reportes let gastosHtml = ''; const repG = document.getElementById('rep-gastos'); if (repG) { repG.innerHTML = movs.filter(m=>m.tipo==='gasto').length ? movs.filter(m=>m.tipo==='gasto').map(m => ` ${esc(m.fecha)} ${esc(m.hora||'')}${esc(m.concepto)}${esc(m.medio||'efectivo')}${esc(m.empleado||'')}${formatMoney(m.monto)} `).join('') : 'Sin gastos en el período'; } const cm = document.getElementById('rep-cajas-medios'); if (cm) { cm.innerHTML = `
Entradas Nequi
${formatMoney(cajaNequi)}
Gastos Nequi
${formatMoney(gastosNq)}
Nequi neto a validar
${formatMoney(nequiNeto)}
Entradas Transferencia
${formatMoney(cajaTransf)}
Gastos Transferencia
${formatMoney(gastosTr)}
Transfer neto a validar
${formatMoney(transfNeto)}
Efectivo ventas
${formatMoney(cajaEfectivoIn)}
Gastos efectivo
${formatMoney(gastosEf)}

Caja efectivo inicio del rango: ${formatMoney(cajaInicio)} · Fin: ${formatMoney(cajaFin)} (según cierres guardados o reconstrucción).

`; } const rv = document.getElementById('rep-ventas'); rv.innerHTML = ventasDia.length ? ventasDia.map(v => ` ${esc(v.fecha)} ${esc(v.hora)} ${esc(v.detalle)} ${esc(v.metodo)} ${esc(v.empleado)} ${formatMoney(v.total)} `).join('') : 'Sin ventas en el período'; const emptyPay = () => ({ qty:0, total:0, efectivo:0, nequi:0, transferencia:0, tarjeta:0 }); const catMap = { cancha: emptyPay(), comida: emptyPay(), tienda: emptyPay(), rc: emptyPay() }; const catNames = { cancha: 'Canchas / Alquiler', comida: 'Comidas', tienda: 'Tienda', rc: 'RC / Otros', cxc: 'Abonos CxC' }; const prodMap = {}; ventasDia.forEach(v => { const pagos = v.pagos || {}; const payTotal = (Number(pagos.efectivo)||0)+(Number(pagos.nequi)||0)+(Number(pagos.transferencia)||0)+(Number(pagos.tarjeta)||0) || v.total || 1; const items = v.items || [{ cat: 'otros', qty:1, total: v.total }]; const itemsSum = items.reduce((s,it)=>s+(it.total||0),0) || v.total || 1; items.forEach(it => { const cat = it.cat || 'otros'; if (!catMap[cat]) catMap[cat] = emptyPay(); catMap[cat].qty += it.qty || 1; catMap[cat].total += it.total || 0; // Reparto proporcional del pago de la venta a la categoría const share = (it.total || 0) / itemsSum; catMap[cat].efectivo += (Number(pagos.efectivo)||0) * share; catMap[cat].nequi += (Number(pagos.nequi)||0) * share; catMap[cat].transferencia += (Number(pagos.transferencia)||0) * share; catMap[cat].tarjeta += (Number(pagos.tarjeta)||0) * share; if (!v.pagos && v.metodo) { const m = String(v.metodo).toLowerCase(); if (m.includes('nequi')) catMap[cat].nequi += (it.total||0); else if (m.includes('transf')) catMap[cat].transferencia += (it.total||0); else if (m.includes('tarjeta')) catMap[cat].tarjeta += (it.total||0); else catMap[cat].efectivo += (it.total||0); } const key = it.productId ? (PRODUCTS.find(p=>p.id===it.productId)?.name || it.cat) : (it.name || it.cat); if (!prodMap[key]) prodMap[key] = { qty: 0, total: 0 }; prodMap[key].qty += it.qty || 1; prodMap[key].total += it.total || 0; }); }); const granTotal = Object.values(catMap).reduce((s,c) => s + c.total, 0) || 1; document.getElementById('rep-categorias').innerHTML = Object.keys(catMap).filter(k => catMap[k].total > 0 || catMap[k].qty > 0).map(k => ` ${catNames[k] || k} ${catMap[k].qty} ${formatMoney(catMap[k].total)} ${formatMoney(Math.round(catMap[k].efectivo))} ${formatMoney(Math.round(catMap[k].nequi))} ${formatMoney(Math.round(catMap[k].transferencia))} ${formatMoney(Math.round(catMap[k].tarjeta))} ${Math.round((catMap[k].total / granTotal) * 100)}% `).join('') || 'Sin datos'; // CxC en reportes const repCxc = document.getElementById('rep-cxc'); if (repCxc) { const lista = (cuentasCobrar||[]).filter(c => { // cuentas tocadas en el período o aún abiertas if (c.estado === 'abierta') return true; if (c.fecha >= desde && c.fecha <= hasta) return true; return (c.abonos||[]).some(a => a.fecha >= desde && a.fecha <= hasta); }); repCxc.innerHTML = lista.length ? lista.map(c => { const ab = (c.abonos||[]).reduce((s,a)=>s+a.monto,0); const sal = Math.max(0, (c.total||0) - ab); return ` ${esc(c.cliente)} ${esc(c.estado)} ${formatMoney(c.total)} ${formatMoney(ab)} ${formatMoney(sal)} ${esc(c.detalle||'')} `; }).join('') : 'Sin cuentas por cobrar'; } const rankP = document.getElementById('rep-ranking-prod'); if (rankP) { const sorted = Object.entries(prodMap).sort((a,b)=>b[1].total-a[1].total).slice(0,15); rankP.innerHTML = sorted.length ? sorted.map(([name,d],i)=>` ${i+1}${esc(name)}${d.qty}${formatMoney(d.total)} `).join('') : 'Sin datos'; } const rankH = document.getElementById('rep-ranking-horas'); if (rankH) { const horas = {}; hoyRes.forEach(r => { horas[r.hora] = (horas[r.hora]||0)+1; }); const sh = Object.entries(horas).sort((a,b)=>b[1]-a[1]); rankH.innerHTML = sh.length ? sh.map(([h,n])=>`${h}:00${n}`).join('') : 'Sin datos'; } const metMap = {}; ventasDia.forEach(v => { if (v.pagos) { Object.entries(v.pagos).forEach(([k,val]) => { if (!val) return; if (!metMap[k]) metMap[k] = { n: 0, total: 0 }; metMap[k].n++; metMap[k].total += Number(val); }); } else { if (!metMap[v.metodo]) metMap[v.metodo] = { n: 0, total: 0 }; metMap[v.metodo].n++; metMap[v.metodo].total += v.total; } }); const metTotal = Object.values(metMap).reduce((s,m) => s + m.total, 0) || 1; const repMet = document.getElementById('rep-metodos'); if (repMet) { const keys = Object.keys(metMap); repMet.innerHTML = keys.length ? keys.map(k => ` ${esc(k)} ${metMap[k].n} ${formatMoney(metMap[k].total)} ${Math.round((metMap[k].total / metTotal) * 100)}% `).join('') : 'Sin ventas en el período'; } const fecha = hasta; document.getElementById('rep-canchas-dia').innerHTML = RESOURCES.map(rec => { const list = hoyRes.filter(r => r.recurso === rec); const ingresos = list.reduce((s,r) => s + (r.valor||0), 0); return ` ${rec} ${list.length} ${list.length} h ${formatMoney(ingresos)} `; }).join(''); // Semana const base = new Date(fecha + 'T12:00:00'); const day = base.getDay(); const diff = day === 0 ? -6 : 1 - day; base.setDate(base.getDate() + diff); const nombres = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom']; let semanaHtml = ''; for (let i = 0; i < 7; i++) { const d = new Date(base); d.setDate(base.getDate() + i); const f = formatDate(d); const delDia = reservas.filter(r => r.fecha === f); const count = (name) => delDia.filter(r => r.recurso === name).length; semanaHtml += ` ${nombres[i]} ${d.getDate()}/${d.getMonth()+1} ${delDia.length} ${count('Cancha 1')} ${count('Cancha 2')} ${count('Cancha 3')} ${count('BBQ')} ${count('Pista RC')} `; } document.getElementById('rep-canchas-semana').innerHTML = semanaHtml; // Cortes const rc = document.getElementById('rep-cortes'); rc.innerHTML = cortesCaja.length ? [...cortesCaja].reverse().map(c => ` ${c.fecha} ${c.hora} ${c.empleado} ${formatMoney(c.total)} `).join('') : 'Aún no hay cierres registrados'; } // ===================== USUARIOS ===================== function renderUsuarios() { const tbody = document.getElementById('usuarios-table'); if (!tbody) return; tbody.innerHTML = USERS.map(u => ` ${u.username} ${u.name} ${u.role} ${u.activo ? 'Activo' : 'Inactivo'} `).join(''); } function editarUsuario(id) { const u = USERS.find(x => x.id === id); if (!u) return; document.getElementById('usr-edit-id').value = id; document.getElementById('usr-username').value = u.username; document.getElementById('usr-name').value = u.name; document.getElementById('usr-pass').value = u.pass; document.getElementById('usr-role').value = u.role; document.getElementById('usr-form-title').textContent = 'Editar usuario'; document.getElementById('btn-cancel-edit-usr').style.display = 'inline-flex'; window.scrollTo({ top: 0, behavior: 'smooth' }); } function cancelarEditUsuario() { document.getElementById('usr-edit-id').value = ''; document.getElementById('usr-username').value = ''; document.getElementById('usr-name').value = ''; document.getElementById('usr-pass').value = ''; document.getElementById('usr-form-title').textContent = 'Nuevo usuario'; document.getElementById('btn-cancel-edit-usr').style.display = 'none'; } function guardarUsuario() { const username = document.getElementById('usr-username').value.trim().toLowerCase(); const name = document.getElementById('usr-name').value.trim(); const pass = document.getElementById('usr-pass').value; const role = document.getElementById('usr-role').value; const editId = document.getElementById('usr-edit-id').value; if (!username || !name || !pass) { alert('Completa usuario, nombre y contraseña'); return; } if (editId) { const u = USERS.find(x => x.id === Number(editId)); if (!u) return; if (USERS.some(x => x.username === username && x.id !== u.id)) { alert('Ese usuario de login ya existe'); return; } u.username = username; u.name = name; u.pass = pass; u.role = role; alert('Usuario actualizado'); } else { if (USERS.some(x => x.username === username)) { alert('Ese usuario de login ya existe'); return; } USERS.push({ id: Date.now(), username, name, role, pass, activo: true }); alert('Usuario creado'); } saveStore(); cancelarEditUsuario(); renderUsuarios(); } function toggleUsuario(id) { if (!requirePerm('usuarios', 'gestionar usuarios')) return; const u = USERS.find(x => x.id === id); if (!u) return; if (u.username === 'admin' && u.activo) { alert('No se puede desactivar el admin principal'); return; } u.activo = !u.activo; saveStore(); renderUsuarios(); } function eliminarUsuario(id) { if (!requirePerm('usuarios', 'gestionar usuarios')) return; const u = USERS.find(x => x.id === id); if (!u) return; if (u.username === 'admin') { alert('No se puede eliminar el admin principal'); return; } if (!confirm(`¿Eliminar al usuario "${u.name}"?`)) return; USERS = USERS.filter(x => x.id !== id); saveStore(); renderUsuarios(); } function renderRecepcion() { const today = formatDate(new Date()); const horaNow = new Date().getHours(); const hoy = reservas.filter(r => r.fecha === today && r.estado !== 'cancelada').sort((a,b)=>a.hora-b.hora); const proximas = hoy.filter(r => r.hora >= horaNow - 1); const cards = document.getElementById('recepcion-cards'); if (cards) { cards.innerHTML = `
Reservas hoy
${hoy.length}
En juego
${hoy.filter(r=>r.estado==='en_juego').length}
Ventas hoy
${formatMoney(ventasDeFecha(today).filter(v=>!v.anulada).reduce((s,v)=>s+v.total,0))}
Lista espera
${listaEspera.filter(e=>e.fecha===today).length}
`; } const tb = document.getElementById('recepcion-table'); if (tb) { tb.innerHTML = (proximas.length ? proximas : hoy).map(r => ` ${r.hora}:00 ${esc(r.recurso)} ${esc(r.cliente)} ${esc(r.estado)} `).join('') || 'Sin reservas'; } } function openBloqueo() { const f = document.getElementById('agenda-date')?.value || formatDate(new Date()); document.getElementById('bl-fecha').value = f; const sel = document.getElementById('bl-hora'); if (sel && !sel.options.length) HOURS.forEach(h => { const o=document.createElement('option'); o.value=h; o.textContent=h+':00'; sel.appendChild(o); }); document.getElementById('modal-bloqueo').classList.add('show'); } function saveBloqueo() { const recurso = document.getElementById('bl-recurso').value; const fecha = document.getElementById('bl-fecha').value; const hora = Number(document.getElementById('bl-hora').value); const motivo = document.getElementById('bl-motivo').value.trim() || 'Bloqueado'; if (reservas.some(r => r.fecha===fecha && r.hora===hora && r.recurso===recurso && r.estado!=='cancelada')) { alert('Hay una reserva en ese horario. Cancélala antes o elige otro.'); return; } bloqueos.push({ id: Date.now(), recurso, fecha, hora, motivo }); audit('bloqueo', 'horario', null, { recurso, fecha, hora, motivo }); saveStore(); closeModal('modal-bloqueo'); renderAgenda(); toast('Horario bloqueado'); } function openTorneo() { document.getElementById('tor-fecha').value = document.getElementById('agenda-date')?.value || formatDate(new Date()); const sel = document.getElementById('tor-hora'); if (sel && !sel.options.length) HOURS.forEach(h => { const o=document.createElement('option'); o.value=h; o.textContent=h+':00'; sel.appendChild(o); }); document.getElementById('modal-torneo').classList.add('show'); } function saveTorneo() { const nombre = document.getElementById('tor-nombre').value.trim() || 'Torneo'; const fecha = document.getElementById('tor-fecha').value; const hora0 = Number(document.getElementById('tor-hora').value); const dur = Number(document.getElementById('tor-duracion').value) || 1; const recursos = [...document.querySelectorAll('#tor-recursos input:checked')].map(i => i.value); if (!recursos.length) { alert('Elige al menos un recurso'); return; } let n = 0; for (let h = hora0; h < hora0 + dur; h++) { if (!HOURS.includes(h)) continue; recursos.forEach(recurso => { if (!bloqueos.some(b => b.fecha===fecha && b.hora===h && b.recurso===recurso)) { bloqueos.push({ id: Date.now()+n++, recurso, fecha, hora: h, motivo: 'Torneo: ' + nombre }); } }); } audit('torneo', 'evento', null, { nombre, fecha, hora0, dur, recursos }); saveStore(); closeModal('modal-torneo'); renderAgenda(); toast('Torneo bloqueado en agenda'); } function addEspera() { const cliente = document.getElementById('espera-cliente').value.trim(); const tel = document.getElementById('espera-tel').value.trim(); const recurso = document.getElementById('espera-recurso').value; const fecha = document.getElementById('espera-fecha').value || formatDate(new Date()); if (!cliente) { alert('Nombre del cliente'); return; } listaEspera.push({ id: Date.now(), cliente, tel, recurso, fecha }); saveStore(); document.getElementById('espera-cliente').value = ''; renderEspera(); toast('Agregado a lista de espera'); } function renderEspera() { const tb = document.getElementById('espera-table'); if (!tb) return; if (!document.getElementById('espera-fecha').value) document.getElementById('espera-fecha').value = formatDate(new Date()); tb.innerHTML = listaEspera.map(e => ` ${esc(e.fecha)}${esc(e.recurso)}${esc(e.cliente)}${esc(e.tel)} `).join('') || 'Vacía'; } function toggleHelp() { showHelp = !showHelp; document.querySelectorAll('.help-box').forEach(el => el.style.display = showHelp ? '' : 'none'); saveStore(); toast(showHelp ? 'Ayuda visible' : 'Ayuda oculta'); } function exportClientesCSV() { const rows = [['Nombre','Telefono','Notas','Tags','Reservas']]; const counts = {}; reservas.forEach(r => { counts[r.cliente]=(counts[r.cliente]||0)+1; }); CLIENTES.forEach(c => rows.push([c.nombre, c.tel||'', c.notas||'', (c.tags||[]).join('|'), counts[c.nombre]||0])); const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob(['\ufeff'+csv],{type:'text/csv'})); a.download = 'clientes-orejona.csv'; a.click(); } function buildPublicGridHtml(fecha) { let html = '
Hora
'; RESOURCES.forEach(r => html += `
${r.replace('Cancha ','C')}
`); HOURS.forEach(h => { html += `
${h}:00
`; RESOURCES.forEach(recurso => { const bl = (bloqueos||[]).find(b => b.fecha === fecha && b.hora === h && b.recurso === recurso); const res = (reservas||[]).find(r => r.fecha === fecha && r.hora === h && r.recurso === recurso && r.estado !== 'cancelada'); if (bl || res) { html += `
${bl ? 'Bloq.' : 'Ocupado'}
`; } else { html += `
Libre
`; } }); }); return html; } function renderPublicAgenda() { try { const today = formatDate(new Date()); const fechaEls = [ document.getElementById('landing-date'), document.getElementById('public-date') ].filter(Boolean); let fecha = today; for (const el of fechaEls) { if (!el.value) el.value = today; } // Prefer landing date if visible const land = document.getElementById('landing-date'); const pub = document.getElementById('public-date'); if (land && land.value) fecha = land.value; else if (pub && pub.value) fecha = pub.value; const html = buildPublicGridHtml(fecha); const cols = `56px repeat(${RESOURCES.length}, minmax(72px, 1fr))`; ['landing-grid', 'public-grid'].forEach(id => { const grid = document.getElementById(id); if (!grid) return; grid.style.gridTemplateColumns = cols; grid.innerHTML = html; }); } catch (err) { console.error('renderPublicAgenda', err); const g = document.getElementById('landing-grid'); if (g) g.innerHTML = '

Error al cargar horarios. Recarga la página.

'; } } let publicSlot = null; function openPublicReserva(recurso, hora, fecha) { publicSlot = { recurso, hora, fecha }; document.getElementById('pub-res-info').textContent = `${recurso} · ${fecha} · ${hora}:00–${hora + 1}:00`; document.getElementById('pub-nombre').value = ''; document.getElementById('pub-tel').value = ''; document.getElementById('pub-notas').value = ''; document.getElementById('modal-public-reserva').classList.add('show'); } function savePublicReserva() { if (!publicSlot) return; const cliente = document.getElementById('pub-nombre').value.trim(); const tel = document.getElementById('pub-tel').value.trim(); const notas = document.getElementById('pub-notas').value.trim(); if (!cliente) { alert('Escribe tu nombre'); return; } if (!tel) { alert('Escribe un teléfono de contacto'); return; } const { recurso, hora, fecha } = publicSlot; if (reservas.some(r => r.fecha === fecha && r.hora === hora && r.recurso === recurso && r.estado !== 'cancelada')) { alert('Ese horario acaba de ocuparse. Elige otro.'); closeModal('modal-public-reserva'); renderPublicAgenda(); return; } const precios = { 'Cancha 1': 80000, 'Cancha 2': 80000, 'Cancha 3': 80000, 'BBQ': 50000, 'Pista RC': 40000 }; const id = Date.now(); reservas.push({ id, recurso, fecha, hora, cliente, tel, valor: precios[recurso] || 80000, estado: 'pendiente', notas: (notas ? notas + ' · ' : '') + 'Solicitud web pública' }); if (!CLIENTES.some(c => c.nombre.toLowerCase() === cliente.toLowerCase())) { CLIENTES.push({ id: Date.now() + 1, nombre: cliente, tel, notas: 'Cliente web', tags: ['web'] }); } saveStore(); closeModal('modal-public-reserva'); renderPublicAgenda(); alert('Solicitud enviada.\n\nQuedó como PENDIENTE. El centro te confirmará por WhatsApp.'); } // Start: mostrar agenda pública try { document.getElementById('app').style.display = 'none'; const pl = document.getElementById('public-landing'); if (pl) { pl.style.display = 'block'; pl.classList.add('show'); } init(); renderPublicAgenda(); } catch (e) { console.error('Init error', e); const pl = document.getElementById('public-landing'); if (pl) pl.innerHTML = '

La Orejona

Error al iniciar. Recarga. Si persiste, avisa al administrador.

'+String(e)+'
'; }