Smart attendance and grade validation overlay
// ==UserScript==
// @name AcademicFlow
// @namespace mtools-by-mason
// @version 1.8.7
// @description Smart attendance and grade validation overlay
// @author Mason – AcademicFlow
// @match https://mymentor.mliesl.edu/student-section*
// @match https://mymentor.mliesl.edu/student-section/*
// @run-at document-idle
// @grant none
// @license All Rights Reserved
// ==/UserScript==
(() => {
const APP_NAME = "School Checker";
const APP_VERSION = "1.8.7";
const PRESENT = ["P", "T"];
const NO_GRADE = ["A", "L", "H", "G"];
const ATT_ABSENT = ["A", "L", "G"];
const DEFAULT_CLASSES_PER_DAY = 5;
const BUSINESS_TOEIC_CLASSES_PER_DAY = 10;
const STORAGE_SELECTED_MODULE = "school_checker_selected_module_v1";
const STORAGE_MINIMIZED = "school_checker_minimized_v1";
const STORAGE_INFO_OPEN = "school_checker_info_open_v1";
const STORAGE_ATTENDANCE_SLOTS = "school_checker_attendance_slots_v1";
let latestGradeFixes = [];
let latestAttendanceFills = [];
let latestMissingGradeFills = [];
let latestModules = new Map();
let latestAttendanceSlots = [];
let latestAttendanceMap = {};
let latestAttendanceStats = {
attEmpty: 0,
attAutoFillable: 0,
attendanceDates: 0,
middleAbsentWarnings: 0
};
function randomGrade() {
return String(Math.floor(Math.random() * 31) + 70);
}
function getSelectedModule() {
return localStorage.getItem(STORAGE_SELECTED_MODULE) || "ALL";
}
function setSelectedModule(value) {
localStorage.setItem(STORAGE_SELECTED_MODULE, value || "ALL");
}
function isMinimized() {
return localStorage.getItem(STORAGE_MINIMIZED) === "1";
}
function setMinimized(value) {
localStorage.setItem(STORAGE_MINIMIZED, value ? "1" : "0");
}
function isModuleAllowed(moduleKey) {
const selected = getSelectedModule();
return selected === "ALL" || selected === moduleKey;
}
function markIfAllowed(input, moduleKey, className) {
if (isModuleAllowed(moduleKey)) {
input.classList.add(className);
}
}
function countIfAllowed(stats, moduleKey, key) {
if (isModuleAllowed(moduleKey)) {
stats[key]++;
}
}
function makeSafeKey(text) {
return String(text || "UNKNOWN")
.replace(/\s+/g, " ")
.trim()
.toUpperCase();
}
function getClassesPerDay(table) {
const text = (table.innerText || "").toLowerCase();
return text.includes("business english") || text.includes("toeic")
? BUSINESS_TOEIC_CLASSES_PER_DAY
: DEFAULT_CLASSES_PER_DAY;
}
function getAllDocs(root = document) {
const docs = [root];
function walk(doc) {
doc.querySelectorAll("iframe").forEach(frame => {
try {
const d = frame.contentDocument || frame.contentWindow?.document;
if (d && !docs.includes(d)) {
docs.push(d);
walk(d);
}
} catch (e) {}
});
}
walk(root);
return docs;
}
function normalizeDate(text) {
const m = String(text || "").match(/\d{1,2}\/\d{1,2}\/\d{4}/);
if (!m) return null;
const [mm, dd, yyyy] = m[0].split("/");
return `${mm.padStart(2, "0")}/${dd.padStart(2, "0")}/${yyyy}`;
}
function isPastDate(dateText) {
const [mm, dd, yyyy] = String(dateText).split("/");
const date = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today; // 👈 WICHTIG: nur vor heute
}
function injectStyle(doc) {
if (doc.querySelector("#school-checker-style-v19")) return;
const style = doc.createElement("style");
style.id = "school-checker-style-v19";
style.textContent = `
.att-empty { outline:4px solid #ef4444 !important; background:#fee2e2 !important; }
.att-present { outline:4px solid #10b981 !important; background:#dcfce7 !important; }
.att-absent { outline:4px solid #f59e0b !important; background:#fef3c7 !important; }
.att-holiday { outline:4px solid #3b82f6 !important; background:#dbeafe !important; }
.att-middle-warning {
outline:6px solid #ec4899 !important;
background:#fce7f3 !important;
box-shadow:0 0 0 3px #ec4899 inset !important;
}
.grade-empty {
outline:4px solid #eab308 !important;
background:#fef9c3 !important;
}
.grade-ok {
outline:4px solid #10b981 !important;
background:#dcfce7 !important;
}
.grade-e {
outline:4px solid #64748b !important;
background:#e2e8f0 !important;
}
.grade-conflict {
outline:6px solid #f59e0b !important;
background:#fef3c7 !important;
box-shadow:0 0 0 3px #f59e0b inset !important;
}
.grade-test-makeup {
outline:6px solid #8b5cf6 !important;
background:#ede9fe !important;
box-shadow:0 0 0 3px #8b5cf6 inset !important;
}
.grade-fillable {
outline:6px solid #ef4444 !important;
background:#fee2e2 !important;
box-shadow:0 0 0 3px #ef4444 inset !important;
}
#school-checker-panel {
position:fixed;
bottom:14px;
left:14px;
right:auto;
top:auto;
width:430px;
z-index:999999;
background:#0b0b0f;
color:#fff;
border:1px solid rgba(255,255,255,.12);
border-radius:18px;
box-shadow:0 20px 60px rgba(0,0,0,.35);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;
font-size:13px;
overflow:hidden;
}
#school-checker-side-panel {
position:fixed;
bottom:14px;
left:460px;
right:auto;
width:340px;
max-height:520px;
overflow:auto;
z-index:999999;
background:#0b0b0f;
color:#fff;
border:1px solid rgba(255,255,255,.12);
border-radius:18px;
box-shadow:0 20px 60px rgba(0,0,0,.35);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;
font-size:13px;
padding:14px;
}
#school-checker-panel * {
box-sizing:border-box;
}
.sc-header {
padding:14px 16px;
border-bottom:1px solid rgba(255,255,255,.10);
display:flex;
justify-content:space-between;
align-items:center;
background:linear-gradient(180deg,#15151d,#0b0b0f);
}
.sc-title {
font-weight:700;
font-size:15px;
letter-spacing:.2px;
}
.sc-subtitle {
margin-top:2px;
color:#a1a1aa;
font-size:12px;
}
.sc-version {
color:#a1a1aa;
font-size:12px;
}
.sc-body {
padding:14px 16px 16px;
}
.sc-select {
width:100%;
margin:8px 0 12px;
padding:9px 10px;
border-radius:10px;
border:1px solid rgba(255,255,255,.14);
background:#17171f;
color:#fff;
font-size:12px;
outline:none;
}
.sc-grid {
display:grid;
grid-template-columns:1fr 1fr;
gap:8px;
margin:10px 0 12px;
}
.sc-card {
background:#17171f;
border:1px solid rgba(255,255,255,.10);
border-radius:12px;
padding:10px;
}
.sc-label {
color:#a1a1aa;
font-size:11px;
margin-bottom:4px;
}
.sc-value {
font-weight:700;
font-size:15px;
}
.sc-green { color:#22c55e; }
.sc-red { color:#ef4444; }
.sc-orange { color:#f59e0b; }
.sc-blue { color:#38bdf8; }
.sc-purple { color:#a78bfa; }
.sc-pink { color:#ec4899; }
.sc-gray { color:#94a3b8; }
.sc-gold { color:#eab308; }
.sc-btn {
width:100%;
margin-top:8px;
padding:10px 12px;
border:none;
border-radius:11px;
color:#111;
font-weight:700;
cursor:pointer;
font-size:13px;
}
.sc-btn-att { background:#22c55e; }
.sc-btn-fill { background:#38bdf8; }
.sc-btn-fix { background:#f59e0b; }
.sc-footer {
display:flex;
gap:8px;
margin-top:10px;
}
.sc-small-btn {
flex:1;
padding:8px;
border-radius:10px;
border:1px solid rgba(255,255,255,.12);
background:#17171f;
color:#fff;
cursor:pointer;
font-size:12px;
}
.sc-box {
margin-top:12px;
padding:12px;
border-radius:12px;
background:#111118;
border:1px solid rgba(255,255,255,.10);
color:#d4d4d8;
line-height:1.45;
}
.sc-box b {
color:#fff;
}
.sc-mini {
padding:12px 14px;
display:flex;
justify-content:space-between;
align-items:center;
}
.sc-mini button {
border:none;
background:#22c55e;
color:#111;
font-weight:700;
padding:8px 12px;
border-radius:10px;
cursor:pointer;
}
`;
doc.head.appendChild(style);
}
function clearMarks(doc) {
doc.querySelectorAll(
".att-empty,.att-present,.att-absent,.att-holiday,.att-middle-warning,.grade-empty,.grade-ok,.grade-e,.grade-conflict,.grade-test-makeup,.grade-fillable"
).forEach(el => {
el.classList.remove(
"att-empty",
"att-present",
"att-absent",
"att-holiday",
"att-middle-warning",
"grade-empty",
"grade-ok",
"grade-e",
"grade-conflict",
"grade-test-makeup",
"grade-fillable"
);
});
}
function saveAttendanceSlots() {
sessionStorage.setItem(
STORAGE_ATTENDANCE_SLOTS,
JSON.stringify(latestAttendanceSlots)
);
}
function loadAttendanceSlots() {
try {
const saved = JSON.parse(sessionStorage.getItem(STORAGE_ATTENDANCE_SLOTS) || "[]");
if (Array.isArray(saved) && saved.length) {
latestAttendanceSlots = saved;
}
} catch (e) {}
}
function getAttendanceDates(table) {
for (const row of [...table.querySelectorAll("tr")]) {
const dates = [...row.querySelectorAll("td")]
.map(td => normalizeDate(td.innerText))
.filter(Boolean);
if (dates.length >= 2) return dates;
}
return [];
}
function getModuleInfoFromText(text) {
const clean = String(text || "").replace(/\s+/g, " ").trim();
const session =
clean.match(/(?:Summer|Fall|Winter|Spring)\s+[ABC]\s+\d{4}/i)?.[0] ||
clean.match(/Session:\s*([^|]+)/i)?.[1]?.trim() ||
"Unknown Season";
const level =
clean.match(/Program\s*\/\s*Level:\s*([^|]+)/i)?.[1]?.trim() ||
clean.match(/Level\s*\/\s*Class Code:\s*([^|]+)/i)?.[1]?.trim() ||
clean.match(/Program:\s*([^|]+)/i)?.[1]?.trim() ||
"Unknown Program";
const classCode =
clean.match(/Class Code:\s*([^|]+)/i)?.[1]?.trim() ||
clean.match(/Class:\s*([^|]+)/i)?.[1]?.trim() ||
"";
const title = [session, level, classCode].filter(Boolean).join(" — ");
return {
key: makeSafeKey(title),
title
};
}
function getModuleInfoForAttendanceTable(table) {
const moduleRegex =
/(?:Summer|Fall|Winter|Spring)\s+[ABC]\s+\d{4}[\s\S]{0,250}?(?:Program\s*\/\s*Level:|Level\s*\/\s*Class Code:|Program:|Class Code:)[\s\S]{0,250}/gi;
const container =
table.closest(".inner_results_table2") ||
table.closest(".TabbedPanelsContent") ||
document.body;
const fullText = container.innerText || "";
const tableText = table.innerText || "";
const tableIndex = fullText.indexOf(tableText);
let bestMatch = "";
const matches = [...fullText.matchAll(moduleRegex)];
matches.forEach(match => {
const matchIndex = match.index || 0;
if (tableIndex === -1 || matchIndex <= tableIndex) {
bestMatch = match[0];
}
});
if (!bestMatch && matches.length) {
bestMatch = matches[0][0];
}
if (!bestMatch) {
bestMatch = fullText;
}
const info = getModuleInfoFromText(bestMatch);
if (!latestModules.has(info.key)) {
latestModules.set(info.key, info.title);
}
return info;
}
function getModuleInfoForGradeTable(table) {
let root =
table.closest(".TabbedPanelsContent") ||
table.closest(".inner_results_table2") ||
table.closest("table")?.parentElement ||
table.parentElement;
let scope = root;
let scopeText = "";
for (let i = 0; scope && i < 8; i++) {
const text = scope.innerText || "";
if (
/Program\s*\/\s*Level:/i.test(text) ||
/Level\s*\/\s*Class Code:/i.test(text) ||
/Class Code:/i.test(text) ||
/Session:/i.test(text) ||
/Summer|Fall|Winter|Spring/i.test(text)
) {
scopeText = text;
break;
}
scope = scope.parentElement;
}
if (!scopeText) {
scopeText = table.closest("body")?.innerText || table.innerText || "";
}
const info = getModuleInfoFromText(scopeText);
if (!latestModules.has(info.key)) {
latestModules.set(info.key, info.title);
}
return info;
}
function isMiddleAbsentPattern(dayFields) {
const codes = dayFields.map(f => f.value).filter(Boolean);
if (!codes.some(v => ATT_ABSENT.includes(v))) return false;
const presentIndexes = dayFields
.map((f, i) => PRESENT.includes(f.value) ? i : -1)
.filter(i => i !== -1);
if (!presentIndexes.length) return false;
const firstP = Math.min(...presentIndexes);
const lastP = Math.max(...presentIndexes);
return dayFields.some((f, i) =>
ATT_ABSENT.includes(f.value) && i > firstP && i < lastP
);
}
function buildAttendanceMap(docs, stats) {
let map = latestAttendanceMap || {};
const hasAttendanceTables = docs.some(doc =>
[...doc.querySelectorAll("table")].some(table =>
table.querySelector("input.attend_text_style")
)
);
if (hasAttendanceTables) {
map = {};
latestAttendanceFills = [];
latestAttendanceSlots = [];
}
docs.forEach(doc => {
doc.querySelectorAll("table").forEach(table => {
const dates = getAttendanceDates(table);
const inputs = [...table.querySelectorAll("input.attend_text_style")]
.filter(input =>
input.id.includes("attend_date") &&
!input.id.includes("makeup")
);
if (!dates.length || !inputs.length) return;
const moduleInfo = getModuleInfoForAttendanceTable(table);
const moduleKey = moduleInfo.key;
table.setAttribute("data-school-checker-module", moduleKey);
const classesPerDay = getClassesPerDay(table);
const dayGroups = {};
inputs.forEach((input, index) => {
const value = String(input.value || "").trim().toUpperCase();
const dateIndex = Math.floor(index / classesPerDay);
const slotIndex = index % classesPerDay;
const date = dates[dateIndex];
if (!date) return;
const moduleDateKey = `${moduleKey}|${date}`;
const dateOnlyKey = `DATE_ONLY|${date}`;
if (!map[moduleDateKey]) map[moduleDateKey] = [];
if (!map[dateOnlyKey]) map[dateOnlyKey] = [];
map[moduleDateKey].push(value);
map[dateOnlyKey].push(value);
latestAttendanceSlots.push({
moduleKey,
date,
slotIndex,
value
});
if (!dayGroups[date]) dayGroups[date] = [];
dayGroups[date][slotIndex] = {
input,
value,
date,
slotIndex
};
if (!value) {
if (isModuleAllowed(moduleKey) && isPastDate(date)) {
input.classList.add("att-empty");
latestAttendanceFills.push({
input,
date,
slot: slotIndex + 1,
moduleKey,
moduleTitle: moduleInfo.title
});
stats.attEmpty++;
stats.attAutoFillable++;
}
} else if (isModuleAllowed(moduleKey) && PRESENT.includes(value)) {
input.classList.add("att-present");
} else if (isModuleAllowed(moduleKey) && value === "H") {
input.classList.add("att-holiday");
} else if (isModuleAllowed(moduleKey) && NO_GRADE.includes(value)) {
input.classList.add("att-absent");
}
});
Object.values(dayGroups).forEach(group => {
const filledGroup = group.filter(Boolean);
if (filledGroup.length !== classesPerDay) return;
if (isMiddleAbsentPattern(filledGroup)) {
filledGroup.forEach(f => {
if (ATT_ABSENT.includes(f.value) && isModuleAllowed(moduleKey)) {
f.input.classList.add("att-middle-warning");
}
});
if (isModuleAllowed(moduleKey)) {
stats.middleAbsentWarnings++;
}
}
});
});
});
if (hasAttendanceTables) {
latestAttendanceMap = map;
saveAttendanceSlots();
}
return map;
}
function isGradeTable(table) {
return /\d{1,2}\/\d{1,2}\/\d{4}/.test(table.innerText || "") &&
table.querySelector("input");
}
function getGradeDates(table) {
const firstRow = table.querySelector("tr");
if (!firstRow) return [];
return [...firstRow.querySelectorAll("td")]
.map(td => normalizeDate(td.innerText));
}
function isZero(value) {
return String(value).trim() === "0";
}
function isValid70To100(value) {
const raw = String(value).trim();
const num = Number(raw);
return raw !== "" && !Number.isNaN(num) && num >= 70 && num <= 100;
}
function getGradeTabName(table) {
const names = [];
let current = table.closest(".TabbedPanelsContent");
while (current) {
const group = current.parentElement;
const panel = group?.closest(".TabbedPanels");
if (group && panel) {
const contents = [...group.children].filter(el =>
el.classList?.contains("TabbedPanelsContent")
);
const index = contents.indexOf(current);
const tabs = [
...panel.querySelectorAll(
":scope > .TabbedPanelsTabGroup1 > .TabbedPanelsTab1, " +
":scope > .TabbedPanelsTabGroup > .TabbedPanelsTab, " +
".TabbedPanelsTab1, .TabbedPanelsTab"
)
];
const name = tabs[index]?.innerText?.trim()?.toUpperCase();
if (name) names.unshift(name);
}
current = panel?.parentElement?.closest(".TabbedPanelsContent");
}
const tableText = (table.innerText || "").toUpperCase();
return [...names, tableText].join(" > ") || "UNKNOWN";
}
function isTestLikeTab(tabName) {
return tabName.includes("TEST") ||
tabName.includes("PRACTICE") ||
tabName.includes("PREP");
}
function isHomeworkTab(tabName) {
return tabName.includes("HOMEWORK");
}
function isParticipationTab(tabName) {
return tabName.includes("PARTICIPATION") || getParticipationSubject(tabName) !== null;
}
function getSuggestedMissingGrade(tabName) {
if (isHomeworkTab(tabName)) return "100";
if (isParticipationTab(tabName)) return randomGrade();
if (isTestLikeTab(tabName)) return randomGrade();
return randomGrade();
}
function addDaysToDate(dateText, days) {
const [mm, dd, yyyy] = String(dateText).split("/");
const date = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
date.setDate(date.getDate() + days);
const newMm = String(date.getMonth() + 1).padStart(2, "0");
const newDd = String(date.getDate()).padStart(2, "0");
const newYyyy = date.getFullYear();
return `${newMm}/${newDd}/${newYyyy}`;
}
function getDateObj(dateText) {
const [mm, dd, yyyy] = String(dateText).split("/");
return new Date(Number(yyyy), Number(mm) - 1, Number(dd));
}
function formatDateObj(date) {
const mm = String(date.getMonth() + 1).padStart(2, "0");
const dd = String(date.getDate()).padStart(2, "0");
const yyyy = date.getFullYear();
return `${mm}/${dd}/${yyyy}`;
}
function getParticipationRangeDates(startDateText, nextDateText) {
const start = getDateObj(startDateText);
const end = nextDateText
? getDateObj(nextDateText)
: getDateObj(addDaysToDate(startDateText, 7));
const dates = [];
const current = new Date(start);
while (current < end) {
dates.push(formatDateObj(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
function getParticipationSubject(tabName) {
const t = String(tabName || "").toUpperCase();
if (t.includes("LISTENING")) return "LISTENING";
if (t.includes("GRAMMAR") || t.includes("WRITING")) return "GRAMMAR";
if (t.includes("CMAR")) return "CMAR";
if (t.includes("IDIOM")) return "IDIOMS";
if (
t.includes("DISCUSSION") ||
t.includes("CONVERSATION") ||
t.includes("SPEAKING") ||
t.includes("READING") ||
t.includes("COMMUNICATION")
) {
return "DISCUSSION";
}
return null;
}
function isSlotRelevantForParticipation(subject, dayIndex, slotIndex) {
const isMonOrWed = dayIndex === 0 || dayIndex === 2;
const isTueOrThu = dayIndex === 1 || dayIndex === 3;
if (subject === "LISTENING") {
if (isMonOrWed) return slotIndex >= 0 && slotIndex <= 2;
if (isTueOrThu) return slotIndex >= 0 && slotIndex <= 1;
}
if (subject === "GRAMMAR") {
if (isMonOrWed) return slotIndex >= 3 && slotIndex <= 4;
if (isTueOrThu) return slotIndex >= 2 && slotIndex <= 4;
}
if (subject === "CMAR") {
return slotIndex === 0;
}
if (subject === "IDIOMS") {
if (isMonOrWed) return slotIndex >= 1 && slotIndex <= 2;
if (isTueOrThu) return slotIndex === 1;
}
if (subject === "DISCUSSION") {
if (isMonOrWed) return slotIndex >= 3 && slotIndex <= 4;
if (isTueOrThu) return slotIndex >= 2 && slotIndex <= 4;
}
return false;
}
function getAttendanceForGrade(attendanceMap, moduleKey, date) {
const moduleDateKey = `${moduleKey}|${date}`;
const dateOnlyKey = `DATE_ONLY|${date}`;
let attendance = (attendanceMap[moduleDateKey] || []).filter(Boolean);
if (!attendance.length) {
attendance = (attendanceMap[dateOnlyKey] || []).filter(Boolean);
}
if (attendance.length) {
return attendance;
}
// Business English + TOEIC special case:
// If grade date has no direct attendance match,
// check the next date and use the first 5 boxes.
const nextDate = addDaysToDate(date, 1);
const nextModuleDateKey = `${moduleKey}|${nextDate}`;
const nextDateOnlyKey = `DATE_ONLY|${nextDate}`;
const nextModuleAttendance =
(attendanceMap[nextModuleDateKey] || []).filter(Boolean);
if (nextModuleAttendance.length >= BUSINESS_TOEIC_CLASSES_PER_DAY) {
return nextModuleAttendance.slice(0, DEFAULT_CLASSES_PER_DAY);
}
const nextDateOnlyAttendance =
(attendanceMap[nextDateOnlyKey] || []).filter(Boolean);
if (nextDateOnlyAttendance.length >= BUSINESS_TOEIC_CLASSES_PER_DAY) {
return nextDateOnlyAttendance.slice(0, DEFAULT_CLASSES_PER_DAY);
}
return [];
}
function renderModuleSelect(panel) {
const select = panel.querySelector("#school-checker-module-select");
if (!select) return;
const current = getSelectedModule();
select.innerHTML = `<option value="ALL">All Modules</option>`;
latestModules.forEach((title, key) => {
const opt = document.createElement("option");
opt.value = key;
opt.textContent = title;
if (key === current) {
opt.selected = true;
}
select.appendChild(opt);
});
select.onchange = () => {
setSelectedModule(select.value);
runChecker({ skipFocusGuard: true });
};
}
function getInfoOpen() {
return localStorage.getItem(STORAGE_INFO_OPEN) || "";
}
function setInfoOpen(type) {
localStorage.setItem(STORAGE_INFO_OPEN, type || "");
}
function getInfoContent(type) {
if (type === "info") {
return `
<b>How it works</b><br><br>
AcademicFlow checks attendance and grade records directly in the browser.
It highlights missing attendance, missing grades, conflicts, and patterns that may require review.<br><br>
<b>Attendance colors</b><br>
<span class="sc-green">Green</span> = Present / Tardy<br>
<span class="sc-red">Red</span> = Missing attendance<br>
<span class="sc-orange">Orange</span> = Absent / Leave code<br>
<span class="sc-blue">Blue</span> = Holiday<br>
<span class="sc-pink">Pink</span> = Attendance sequence warning<br><br>
<b>Grade colors</b><br>
<span class="sc-green">Green</span> = Correct / no issue found<br>
<span class="sc-gold">Gold</span> = Missing grade / missing attendance match<br>
<span class="sc-red">Red</span> = Missing grade that needs review<br>
<span class="sc-orange">Orange</span> = Review needed<br>
<span class="sc-purple">Purple</span> = Special case / makeup review<br>
<span class="sc-gray">Gray</span> = Excused grade
`;
}
if (type === "rules") {
return `
<b>How to use</b><br><br>
1. Open <b>Attendance</b> first to load attendance data.<br>
2. Open <b>Grades</b> after that to activate the checker.<br>
3. Select a module if you only want to review one module.<br>
4. For grade review, open each grade tab separately so the checker can read the fields.<br>
5. Tabs such as <b>Grammar & Writing</b>, <b>Listening</b>, <b>Speaking & Reading</b>, <b>CMAR</b>, <b>Idioms</b>, and <b>Discussion</b> should be selected one by one.<br><br>
<b>Important</b><br>
AcademicFlow is designed to assist with attendance and grade review.
`;
}
if (type === "updates") {
return `
<b>Version ${APP_VERSION}</b><br><br>
<b style="color:#22c55e;">New in this version</b><br>
- Improved review workflow and interface<br>
- Cleaner validation and highlighting system<br>
- Improved module filtering and review flow<br>
- Updated instructions and color indicators<br>
- Performance and stability improvements<br><br>
<span style="color:#71717a;">
<b>Previous updates</b><br>
- Participation review improvements<br>
- Better attendance and grade validation<br>
- Business English + TOEIC support<br>
- Improved module detection<br>
- Practice Prep / Practice Test review support
</span><br><br>
<b>Update link</b><br>
<a href="https://update.greasyfork.org/scripts/576027/AcademicFlow.user.js" target="_blank" style="color:#38bdf8;text-decoration:none;font-weight:700;">
Install / Update AcademicFlow
</a>
`;
}
return "";
}
function renderSidePanel() {
let side = document.querySelector("#school-checker-side-panel");
const activeInfo = getInfoOpen();
if (!activeInfo) {
if (side) side.remove();
return;
}
if (!side) {
side = document.createElement("div");
side.id = "school-checker-side-panel";
document.body.appendChild(side);
}
const title =
activeInfo === "info" ? "Info" :
activeInfo === "rules" ? "Instructions" :
activeInfo === "updates" ? "Updates" :
"Information";
side.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<b>${title}</b>
<button id="school-checker-side-close" style="background:#17171f;color:white;border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:5px 9px;cursor:pointer;">Close</button>
</div>
<div class="sc-box">${getInfoContent(activeInfo)}</div>
`;
side.querySelector("#school-checker-side-close").onclick = () => {
setInfoOpen("");
renderSidePanel();
};
}
function toggleInfoBox(type) {
const current = getInfoOpen();
setInfoOpen(current === type ? "" : type);
runChecker({ skipFocusGuard: true });
}
function getWeekAttendanceForParticipation(moduleKey, dateText, nextDateText) {
const weekDates = getParticipationRangeDates(dateText, nextDateText);
let rows = latestAttendanceSlots.filter(row =>
row.moduleKey === moduleKey &&
weekDates.includes(row.date)
);
if (!rows.length) {
rows = latestAttendanceSlots.filter(row =>
weekDates.includes(row.date)
);
}
return rows
.map(row => String(row.value || "").trim().toUpperCase())
.filter(v => v && v !== "H");
}
function checkGradeInput({
input,
index,
date,
nextDate,
value,
attendance,
moduleKey,
moduleInfo,
tabName,
isTestTab,
stats
}) {
const hasPresent = attendance.some(v => PRESENT.includes(v));
const hasNoGradeCode = attendance.some(v => NO_GRADE.includes(v));
const allNoGrade =
attendance.length > 0 &&
attendance.every(v => NO_GRADE.includes(v));
if (!isPastDate(date)) {
return;
}
stats.gradesChecked++;
if (isParticipationTab(tabName)) {
const weekAttendance = getWeekAttendanceForParticipation(moduleKey, date, nextDate);
if (!weekAttendance.length) {
markIfAllowed(input, moduleKey, "grade-empty");
return;
}
const presentCount = weekAttendance.filter(v => PRESENT.includes(v)).length;
const absentCount = weekAttendance.filter(v => NO_GRADE.includes(v)).length;
let recommended;
if (absentCount === weekAttendance.length) {
recommended = 0;
} else if (presentCount === weekAttendance.length) {
recommended = 100;
} else {
recommended = randomGrade();
}
if (!value) {
markIfAllowed(input, moduleKey, "grade-fillable");
if (isModuleAllowed(moduleKey)) {
stats.missingGradeFillable++;
latestMissingGradeFills.push({
input,
date,
tab: "PARTICIPATION",
newValue: String(recommended),
moduleKey,
moduleTitle: moduleInfo.title
});
}
return;
}
const current = Number(value);
if (!Number.isNaN(current) && current >= 70 && current <= 100) {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
return;
}
markIfAllowed(input, moduleKey, "grade-conflict");
countIfAllowed(stats, moduleKey, "gradeConflicts");
if (isModuleAllowed(moduleKey)) {
latestGradeFixes.push({
input,
date,
oldValue: value,
tab: "PARTICIPATION",
moduleKey,
moduleTitle: moduleInfo.title,
forceValue: String(recommended)
});
}
return;
}
// Business English / TOEIC Practice Prep:
// Reading + Listening are manual score bars.
// Manual review tab. Empty = review, filled = OK.
if (tabName.includes("PRACTICE PREP") || tabName.includes("PRACTICE TEST")) {
if (!value) {
markIfAllowed(input, moduleKey, "grade-empty");
} else {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
}
return;
}
if (isTestTab && index === 0) {
if (!value) {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
stats.testFirstEmptyOk++;
} else {
markIfAllowed(input, moduleKey, "grade-conflict");
countIfAllowed(stats, moduleKey, "gradeConflicts");
countIfAllowed(stats, moduleKey, "testFirstFieldConflicts");
if (isModuleAllowed(moduleKey)) {
latestGradeFixes.push({
input,
date,
oldValue: value,
tab: tabName + " FIRST FIELD",
moduleKey,
moduleTitle: moduleInfo.title,
clearInsteadOfZero: true
});
}
}
return;
}
if (!attendance.length) {
stats.noAttendanceMatch++;
if (!value) {
markIfAllowed(input, moduleKey, "grade-empty"); // Gold
} else if (value === "E") {
markIfAllowed(input, moduleKey, "grade-e"); // Gray
stats.e++;
} else if (isHomeworkTab(tabName)) {
markIfAllowed(input, moduleKey, "grade-test-makeup"); // Purple
countIfAllowed(stats, moduleKey, "makeupWarnings");
} else {
markIfAllowed(input, moduleKey, "grade-conflict"); // Orange
countIfAllowed(stats, moduleKey, "gradeConflicts");
}
return;
}
if (value === "E") {
markIfAllowed(input, moduleKey, "grade-e");
stats.e++;
return;
}
if (hasPresent) {
const num = Number(value);
const isNum = value !== "" && !Number.isNaN(num);
if (!value) {
markIfAllowed(input, moduleKey, "grade-fillable");
if (isModuleAllowed(moduleKey)) {
const newValue = getSuggestedMissingGrade(tabName);
stats.missingGradeFillable++;
latestMissingGradeFills.push({
input,
date,
tab: tabName,
newValue,
moduleKey,
moduleTitle: moduleInfo.title
});
}
return;
}
if (isHomeworkTab(tabName)) {
if (value === "100") {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
} else {
markIfAllowed(input, moduleKey, "grade-conflict");
countIfAllowed(stats, moduleKey, "gradeConflicts");
if (isModuleAllowed(moduleKey)) {
latestGradeFixes.push({
input,
date,
oldValue: value || "empty",
tab: tabName,
moduleKey,
moduleTitle: moduleInfo.title,
forceValue: "100"
});
}
}
return;
}
if (isTestTab) {
if (value === "0") {
markIfAllowed(input, moduleKey, "grade-conflict");
countIfAllowed(stats, moduleKey, "gradeConflicts");
if (isModuleAllowed(moduleKey)) {
latestGradeFixes.push({
input,
date,
oldValue: value,
tab: tabName,
moduleKey,
moduleTitle: moduleInfo.title,
forceValue: randomGrade()
});
}
} else {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
}
return;
}
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
return;
}
if (allNoGrade || hasNoGradeCode) {
if (isHomeworkTab(tabName)) {
if (!value) {
markIfAllowed(input, moduleKey, "grade-fillable");
if (isModuleAllowed(moduleKey)) {
stats.missingGradeFillable++;
latestMissingGradeFills.push({
input,
date,
tab: tabName + " ABSENT HOMEWORK",
newValue: "0",
moduleKey,
moduleTitle: moduleInfo.title
});
}
} else if (isZero(value)) {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
} else {
markIfAllowed(input, moduleKey, "grade-test-makeup");
countIfAllowed(stats, moduleKey, "makeupWarnings");
}
return;
}
if (!value || isZero(value)) {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
if (!value) {
stats.emptyOk++;
}
} else if (isTestTab) {
markIfAllowed(input, moduleKey, "grade-test-makeup");
countIfAllowed(stats, moduleKey, "makeupWarnings");
} else {
markIfAllowed(input, moduleKey, "grade-conflict");
countIfAllowed(stats, moduleKey, "gradeConflicts");
if (isModuleAllowed(moduleKey)) {
latestGradeFixes.push({
input,
date,
oldValue: value,
tab: tabName,
moduleKey,
moduleTitle: moduleInfo.title
});
}
}
return;
}
if (!value) {
markIfAllowed(input, moduleKey, "grade-empty");
} else {
markIfAllowed(input, moduleKey, "grade-ok");
stats.ok++;
}
}
function runChecker(options = {}) {
const active = document.activeElement;
if (
!options.skipFocusGuard &&
active &&
active.id === "school-checker-module-select"
) {
return;
}
const docs = getAllDocs();
latestGradeFixes = [];
latestMissingGradeFills = [];
// latestModules NICHT jedes Mal löschen,
// sonst verschwinden Module beim Tab-Wechsel.
// Attendance NICHT jedes Mal löschen.
// Sonst verliert Grades die Attendance-Daten beim Tab-Wechsel.
const stats = {
attendanceDates: 0,
gradesChecked: 0,
ok: 0,
attEmpty: 0,
attAutoFillable: 0,
middleAbsentWarnings: 0,
missingGradeFillable: 0,
gradeConflicts: 0,
makeupWarnings: 0,
testFirstEmptyOk: 0,
testFirstFieldConflicts: 0,
e: 0,
emptyOk: 0,
noAttendanceMatch: 0
};
docs.forEach(doc => {
injectStyle(doc);
clearMarks(doc);
});
const hasAttendanceVisible = docs.some(doc =>
[...doc.querySelectorAll("input.attend_text_style")].some(input =>
input.id.includes("attend_date") && !input.id.includes("makeup")
)
);
let attendanceMap = latestAttendanceMap || {};
if (hasAttendanceVisible) {
attendanceMap = buildAttendanceMap(docs, stats) || latestAttendanceMap || {};
} else {
loadAttendanceSlots();
}
if (hasAttendanceVisible) {
latestAttendanceStats = {
attEmpty: stats.attEmpty,
attAutoFillable: stats.attAutoFillable,
attendanceDates: Object.keys(attendanceMap).length,
middleAbsentWarnings: stats.middleAbsentWarnings
};
} else {
stats.attEmpty = latestAttendanceStats.attEmpty;
stats.attAutoFillable = latestAttendanceStats.attAutoFillable;
stats.middleAbsentWarnings = latestAttendanceStats.middleAbsentWarnings;
}
stats.attendanceDates = hasAttendanceVisible
? Object.keys(attendanceMap).length
: latestAttendanceStats.attendanceDates;
docs.forEach(doc => {
doc.querySelectorAll("table").forEach(table => {
if (!isGradeTable(table)) return;
const moduleInfo = getModuleInfoForGradeTable(table);
const moduleKey = moduleInfo.key;
table.setAttribute("data-school-checker-module", moduleKey);
const tabName = getGradeTabName(table);
const isTestTab = isTestLikeTab(tabName);
const dates = getGradeDates(table);
const inputs = [
...table.querySelectorAll("input[type='text'], input:not([type])")
];
inputs.forEach((input, index) => {
const value = String(input.value || "").trim().toUpperCase();
const date = dates[index];
const nextDate = dates[index + 1] || null;
if (!date) return;
const attendance = getAttendanceForGrade(
attendanceMap,
moduleKey,
date
);
checkGradeInput({
input,
index,
date,
nextDate,
value,
attendance,
moduleKey,
moduleInfo,
tabName,
isTestTab,
stats
});
});
});
});
let panel = document.querySelector("#school-checker-panel");
if (!panel) {
panel = document.createElement("div");
panel.id = "school-checker-panel";
document.body.appendChild(panel);
}
if (isMinimized()) {
panel.innerHTML = `
<div class="sc-mini">
<div>
<div class="sc-title">${APP_NAME}</div>
<div class="sc-subtitle">v${APP_VERSION}</div>
</div>
<button id="school-checker-expand">Open</button>
</div>
`;
renderSidePanel();
panel.querySelector("#school-checker-expand").onclick = () => {
setMinimized(false);
runChecker({ skipFocusGuard: true });
};
return;
}
panel.innerHTML = `
<div class="sc-header">
<div>
<div class="sc-title">${APP_NAME}</div>
<div class="sc-subtitle">Review Assistant</div>
</div>
<div class="sc-version">v${APP_VERSION}</div>
</div>
<div class="sc-body">
<select id="school-checker-module-select" class="sc-select"></select>
<div class="sc-grid">
<div class="sc-card">
<div class="sc-label">Missing Attendance</div>
<div class="sc-value sc-red">${stats.attEmpty}</div>
</div>
<div class="sc-card">
<div class="sc-label">Missing Grades</div>
<div class="sc-value sc-red">${stats.missingGradeFillable}</div>
</div>
<div class="sc-card">
<div class="sc-label">Review Needed</div>
<div class="sc-value sc-orange">${stats.gradeConflicts}</div>
</div>
<div class="sc-card">
<div class="sc-label">Checked OK</div>
<div class="sc-value sc-green">${stats.ok}</div>
</div>
</div>
<div class="sc-card">
<div class="sc-label">Details</div>
Modules detected: ${latestModules.size}<br>
Dates: ${stats.attendanceDates}<br>
Grades checked: ${stats.gradesChecked}<br>
Test first field issues:
<span class="sc-orange">${stats.testFirstFieldConflicts}</span><br>
Makeup warnings:
<span class="sc-purple">${stats.makeupWarnings}</span><br>
Attendance pattern warnings:
<span class="sc-pink">${stats.middleAbsentWarnings}</span><br>
Missing grade / attendance match:
<span class="sc-gold">${stats.noAttendanceMatch}</span>
</div>
<div class="sc-footer">
<button class="sc-small-btn" id="school-checker-info">Info</button>
<button class="sc-small-btn" id="school-checker-rules">Instructions</button>
<button class="sc-small-btn" id="school-checker-updates">Updates</button>
<button class="sc-small-btn" id="school-checker-minimize">Minimize</button>
</div>
</div>
`;
renderSidePanel();
renderModuleSelect(panel);
panel.querySelector("#school-checker-info").onclick = e => {
e.stopPropagation();
toggleInfoBox("info");
};
panel.querySelector("#school-checker-rules").onclick = e => {
e.stopPropagation();
toggleInfoBox("rules");
};
panel.querySelector("#school-checker-updates").onclick = e => {
e.stopPropagation();
toggleInfoBox("updates");
};
panel.querySelector("#school-checker-minimize").onclick = () => {
setMinimized(true);
runChecker({ skipFocusGuard: true });
};
console.log(`${APP_NAME} v${APP_VERSION}`, stats);
}
runChecker({ skipFocusGuard: true });
setInterval(() => {
const active = document.activeElement;
if (active && active.id === "school-checker-module-select") return;
runChecker();
}, 3000);
document.addEventListener("click", () => {
setTimeout(() => runChecker(), 300);
setTimeout(() => runChecker(), 900);
}, true);
console.log(`${APP_NAME} v${APP_VERSION} active.`);
})();