/* ============================================================ ADMIN · información del restaurante + reservas ============================================================ */ function BrandEditor() { const store = useStore(); const [confirm, confirmNode] = useConfirm(); const [f, setF] = useState(structuredClone(store.data.brand)); const set = (k,v) => setF(s => ({...s,[k]:v})); const dirty = JSON.stringify(f) !== JSON.stringify(store.data.brand); const onLogo = async (e) => { const file = e.target.files[0]; if (!file) return; try { const url = await store.fileToDataURL(file, 400, 0.85); set('logo', url); } catch { store.toast('No se pudo cargar el logo','err'); } }; return (

Información del restaurante

Datos de contacto, redes sociales, logo y reservas. Se muestran en la carta pública.

Identidad

set('name',e.target.value)}/> set('tagline',e.target.value)}/> set('website',e.target.value)}/>
{f.logo?:'GC'}
{f.logo&&}

Contacto

set('phone',e.target.value)}/> set('whatsapp',e.target.value)}/>
set('email',e.target.value)}/> set('address',e.target.value)}/> set('hours',e.target.value)}/>

Redes sociales

set('instagram',e.target.value)} placeholder="granchimu"/> set('facebook',e.target.value)} placeholder="granchimu"/> set('tiktok',e.target.value)} placeholder="(opcional)"/>

Reservas & servidor

set('reservationsOn',v)} label="Formulario de reserva activado"/>
set('serverKey',e.target.value)} placeholder="chimu2027"/>

Las claves de cada persona se gestionan en Usuarios. Esta clave es solo la que usa el sitio para guardar en el servidor.

Confirmación de reservas por correo

{(f.emailjs&&f.emailjs.enabled)?'Activo':'Inactivo'}

Activación rápida con EmailJS (sin servidor). Cuando un cliente reserva: recibe un correo de “solicitud recibida — pendiente” y a ti te llega un aviso. Apruebas o rechazas en Reservas y el cliente recibe automáticamente tu respuesta.

set('emailjs',{...(f.emailjs||{}),enabled:v})} label="Activar confirmación por correo"/>
set('emailjs',{...(f.emailjs||{}),publicKey:e.target.value})} placeholder="xxxxxxxxxxxx"/> set('emailjs',{...(f.emailjs||{}),serviceId:e.target.value})} placeholder="service_xxxxx"/> set('emailjs',{...(f.emailjs||{}),templateId:e.target.value})} placeholder="template_xxxxx"/>
Cómo activarlo en 4 pasos:
1. Crea una cuenta gratis en emailjs.com y conecta tu correo (Add Service).
2. Crea un Template con estos campos: To Email = {'{{to_email}}'}, Subject = {'{{subject}}'}, Content = {'{{message}}'}.
3. Copia tu Public Key, Service ID y Template ID y pégalos arriba.
4. Activa el interruptor y pulsa Guardar cambios. ¡Listo!
Alternativa avanzada: servidor PHP (reserva.php) set('reservationEndpoint',e.target.value)} placeholder="https://granchimu.cl/reserva.php"/>

Datos del sistema

Los datos se guardan en este dispositivo/navegador. Exporta un respaldo con regularidad y úsalo para pasar la información a otro equipo.

{confirmNode}
); } function ReservationsView() { const store = useStore(); const [confirm, confirmNode] = useConfirm(); const reservations = store.data.reservations || []; const [filter, setFilter] = useState('todas'); const list = reservations.filter(r => filter==='todas' || r.status===filter); const fmt = (s) => { if(!s) return '—'; const d=new Date(s); return d.toLocaleString('es-CL',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}); }; const statusColor = {nueva:'var(--ocre-deep)',aprobada:'var(--ok)',rechazada:'var(--ink-faint)'}; const decide = (r, approve) => { store.updateReservation(r.id, { status: approve?'aprobada':'rechazada' }); const b = store.data.brand; if (r.email) { if (approve) store.sendMail({ to:r.email, subject:`Tu reserva en ${b.name} fue APROBADA`, message:`Hola ${r.nombre},\n\n¡Buenas noticias! Tu reserva fue APROBADA.\n\n• Fecha y hora: ${(r.fecha||'').replace('T',' ')}\n• Personas: ${r.personas}\n• Local: ${r.local}\n\nTe esperamos.\n${b.name}` }).then(ok=>ok&&store.toast('Correo de aprobación enviado')); else store.sendMail({ to:r.email, subject:`Tu reserva en ${b.name}`, message:`Hola ${r.nombre},\n\nLamentablemente no pudimos confirmar tu reserva para esa fecha/hora. Por favor contáctanos para buscar otra alternativa.\n\n${b.name}` }).then(ok=>ok&&store.toast('Correo de respuesta enviado')); } }; return (

Reservas

Solicitudes recibidas desde la carta. {reservations.filter(r=>r.status==='nueva').length} nuevas.

{['todas','nueva','aprobada','rechazada'].map(s=>( ))}
{list.length===0 ?
No hay reservas {filter!=='todas'?`(${filter})`:''} por ahora.
: (
{list.map(r=>(
{r.nombre} {r.status}
{r.fecha?r.fecha.replace('T',' · '):'—'} {r.personas} pers. {r.local} {r.telefono} {r.email&& {r.email}}
{r.comentario&&
“{r.comentario}”
}
Recibida {fmt(r.createdAt)}
{r.status!=='aprobada'&&} {r.status!=='rechazada'&&}
))}
)} {confirmNode}
); } function UserModal({ user, onClose }) { const store = useStore(); const editing = !!user; const [f, setF] = useState(user ? { ...user, password:'' } : { name:'', username:'', password:'', role:'editor', active:true }); const set = (k,v) => setF(s => ({...s,[k]:v})); const save = () => { if (editing) { const patch = { name:f.name, username:f.username, role:f.role }; if (f.password) patch.password = f.password; store.updateUser(user.id, patch); } else { store.addUser(f); } onClose(); }; const valid = f.name.trim() && f.username.trim() && (editing || f.password); return ( }> set('name',e.target.value)} placeholder="Nombre y apellido"/>
set('username',e.target.value)} placeholder="ej. cocina"/> set('password',e.target.value)} placeholder="••••••"/>
Permisos del rol: {(store.ROLES[f.role]||store.ROLES.editor).perms.join(' · ')}
); } function UsersView() { const store = useStore(); const [confirm, confirmNode] = useConfirm(); const users = store.data.users || []; const [modal, setModal] = useState(null); const activeAdmins = users.filter(u => u.role==='admin' && u.active).length; const roleColor = { admin:'var(--clay)', editor:'var(--coffee)', reservas:'var(--ocre-deep)', caja:'var(--green)' }; return (

Usuarios y perfiles

Crea cuentas para tu equipo y define qué puede ver y editar cada perfil.

{users.map(u => { const isMe = store.currentUser && store.currentUser.id === u.id; const role = store.ROLES[u.role] || store.ROLES.editor; return (
{(u.name||'?').charAt(0).toUpperCase()}
{u.name} {isMe && }
@{u.username}
{role.label} {u.active?'Activo':'Inactivo'}
); })}
Perfiles disponibles:
{Object.entries(store.ROLES).map(([k,v])=>(
{v.label}: {v.perms.join(', ')}
))}
{modal && setModal(null)} />} {confirmNode}
); } Object.assign(window, { BrandEditor, ReservationsView, UsersView, UserModal });