// ===== RIVAMAR Group — Panel de Administración shell (v1.1) ===== const { useState, useEffect } = React; function KpiCard({ label, value, sub, accent, icon }) { return (
{value}
{label}
{icon}
{sub &&
{sub}
}
); } function AdminLogin({ onLogin, users }) { const [u, setU] = useState('admin@rivamar.cl'); const [p, setP] = useState(''); const [err, setErr] = useState(''); const submit = (e) => { e.preventDefault(); const lista = users || (window.storeGet ? window.storeGet().users : []); const found = (lista || []).find((x) => x.correo.toLowerCase() === u.trim().toLowerCase()); if (found && found.estado === 'activo' && found.pass && found.pass === p) { setErr(''); onLogin(); } else if (found && found.pass && found.pass !== p) setErr('Contraseña incorrecta.'); else if (found && found.estado !== 'activo') setErr('Usuario pendiente de activación.'); else if (found && !found.pass) setErr('Este usuario no tiene contraseña asignada. Contacte al administrador.'); else setErr('Correo no registrado.'); }; return (

Panel de Administración

admin.rivamar.cl

setU(e.target.value)} /> setP(e.target.value)} placeholder="Tu contraseña" /> {err &&

{err}

} Ingresar al panel

Privacidad de datos: el acceso es exclusivo para personal autorizado de RIVAMAR Group. Toda la información de clientes y reservas se trata conforme a la Ley N° 19.628 sobre protección de la vida privada. El uso indebido o acceso no autorizado queda registrado y es responsabilidad del titular de la cuenta.

Demo: admin@rivamar.cl · rivamar2026 — Supabase Auth (JWT) en producción.

); } // ---------- Dashboard ---------- function ProximasVencer({ reservas }) { const hoy = new Date(); hoy.setHours(0, 0, 0, 0); const limite = new Date(hoy); limite.setDate(limite.getDate() + 2); const prox = reservas.filter((r) => { if (r.estado === 'cancelada' || r.estado === 'eliminada') return false; if (!r.fecha) return false; const f = new Date(r.fecha + 'T00:00:00'); return f >= hoy && f <= limite; }).sort((a, b) => (a.fecha + a.hora).localeCompare(b.fecha + b.hora)); const site = (typeof window !== 'undefined' && window.SITE) || {}; const diaTxt = (r) => { const f = new Date(r.fecha + 'T00:00:00'); const d = Math.round((f - new Date(new Date().setHours(0,0,0,0))) / 86400000); return d === 0 ? 'HOY' : d === 1 ? 'Mañana' : r.fecha; }; const recordarWA = (r) => { const nom = LOCAL_MAP[r.local] ? LOCAL_MAP[r.local].nombre : r.local; const msg = `¡Hola ${r.nombre}! Le recordamos su reserva ${r.id} en ${(site.brand_nombre || 'RIVAMAR')} — ${nom} para ${r.fecha} a las ${r.hora} hrs (${r.pax} pax). ¡Le esperamos! Si necesita reagendar, responda este mensaje.`; const tel = (r.telefono || '').replace(/[^0-9]/g, ''); const num = tel.startsWith('56') ? tel : ('56' + tel); window.open(`https://wa.me/${num}?text=${encodeURIComponent(msg)}`, '_blank'); }; const recordarMail = (r) => { const nom = LOCAL_MAP[r.local] ? LOCAL_MAP[r.local].nombre : r.local; const body = `Hola ${r.nombre},%0D%0A%0D%0ALe recordamos su reserva ${r.id} en ${nom} para el ${r.fecha} a las ${r.hora} hrs (${r.pax} personas).%0D%0A%0D%0A¡Le esperamos!%0D%0A${(site.brand_nombre || 'RIVAMAR')} — Antofagasta`; window.open(`mailto:${r.correo}?subject=${encodeURIComponent('Recordatorio de su reserva ' + r.id + ' — RIVAMAR')}&body=${body}`, '_blank'); }; if (prox.length === 0) return null; return (

Reservas próximas a vencer

({prox.length} en las próximas 48 h — recordar al cliente)
{prox.map((r) => (
{diaTxt(r)} {r.nombre} {r.hora} hrs

{LOCAL_MAP[r.local] ? LOCAL_MAP[r.local].nombre : r.local} · {r.pax} pax · {r.telefono}

))}
); } function Dashboard({ reservas, onGoReservas }) { const total = reservas.length; const pendientes = reservas.filter((r) => r.estado === 'pendiente').length; const confirmadas = reservas.filter((r) => r.estado === 'confirmada').length; const exponor = reservas.filter((r) => r.exponor).length; const hoyStr = new Date().toISOString().slice(0, 10); const hoy = reservas.filter((r) => r.fecha === hoyStr).length; const activas = reservas.filter((r) => r.estado !== 'cancelada' && r.estado !== 'eliminada'); const comensales = activas.reduce((s, r) => s + (Number(r.pax) || 0), 0); const comensalesHoy = reservas.filter((r) => r.fecha === hoyStr && r.estado !== 'cancelada' && r.estado !== 'eliminada').reduce((s, r) => s + (Number(r.pax) || 0), 0); const recientes = reservas.slice(0, 5); return (
} /> } /> } /> } /> } /> } />

