const { Card, Button, Badge, Field, Input, Avatar, StatBlock, ScheduleRow, Dialog, Toast } = window.PilatesLifeStudioDesignSystem_f49721; const TIPO_LABEL_CUENTA = { grupal: 'Clase grupal', privada: 'Clase privada', valoracion: 'Valoración' }; const ESTADO_BADGE = { confirmada: 'info', completada: 'success', cancelada: 'neutral' }; const ESTADO_LABEL = { confirmada: 'Confirmada', completada: 'Asistida', cancelada: 'Cancelada' }; const TABS_CUENTA = [ { id: 'resumen', label: 'Resumen' }, { id: 'reservas', label: 'Mis reservas' }, { id: 'plan', label: 'Mi tiquetera' }, { id: 'datos', label: 'Mis datos' }, ]; // Mismo criterio que motivo_no_cancelable() en reservas/lib/helpers.php: solo entre // 8:00 am y 9:00 pm, y con al menos 6 horas de anticipación a la clase. Es una // verificación de UI (para no mostrar un botón que el servidor va a rechazar) — la // regla real que manda siempre es la del backend. function puedeCancelarAhora(reserva) { const ahora = new Date(); const minutosDelDia = ahora.getHours() * 60 + ahora.getMinutes(); if (minutosDelDia < 8 * 60 || minutosDelDia >= 21 * 60) return false; const inicioClase = new Date(`${reserva.fecha}T${reserva.hora}:00`); const limite = new Date(inicioClase.getTime() - 6 * 60 * 60 * 1000); return ahora <= limite; } function formatearMiembroDesde(fechaSql) { const d = new Date(String(fechaSql || '').replace(' ', 'T')); if (Number.isNaN(d.getTime())) return ''; const texto = d.toLocaleDateString('es-CO', { month: 'long', year: 'numeric' }); return 'Cliente desde ' + texto; } function formatearFechaVencimiento(fechaSql) { const d = new Date(String(fechaSql || '').replace(' ', 'T')); if (Number.isNaN(d.getTime())) return ''; return d.toLocaleDateString('es-CO', { day: 'numeric', month: 'long', year: 'numeric' }); } function MiCuentaPage({ sesion, onNavigate, onCambio, onLogout }) { const cliente = sesion.cliente; const proximas = sesion.proximas_reservas || []; const historial = sesion.historial_reservas || []; const movimientos = sesion.movimientos || []; const listaEspera = sesion.lista_espera || []; const creditosPorVencer = sesion.creditos_por_vencer || []; const [tab, setTab] = React.useState('resumen'); const [reservaACancelar, setReservaACancelar] = React.useState(null); const [cancelando, setCancelando] = React.useState(false); const [saliendoEsperaId, setSaliendoEsperaId] = React.useState(null); const [toast, setToast] = React.useState(null); const [nombre, setNombre] = React.useState(cliente.nombre); const [telefonoLocal, setTelefonoLocal] = React.useState(telefonoSinIndicativo(cliente.telefono)); const [correo, setCorreo] = React.useState(cliente.correo || ''); const [errorDatos, setErrorDatos] = React.useState(''); const [guardandoDatos, setGuardandoDatos] = React.useState(false); const [passwordActual, setPasswordActual] = React.useState(''); const [passwordNueva, setPasswordNueva] = React.useState(''); const [passwordConfirmacion, setPasswordConfirmacion] = React.useState(''); const [errorPassword, setErrorPassword] = React.useState(''); const [cambiandoPassword, setCambiandoPassword] = React.useState(false); React.useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 4000); return () => clearTimeout(t); }, [toast]); const confirmarCancelacion = async () => { if (!reservaACancelar) return; setCancelando(true); try { const datos = await cancelarReservaPropia(reservaACancelar.id); setToast({ tone: 'success', message: `Reserva cancelada · te quedan ${datos.saldo_tiquetera} tiquete(s)` }); setReservaACancelar(null); onCambio(); } catch (err) { setToast({ tone: 'danger', message: err.message }); } finally { setCancelando(false); } }; const salirDeEspera = async (esperaId) => { setSaliendoEsperaId(esperaId); try { await salirListaEspera(esperaId); setToast({ tone: 'success', message: 'Saliste de la lista de espera.' }); onCambio(); } catch (err) { setToast({ tone: 'danger', message: err.message }); } finally { setSaliendoEsperaId(null); } }; const guardarDatos = async (e) => { e.preventDefault(); setErrorDatos(''); if (!nombre.trim()) return setErrorDatos('Escribe tu nombre.'); if (!telefonoLocalValido(telefonoLocal)) return setErrorDatos('Ingresa un teléfono de 10 dígitos, sin el indicativo.'); if (correo.trim() && !/^\S+@\S+\.\S+$/.test(correo.trim())) return setErrorDatos('El correo no es válido.'); setGuardandoDatos(true); try { await actualizarDatosCliente({ nombre: nombre.trim(), telefono: telefonoE164(telefonoLocal), correo: correo.trim() }); setToast({ tone: 'success', message: 'Tus datos quedaron actualizados.' }); onCambio(); } catch (err) { setErrorDatos(err.message); } finally { setGuardandoDatos(false); } }; const cambiarPassword = async (e) => { e.preventDefault(); setErrorPassword(''); if (passwordNueva.length < 8) return setErrorPassword('La nueva contraseña debe tener al menos 8 caracteres.'); if (passwordNueva !== passwordConfirmacion) return setErrorPassword('Las contraseñas nuevas no coinciden.'); setCambiandoPassword(true); try { await cambiarPasswordCliente({ passwordActual, passwordNueva, passwordConfirmacion }); setToast({ tone: 'success', message: 'Tu contraseña quedó actualizada.' }); setPasswordActual(''); setPasswordNueva(''); setPasswordConfirmacion(''); } catch (err) { setErrorPassword(err.message); } finally { setCambiandoPassword(false); } }; return (
Cuenta

