/** * cuestionarios/js/quiz.js * Lógica del cuestionario TAI con fórmula de corrección AGE. * Fórmula: Nota = (Aciertos - Fallos/3) / TotalPreguntas * 10 */ // ── Estado ────────────────────────────────────────────────── let preguntas = []; let supuestosData = {}; // { "Supuesto I": [...], "Supuesto II": [...] } let faseActual = 'test'; // 'test' | 'supuesto' let indice = 0; let aciertos = 0; let fallos = 0; let respondida = false; let preguntasFalladas = []; // { pregunta, elegida } // ── Mapa de temas y palabras clave ────────────────────────── const TEMAS_KW = [ { id:'const', label:'Constitución Española', link:'../curso.html?bloque=1', kw:['constitución','constitucional','rey ','cortes generales','senado','congreso','diputad','tribunal constitucional','defensor del pueblo','artículo 62','artículo 63','título i','título ii','artículo 1 ','capítulo'] }, { id:'p39', label:'Procedimiento Adm. (Ley 39/2015)', link:'../curso.html?bloque=1', kw:['ley 39','procedimiento administrativo','recurso de alzada','silencio administrativo','notificación','expediente administrativo','recurso potestativo','recurso extraordinario'] }, { id:'p40', label:'Régimen Jurídico (Ley 40/2015)', link:'../curso.html?bloque=1', kw:['ley 40','órgano colegiado','delegación de competencia','avocación','administración general del estado','convenio interadministrativo'] }, { id:'lcsp', label:'Contratación Pública (LCSP)', link:'../curso.html?bloque=1', kw:['contratos del sector público','lcsp','licitación','adjudicación','pliego','contrato menor','concesión de servicios','poder adjudicador'] }, { id:'trebep', label:'Función Pública (TREBEP)', link:'../curso.html?bloque=1', kw:['trebep','funcionario','empleado público','oposición','provisión de puestos','situaciones administrativas','excedencia','régimen disciplinario','carrera profesional'] }, { id:'hac', label:'Hacienda Pública / Presupuestos', link:'../curso.html?bloque=1', kw:['presupuesto','hacienda pública','igae','tribunal de cuentas','crédito presupuestario','gasto público','control financiero','intervención general'] }, { id:'html', label:'HTML / CSS / JavaScript', link:'../curso.html?bloque=3', kw:['html','css','javascript','dom',' t.includes(k))) return tema; } return { id:'otro', label:'Otros / Material general', link:'../curso.html' }; } // ── Elementos DOM ──────────────────────────────────────────── const selExamen = document.getElementById('sel-examen'); const btnIniciar = document.getElementById('btn-iniciar'); const btnSiguiente = document.getElementById('btn-siguiente'); const seccionQuiz = document.getElementById('seccion-quiz'); const seccionFinal = document.getElementById('seccion-final'); const seccionEmpty = document.getElementById('seccion-empty'); const seccionSupSel = document.getElementById('seccion-supuesto-sel'); const supCardsWrap = document.getElementById('sup-cards'); const contextoPanel = document.getElementById('contexto-pregunta'); const examPdfsPanel = document.getElementById('exam-pdfs'); // ── Mapa de PDFs por examen ─────────────────────────────────── const EXAM_PDFS = { 'data/TAI_2019.json': [ { label: 'Cuestionario oficial', icon: 'fa-file-alt', url: 'pdfs/cues_1er_ejer_TAI-L_oep19_154AB89SD658.pdf' }, { label: 'Plantilla definitiva', icon: 'fa-check-square', url: 'pdfs/Plantilla_defTAI-L1ejer_154AB89SD658.pdf' }, { label: 'Plantilla provisional', icon: 'fa-clipboard', url: 'pdfs/plant_prov_1er_ejer_TAI-L_oep19_154AB89SD658.pdf' }, { label: 'Material adicional (supuesto)', icon: 'fa-book-open', url: 'pdfs/07TAIL_154AB89SD658.pdf' }, ], 'data/TAI_2023.json': [ { label: 'Cuestionario oficial', icon: 'fa-file-alt', url: 'pdfs/Cuestionario TAI-L_2023.pdf' }, { label: 'Plantilla de respuestas', icon: 'fa-check-square', url: 'pdfs/PlantillaRespuestas TAI-L_2023.pdf' }, ], 'data/TAI_2024A.json': [ { label: 'Cuestionario oficial', icon: 'fa-file-alt', url: 'pdfs/Cuestionario_TAI_LI_2024_A_M8L91VL1CL_154AB89SD658.pdf' }, { label: 'Plantilla de respuestas', icon: 'fa-check-square', url: 'pdfs/Plantilla_Respuestas_PROV_TAI_LI_2024_A_6J5MFQ8OEN_154AB89SD658.pdf' }, { label: 'Material supuesto II', icon: 'fa-book-open', url: 'data/tai_2024A_supuesto2.md' }, ], 'data/TAI_2024B.json': [ { label: 'Cuestionario oficial', icon: 'fa-file-alt', url: 'pdfs/Cuestionario_TAI_LI_2024_B_DNFGFEK45R_154AB89SD658.pdf' }, { label: 'Plantilla de respuestas', icon: 'fa-check-square', url: 'pdfs/Plantilla_Respuestas_PROV_TAI_LI_2024_B_JQE95HBC1R_154AB89SD658.pdf' }, ], }; const elPreguntaNum = document.getElementById('pregunta-num'); const elPreguntaTxt = document.getElementById('pregunta-txt'); const elOpciones = document.getElementById('opciones'); const elFeedback = document.getElementById('feedback'); const elAciertos = document.getElementById('val-aciertos'); const elFallos = document.getElementById('val-fallos'); const elProgreso = document.getElementById('val-progreso'); const elNota = document.getElementById('val-nota'); // ── Eventos ────────────────────────────────────────────────── btnIniciar.addEventListener('click', iniciarExamen); btnSiguiente.addEventListener('click', siguiente); selExamen.addEventListener('change', () => { btnIniciar.disabled = !selExamen.value; renderExamPdfs(selExamen.value); }); function renderExamPdfs(url) { const pdfs = EXAM_PDFS[url]; if (!pdfs || !pdfs.length) { examPdfsPanel.style.display = 'none'; return; } examPdfsPanel.innerHTML = ' Documentos INAP:' + pdfs.map(p => `` + ` ${escHtml(p.label)}` ).join(''); examPdfsPanel.style.display = 'flex'; } // ── Funciones ──────────────────────────────────────────────── async function iniciarExamen() { const url = selExamen.value; if (!url) return; btnIniciar.disabled = true; btnIniciar.innerHTML = ' Cargando…'; try { const datos = await fetch(url).then(r => r.json()); const todas = Array.isArray(datos) ? datos : (datos.preguntas || []); // Separar preguntas tipo test de supuestos prácticos const testQs = todas.filter(p => !p.contexto?.supuesto); const supuestoQs = todas.filter(p => p.contexto?.supuesto); preguntas = mezclar(testQs); supuestosData = {}; for (const p of supuestoQs) { const nombre = p.contexto.supuesto; if (!supuestosData[nombre]) supuestosData[nombre] = []; supuestosData[nombre].push(p); } } catch (e) { alert('Error cargando el examen. Comprueba la ruta del fichero.'); btnIniciar.disabled = false; btnIniciar.innerHTML = ' Iniciar'; return; } faseActual = 'test'; indice = 0; aciertos = 0; fallos = 0; respondida = false; preguntasFalladas = []; mostrarSolo('quiz'); actualizarMarcador(); mostrarPregunta(); btnIniciar.disabled = false; btnIniciar.innerHTML = ' Reiniciar'; } function mostrarSolo(seccion) { seccionEmpty.style.display = seccion === 'empty' ? 'block' : 'none'; seccionQuiz.style.display = seccion === 'quiz' ? 'block' : 'none'; seccionFinal.style.display = seccion === 'final' ? 'block' : 'none'; seccionSupSel.style.display = seccion === 'supsel' ? 'block' : 'none'; } function mostrarSelectorSupuesto() { supCardsWrap.innerHTML = ''; for (const [nombre, pregs] of Object.entries(supuestosData)) { const ctx = pregs[0]?.contexto || {}; const ref = ctx.referencia_diagrama || ctx.referencia || null; const esImg = ref && /\.(png|jpg|jpeg|gif|webp)$/i.test(ref); const esPdf = ref && /\.pdf$/i.test(ref); const card = document.createElement('div'); card.className = 'sup-card'; card.innerHTML = `
${escHtml(nombre)} ${pregs.length} preguntas

