// SIMULADOR LAUNCHER β€” carga HTML en iframe, gestiona lΓ­mite de intentos y guarda resultados window.SimuladorLauncher = function SimuladorLauncher({ navigate, store, pruebaId, modo }) { const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const prueba = PRUEBAS.find(p => p.id === pruebaId); const isCompleto = modo === 'completo'; if (!store.session) { navigate('/login'); return null; } if (!prueba) { return (

Prueba no encontrada

); } if (isCompleto && !store.hasPremium(pruebaId)) { return (
{prueba.icono || 'πŸ”’'}

Simulador completo β€” acceso premium

Desbloquea {prueba.nombre} con un pago ΓΊnico de{' '} {window.formatPrice(prueba.precio || prueba.precio_mxn)}.

); } return ; }; // ── Frame: pide URL al API, respeta lΓ­mite, inyecta monitor ── function SimuladorFrame({ prueba, modo, isCompleto, navigate, store }) { const BASE = window.API_BASE || '/simuladordocente/api'; const [iframeUrl, setIframeUrl] = React.useState(null); const [resultadoId, setResultadoId] = React.useState(null); const [intentosInfo, setIntentosInfo] = React.useState(null); // { intentos_usados, limite } const [limiteMsg, setLimiteMsg] = React.useState(null); // objeto si lΓ­mite alcanzado const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(true); const iframeRef = React.useRef(null); const lsSnapshot = React.useRef({}); const startTime = React.useRef(Date.now()); const savedRef = React.useRef(null); // null | 'noScore' | 'withScore' // ── 1. Solicitar URL al API ─────────────────────────────── React.useEffect(() => { const tok = localStorage.getItem('sd_token'); if (!tok) { setLoading(false); return; } fetch(`${BASE}/pruebas/${prueba.id}/abrir/${modo}`, { headers: { Authorization: `Bearer ${tok}` } }) .then(r => r.json().then(d => ({ ok: r.ok, d }))) .then(({ ok, d }) => { if (!ok) { if (d.error === 'limite_alcanzado') { setLimiteMsg(d); } else { setError(d.error || 'No se pudo cargar'); } } else { setIframeUrl(d.url); setResultadoId(d.resultado_id); setIntentosInfo({ intentos_usados: d.intentos_usados, limite: d.limite }); startTime.current = Date.now(); } setLoading(false); }) .catch(() => { setError('Error de conexiΓ³n'); setLoading(false); }); }, [prueba.id, modo]); // ── 2. Inyectar monitor cuando el iframe carga ──────────── const onIframeLoad = () => { // Snapshot inicial de localStorage const snap = {}; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); snap[k] = localStorage.getItem(k); } lsSnapshot.current = snap; try { const doc = iframeRef.current?.contentDocument; if (!doc) return; const s = doc.createElement('script'); // Monitor que: // 1. Se engancha en mostrarResultados() si existe (biologΓ­a y similares) // 2. Copia localStorage + variables globales de score // 3. Vigila clicks en botones de finalizar s.textContent = `(function(){ function capturar(){ var ls={}; for(var i=0;i { if (!iframeUrl) return; const handler = () => { // El iframe escribiΓ³ localStorage β†’ pedir captura iframeRef.current?.contentWindow?.postMessage({ type: 'request_capture' }, '*'); }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); }, [iframeUrl]); // ── 3b. Recibir resultado del iframe ────────────────────── React.useEffect(() => { const handler = (e) => { if (e.data?.type !== 'sim_resultado') return; guardarResultado(e.data.datos, e.data.extras); }; window.addEventListener('message', handler); return () => window.removeEventListener('message', handler); }, [resultadoId]); // ── 4. Guardar / actualizar resultado en API ────────────── const guardarResultado = React.useCallback((datosIframe, extras) => { if (!resultadoId) return; const hasScore = extras?.pct != null; if (savedRef.current === 'withScore') return; if (savedRef.current === 'noScore' && !hasScore) return; const tok = localStorage.getItem('sd_token'); if (!tok) return; const changed = {}; if (datosIframe) { Object.keys(datosIframe).forEach(k => { if (lsSnapshot.current[k] !== datosIframe[k]) changed[k] = datosIframe[k]; }); } if (extras && Object.keys(extras).length) changed._sim_extras = extras; // pct ya viene 0-100 del DOM (.score-sub) const calificacion = hasScore ? parseFloat(extras.pct) : null; fetch(`${BASE}/resultados/${resultadoId}`, { method: 'PUT', headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ calificacion, datos: Object.keys(changed).length ? changed : null, duracion_seg: Math.round((Date.now() - startTime.current) / 1000), }), }).catch(() => {}); savedRef.current = calificacion !== null ? 'withScore' : 'noScore'; }, [resultadoId]); const salir = () => { // Si no se guardΓ³ con score, registrar al menos la duraciΓ³n if (savedRef.current !== 'withScore' && resultadoId) { const tok = localStorage.getItem('sd_token'); if (tok) { fetch(`${BASE}/resultados/${resultadoId}`, { method: 'PUT', headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ duracion_seg: Math.round((Date.now() - startTime.current) / 1000) }), keepalive: true, }).catch(() => {}); } } navigate('/dashboard'); }; // ── Pantalla: lΓ­mite alcanzado ──────────────────────────── if (limiteMsg) { const esCompleto = limiteMsg.modo === 'completo'; return (
πŸ”’

{esCompleto ? 'Has agotado tus intentos del simulador completo' : 'Has usado todos tus intentos gratuitos'}

{esCompleto ? <>Usaste tus {limiteMsg.limite} intentos del simulador completo de{' '} {prueba.nombre}. Contacta al administrador para obtener mΓ‘s intentos. : <>Usaste tus {limiteMsg.limite} intentos de muestra para{' '} {prueba.nombre}. Desbloquea el simulador completo para acceso ampliado.}

{Array.from({length: limiteMsg.limite}).map((_,i) => (
))}
{!esCompleto && ( )}
); } // ── Render principal ────────────────────────────────────── return (
{prueba.icono} {prueba.nombre} {isCompleto ? β˜… Completo : Muestra}
{intentosInfo?.limite && !isCompleto ? `${intentosInfo.intentos_usados}/${intentosInfo.limite} intentos` : 'Acceso desbloqueado'}
{loading && (
⏳

Cargando simulador…

)} {!loading && error && (
πŸ“­

Simulador no disponible

{error}

El administrador aΓΊn no ha subido el archivo HTML.

)} {!loading && !error && iframeUrl && (