// 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'}
);
})}
)}
{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
)}
| # | Calificación |
Habilidad |
Duración | Fecha |
{(modalInfo?.resultados || []).map((r, i) => (
| {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)} |
))}
{(!modalInfo?.resultados || modalInfo.resultados.length === 0) && (
| 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) => (
))}
)}
| Modo | Calificación | Duración | Fecha |
{intentos.map(i => (
|
{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
| Fecha | Concepto | Método | Monto | Estado | |
{COMPRAS_DEMO.map((c) => (
| {c.fecha} |
{c.concepto} |
{c.metodo} |
{window.formatPrice(c.monto)} |
{c.estado} |
|
))}
);
}
window.DashHeader = function DashHeader({ navigate, store }) {
return (
);
};