{"id":5174,"date":"2025-10-20T04:40:24","date_gmt":"2025-10-20T04:40:24","guid":{"rendered":"https:\/\/luyenthitokutei.com\/?page_id=5174"},"modified":"2026-03-12T14:02:22","modified_gmt":"2026-03-12T14:02:22","slug":"jlpt-ttdt","status":"publish","type":"page","link":"https:\/\/luyenthitokutei.com\/ja\/jlpt-ttdt\/","title":{"rendered":"JLPT TT\u0110T"},"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 {\n                flex-direction: column;\n            }\n            \n            .test-container .sidebar {\n                position: fixed;\n                top: 0;\n                left: 0;\n                height: 100vh;\n                width: 280px;\n                z-index: 1001; \/* T\u0103ng z-index \u0111\u1ec3 n\u1eb1m tr\u00ean header *\/\n                transform: translateX(-100%);\n            }\n            \n            .test-container .sidebar.visible {\n                transform: translateX(0);\n            }\n            \n            .test-container .content {\n                margin-left: 0;\n                padding-bottom: 70px; \/* Th\u00eam kho\u1ea3ng tr\u1ed1ng \u1edf d\u01b0\u1edbi \u0111\u1ec3 kh\u00f4ng b\u1ecb header \u0111\u00e8 *\/\n                padding-top: 10px;    \/* B\u1ecf padding-top c\u0169 *\/\n            }\n            \n            .test-container .menu-toggle {\n                display: block;\n            }\n            \n            \/* Header c\u1ed1 \u0111\u1ecbnh n\u1eb1m d\u01b0\u1edbi cho Mobile *\/\n            .test-container .header {\n                position: fixed;   \n                width: 100%;\n                bottom: 0;         \/* \u0110\u01b0a xu\u1ed1ng d\u01b0\u1edbi *\/\n                left: 0;\n                top: auto;         \/* H\u1ee7y b\u1ecf top n\u1ebfu c\u00f3 *\/\n                padding: 10px 15px;\n                background: #fff;\n                z-index: 1000;\n                box-shadow: 0 -2px 10px rgba(0,0,0,.1); \/* \u0110\u1ed5 b\u00f3ng ng\u01b0\u1ee3c l\u00ean tr\u00ean *\/\n            }\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 (th\u00eam m\u1edbi) -->\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\/AKfycbzh3XMfl7RYqTzvaudZqZEi84GLiavfu1N0Y1TNHhTMWPT2ijlYTVb06ijJu9nLDSpE0w\/exec\";\n  const SKILL_LABELS = { G: 'T\u1eeb v\u1ef1ng', BP: 'Ng\u1eef ph\u00e1p', CK: 'Nghe hi\u1ec3u' };\n  const SKILL_ORDER  = ['G','BP','CK'];\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  \/\/ Thay th\u1ebf h\u00e0m loadExams\nfunction loadExams() {\n    const classId = localStorage.getItem('classId') || '';\n    \/\/ Chuy\u1ec3n h\u01b0\u1edbng sang endpoint AJAX c\u1ee7a WordPress\n    const ajaxUrl = `\/wp-admin\/admin-ajax.php?action=get_cached_jlpt_n3_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 parse JSON: ${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 kh\u00f4ng \u0111\u00fang \u0111\u1ecbnh d\u1ea1ng.');\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            \/\/ T\u1ea3i d\u1eef li\u1ec7u c\u00e2u h\u1ecfi t\u1eeb Cache\n            const loadPromises = result.exams.map(exam =>\n                loadExamData(exam.id).then(questions => {\n                    const bySkill = { G:[], BP:[], CK:[] };\n                    (questions||[]).forEach(q => {\n                        const s = (q.skill||'').toUpperCase();\n                        if(['G','BP','CK'].includes(s)) bySkill[s].push(q);\n                    });\n                    allExamsData[exam.id] = { id: exam.id, name: exam.name, duration: exam.duration, questionsBySkill: bySkill };\n                    examState.exams[exam.id] = { skills:{} };\n\n                    ['G','BP','CK'].forEach(k => {\n                        const arr = bySkill[k] || [];\n                        if(arr.length){\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                    });\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.filter(k => (examData.questionsBySkill[k]||[]).length);\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\n                    prepareExam(exam.id, subMenu);\n                    openStartPopup();\n                };\n\n                examList.appendChild(examItem);\n                examList.appendChild(subMenu);\n            });\n            updateTimerUI(remainingSec());\n        })\n        .catch(err => { alert('L\u1ed7i: ' + err.message); });\n}\n\n\/\/ Thay th\u1ebf h\u00e0m loadExamData\nfunction loadExamData(examId) {\n    \/\/ Chuy\u1ec3n h\u01b0\u1edbng sang endpoint AJAX c\u1ee7a WordPress\n    const ajaxUrl = `\/wp-admin\/admin-ajax.php?action=get_cached_jlpt_n3_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 JSON questions: ${raw.slice(0,100)}`); }\n            if(data.error) throw new Error(data.error);\n            if(!Array.isArray(data)) throw new Error('D\u1eef li\u1ec7u c\u00e2u h\u1ecfi kh\u00f4ng h\u1ee3p l\u1ec7.');\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;            \/\/ l\u01b0u l\u1ea1i \u0111\u1ec3 n\u00fat B\u1eaft \u0111\u1ea7u d\u00f9ng\n    examState.currentExamId = examId;\n\n    if (subMenuEl && subMenuEl.style.display !== 'block') subMenuEl.style.display = 'block';\n\n    const firstSkill = SKILL_ORDER.find(k => (examData.questionsBySkill[k]||[]).length);\n    if(!firstSkill){ alert('\u0110\u1ec1 thi ch\u01b0a c\u00f3 c\u00e2u h\u1ecfi.'); return; }\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    \/\/ \u2728 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    \/\/ c\u1eadp nh\u1eadt hi\u1ec3n th\u1ecb th\u1eddi gian = full duration, ch\u01b0a ch\u1ea1y\n    updateTimerUI(examTimer.durationSec);\n  }\n\n  \/\/ \u2728 M\u1edf\/\u0111\u00f3ng 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  \/\/ \u2728 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    \/\/ n\u1ebfu \u0111ang ch\u1ea1y th\u00ec b\u1ecf\n    if (examTimer.interval) clearInterval(examTimer.interval);\n\n    \/\/ kh\u1edfi \u0111\u1ed9ng t\u1eeb \u0111\u1ea7u (theo th\u1eddi l\u01b0\u1ee3ng \u0111\u1ec1 hi\u1ec7n t\u1ea1i)\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    \/\/ hi\u1ec7n c\u00e2u h\u1ecfi\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    \/\/ N\u1ebfu ch\u01b0a b\u1eaft \u0111\u1ea7u (t\u1eeb popup), v\u1eabn \u1ea9n c\u00e2u h\u1ecfi\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;  \/\/ Safari\/modern\n      document.body.scrollTop = 0;             \/\/ iOS fallback\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, video:q.questionVideo, questions:[] };\n        groups.push(currentGroup);\n      }\n      if (currentGroup) currentGroup.questions.push(q);\n      else groups.push({ title:'Ungrouped', subtitle:'', image:'', video:q.questionVideo, 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.video ? renderYouTube(g.video) : ''}\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.questionVideo ? renderYouTube(q.questionVideo) : ''}\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 renderYouTube(url){\n    const id = extractYouTubeId(url);\n    if(!id) return '';\n    const embed = `https:\/\/www.youtube.com\/embed\/${id}?rel=0&modestbranding=1&playsinline=1`;\n    return `<div class=\"yt-wrapper\"><iframe src=\"${embed}\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen><\/iframe><\/div>`;\n  }\n  function extractYouTubeId(url){\n    try{\n      const u = new URL(url);\n      if(u.hostname.includes('youtu.be')) return (u.pathname.split('\/')[1] || '').split('?')[0];\n      const v = u.searchParams.get('v'); if(v) return v;\n      const path = u.pathname.split('\/');\n      const idx = path.indexOf('embed');\n      if(idx !== -1 && path[idx+1]) return path[idx+1];\n      return '';\n    }catch(e){ return ''; }\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 \u2014 gi\u1eef nguy\u00ean t\u00ednh \u0111i\u1ec3m\/bi\u1ebfn g\u1eedi \u0111i =====\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    \/\/ Th\u1eddi gian l\u00e0m\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    \/\/ G\u1ed9p t\u1ea5t c\u1ea3 k\u1ef9 n\u0103ng theo th\u1ee9 t\u1ef1 G, BP, CK\n    const packs = examState.exams[examId]?.skills || {};\n    const ORDER = ['G','BP','CK'];\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    \/\/ T\u00cdNH THEO TR\u1eccNG S\u1ed0 questionScore\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    \/\/ Hi\u1ec7n gi\u1ea3i th\u00edch ph\u1ea7n \u0111ang m\u1edf\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-5174","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5174","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=5174"}],"version-history":[{"count":36,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5174\/revisions"}],"predecessor-version":[{"id":5238,"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/pages\/5174\/revisions\/5238"}],"wp:attachment":[{"href":"https:\/\/luyenthitokutei.com\/ja\/wp-json\/wp\/v2\/media?parent=5174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}