{"id":5215,"date":"2025-12-03T05:42:35","date_gmt":"2025-12-03T05:42:35","guid":{"rendered":"https:\/\/luyenthitokutei.com\/?page_id=5215"},"modified":"2026-03-12T11:51:39","modified_gmt":"2026-03-12T11:51:39","slug":"de-tong-hop","status":"publish","type":"page","link":"https:\/\/luyenthitokutei.com\/ja\/de-tong-hop\/","title":{"rendered":"\u0110\u1ec1 t\u1ed5ng h\u1ee3p"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"vi\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>H\u1ec7 Th\u1ed1ng Thi Tr\u1eafc Nghi\u1ec7m<\/title>\n  <style>\n    body { -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; -webkit-touch-callout:none; }\n    @media print { body * { visibility:hidden; } }\n\n    .test-container * { box-sizing:border-box; margin:0; padding:0; font-family:'Roboto', Arial, sans-serif; }\n    .test-container { background:#f5f5f5; color:#333; position:relative; }\n\n    .test-container .popup { position:fixed; inset:0; background:rgba(0,0,0,.7); display:none; justify-content:center; align-items:center; z-index:1000; }\n    .test-container .popup-content { background:#fff; padding:25px; border-radius:10px; width:90%; max-width:500px; box-shadow:0 5px 15px rgba(0,0,0,.3); }\n    .test-container .popup-content h3 { color:#00448D; margin-bottom:10px; text-align:center; }\n    .test-container .popup-content p { color:#333; text-align:center; }\n    .test-container .popup-content button { width:100%; padding:12px; background:#00448D; color:#fff; border:none; border-radius:5px; font-size:16px; cursor:pointer; transition:.3s; }\n    .test-container .popup-content button:hover { background:#003366; }\n\n    .test-container .container { display:flex; min-height:100vh; }\n    .test-container .sidebar { width:280px; background:#fff; border-right:1px solid #ddd; box-shadow:2px 0 5px rgba(0,0,0,.1); overflow-y:auto; transition:transform .3s ease; }\n    .test-container .sidebar-header { padding:20px; background:#00448D; color:#fff; text-align:center; font-weight:bold; display:flex; justify-content:space-between; align-items:center; }\n    .test-container .exam-list { padding:10px; }\n\n    .test-container .exam-item { padding:10px; margin-bottom:5px; cursor:pointer; border-radius:5px; transition:.2s; }\n    .test-container .exam-item:hover { background:#f0f0f0; }\n    .test-container .exam-item.active { background:#00448D; color:#fff; font-weight:bold; }\n    .skill-submenu { display:none; margin-left:12px; }\n    .skill-item { padding:7px 10px 7px 22px; font-size:14px; border-radius:5px; cursor:pointer; position:relative; }\n    .skill-item:hover { background:#f5f7fb; }\n    .skill-item.active { background:#e9f7ef; font-weight:600; }\n    .skill-item.active::before { content:\"\u2713\"; position:absolute; left:6px; top:8px; color:#1aae55; font-weight:900; }\n\n    .test-container .content { flex:1; padding:0; height:100vh; overflow-y:auto; position:relative; }\n    .test-container .header {  display:flex; justify-content:space-between; align-items:center;  padding:10px 15px; background:#fff;  position:relative;     top:auto;  z-index:auto;  box-shadow:0 2px 5px rgba(0,0,0,.1);}\n    .test-container .header-left { display:flex; align-items:center; gap:15px; }\n    .test-container .timer-container { background:#00448D; color:#fff; padding:5px 10px; border-radius:20px; font-size:15px; white-space:nowrap; }\n    .test-container .header-right { display:flex; align-items:center; }\n    .test-container .submit-button { padding:5px 15px; background:#28a745; color:#fff; border:none; border-radius:20px; font-size:15px; cursor:pointer; transition:.3s; }\n    .test-container .submit-button:hover { background:#218838; }\n\n    .test-container .question-group { background:#fff; border-radius:5px; margin-bottom:20px; box-shadow:0 2px 5px rgba(0,0,0,.1); }\n    .test-container .group-header { padding:10px; background:#f8f9fa; border-bottom:1px solid #eee; border-radius:5px 5px 0 0; }\n    .test-container .group-title { font-weight:normal; color:#00448D; }\n    .test-container .group-subtitle { color:#666; font-size:14px; margin-top:5px; }\n\n    .test-container .question { padding:15px; font-weight:normal; border-bottom:2px solid #eee; }\n    .test-container .question-container { display:block; }\n    .test-container .question-text { margin-bottom:15px; font-weight:normal; }\n    .test-container .question-image { max-width:100%; height:auto; margin:10px 0; border-radius:5px; }\n    .test-container .question-option { display:block; padding:10px; margin:5px 0; border-radius:5px; transition:.2s; font-weight:normal; }\n    .test-container .question-option:hover { background:#f0f7ff; }\n    .test-container .question-option input { margin-right:10px; }\n    .test-container .explanation { display:none; background:#e6f7ff; padding:10px; margin-top:10px; border-radius:5px; border-left:4px solid #00448D; font-size:14px; font-weight:normal; }\n\n    .test-container .no-exam-message { text-align:center; padding:50px; font-size:18px; color:#dc3545; }\n\n    .test-container .menu-toggle { display:none; background:none; border:none; color:#00448D; font-size:22px; cursor:pointer; padding:4px; margin-right:15px; border-radius:50%; width:40px; height:40px; align-items:center; justify-content:center; transition:.3s; }\n    .test-container .menu-toggle:hover { background:#fff; transform:scale(1.1); }\n    .test-container .menu-toggle#closeMenu { color:#fff; background:transparent; box-shadow:none; font-size:22px; margin-right:0; }\n    .test-container .menu-toggle#closeMenu:hover { color:#f0f0f0; }\n\n    .test-container .result-popup { position:fixed; inset:0; background:rgba(0,0,0,.7); display:none; justify-content:center; align-items:center; z-index:1000; }\n    .test-container .result-popup-content { background:#fff; padding:20px; border-radius:10px; width:90%; max-width:500px; box-shadow:0 5px 15px rgba(0,0,0,.3); text-align:center; }\n    .test-container .result-popup h3 { color:#00448D; margin-bottom:20px; }\n    .test-container .result-details { text-align:left; margin:20px 0; line-height:1.8; }\n    .test-container .result-row { display:flex; justify-content:space-between; margin-bottom:10px; }\n    .test-container .result-label { font-weight:bold; color:#555; }\n    .test-container .result-value { color:#00448D; font-weight:bold; }\n    .test-container .result-popup button { padding:10px 20px; background:#00448D; color:#fff; border:none; border-radius:5px; cursor:pointer; transition:.3s; }\n    .test-container .result-popup button:hover { background:#003366; }\n\n    @media (max-width: 768px) {\n      .test-container .container { flex-direction:column; }\n      .test-container .sidebar { position:fixed; top:0; left:0; height:100vh; width:280px; z-index:1000; transform:translateX(-100%); }\n      .test-container .sidebar.visible { transform:translateX(0); }\n      .test-container .content { margin-left:0; padding-top:60px; }\n      .test-container .menu-toggle { display:block; }\n      .test-container .header {  position:relative;    width:auto;  top:auto;  padding:10px 15px;}\n    }\n\n    .yt-wrapper { position:relative; width:100%; padding-top:56.25%; margin:10px 0; border-radius:8px; overflow:hidden; background:#000; }\n    .yt-wrapper iframe { position:absolute; top:0; left:0; width:100%; height:100%; border:0; }\n  <\/style>\n<\/head>\n<body>\n<div class=\"test-container\">\n  <!-- Popup \u0111\u0103ng nh\u1eadp -->\n  <div id=\"userIdPopup\" class=\"popup\" style=\"display:flex;\">\n    <div class=\"popup-content\">\n      <h3>NH\u1eacP TH\u00d4NG TIN \u0110\u1ec2 TI\u1ebeP T\u1ee4C<\/h3>\n      <input style=\"display:none;\" id=\"userName\" placeholder=\"H\u1ecd v\u00e0 t\u00ean\" \/>\n      <input id=\"userId\" placeholder=\"M\u00e3 h\u1ecdc vi\u00ean\" \/>\n      <input style=\"display:none;\" id=\"classId\" placeholder=\"L\u1edbp h\u1ecdc\" \/>\n      <button id=\"userInfo-button\" onclick=\"checkUserId()\">X\u00e1c nh\u1eadn<\/button>\n    <\/div>\n  <\/div>\n\n  <!-- \u2728 Popup B\u1eaeT \u0110\u1ea6U -->\n  <div id=\"startQuizPopup\" class=\"popup\">\n    <div class=\"popup-content\">\n      <h3>B\u1eaeT \u0110\u1ea6U<\/h3>\n      <p>Nh\u1ea5n \u201cB\u1eaft \u0111\u1ea7u\u201d \u0111\u1ec3 t\u00ednh gi\u1edd v\u00e0 hi\u1ec7n c\u00e2u h\u1ecfi.<\/p>\n      <button onclick=\"startExamNow()\">B\u1eaft \u0111\u1ea7u<\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"container\" id=\"mainContainer\" style=\"display:none;\">\n    <div class=\"sidebar\" id=\"sidebar\">\n      <div class=\"sidebar-header\">\n        Danh s\u00e1ch \u0111\u1ec1 thi\n        <button class=\"menu-toggle\" id=\"closeMenu\">\u00d7<\/button>\n      <\/div>\n      <div class=\"exam-list\" id=\"examList\"><\/div>\n    <\/div>\n\n    <div class=\"content\" id=\"contentArea\">\n      <div class=\"header\">\n        <div class=\"header-left\">\n          <button class=\"menu-toggle\" onclick=\"toggleMenu()\">\u2630<\/button>\n          <div class=\"timer-container\"><span id=\"timer\">00:00<\/span><\/div>\n        <\/div>\n        <div class=\"header-right\">\n          <button class=\"submit-button\" id=\"submitButton\" onclick=\"submitQuiz()\">N\u1ed9p b\u00e0i<\/button>\n        <\/div>\n      <\/div>\n      <div id=\"questionContainer\" class=\"question-container\"><\/div>\n    <\/div>\n  <\/div>\n\n  <div id=\"noExamMessage\" class=\"no-exam-message\" style=\"display:none;\">\n    <h2>Kh\u00f4ng c\u00f3 \u0111\u1ec1 thi n\u00e0o \u0111\u01b0\u1ee3c hi\u1ec3n th\u1ecb<\/h2>\n    <p>Hi\u1ec7n t\u1ea1i kh\u00f4ng c\u00f3 \u0111\u1ec1 thi n\u00e0o s\u1eb5n s\u00e0ng. Vui l\u00f2ng quay l\u1ea1i sau.<\/p>\n  <\/div>\n\n  <div id=\"resultPopup\" class=\"result-popup\">\n    <div class=\"result-popup-content\">\n      <h3>K\u1ebeT QU\u1ea2 B\u00c0I THI<\/h3>\n      <div class=\"result-details\" id=\"resultDetails\"><\/div>\n      <button onclick=\"closeResultPopup()\">\u0110\u00f3ng<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n  const scriptUrl = \"https:\/\/script.google.com\/macros\/s\/AKfycbzw_NuuoqFPbOQ6BFXFcTqAm56aYi0ZXSTwHwv_dz0j6hK94weuzxWfaD7kcAvq7AdMIg\/exec\";\n\n  \/\/ \u2728 C\u1eadp nh\u1eadt: th\u00eam KJ + chu\u1ea9n ho\u00e1 skill\n  const SKILL_LABELS = {\n    G:  'T\u1eeb v\u1ef1ng',\n    BP: 'Ng\u1eef ph\u00e1p',\n    CK: 'Nghe hi\u1ec3u',\n    KJ: 'Kanji'\n  };\n\n  \/\/ Th\u1ee9 t\u1ef1 \u01b0u ti\u00ean hi\u1ec3n th\u1ecb \/ t\u00ednh \u0111i\u1ec3m\n  const SKILL_ORDER  = ['G','KJ','BP','CK'];\n\n  \/\/ Chu\u1ea9n ho\u00e1 gi\u00e1 tr\u1ecb skill t\u1eeb Sheets\n  function normalizeSkill(raw){\n    const s = (raw || '').toString().trim().toUpperCase();\n    if (!s) return '';\n    if (s === 'KANJI') return 'KJ';\n    if (['TU VUNG','TUVUNG','TV','VOCAB'].includes(s)) return 'G';\n    if (['NGU PHAP','NGUPHAP','NP','GRAMMAR'].includes(s)) return 'BP';\n    if (['NGHE','NGHE HIEU','LISTENING','NH'].includes(s)) return 'CK';\n    return s; \/\/ fallback: d\u00f9ng \u0111\u00fang m\u00e3 \u0111\u00e3 nh\u1eadp (G\/BP\/CK\/KJ\/...)\n  }\n\n  let allExamsData = {};\n  let examState = { currentExamId: null, currentSkill: null, exams: {} };\n\n  \/\/ \u0110\u1ed3ng h\u1ed3 chung cho to\u00e0n \u0111\u1ec1\n  const examTimer = { startedAt: null, durationSec: 0, interval: null };\n\n  \/\/ Gi\u1eef exam chu\u1ea9n b\u1ecb start (\u0111\u1ec3 popup x\u00e1c nh\u1eadn)\n  let pendingExamId = null;\n\n  (function disableCopyPaste(){\n    const userIdInput = document.getElementById('userId');\n    if (userIdInput) userIdInput.onpaste = () => true;\n    document.addEventListener('contextmenu', e => { e.preventDefault(); return false; });\n    document.addEventListener('dragstart',   e => { e.preventDefault(); return false; });\n    document.addEventListener('keydown',     e => {\n      if (e.ctrlKey && [65,67,86,88].includes(e.keyCode)) { e.preventDefault(); return false; }\n      if (e.keyCode === 123) { e.preventDefault(); return false; }\n    });\n  })();\n\n  document.addEventListener('DOMContentLoaded', () => {\n    const savedUserId  = localStorage.getItem('userId');\n    const savedName    = localStorage.getItem('userName');\n    const savedClassId = localStorage.getItem('classId');\n\n    if (savedUserId && savedName && savedClassId) {\n      document.getElementById('userName').value = savedName;\n      document.getElementById('classId').value  = savedClassId;\n      document.getElementById('userIdPopup').style.display = 'none';\n      document.getElementById('mainContainer').style.display = 'flex';\n      loadExams();\n    }\n\n    document.getElementById('closeMenu').addEventListener('click', () => {\n      document.getElementById('sidebar').classList.remove('visible');\n    });\n  });\n\n  function toggleMenu(){ document.getElementById('sidebar').classList.toggle('visible'); }\n\n  function checkUserId(){\n    const userId = document.getElementById('userId').value.trim();\n    const btn = document.getElementById('userInfo-button');\n    if(!userId){ alert('Vui l\u00f2ng nh\u1eadp \u0111\u1ea7y \u0111\u1ee7 th\u00f4ng tin!'); return; }\n    btn.disabled = true; btn.textContent = '\u0110ang ki\u1ec3m tra...';\n\n    fetch(`${scriptUrl}?action=checkUserId&userId=${encodeURIComponent(userId)}`)\n      .then(r => r.json())\n      .then(res => {\n        if(res.error) throw new Error(res.error);\n        if(!res.exists) { throw new Error('M\u00e3 h\u1ecdc vi\u00ean kh\u00f4ng t\u1ed3n t\u1ea1i.'); }\n\n        const classId  = res.classId  || '';\n        const userName = res.userName || '';\n        if(!classId){ throw new Error('Kh\u00f4ng t\u00ecm th\u1ea5y l\u1edbp h\u1ecdc c\u1ee7a h\u1ecdc vi\u00ean.'); }\n\n        localStorage.setItem('userId', userId);\n        localStorage.setItem('userName', userName);\n        localStorage.setItem('classId', classId);\n        if(!localStorage.getItem('deviceId')) localStorage.setItem('deviceId', generateRandomID());\n\n        document.getElementById('userName').value = userName;\n        document.getElementById('classId').value  = classId;\n        document.getElementById('userIdPopup').style.display = 'none';\n        document.getElementById('mainContainer').style.display = 'flex';\n        loadExams();\n      })\n      .catch(err => { alert(err.message); btn.disabled=false; btn.textContent='X\u00e1c nh\u1eadn'; });\n  }\n\n  function loadExams() {\n    const classId = localStorage.getItem('classId') || '';\n    \/\/ Thay \u0111\u1ed5i \u0111\u01b0\u1eddng d\u1eabn fetch v\u1ec1 WordPress AJAX endpoint\n    const ajaxUrl = `\/wp-admin\/admin-ajax.php?action=get_cached_jlpt_total_list&classId=${encodeURIComponent(classId)}`;\n\n    fetch(ajaxUrl)\n        .then(async r => {\n            const raw = await r.text();\n            let result; try { result = JSON.parse(raw); }\n            catch(e){ throw new Error(`L\u1ed7i t\u1ea3i \u0111\u1ec1 thi (Cache): ${raw.slice(0,100)}`); }\n            return result;\n        })\n        .then(async result => {\n            if (result.error) throw new Error(result.error);\n            if (result.status !== 'success' || !Array.isArray(result.exams)) {\n                throw new Error('D\u1eef li\u1ec7u ph\u1ea3n h\u1ed3i kh\u00f4ng h\u1ee3p l\u1ec7.');\n            }\n            if (!result.exams.length){\n                document.getElementById('mainContainer').style.display='none';\n                document.getElementById('noExamMessage').style.display='block';\n                return;\n            }\n            document.getElementById('mainContainer').style.display='flex';\n            document.getElementById('noExamMessage').style.display='none';\n\n            const examList = document.getElementById('examList');\n            examList.innerHTML = '';\n\n            const loadPromises = result.exams.map(exam =>\n                loadExamData(exam.id).then(questions => {\n                    const bySkill = {};\n                    (questions || []).forEach(q => {\n                        const s = normalizeSkill(q.skill);\n                        if (!s) return;\n                        if (!bySkill[s]) bySkill[s] = [];\n                        bySkill[s].push(q);\n                    });\n\n                    allExamsData[exam.id] = {\n                        id: exam.id, name: exam.name, duration: exam.duration, questionsBySkill: bySkill\n                    };\n                    examState.exams[exam.id] = { skills:{} };\n\n                    Object.keys(bySkill).forEach(k => {\n                        const arr = bySkill[k] || [];\n                        examState.exams[exam.id].skills[k] = {\n                            questions: arr,\n                            answers: new Array(arr.length).fill(null),\n                            correctAnswers: arr.map(x=>x.correctAnswer),\n                            questionScores: arr.map(x=>parseFloat(x.questionScore)||1),\n                            questionIds: arr.map(x=>x.questionId)\n                        };\n                    });\n                    return exam;\n                })\n            );\n\n            const exams = await Promise.all(loadPromises);\n\n            exams.forEach(exam => {\n                const examItem = document.createElement('div');\n                examItem.className = 'exam-item';\n                examItem.textContent = exam.name;\n                examItem.dataset.examId = exam.id;\n\n                const subMenu = document.createElement('div');\n                subMenu.className = 'skill-submenu';\n                subMenu.dataset.parentExam = exam.id;\n\n                const examData = allExamsData[exam.id];\n                const skillList = SKILL_ORDER\n                    .filter(k => (examData.questionsBySkill[k] || []).length)\n                    .concat(Object.keys(examData.questionsBySkill).filter(k => !SKILL_ORDER.includes(k)));\n\n                skillList.forEach((code) => {\n                    const sub = document.createElement('div');\n                    sub.className = 'skill-item';\n                    sub.dataset.examId  = exam.id;\n                    sub.dataset.skill   = code;\n                    sub.textContent = `\u2022 ${SKILL_LABELS[code] || code}`;\n                    sub.onclick = (ev) => {\n                        ev.stopPropagation();\n                        showExamWithSkill(exam.id, code);\n                        document.getElementById('sidebar').classList.remove('visible');\n                    };\n                    subMenu.appendChild(sub);\n                });\n\n                examItem.onclick = () => {\n                    document.querySelectorAll('.skill-submenu').forEach(el => { if(el !== subMenu) el.style.display = 'none'; });\n                    subMenu.style.display = (subMenu.style.display === 'block') ? 'none' : 'block';\n                    document.querySelectorAll('.exam-item').forEach(i=>i.classList.remove('active'));\n                    examItem.classList.add('active');\n                    prepareExam(exam.id, subMenu);\n                    openStartPopup();\n                };\n                examList.appendChild(examItem);\n                examList.appendChild(subMenu);\n            });\n            updateTimerUI(remainingSec());\n        })\n        .catch(err => { alert('L\u1ed7i: ' + err.message); });\n}\nfunction loadExamData(examId) {\n    \/\/ fetch t\u1eeb WordPress Cache thay v\u00ec Google Script\n    const ajaxUrl = `\/wp-admin\/admin-ajax.php?action=get_cached_jlpt_total_questions&examId=${encodeURIComponent(examId)}`;\n\n    return fetch(ajaxUrl)\n        .then(async r => {\n            const raw = await r.text();\n            let data; try { data = JSON.parse(raw); }\n            catch(e){ throw new Error(`L\u1ed7i t\u1ea3i c\u00e2u h\u1ecfi: ${raw.slice(0,100)}`); }\n            if(data.error) throw new Error(data.error);\n            return data;\n        });\n}\n\n  \/\/ ===== Chu\u1ea9n b\u1ecb \u0111\u1ec1 (render tr\u01b0\u1edbc, \u1ea9n c\u00e2u h\u1ecfi & reset timer) =====\n  function prepareExam(examId, subMenuEl){\n    const examData = allExamsData[examId];\n    if(!examData){ alert('Kh\u00f4ng th\u1ec3 t\u1ea3i d\u1eef li\u1ec7u \u0111\u1ec1 thi.'); return; }\n\n    pendingExamId = examId;\n    examState.currentExamId = examId;\n\n    if (subMenuEl && subMenuEl.style.display !== 'block') subMenuEl.style.display = 'block';\n\n    \/\/ \u2728 T\u00ecm skill \u0111\u1ea7u ti\u00ean c\u00f3 c\u00e2u h\u1ecfi:\n    let firstSkill = SKILL_ORDER.find(k => (examData.questionsBySkill[k]||[]).length);\n    if (!firstSkill) {\n      firstSkill = Object.keys(examData.questionsBySkill)\n        .find(k => (examData.questionsBySkill[k]||[]).length);\n    }\n    if(!firstSkill){\n      alert('\u0110\u1ec1 thi ch\u01b0a c\u00f3 c\u00e2u h\u1ecfi.');\n      return;\n    }\n\n    \/\/ Hi\u1ec3n th\u1ecb k\u1ef9 n\u0103ng \u0111\u1ea7u ti\u00ean (nh\u01b0ng \u1ea8N c\u00e2u h\u1ecfi cho \u0111\u1ebfn khi b\u1eaft \u0111\u1ea7u)\n    showExamWithSkill(examId, firstSkill);\n    const qc = document.getElementById('questionContainer');\n    if (qc) qc.style.display = 'none';\n\n    \/\/ RESET TIMER m\u1ed7i khi ch\u1ecdn \u0111\u1ec1 kh\u00e1c\n    if (examTimer.interval) clearInterval(examTimer.interval);\n    examTimer.interval = null;\n    examTimer.startedAt = null;\n    examTimer.durationSec = (parseInt(examData.duration,10) || 10) * 60;\n\n    updateTimerUI(examTimer.durationSec);\n  }\n\n  \/\/ Popup B\u1eaft \u0111\u1ea7u\n  function openStartPopup(){\n    const el = document.getElementById('startQuizPopup');\n    if (el) el.style.display = 'flex';\n  }\n  function closeStartPopup(){\n    const el = document.getElementById('startQuizPopup');\n    if (el) el.style.display = 'none';\n  }\n\n  \/\/ B\u1eaft \u0111\u1ea7u \u0111\u1ebfm gi\u1edd & show c\u00e2u h\u1ecfi\n  function startExamNow(){\n    const examId = pendingExamId || examState.currentExamId;\n    if(!examId) { closeStartPopup(); return; }\n\n    const examData = allExamsData[examId];\n    if(!examData){ closeStartPopup(); return; }\n\n    if (examTimer.interval) clearInterval(examTimer.interval);\n\n    examTimer.startedAt   = Date.now();\n    examTimer.interval    = setInterval(() => {\n      updateTimerUI(remainingSec());\n      if (remainingSec() <= 0) {\n        clearInterval(examTimer.interval);\n        endQuiz();\n      }\n    }, 1000);\n\n    const qc = document.getElementById('questionContainer');\n    if (qc) qc.style.display = 'block';\n\n    closeStartPopup();\n  }\n\n  function updateActiveSkillUI(examId, skillCode){\n    document.querySelectorAll(`.skill-submenu[data-parent-exam=\"${examId}\"] .skill-item`)\n      .forEach(el => {\n        if (el.dataset.skill === skillCode) el.classList.add('active');\n        else el.classList.remove('active');\n      });\n  }\n\n  function showExamWithSkill(examId, skillCode){\n    captureCurrentAnswers();\n\n    const examData = allExamsData[examId];\n    const pack = examState.exams[examId]?.skills?.[skillCode];\n    if(!examData || !pack){ alert('K\u1ef9 n\u0103ng n\u00e0y ch\u01b0a c\u00f3 c\u00e2u h\u1ecfi.'); return; }\n\n    examState.currentExamId = examId;\n    examState.currentSkill  = skillCode;\n    updateActiveSkillUI(examId, skillCode);\n\n    const contentArea = document.getElementById('contentArea');\n    contentArea.innerHTML = `\n      <div class=\"header\">\n        <div class=\"header-left\">\n          <button class=\"menu-toggle\" onclick=\"toggleMenu()\">\u2630<\/button>\n          <div class=\"timer-container\"><span id=\"timer\">${formatTime(remainingSec())}<\/span><\/div>\n        <\/div>\n        <div class=\"header-right\">\n          <button class=\"submit-button\" id=\"submitButton\" onclick=\"submitQuiz()\">N\u1ed9p b\u00e0i<\/button>\n        <\/div>\n      <\/div>\n      <div class=\"group-header\" style=\"padding:10px;border-bottom:1px solid #eee;\">\n        <div class=\"group-title\">\u0110\u1ec1: ${escapeHtml(examData.name)} \u2022 K\u1ef9 n\u0103ng: ${SKILL_LABELS[skillCode] || skillCode}<\/div>\n      <\/div>\n      <div id=\"questionContainer\" class=\"question-container\"><\/div>`;\n\n    displayQuestions(pack.questions);\n\n    if (!examTimer.startedAt) {\n      const qc = document.getElementById('questionContainer');\n      if (qc) qc.style.display = 'none';\n    }\n\n    restoreAnswersForCurrentSkill();\n    if (contentArea) contentArea.scrollTop = 0;\n    document.documentElement.scrollTop = 0;\n    document.body.scrollTop = 0;\n  }\n\n  \/\/ GI\u1eee NGUY\u00caN TH\u1ee8 T\u1ef0\n  function displayQuestions(questions){\n    const container = document.getElementById('questionContainer');\n    container.innerHTML = '';\n    if(!questions || !questions.length){ container.innerHTML='<p>Kh\u00f4ng c\u00f3 c\u00e2u h\u1ecfi.<\/p>'; return; }\n\n    const groups = [];\n    let currentGroup = null;\n    questions.forEach(q => {\n      if (q.groupTitle) {\n        currentGroup = { title:q.groupTitle, subtitle:q.groupSubtitle, image:q.groupImage, audio:q.questionAudio, questions:[] };\n        groups.push(currentGroup);\n      }\n      if (currentGroup) currentGroup.questions.push(q);\n      else groups.push({ title:'Ungrouped', subtitle:'', image:'', audio:q.questionAudio, questions:[q] });\n    });\n\n    let idx = 0;\n\n    groups.forEach((g, gi) => {\n      const qs = g.questions;\n      let groupHTML = '';\n      if(g.title !== 'Ungrouped'){\n        groupHTML += `\n          <div class=\"group-header\">\n            <div class=\"group-title\">${escapeHtml(g.title||'')}<\/div>\n            ${g.subtitle ? `<div class=\"group-subtitle\">${sanitizeHtml(g.subtitle)}<\/div>` : ''}\n            ${g.image ? `<img decoding=\"async\" class=\"question-image\" src=\"${escapeAttr(g.image)}\">` : ''}\n            ${g.audio ? renderAudio(g.audio) : ''}\n          <\/div>`;\n      }\n\n      qs.forEach(q => {\n        const questionIndex = idx;\n        const hasC = q.optionC !== undefined && q.optionC !== null && String(q.optionC).trim() !== '';\n        const hasD = q.optionD !== undefined && q.optionD !== null && String(q.optionD).trim() !== '';\n\n        groupHTML += `\n          <div class=\"question\" id=\"question-${questionIndex}\">\n            <div class=\"question-text\">${sanitizeHtml(q.questionText||'')}<\/div>\n            ${q.questionImage ? `<img decoding=\"async\" class=\"question-image\" src=\"${escapeAttr(q.questionImage)}\">` : ''}\n            ${q.questionAudio ? renderAudio(q.questionAudio) : ''}\n            <label class=\"question-option\">\n              <input type=\"radio\" name=\"question${questionIndex}\" value=\"A\"> ${sanitizeHtml(q.optionA||'N\/A')}\n            <\/label>\n            <label class=\"question-option\">\n              <input type=\"radio\" name=\"question${questionIndex}\" value=\"B\"> ${sanitizeHtml(q.optionB||'N\/A')}\n            <\/label>\n            ${hasC ? `<label class=\"question-option\"><input type=\"radio\" name=\"question${questionIndex}\" value=\"C\"> ${sanitizeHtml(q.optionC)}<\/label>` : ''}\n            ${hasD ? `<label class=\"question-option\"><input type=\"radio\" name=\"question${questionIndex}\" value=\"D\"> ${sanitizeHtml(q.optionD)}<\/label>` : ''}\n            ${q.explanation ? `<div class=\"explanation\" id=\"explanation-${questionIndex}\"><strong>Gi\u1ea3i th\u00edch:<\/strong> ${sanitizeHtml(q.explanation)}<\/div>` : ''}\n          <\/div>`;\n        idx++;\n      });\n\n      container.innerHTML += `<div class=\"question-group\">${groupHTML}<\/div>`;\n    });\n\n    setupAnswerSelection();\n  }\n\n  function renderAudio(url) {\n    if (!url) return '';\n    return `\n      <div class=\"audio-player\">\n        <audio controls controlsList=\"nodownload\">\n          <source src=\"${url}\" type=\"audio\/mpeg\">\n        <\/audio>\n      <\/div>`;\n  }\n\n\n  function setupAnswerSelection(){\n    document.querySelectorAll('input[type=\"radio\"]').forEach(r => {\n      r.addEventListener('change', function(){\n        const questionDiv = this.closest('.question');\n        questionDiv.querySelectorAll('.question-option').forEach(l => { l.style.backgroundColor=''; });\n        this.closest('.question-option').style.backgroundColor = '#e6f7ff';\n        persistCurrentAnswerFromInput(this);\n      });\n    });\n  }\n\n  function persistCurrentAnswerFromInput(input){\n    const examId = examState.currentExamId;\n    const skill  = examState.currentSkill;\n    const pack = examState.exams[examId]?.skills?.[skill];\n    if(!pack) return;\n    const idx = parseInt(input.name.replace('question',''), 10);\n    pack.answers[idx] = input.value;\n  }\n\n  function captureCurrentAnswers(){\n    const examId = examState.currentExamId;\n    const skill  = examState.currentSkill;\n    const pack = examState.exams[examId]?.skills?.[skill];\n    if(!pack) return;\n    for(let i=0;i<pack.questions.length;i++){\n      const selected = document.querySelector(`input[name=\"question${i}\"]:checked`);\n      if(selected) pack.answers[i] = selected.value;\n    }\n  }\n\n  function restoreAnswersForCurrentSkill(){\n    const examId = examState.currentExamId;\n    const skill  = examState.currentSkill;\n    const pack = examState.exams[examId]?.skills?.[skill];\n    if(!pack) return;\n    for(let i=0;i<pack.answers.length;i++){\n      const ans = pack.answers[i];\n      if(ans){\n        const radio = document.querySelector(`input[name=\"question${i}\"][value=\"${ans}\"]`);\n        if(radio){ radio.checked = true; radio.closest('.question-option').style.backgroundColor = '#e6f7ff'; }\n      }\n    }\n  }\n\n  \/\/ ===== \u0110\u1ed3ng h\u1ed3 chung =====\n  function remainingSec(){\n    if(!examTimer.durationSec) return 0;\n    const elapsed = examTimer.startedAt ? Math.floor((Date.now() - examTimer.startedAt)\/1000) : 0;\n    return Math.max(0, examTimer.durationSec - elapsed);\n  }\n  function updateTimerUI(sec){\n    const el = document.getElementById('timer');\n    if (el) el.textContent = formatTime(sec);\n  }\n  function endQuiz(){\n    document.querySelectorAll('input[type=\"radio\"]').forEach(r => r.disabled = true);\n    alert('Th\u1eddi gian l\u00e0m b\u00e0i \u0111\u00e3 h\u1ebft! H\u1ec7 th\u1ed1ng s\u1ebd t\u1ef1 \u0111\u1ed9ng n\u1ed9p b\u00e0i.');\n    submitQuiz();\n  }\n\n  \/\/ ===== N\u1ed9p b\u00e0i =====\n  function submitQuiz(){\n    captureCurrentAnswers();\n\n    const userName = document.getElementById('userName').value;\n    const classId  = document.getElementById('classId').value;\n    const userId   = localStorage.getItem('userId');\n\n    const examId = examState.currentExamId;\n    const exam   = allExamsData[examId];\n    if(!exam){ alert('Kh\u00f4ng t\u00ecm th\u1ea5y \u0111\u1ec1 thi \u0111\u1ec3 n\u1ed9p.'); return; }\n    if(!confirm('B\u1ea1n c\u00f3 ch\u1eafc ch\u1eafn mu\u1ed1n n\u1ed9p b\u00e0i thi n\u00e0y?')) return;\n\n    if (examTimer.interval) clearInterval(examTimer.interval);\n    document.querySelectorAll('input[type=\"radio\"]').forEach(r => r.disabled = true);\n    const btn = document.getElementById('submitButton'); if(btn){ btn.style.display='none'; btn.disabled=true; }\n\n    const elapsedSec = examTimer.startedAt ? Math.floor((Date.now() - examTimer.startedAt)\/1000) : 0;\n    const minutes = Math.max(0, Math.floor(elapsedSec\/60));\n    const seconds = Math.max(0, elapsedSec % 60);\n\n    const packs = examState.exams[examId]?.skills || {};\n\n    \/\/ \u2728 Th\u1ee9 t\u1ef1 merge: SKILL_ORDER + ph\u1ea7n c\u00f2n l\u1ea1i (n\u1ebfu c\u00f3)\n    const ORDER = SKILL_ORDER.concat(\n      Object.keys(packs).filter(k => !SKILL_ORDER.includes(k))\n    );\n\n    let mergedAnswers=[], mergedCorrect=[], mergedScores=[], mergedIds=[];\n    ORDER.forEach(k => {\n      if(packs[k]){\n        mergedAnswers = mergedAnswers.concat(packs[k].answers.map(a => a ?? 'N\/A'));\n        mergedCorrect = mergedCorrect.concat(packs[k].correctAnswers);\n        mergedScores  = mergedScores.concat(packs[k].questionScores);\n        mergedIds     = mergedIds.concat(packs[k].questionIds);\n      }\n    });\n\n    const totalQuestionsAll = mergedCorrect.length;\n\n    let correctCount = 0, totalScore = 0, maxPossibleScore = 0;\n    for(let i=0;i<totalQuestionsAll;i++){\n      const weight = parseFloat(mergedScores[i]) || 1;\n      if(mergedAnswers[i] === mergedCorrect[i]){ correctCount++; totalScore += weight; }\n      maxPossibleScore += weight;\n    }\n\n    const curPack = packs[examState.currentSkill];\n    if(curPack){\n      for(let i=0;i<curPack.questions.length;i++){\n        const ex = document.getElementById(`explanation-${i}`);\n        if(ex) ex.style.display = 'block';\n      }\n    }\n\n    const payload = {\n      action: 'submitQuiz',\n      userId,\n      userName,\n      classId,\n      examId: exam.id,\n      correctCount,\n      minutes,\n      totalQuestions: totalQuestionsAll,\n      totalScore,\n      answers: mergedAnswers,\n      questionIds: mergedIds,\n      correctAnswers: mergedCorrect\n    };\n\n    fetch(scriptUrl, {\n      method: 'POST',\n      headers: { 'Content-Type': 'text\/plain;charset=utf-8' },\n      body: JSON.stringify(payload)\n    })\n    .then(async (r) => {\n      const raw = await r.text();\n      let resp;\n      try { resp = JSON.parse(raw); } catch { resp = { status:'unknown', raw }; }\n\n      showResultPopup({\n        totalQuestions: totalQuestionsAll,\n        correctCount,\n        totalScore,\n        maxPossibleScore,\n        minutes,\n        seconds\n      });\n    })\n    .catch(err => { alert('C\u00f3 l\u1ed7i x\u1ea3y ra khi n\u1ed9p b\u00e0i: ' + err.message); });\n  }\n\n  function showResultPopup(res){\n    const pc = (res.correctCount \/ res.totalQuestions) * 100;\n    document.getElementById('resultDetails').innerHTML = `\n      <div class=\"result-row\"><span class=\"result-label\">S\u1ed1 c\u00e2u \u0111\u00fang:<\/span><span class=\"result-value\">${res.correctCount}\/${res.totalQuestions}<\/span><\/div>\n      <div class=\"result-row\"><span class=\"result-label\">T\u1ed5ng \u0111i\u1ec3m (tr\u1ecdng s\u1ed1):<\/span><span class=\"result-value\">${res.totalScore.toFixed(1)}\/${res.maxPossibleScore.toFixed(1)}<\/span><\/div>\n      <div class=\"result-row\"><span class=\"result-label\">T\u1ec9 l\u1ec7 \u0111\u00fang (s\u1ed1 c\u00e2u):<\/span><span class=\"result-value\">${pc.toFixed(1)}%<\/span><\/div>\n      <div class=\"result-row\"><span class=\"result-label\">Th\u1eddi gian l\u00e0m b\u00e0i:<\/span><span class=\"result-value\">${res.minutes} ph\u00fat ${res.seconds} gi\u00e2y<\/span><\/div>`;\n    document.getElementById('resultPopup').style.display = 'flex';\n  }\n  function closeResultPopup(){ document.getElementById('resultPopup').style.display = 'none'; }\n\n  \/\/ Helpers\n  function formatTime(sec){ const m=Math.floor(sec\/60), s=sec%60; return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }\n  function generateRandomID(){ return Math.random().toString(36).substr(2,9); }\n  function escapeHtml(s){ return String(s||'').replace(\/[&<>\"']\/g, m=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\"&gt;\":\">\",\"\\\"\":\"&quot;\",\"'\":\"&#39;\"}[m])); }\n  function escapeAttr(s){ return escapeHtml(s).replace(\/\\\"\/g,'&quot;'); }\n  function sanitizeHtml(html){\n    if(!html) return '';\n    let out = String(html);\n    out = out.replace(\/<script[\\s\\S]*?>[\\s\\S]*?<\\\/script>\/gi, '');\n    out = out.replace(\/on[a-z]+\\s*=\\s*([\"']).*?\\1\/gi, '');\n    return out;\n  }\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>H\u1ec7 Th\u1ed1ng Thi Tr\u1eafc Nghi\u1ec7m NH\u1eacP TH\u00d4NG TIN \u0110\u1ec2 TI\u1ebeP T\u1ee4C X\u00e1c nh\u1eadn B\u1eaeT \u0110\u1ea6U Nh\u1ea5n \u201cB\u1eaft \u0111\u1ea7u\u201d \u0111\u1ec3 t\u00ednh gi\u1edd v\u00e0 hi\u1ec7n c\u00e2u h\u1ecfi. B\u1eaft \u0111\u1ea7u Danh s\u00e1ch \u0111\u1ec1 thi \u00d7 \u2630 00:00 N\u1ed9p b\u00e0i Kh\u00f4ng c\u00f3 \u0111\u1ec1 thi n\u00e0o \u0111\u01b0\u1ee3c hi\u1ec3n th\u1ecb Hi\u1ec7n t\u1ea1i kh\u00f4ng c\u00f3 \u0111\u1ec1 thi n\u00e0o s\u1eb5n s\u00e0ng. Vui l\u00f2ng quay l\u1ea1i sau. K\u1ebeT QU\u1ea2 B\u00c0I THI \u0110\u00f3ng<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-5215","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5215","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/comments?post=5215"}],"version-history":[{"count":7,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5215\/revisions"}],"predecessor-version":[{"id":5229,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5215\/revisions\/5229"}],"wp:attachment":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/media?parent=5215"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}