// 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.>}