Skip to main content

Hi, Welcome to Johor English Language Academy

Enter your details to log in your account

Forgot your password?

Or login using your account

Some courses may allow guest access

Cookies notice
Contact site support
You are not logged in.
Data retention summary
Get the mobile app
(async () => { const LOG = '[Completion]'; const log = (...a) => console.log(LOG, ...a); const warn = (...a) => console.warn(LOG, ...a); const err = (...a) => console.error(LOG, ...a); // 1) New token (as requested) const token = "03978123ccb6ba97da164343c0224b41"; const domain = window.location.origin; // 3) Page-type whitelist (by body id prefix) // Examples: "page-mod-lti-view", "page-mod-scorm-view" const ALLOWED_PAGE_ID_PREFIXES = [ "page-mod-lti-", ]; const bodyId = String(document.body?.id || ""); const isAllowedPageType = ALLOWED_PAGE_ID_PREFIXES.some(prefix => bodyId.startsWith(prefix)); if (!isAllowedPageType) { warn("Not an allowed activity page type. Script will not run.", { bodyId, allowedPrefixes: ALLOWED_PAGE_ID_PREFIXES }); return; } // ---- ID RESOLUTION (using M.cfg + cmid method) const courseId = window.M?.cfg?.courseId ?? window.M?.cfg?.courseid ?? null; const userId = window.M?.cfg?.userid ?? window.M?.cfg?.userId ?? null; const cmid = Number(new URLSearchParams(location.search).get('cmid')) || Number(new URLSearchParams(location.search).get('id')) || null; console.log('M.cfg + cmid:', { courseId, userId, cmid, bodyId }); if (!courseId || !userId || !cmid) { warn('Missing IDs. Polling cannot start.', { courseId, userId, cmid }); return; } // ---- CONFIG const pollIntervalMs = 5000; const windowMinutes = 10; const windowMs = windowMinutes * 60 * 1000; const startedAt = Date.now(); let completed = false; let pollTimer = null; // ---- UI IDs const PANEL_ID = 'completionControlPanel'; const BTN_ID = 'completionManualRefresh'; const AUTO_ID = 'completionAutoStatus'; const SEP_ID = 'completionSeparator'; // ---- TIME const formatMMSS = (ms) => { const total = Math.max(0, Math.ceil(ms / 1000)); const m = String(Math.floor(total / 60)).padStart(2, '0'); const s = String(total % 60).padStart(2, '0'); return `${m}:${s}`; }; const remainingMs = () => Math.max(0, windowMs - (Date.now() - startedAt)); const updateAutoText = () => { const auto = document.getElementById(AUTO_ID); if (!auto) return; const timeEl = auto.querySelector('[data-role="time"]'); if (timeEl) timeEl.textContent = formatMMSS(remainingMs()); }; const hideAuto = () => { const auto = document.getElementById(AUTO_ID); const sep = document.getElementById(SEP_ID); if (auto) auto.style.display = 'none'; if (sep) sep.style.display = 'none'; }; const showAuto = () => { const auto = document.getElementById(AUTO_ID); const sep = document.getElementById(SEP_ID); if (auto) auto.style.display = 'inline-flex'; if (sep) sep.style.display = 'inline-flex'; }; // ---- UI refresh async function refreshActivityHeader() { try { const r = await fetch(window.location.href, { credentials: 'include' }); const html = await r.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const newHeader = doc.querySelector('.activity-header[data-for="page-activity-header"]'); const currentHeader = document.querySelector('.activity-header[data-for="page-activity-header"]'); if (newHeader && currentHeader) { currentHeader.replaceWith(newHeader); log('Activity header refreshed (DOM replaced).'); ensurePanel(); return true; } warn('Activity header not found to refresh.'); ensurePanel(); return false; } catch (e) { err('Header refresh failed:', e); ensurePanel(); return false; } } function refreshCourseNav() { try { require(['core_courseformat/courseeditor'], function(ce) { const editor = ce.getCurrentCourseEditor(); log('Dispatching courseformat refresh for cmid:', cmid); editor.dispatch('cmState', [cmid]); editor.dispatch('courseState'); }); } catch (e) { err('Course navigation refresh failed:', e); } } async function refreshAll(button = null) { try { ensurePanel(); if (button) { const icon = button.querySelector('i'); if (icon) icon.classList.add('fa-spin'); button.disabled = true; } log('Manual refresh triggered.'); await refreshActivityHeader(); refreshCourseNav(); } finally { const btn = document.getElementById(BTN_ID); if (btn) { const icon = btn.querySelector('i'); if (icon) icon.classList.remove('fa-spin'); btn.disabled = false; } ensurePanel(); } } // ---- Panel layout // [(icon) Refresh completion manually] ——— (icon) Automatically checking completion (active — 09:42 left) const buildPanel = () => { const panel = document.createElement('div'); panel.id = PANEL_ID; panel.style.cssText = ` margin-top: 10px; padding: 10px 0 0 0; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 0.92rem; `; const btn = document.createElement('button'); btn.id = BTN_ID; btn.type = 'button'; btn.className = 'btn btn-outline-primary'; btn.style.cssText = ` border-radius: 25px; display: inline-flex; align-items: center; gap: 6px; padding: 6px 16px; font-weight: 500; white-space: nowrap; `; btn.innerHTML = ` Refresh completion manually `; btn.addEventListener('click', () => refreshAll(btn)); const sep = document.createElement('span'); sep.id = SEP_ID; sep.textContent = '———'; sep.style.cssText = `color:#adb5bd; user-select:none;`; const auto = document.createElement('span'); auto.id = AUTO_ID; auto.style.cssText = ` color: #6c757d; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; `; auto.innerHTML = ` Automatically checking completion (active — left) `; panel.appendChild(btn); panel.appendChild(sep); panel.appendChild(auto); return panel; }; const ensurePanel = () => { if (!document.getElementById(PANEL_ID)) { const panel = buildPanel(); // Prefer placing directly below the top completion block const completionInfo = document.querySelector('[data-region="completion-info"]'); if (completionInfo) { completionInfo.insertAdjacentElement('afterend', panel); log('Control panel inserted below top completion.'); } else { const activityInfo = document.querySelector('[data-region="activity-information"]'); if (activityInfo) { activityInfo.insertAdjacentElement('afterend', panel); warn('completion-info not found; panel inserted after activity-information.'); } else { document.body.prepend(panel); warn('completion-info & activity-information not found; panel inserted at top of body.'); } } } // Update timer display and visibility rules updateAutoText(); if (completed) hideAuto(); }; // Keep panel alive if DOM is replaced const observer = new MutationObserver(() => { if (!document.getElementById(PANEL_ID)) { log('Control panel missing after DOM mutation; restoring.'); ensurePanel(); } }); observer.observe(document.body, { childList: true, subtree: true }); // ---- Polling via REST token const stopPollingAndHideAuto = (reason) => { if (pollTimer) clearInterval(pollTimer); pollTimer = null; log(`Polling stopped (${reason}).`); ensurePanel(); hideAuto(); }; async function checkCompletion() { if (completed) return; ensurePanel(); updateAutoText(); if (remainingMs() <= 0) { stopPollingAndHideAuto(`time window ended (${windowMinutes} minutes)`); return; } const params = new URLSearchParams({ wstoken: token, wsfunction: "core_completion_get_activities_completion_status", moodlewsrestformat: "json", courseid: courseId, userid: userId }); try { const response = await fetch(`${domain}/webservice/rest/server.php?${params.toString()}`, { method: "GET" }); const data = await response.json(); if (data.exception) { err("Web service error:", data); return; } const activity = data.statuses?.find(s => Number(s.cmid) === Number(cmid)); log("Completion status:", activity); if (activity && Number(activity.state) > 0) { completed = true; log("Completion detected. Updating UI."); // Refresh UI once, then hide auto status await refreshActivityHeader(); refreshCourseNav(); ensurePanel(); hideAuto(); stopPollingAndHideAuto("completed"); } } catch (e) { err("Web service fetch failed:", e); } } // ---- Boot ensurePanel(); showAuto(); updateAutoText(); // Update the displayed timer every second (UI only) const uiTimer = setInterval(() => { if (completed || remainingMs() <= 0) { clearInterval(uiTimer); return; } ensurePanel(); updateAutoText(); }, 1000); log(`Auto checking enabled for this page type. Window: ${windowMinutes} minutes.`); await checkCompletion(); pollTimer = setInterval(checkCompletion, pollIntervalMs); })();
Powered by Moodle