${escHtml(ctx.descripcion || '')}

${esImg ? `
Material ${escHtml(nombre)}
` : ''} ${esPdf ? `
Ver enunciado (PDF)
` : ''} `; supCardsWrap.appendChild(card); } mostrarSolo('supsel'); } function iniciarSupuesto(nombre) { preguntas = supuestosData[nombre] || []; faseActual = 'supuesto'; indice = 0; aciertos = 0; fallos = 0; respondida = false; preguntasFalladas = []; mostrarSolo('quiz'); actualizarMarcador(); mostrarPregunta(); } function mostrarPregunta() { respondida = false; btnSiguiente.style.display = 'none'; elFeedback.className = 'question-feedback'; elFeedback.textContent = ''; if (indice >= preguntas.length) { if (faseActual === 'test' && Object.keys(supuestosData).length > 0) { mostrarSelectorSupuesto(); } else { finalizarExamen(); } return; } const p = preguntas[indice]; elPreguntaNum.textContent = `Pregunta ${indice + 1} de ${preguntas.length}`; elPreguntaTxt.textContent = p.pregunta; // Mostrar contexto si estamos en un supuesto práctico if (faseActual === 'supuesto' && p.contexto?.descripcion) { contextoPanel.style.display = 'block'; contextoPanel.innerHTML = ` Contexto: ${escHtml(p.contexto.descripcion)}`; } else { contextoPanel.style.display = 'none'; } elOpciones.innerHTML = ''; for (const [letra, texto] of Object.entries(p.opciones)) { const li = document.createElement('li'); li.className = 'options-list__item'; const label = document.createElement('label'); label.className = 'option-label'; label.innerHTML = ` ${letra.toUpperCase()}) ${escHtml(texto)}`; label.querySelector('input').addEventListener('change', () => { if (!respondida) comprobar(p); }); li.appendChild(label); elOpciones.appendChild(li); } actualizarMarcador(); } function comprobar(p) { respondida = true; const marcada = document.querySelector('input[name="resp"]:checked'); if (!marcada) return; // Deshabilitar todos los radio document.querySelectorAll('input[name="resp"]').forEach(r => r.disabled = true); // Marcar opciones document.querySelectorAll('.option-label').forEach(label => { const val = label.querySelector('input').value; if (val === p.correcta) label.classList.add('correct'); else if (val === marcada.value) label.classList.add('incorrect'); }); if (marcada.value === p.correcta) { aciertos++; elFeedback.textContent = '✔ ¡Correcto!'; elFeedback.className = 'question-feedback show ok'; } else { fallos++; preguntasFalladas.push({ pregunta: p, elegida: marcada.value }); elFeedback.textContent = `✘ Incorrecto. La respuesta correcta era la ${p.correcta.toUpperCase()})`; elFeedback.className = 'question-feedback show ko'; } actualizarMarcador(); btnSiguiente.style.display = 'inline-flex'; btnSiguiente.focus(); } function siguiente() { indice++; mostrarPregunta(); // mostrarPregunta() detecta si el índice superó el total y actúa } function actualizarMarcador() { const contestadas = aciertos + fallos; const puntosNetos = aciertos - fallos / 3; const nota = contestadas > 0 ? Math.max(0, (puntosNetos / preguntas.length) * 10).toFixed(2) : '—'; elAciertos.textContent = aciertos; elFallos.textContent = fallos; elProgreso.textContent = `${contestadas} / ${preguntas.length}`; elNota.textContent = nota; } function finalizarExamen() { mostrarSolo('final'); const total = preguntas.length; const puntosNetos = aciertos - fallos / 3; const nota = Math.max(0, (puntosNetos / total) * 10).toFixed(2); const sinRespuesta = total - aciertos - fallos; document.getElementById('final-nota').textContent = nota; document.getElementById('final-aciertos').textContent = aciertos; document.getElementById('final-fallos').textContent = fallos; document.getElementById('final-sin').textContent = sinRespuesta; document.getElementById('final-total').textContent = total; const notaNum = parseFloat(nota); const color = notaNum >= 5 ? 'var(--success)' : notaNum >= 4 ? 'var(--warning)' : 'var(--error)'; document.getElementById('final-nota').style.color = color; renderRepaso(); } function renderRepaso() { const wrap = document.getElementById('repaso-wrap'); if (!wrap) return; if (preguntasFalladas.length === 0) { wrap.innerHTML = '

¡Sin fallos! Dominas todo el temario de este examen.

'; wrap.style.display = 'block'; return; } // Agrupar fallos por tema detectado const grupos = {}; for (const { pregunta, elegida } of preguntasFalladas) { const tema = detectarTema(pregunta.pregunta); if (!grupos[tema.id]) grupos[tema.id] = { tema, items: [] }; grupos[tema.id].items.push({ pregunta, elegida }); } const n = preguntasFalladas.length; wrap.innerHTML = `

Necesitas repasar

Has fallado ${n} pregunta${n > 1 ? 's' : ''}. Estos son los temas donde debes reforzar:

${Object.values(grupos).map(g => `
${escHtml(g.tema.label)} ${g.items.length} fallo${g.items.length > 1 ? 's' : ''} Estudiar este tema
`).join('')} `; wrap.style.display = 'block'; } // ── Helpers ─────────────────────────────────────────────────── function mezclar(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function escHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }