// DASHBOARD CLIENTE window.Dashboard = function Dashboard({ navigate, store }) { const BASE = window.API_BASE || '/simuladordocente/api'; const { NIVELES } = window.SD_SEED; const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const [tab, setTab] = React.useState('materias'); const [intentosData, setIntentosData] = React.useState(null); // Cargar contadores de intentos cuando hay sesión real React.useEffect(() => { const tok = localStorage.getItem('sd_token'); if (!tok || !store.session) return; fetch(`${BASE}/me/intentos`, { headers: { Authorization: `Bearer ${tok}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (d) setIntentosData(d); }) .catch(() => {}); }, [store.session?.sub]); if (!store.session) { navigate('/login'); return null; } return (
Mi panel

Hola, {store.session.nombre.split(' ')[0]} 👋

Practica con simuladores muestra (gratis) o desbloquea simuladores completos por materia.

{store.premiumIds.length}simuladores premium
{tab === 'materias' && (
{NIVELES.map((n) => { const pruebas = PRUEBAS.filter(p => (p.nivel || p.nivel_id) === n.id); return (

{n.nombre}

{pruebas.length} {pruebas.length === 1 ? 'prueba' : 'pruebas'}
{pruebas.map(p => ( ))}
); })}
)} {tab === 'resultados' && ( )} {tab === 'compras' && }
); }; function PruebaCard({ prueba, store, navigate, intentosInfo, limiteMuestra = 5, limiteCompleto = 3 }) { const isPremium = store.hasPremium(prueba.id); const inCart = store.cart.includes(prueba.id); const tieneMuestra = !!prueba.muestra; const tieneCompleto = !!prueba.completo; const precio = prueba.precio || prueba.precio_mxn || 149; // Datos de muestra const muestraInfo = intentosInfo?.muestra; const intentosM = muestraInfo?.intentos ?? 0; const bonusM = muestraInfo?.bonus ?? 0; const limEfM = limiteMuestra + bonusM; const agotados = !isPremium && tieneMuestra && intentosM >= limEfM; // Datos de completo const completoInfo = intentosInfo?.completo; const intentosC = completoInfo?.intentos ?? 0; const bonusC = completoInfo?.bonus ?? 0; const limEfC = limiteCompleto + bonusC; const completoAgotado = isPremium && tieneCompleto && intentosC >= limEfC; const tieneHistorial = (muestraInfo?.intentos ?? 0) > 0 || (completoInfo?.intentos ?? 0) > 0; const [modal, setModal] = React.useState(false); const [modalTab, setModalTab] = React.useState('muestra'); // Pill de estado let pill; if (isPremium) pill = ★ Premium; else if (tieneMuestra) pill = Muestra gratis; else pill = Próximamente; const fmtDur = (s) => !s ? '—' : s < 60 ? `${s}s` : `${Math.floor(s/60)}m ${s%60}s`; const fmtFecha = (d) => new Date(d).toLocaleDateString('es-MX', { day:'numeric', month:'short' }); const scoreColor = (v) => v >= 70 ? 'var(--green)' : v >= 50 ? 'var(--amber)' : 'var(--red)'; // Qué info/límite mostrar en el modal según el tab activo const modalInfo = modalTab === 'completo' ? completoInfo : muestraInfo; const modalLimit = modalTab === 'completo' ? limEfC : limEfM; const modalUsed = modalTab === 'completo' ? intentosC : intentosM; return ( <>
{prueba.icono || '📘'}
{pill}

{prueba.materiaNombre || prueba.nombre.replace(/^[^·]+·\s*/, '')}

{prueba.preguntas} preguntas
{/* ── Contador de intentos ── */} {tieneHistorial && ( )}
{tieneMuestra && !agotados ? ( ) : tieneMuestra && agotados ? ( ) : ( )} {isPremium && !completoAgotado ? ( ) : isPremium && completoAgotado ? ( ) : !tieneCompleto ? ( ) : inCart ? ( ) : ( )}
{/* ── Mini modal de resultados ── */} {modal && (
setModal(false)}>
e.stopPropagation()} style={{ maxWidth: 440 }}>

{prueba.icono} {prueba.materiaNombre || prueba.nombre.replace(/^[^·]+·\s*/, '')}

Historial de intentos
{/* Tabs muestra / completo */} {(muestraInfo || completoInfo) && (
{muestraInfo && ( )} {completoInfo && ( )}
)} {/* Resumen */}
{modalUsed}/{modalLimit}
intentos usados
{modalInfo?.calificacion_max != null && (
{modalInfo.calificacion_max.toFixed(1)}%
mejor calificación
)}
{(modalInfo?.resultados || []).map((r, i) => ( ))} {(!modalInfo?.resultados || modalInfo.resultados.length === 0) && ( )}
#Calificación Habilidad DuraciónFecha
{i + 1} {r.calificacion != null ? {r.calificacion.toFixed(1)}% : } {r.analisis_pct != null ? 🟠 {r.analisis_pct.toFixed(1)}% : r.comprension_pct != null ? 🟢 {r.comprension_pct.toFixed(1)}% : } {fmtDur(r.duracion_seg)} {fmtFecha(r.creado_en)}
Sin resultados registrados
{!isPremium && tieneCompleto && (
¿Listo para el simulador completo?
Pago único {window.formatPrice(precio)}.
)}
)} ); } // ── Mis Resultados ───────────────────────────────────────────── function ResultadosTab({ store, navigate, limiteMuestra = 5, limiteCompleto = 3 }) { const BASE = window.API_BASE || '/simuladordocente/api'; const [resultados, setResultados] = React.useState([]); const [loading, setLoading] = React.useState(true); React.useEffect(() => { const tok = localStorage.getItem('sd_token'); if (!tok) { setLoading(false); return; } fetch(`${BASE}/me/resultados`, { headers: { Authorization: `Bearer ${tok}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (d?.resultados) setResultados(d.resultados); setLoading(false); }) .catch(() => setLoading(false)); }, []); if (loading) return
Cargando…
; if (resultados.length === 0) { return (
📊

Sin resultados todavía

Completa una muestra o simulador completo para ver tu historial aquí.

); } // Agrupar por prueba const porPrueba = {}; resultados.forEach(r => { if (!porPrueba[r.prueba_id]) porPrueba[r.prueba_id] = { nombre: r.prueba_nombre, icono: r.icono, intentos: [] }; porPrueba[r.prueba_id].intentos.push(r); }); const fmtDur = (seg) => { if (!seg) return '—'; if (seg < 60) return `${seg}s`; return `${Math.floor(seg/60)}m ${seg%60}s`; }; const fmtFecha = (d) => new Date(d).toLocaleDateString('es-MX', { day:'numeric', month:'short', year:'numeric' }); return (
{Object.entries(porPrueba).map(([pruebaId, { nombre, icono, intentos }]) => { const isPremium = store.hasPremium(pruebaId); const muestras = intentos.filter(i => i.modo === 'muestra'); const completos = intentos.filter(i => i.modo === 'completo'); const mejorCalif = intentos .map(i => i.calificacion).filter(Boolean) .reduce((best, c) => c > best ? c : best, 0); return (
{icono || '📘'}

{nombre}

{muestras.length} intento{muestras.length!==1?'s':''} de muestra {!isPremium && ` · ${Math.max(0, limiteMuestra - muestras.length)} restante${limiteMuestra-muestras.length!==1?'s':''}`} {completos.length > 0 && ` · ${completos.length} simulador${completos.length!==1?'es':''} completo${completos.length!==1?'s':''}`}
{mejorCalif > 0 && ( Mejor: {mejorCalif.toFixed(1)}% )} {!isPremium && muestras.length >= limiteMuestra && ( )}
{/* Barra de intentos muestra para no-premium */} {!isPremium && (
Intentos de muestra {muestras.length} / {limiteMuestra} usados
{Array.from({ length: limiteMuestra }).map((_, i) => (
))}
)} {intentos.map(i => ( ))}
ModoCalificaciónDuraciónFecha
{i.modo === 'completo' ? ★ Completo : Muestra} {i.calificacion != null ? `${i.calificacion.toFixed(1)}%` : } {fmtDur(i.duracion_seg)} {fmtFecha(i.creado_en)}
); })}
); } function ComprasTable({ navigate }) { const { COMPRAS_DEMO } = window.SD_SEED; return (

Historial de compras

{COMPRAS_DEMO.map((c) => ( ))}
FechaConceptoMétodoMontoEstado
{c.fecha} {c.concepto} {c.metodo} {window.formatPrice(c.monto)} {c.estado}
); } window.DashHeader = function DashHeader({ navigate, store }) { return (
{e.preventDefault();navigate('/');}}>
{store.cart.length > 0 && ( )}
{ if (confirm('¿Cerrar sesión?')) { store.logout(); navigate('/'); } }}>
{store.session.nombre.split(' ').map(s=>s[0]).slice(0,2).join('')}
{store.session.nombre}
{store.session.email}
); };