Hola, {cliente.nombre.split(' ')[0]}

{cliente.nombre}
{formatearMiembroDesde(cliente.creado_en)}
{tab === 'resumen' && (
Tus próximas clases

Se puede cancelar entre las 8:00 am y las 9:00 pm, con al menos 6 horas de anticipación.

{proximas.map(r => ( setReservaACancelar(r) : undefined} /> ))} {proximas.length === 0 && (

No tienes clases próximas todavía.

)}
{listaEspera.length > 0 && (
Tu lista de espera

Te avisamos por WhatsApp si se libera un cupo en alguno de estos horarios — es por orden de llegada.

{listaEspera.map(le => (
{le.fecha} {TIPO_LABEL_CUENTA[le.tipo] || le.tipo} · {le.etiqueta} {le.estado === 'notificado' ? '¡Se liberó un cupo!' : 'En espera'}
))}
)}
)} {tab === 'reservas' && (
Historial de reservas
{historial.map(r => (
{r.fecha} {TIPO_LABEL_CUENTA[r.tipo] || r.tipo} · {r.etiqueta} {ESTADO_LABEL[r.estado] || r.estado}
))} {historial.length === 0 && (

Sin reservas todavía.

)}
)} {tab === 'plan' && (

Cada clase reservada descuenta un crédito; cancelar a tiempo (hasta 6 horas antes, entre las 8:00 am y las 9:00 pm) lo devuelve. Cada carga de créditos vale por 30 días desde que se hace. Para cargar más créditos, paga en efectivo o por transferencia a {CONTACT.bank} y avísanos por WhatsApp.

{creditosPorVencer.length > 0 && (
{creditosPorVencer.map((v, i) => (

{v.cantidad} {v.cantidad === 1 ? 'crédito vence' : 'créditos vencen'} el {formatearFechaVencimiento(v.fecha)}

))}
)}
Movimientos de tu tiquetera
{movimientos.map((m, i) => (
{m.creado_en} · {m.nota || m.tipo} 0 ? 'var(--moss)' : 'var(--coral)' }}>{m.cantidad > 0 ? '+' : ''}{m.cantidad}
))} {movimientos.length === 0 && (

Sin movimientos todavía.

)}
)} {tab === 'datos' && (
Tus datos
setNombre(e.target.value)} /> setTelefonoLocal(e.target.value)} /> setCorreo(e.target.value)} />
{errorDatos &&

{errorDatos}

}
Contraseña
setPasswordActual(e.target.value)} /> setPasswordNueva(e.target.value)} /> setPasswordConfirmacion(e.target.value)} />
{errorPassword &&

{errorPassword}

}
Recordatorio

Las cancelaciones deben hacerse entre las 8:00 am y las 9:00 pm, con al menos 6 horas de anticipación a la clase.

)}
(cancelando ? null : setReservaACancelar(null))} footer={<> }> {reservaACancelar && `${TIPO_LABEL_CUENTA[reservaACancelar.tipo] || reservaACancelar.tipo} · ${reservaACancelar.fecha} · ${reservaACancelar.etiqueta}`}
El crédito se devuelve a tu tiquetera.
{toast && (
setToast(null)} />
)}
); } Object.assign(window, { MiCuentaPage });