// ===== RIVAMAR Group — Panel de Administración shell (v1.1) =====
const { useState, useEffect } = React;
function KpiCard({ label, value, sub, accent, icon }) {
return (
);
}
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 (
);
}
// ---------- 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}
R
${(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.
' : ''}