Reservas recientes

{recientes.map((r) => (
{r.id}
{r.nombre}
{LOCAL_MAP[r.local].nombre} · {r.fecha} · {r.pax} pax
{r.exponor && }
))}
); } // ---------- Gestión de reservas ---------- function GestionReservas({ reservas, setReservas }) { const [query, setQuery] = useState(''); const [filterEstado, setFilterEstado] = useState('todos'); const [filterLocal, setFilterLocal] = useState('todos'); const [vista, setVista] = useState('lista'); const [editing, setEditing] = useState(null); const [toast, setToast] = useState(''); const flash = (msg) => { setToast(msg); setTimeout(() => setToast(''), 2600); }; const filtered = reservas.filter((r) => { if (filterEstado === 'todos') { if (r.estado === 'eliminada') return false; } else if (r.estado !== filterEstado) return false; if (filterLocal !== 'todos' && r.local !== filterLocal) return false; const q = query.trim().toLowerCase(); if (q && !(`${r.nombre} ${r.empresa} ${r.id} ${r.correo} ${r.telefono}`.toLowerCase().includes(q))) return false; return true; }); const setEstado = (id, estado) => setReservas((prev) => prev.map((r) => r.id === id ? { ...r, estado } : r)); const eliminar = (id) => { if (confirm('¿Eliminar esta reserva? Se conservará el código correlativo y quedará marcada como "Eliminada".')) { setEstado(id, 'eliminada'); flash('Reserva eliminada (registro conservado)'); } }; const saveEdit = (r) => { setReservas((prev) => { const existe = prev.some((x) => x.id === r.id); if (existe) return prev.map((x) => x.id === r.id ? r : x); return [{ ...r }, ...prev]; }); try { if (r._nueva) { delete r._nueva; window.pushToInbox && window.pushToInbox('reserva', r); } } catch (e) {} setEditing(null); flash('Reserva guardada'); }; const nuevaReserva = () => { const base = { nombre: '', empresa: '', correo: '', telefono: '', local: 'rivamar', fecha: '', hora: '', pax: 2, exponor: false, credencial: '', comentarios: '', estado: 'pendiente', _nueva: true }; // Folio provisional inmediato; se reemplaza por el correlativo central del servidor al resolver. setEditing({ ...base, id: 'RV-2026-…' }); const pedir = window.nextReservaIdAsync ? window.nextReservaIdAsync() : Promise.resolve({ id: window.nextReservaId() }); pedir.then((res) => setEditing((cur) => (cur && cur._nueva) ? { ...cur, id: res.id } : cur)); }; const exportCSV = () => { const headers = ['id', 'nombre_completo', 'empresa', 'correo', 'telefono', 'local', 'fecha', 'hora', 'numero_personas', 'es_exponor', 'credencial', 'estado', 'comentarios']; const rows = filtered.map((r) => [r.id, r.nombre, r.empresa, r.correo, r.telefono, r.local, r.fecha, r.hora, r.pax, r.exponor, r.credencial || '', r.estado, r.comentarios || '']); const csv = [headers.join(','), ...rows.map((row) => row.map((c) => `"${String(c ?? '').replace(/"/g, '""')}"`).join(','))].join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `reservas_rivamar_${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); flash(`CSV exportado · ${filtered.length} registros`); }; const site = (typeof window !== 'undefined' && window.SITE) || {}; // Comprobante PDF de la reserva (imprimible / guardar como PDF) const generarPDF = (r) => { const localNom = LOCAL_MAP[r.local] ? LOCAL_MAP[r.local].nombre : r.local; const estadoTxt = { pendiente: 'Pendiente', confirmada: 'Confirmada', cancelada: 'Cancelada' }[r.estado] || r.estado; const fila = (k, v) => `${k}${v}`; const html = `Reserva ${r.id}

${(site.brand_nombre || 'RIVAMAR')} ${(site.brand_sub || 'GROUP')}

Comprobante de reserva · Antofagasta, Chile

Reserva ${estadoTxt}
${fila('Código', r.id)} ${fila('Local', localNom)} ${fila('A nombre de', r.nombre)} ${r.empresa ? fila('Empresa', r.empresa) : ''} ${fila('Fecha y hora', r.fecha + ' · ' + r.hora + ' hrs')} ${fila('N° de personas', r.pax + ' pax')} ${fila('Teléfono', r.telefono)} ${fila('Correo', r.correo)} ${r.comentarios ? fila('Comentarios', r.comentarios) : ''}
${r.exponor ? '
★ Beneficio EXPONOR 2026 — 15% de descuento aplicado. Presente su credencial al solicitar la cuenta.
' : ''}

${site.footer_copy || '© 2026 RIVAMAR Group — Antofagasta, Chile · MAEDY GROUP'}
Documento generado desde el panel de administración.