// ==UserScript==
// @name sv.HaUI
// @namespace https://github.com/vuquan2005/ScriptsMonkey
// @version 20.18.2
// @description Công cụ hỗ trợ cho sinh viên HaUI
// @author QuanVu
// @downloadURL https://github.com/vuquan2005/ScriptsMonkey/raw/main/Scripts/svHaUI_Helper.user.js
// @updateURL https://github.com/vuquan2005/ScriptsMonkey/raw/main/Scripts/svHaUI_Helper.user.js
// @match https://sv.haui.edu.vn/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @require https://cdn.jsdelivr.net/npm/notyf/notyf.min.js
// ==/UserScript==
(function () {
"use strict";
function waitForSelector(selector, timeout = 10000, delay = 10) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
return setTimeout(() => resolve(element), delay);
}
let timeoutId;
if (timeout > 0) {
timeoutId = setTimeout(() => {
observer.disconnect();
reject(
new Error(`⏱️ Timeout: Không tìm thấy "${selector}" trong ${timeout}ms.`)
);
}, timeout);
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
clearTimeout(timeoutId);
observer.disconnect();
setTimeout(() => resolve(element), delay);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
});
}
function runOnUrl(callback, ...validLinks) {
const href = window.location.href;
const pathname = window.location.pathname.replace(/\/+$/, "") || "/";
const callbackName = callback.name; // || new Error().stack.replace("Error", "Callback: ");
for (const link of validLinks) {
if (typeof link === "string") {
if (link === pathname || link === href || link === "") {
console.log(`✅ ${callbackName} :`, link || "All");
return callback();
}
} else if (link instanceof RegExp) {
if (link.test(href)) {
console.log(`✅ ${callbackName} :`, link);
return callback();
}
}
}
// console.log(`❌ ${callback.name || "'Callback'"} :`, validLinks);
}
async function fetchDOM(url) {
try {
const response = await fetch(url, {
method: "GET",
credentials: "include",
headers: {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"accept-language": "vi,en-US;q=0.9,en;q=0.8",
"upgrade-insecure-requests": "1",
},
});
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
} catch (err) {
console.error("❌ Lỗi khi fetch dữ liệu:", err);
throw err;
}
}
function getTimeDifference(inputDateTime) {
// Chuyển đổi chuỗi thời gian đầu vào (định dạng: "15h00 01/03/2024")
const [time, date] = inputDateTime.split(" ");
const [hour, minute] = time.split("h").map(Number);
const [day, month, year] = date.split("/").map(Number);
// Tạo đối tượng Date từ đầu vào (tháng trong JavaScript bắt đầu từ 0)
const inputDate = new Date(year, month - 1, day, hour, minute);
const currentDate = new Date();
// Tính khoảng cách thời gian (miligiây)
const diffMs = Math.abs(currentDate - inputDate);
const days = Math.floor(diffMs / 86400000);
const hours = Math.floor((diffMs % 86400000) / 3600000);
const minutes = Math.floor((diffMs % 3600000) / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
// Xác định thời gian đầu vào nằm trong quá khứ hay tương lai
const direction = inputDate < currentDate ? -1 : 1;
// Trả về kết quả
return {
days,
hours,
minutes,
seconds,
direction,
toString: ({
showDays = true,
showHours = true,
showMinutes = false,
showSeconds = false,
} = {}) => {
const parts = [];
if (showDays && days > 0) parts.push(`${days} ngày`);
if (showHours && hours > 0) parts.push(`${hours} giờ`);
if (showMinutes && minutes > 0) parts.push(`${minutes} phút`);
if (showSeconds && seconds > 0) parts.push(`${seconds} giây`);
const timeString = parts.length > 0 ? parts.join(", ") : "0 giây";
return inputDate < currentDate ? `${timeString} trước` : `Còn ${timeString}`;
},
};
}
// From repo AdBlock
GM_addStyle(`
[href="/sso/qpan"],
#sub_testonline,
#sub_testonlineqpan,
#sub_dakltnonline {
display: none !important;
}
`);
GM_addStyle(`
@import url("https://cdn.jsdelivr.net/npm/notyf/notyf.min.css");
`);
var notyf;
const creditsBoxColor = {
"5.0": "rgb(200, 0, 100)",
"4.0": "rgb(255, 0, 0)",
"3.0": "rgb(255, 165, 0)",
"2.0": "rgb(0, 191, 255)",
"1.0": "rgb(46, 204, 64)",
};
const scoresBoxColor = {
4: "rgb(64,212,81)", // A
3.5: "rgb(49, 163, 255)", // B+
3: "rgb(20, 120, 230)", // B
2.5: "rgb(255,186,0)", // C+
2: "rgb(255,144,0)", // C
1.5: "rgb(255, 50, 0)", // D+
1: "rgb(200, 0, 0)", // D
0: "rgb(157, 0, 255)", // F
};
function checkNonCreditCourse(courseCredit) {
const isNonCreditCourse = courseCredit.getAttribute("nonCreditCourse") === "true";
return isNonCreditCourse;
}
function letterTo4(scoreLetter) {
return {
F: 0,
D: 1,
"D+": 1.5,
C: 2,
"C+": 2.5,
B: 3,
"B+": 3.5,
A: 4,
}[scoreLetter.trim().toUpperCase()];
}
function scoreToLetter(score4) {
return {
4.0: "A",
3.5: "B+",
3.0: "B",
2.5: "C+",
2.0: "C",
1.5: "D+",
1.0: "D",
0.0: "F",
}[score4];
}
//===============================================================
// Sửa tiêu đề trang
function changeTitle() {
let title = document.querySelector("span.k-panel-header-text:first-child")?.textContent;
if (title) {
title = title
.replace(/trường đại học công nghiệp hà nội/gi, "🏫")
.replace(/đại học công nghiệp hà nội/gi, "🏫")
.replace("CHI TIẾT HỌC PHẦN", "ℹ️")
.replace("CHI TIẾT", "ℹ️")
.replace("Kết quả thi các môn", "🎯 Điểm học phần")
.replace("Kết quả học tập các học phần", "🎯 Điểm TX");
title =
runOnUrl(
() => {
const kgrid = document.querySelector("div.kGrid");
const name = kgrid
.querySelector("table > tbody > tr > td:nth-child(2)")
.textContent.trim();
const className = kgrid
.querySelector("table > tbody > tr:nth-child(3) > td:nth-child(2)")
.textContent.trim();
title = title.replace("🎯 Điểm học phần", "🎯 Điểm: ");
title = title.replace("Kết quả học tập các môn", "🎯 Điểm TX: ");
return (
title + " " + (name ? name : "") + ": " + (className ? className : "")
);
},
"/student/result/viewexamresult",
"/student/result/viewstudyresult"
) || title;
title =
runOnUrl(
() => {
const className = document
.querySelector("table > tbody > tr > td:nth-child(2)")
.textContent.trim();
const classCode = document
.querySelector("table > tbody > tr:nth-child(3) > td:nth-child(2)")
.textContent.trim();
title = title.replace("Kết quả học tập trên lớp", "🎯 Điểm TX: ");
title = title.replace("Bảng kết quả thi", "🎯 Điểm thi: ");
return (
title +
" " +
(className ? className : "") +
": " +
(classCode ? classCode : "")
);
},
"/student/result/viewexamresultclass",
"/student/result/viewstudyresultclass"
) || title;
document.title = title;
}
}
// Thay đổi đường dẫn trang chủ
function changeHomePagePath() {
const homeElement = document.querySelector("div.left-sidebar-content a[href='/']");
homeElement.href = "/home";
const logo = document.querySelector(".navbar-brand");
logo.href = "/home";
}
// Hiển thị GPA trên thanh menu
function displayGPA() {
const info = GM_getValue("yourInfo") || {};
const menuTitle = document.querySelector("ul.sidebar-elements");
const container = document.createElement("li");
container.className = "bar-container";
container.innerHTML = `
`;
menuTitle.insertAdjacentElement("afterbegin", container);
GM_addStyle(`
.bar-container {
width: 80%;
margin: auto;
display: flex;
}
.bar-data {
color: #1c274d;
font-size: 14px;
font-weight: bold;
padding: 10px;
margin: auto;
background-color: white;
border-radius: 8px;
text-align: center;
display: inline-block;
}
`);
}
// Khảo sát nhanh
function fastSurvey() {
waitForSelector("table.card-body.table-responsive.table.table-bordered.table-striped").then(
(element) => {
const scores = element.querySelectorAll("thead > tr:nth-child(2) > td");
for (const score of scores) {
const scoreId = score.textContent.trim().match(/\d+/)[0];
const inputSelectScore = document.createElement("input");
inputSelectScore.type = "radio";
inputSelectScore.name = "select_score";
inputSelectScore.value = scoreId;
score.prepend(inputSelectScore);
inputSelectScore.addEventListener("change", function () {
const scoreElements = element.querySelectorAll(
`td[title="${scoreId} điểm"] > input`
);
for (const scoreElement of scoreElements) {
scoreElement.checked = true;
}
notyf.success(`Đã chọn ${scoreId} điểm`);
});
}
}
);
}
// Hỗ trợ captcha
function captchaHelper(captchaInput, captchaSubmit) {
captchaInput.style.textTransform = "lowercase";
captchaInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
if (captchaInput.value.length == 5) captchaInput.blur();
}
});
captchaInput.addEventListener("blur", (e) => {
captchaInput.value = captchaInput.value.trim().toLowerCase();
captchaInput.value = captchaInput.value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d");
if (captchaInput.value.length == 5) captchaSubmit.click();
});
}
function captchaHelperLogin() {
captchaHelper(
document.querySelector("input#ctl00_txtimgcode"),
document.querySelector("input#ctl00_butLogin")
);
}
function captchaHelperRegister() {
captchaHelper(
document.querySelector("input#ctl02_txtimgcode"),
document.querySelector("input#ctl02_btnSubmit")
);
}
// Tùy biến trang chủ
function customizeHomePage() {
const frmMain = document.querySelector("form#frmMain");
if (frmMain) {
const html = `
`;
frmMain.innerHTML = html;
GM_addStyle(`
.panel {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
padding: 10px;
}
.panel-border-color-primary {
border: 2px solid #eaf3fdff;
}
.panel-heading {
color: black;
padding: 12px 16px;
border-bottom: 1px solid #0056b3;
}
.panel-title {
margin: 0;
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.panel-title a {
color: black;
text-decoration: none;
transition: color 0.3s;
}
.panel-title a:hover {
color: #3a68ffff;
}
.panel-icon {
stroke: white;
}
.shortcut-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
padding: 16px;
}
.shortcut-list li {
display: flex;
}
.shortcut-list a {
font-size: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
text-decoration: none;
color: #333;
padding: 12px;
border-radius: 6px;
transition: background-color 0.3s, transform 0.2s;
width: 100%;
}
.shortcut-list a:hover {
color: #3a68ffff;
background-color: #f1f5f9;
transform: translateY(-2px);
}
.shortcut-list a:hover .icon {
stroke: #0056b3;
}
.shortcut-list span {
font-weight: 400;
line-height: 1.4;
}
.shortcut-icon {
width: 40px;
height: 40px;
margin-bottom: 5px;
transition: transform 0.2s;
}
.shortcut-icon:hover {
transform: translateY(-3px);
}
.table {
width: 100%;
border-collapse: collapse;
margin: 16px 20px 0;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
padding: 10px;
text-align: left;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f8f9fa;
}
.kTableHeader {
background-color: #e9ecef;
font-weight: 500;
color: #333;
}
.table tbody tr:hover {
background-color: #e0e7ff;
}
@media (max-width: 600px) {
.shortcut-list {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.shortcut-list a {
padding: 8px;
}
.shortcut-list span {
font-size: 0.85rem;
}
.table {
font-size: 0.85rem;
}
.table th,
.table td {
padding: 8px;
}
}
`);
}
const autoCheckExamPlan = GM_getValue("autoCheckExamPlan");
console.log("Auto check exam plan, exam schedule: ", autoCheckExamPlan);
if (autoCheckExamPlan == undefined)
GM_setValue("autoCheckExamPlan", confirm("Bạn có muốn tự động kiểm tra lịch thi?"));
if (GM_getValue("autoCheckExamPlan") === true) {
showExamPlanInHomePage();
showExamScheduleInHomePage();
}
document.querySelector("#exam-schedule-icon").addEventListener("click", () => {
showExamScheduleInHomePage();
});
document.querySelector("#exam-plan-icon").addEventListener("click", () => {
showExamPlanInHomePage();
});
}
// Lấy mã học phần từ trang kế hoạch thi
function getCourseCode(scope = document) {
const listCourseCodeElement = scope.querySelectorAll(
"div:nth-child(3) > div > div > table > tbody > tr > td:nth-child(2) > a"
);
let listHPCode = [];
for (const element of listCourseCodeElement) {
const hpCode = element.textContent.trim();
if (hpCode) {
listHPCode.push(hpCode);
}
}
listHPCode.reverse();
return listHPCode;
}
// Lấy kế hoạch thi của học phần
async function getExamPlan(courseCode) {
const url = `https://sv.haui.edu.vn/student/schedulefees/examplant?code=${courseCode}`;
try {
const dom = await fetchDOM(url);
return dom.querySelector("#ctl02_ctl00_viewResult > div > div > table > tbody > tr");
} catch (err) {
console.error(`❌ Lỗi khi lấy lịch thi cho ${getHPCode}: `, err);
notyf.error(`Lỗi khi lấy lịch thi cho ${getHPCode}: `, err);
}
}
// Hàm xử lý logic chung cho việc hiển thị kế hoạch thi
async function processExamPlans(listCourseCode, container) {
// Lấy 12 học phần gần nhất
listCourseCode = listCourseCode.slice(0, 12);
let i = 0;
for (const courseCode of listCourseCode) {
let examPlan = await getExamPlan(courseCode);
if (examPlan == null) continue;
const examDate = examPlan.children[3].textContent.trim();
const examHour = examPlan.children[4].textContent.trim();
const examTime = `${examHour} ${examDate}`;
const diffTime = getTimeDifference(examTime);
if ((diffTime.direction === -1 && diffTime.days <= 20) || diffTime.direction === 1) {
i++;
const stt = examPlan.children[0];
stt.textContent = `${i} 📥`;
if (diffTime.direction === 1 && diffTime.days <= 7)
examPlan.style.backgroundColor = "#f89c87";
else if (diffTime.direction === 1) examPlan.style.backgroundColor = "#f8e287";
examPlan.children[3].innerHTML += `
(${diffTime.toString()})`;
stt.setAttribute("title", "Click để tải file ICS lịch thi");
stt.addEventListener("click", () => {
const name = examPlan.children[2].textContent.trim();
const classCode = examPlan.children[1].textContent.trim();
const startDate = examDate.split("/").reverse().join("");
const startTime = examHour.replace("h", "").padStart(4, "0");
const endTime = (Number(startTime) + 200).toString().padStart(4, "0");
const eventData = {
summary: `Thi ${name}`,
uid: `exam-${classCode}@sv.haui.edu.vn`,
startDate: startDate,
startTime: startTime,
endTime: endTime,
description: ``,
location: "",
alarms: [15, 30],
};
console.log(eventData);
createICSFile(eventData, `Lich_thi_${name}_${classCode}`);
});
container.appendChild(examPlan);
}
await delay(10);
}
return i;
}
async function showExamPlan() {
let listCourseCode = getCourseCode(document);
const container = document.querySelector(
"#ctl02_ctl00_viewResult > div > div > table > tbody"
);
await processExamPlans(listCourseCode, container);
}
async function showExamPlanInHomePage() {
const examPlanDOM = await fetchDOM("https://sv.haui.edu.vn/student/schedulefees/examplant");
let listCourseCode = getCourseCode(examPlanDOM);
const container = document.querySelector("#exam-plan-body");
const count = await processExamPlans(listCourseCode, container);
if (count === 0) notyf.error("Không có kế hoạch thi");
else notyf.success("Đã lấy thành công kế hoạch thi");
}
// Hiển thị lịch thi
async function showExamScheduleInHomePage() {
const examScheduleDOM = await fetchDOM(
"https://sv.haui.edu.vn/student/schedulefees/transactionmodules"
);
const examScheduleContainer = document.querySelector("#exam-schedule-body");
const examSchedule = examScheduleDOM.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
let i = 0;
for (const examScheduleElement of examSchedule) {
const examDate = examScheduleElement.children[2].textContent.trim();
const examHour = examScheduleElement.children[3].textContent.trim();
const examTime = `${examHour} ${examDate}`;
// Kiểm tra thời gian thi
const diffTime = getTimeDifference(examTime);
if ((diffTime.direction === -1 && diffTime.days <= 20) || diffTime.direction === 1) {
i++;
const checkItem = examScheduleElement.children[13];
const indexItem = examScheduleElement.children[0];
checkItem.remove();
indexItem.textContent = `${i}`;
// Tô màu sắp thi
if (diffTime.direction === 1 && diffTime.days <= 7)
examScheduleElement.style.backgroundColor = "#f89c87";
else if (diffTime.direction === 1)
examScheduleElement.style.backgroundColor = "#f8e287";
// Hiển thị khoảng cách ngày
examScheduleElement.children[2].innerHTML += `
(${diffTime.toString()})`;
examScheduleContainer.appendChild(examScheduleElement);
}
}
if (i === 0) notyf.error("Không có lịch thi");
else notyf.success("Đã lấy thành công lịch thi");
}
// Xắp xếp lịch thi
function sortExamSchedule() {
// xắp xếp lịch thi
const examScheduleContainer = document.querySelector(
"div.kGrid > div > table:nth-child(3) > tbody"
);
const examSchedule = document.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
// console.log("examSchedule: ", examSchedule);
for (let i = examSchedule.length - 1; i >= 0; i--) {
examScheduleContainer.appendChild(examSchedule[i]);
}
}
// Tô màu lịch thi
function highlightExamSchedule() {
const examSchedule = document.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
for (const examElement of examSchedule) {
const examDate = examElement.children[2].textContent.trim();
const examHour = examElement.children[3].textContent.trim();
const examTime = `${examHour} ${examDate}`;
// Kiểm tra thời gian thi
const diffTime = getTimeDifference(examTime);
if (diffTime.direction === 1) {
examElement.style.backgroundColor = "rgb(248,226,135)";
// Hiển thị khoảng cách ngày
examElement.children[2].innerHTML += `
(${diffTime.toString()})`;
}
}
}
// Kiểm tra học phần không tính tín chỉ theo mặc định
function checkDefaultNonCreditCourse(courseCode) {
courseCode = courseCode.trim().toUpperCase();
const nonCreditCourse = [
"PE60", // Giáo dục thể chất
"DC600", // Giáo dục quốc phòng
"IC6005", // Công nghệ thông tin cơ bản
"IC6006", // Công nghệ thông tin nâng cao khối KTXH
"IC6007", // Công nghệ thông tin nâng cao khối Kỹ thuật
"/FL60(91|92|93|94)/", // TA cơ khí cơ bản
"FL61",
"FL62",
// "FL63" // Ngôn ngữ chuyên ngành
"/FL65(?!82|83)\\d{2}/", // Ngôn ngữ cơ bản từ K20, loại trừ FL682, FL683 tiếng Đức
"/FL\\d+OT/", // Ôn tập ngôn ngữ
];
let nCodes = GM_getValue("nonCreditCourse", []);
if (nCodes.length == 0) {
GM_setValue("nonCreditCourse", Array.from(new Set([...nonCreditCourse, ...nCodes])));
nCodes = GM_getValue("nonCreditCourse");
console.log("Set nonCreditCourse", nCodes);
}
nCodes = nCodes.map((code) => {
if (typeof code === "string" && code.startsWith("/") && code.endsWith("/")) {
const pattern = code.slice(1, -1);
return new RegExp(pattern);
}
return code;
});
for (const nCode of nCodes) {
if (typeof nCode === "string") {
if (courseCode.startsWith(nCode)) {
return true;
}
} else if (nCode instanceof RegExp) {
if (nCode.test(courseCode)) {
return true;
}
}
}
return false;
}
// Tô màu tín chỉ
function highlightCreditsCourse() {
const kgrid = document.querySelector("div.kGrid");
const hocPhan = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
for (const row of hocPhan) {
const courseCode = row.children[1].textContent.trim();
const courseCredit = row.children[5];
const scoreCell = row.children[13];
courseCredit.setAttribute("title", "👆");
courseCredit.addEventListener("click", () => {
const isNonCreditCourse = courseCredit.getAttribute("nonCreditCourse") === "false";
courseCredit.setAttribute("nonCreditCourse", isNonCreditCourse ? "true" : "false");
console.log("isNonCreditCourse", isNonCreditCourse);
if (isNonCreditCourse) {
const originalScore = scoreCell.getAttribute("originalScore");
scoreCell.textContent = originalScore ? originalScore : scoreCell.textContent;
scoreCell.focus();
scoreCell.blur();
courseCredit.style.backgroundColor = "";
courseCredit.style.color = "";
scoreCell.setAttribute("contenteditable", "false");
} else {
courseCredit.style.backgroundColor =
creditsBoxColor[courseCredit.textContent.trim()];
courseCredit.style.color = "#FFFFFF";
scoreCell.setAttribute("contenteditable", "true");
scoreCell.focus();
scoreCell.blur();
}
});
if (checkDefaultNonCreditCourse(courseCode)) {
courseCredit.setAttribute("nonCreditCourse", "true");
continue;
}
courseCredit.setAttribute("nonCreditCourse", "false");
courseCredit.style.backgroundColor = creditsBoxColor[courseCredit.textContent.trim()];
courseCredit.style.color = "#FFFFFF";
}
}
// Tô màu điểm thi
function highlightExamScores() {
const kgrid = document.querySelector("div.kGrid");
const hocPhan = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
for (const row of hocPhan) {
// Bỏ qua nonCreditCourse
const courseCredit = row.children[5];
const score4Text = row.children[12].textContent.trim();
const scoreLetter = row.children[13];
if (checkNonCreditCourse(courseCredit) || score4Text == "" || score4Text == "**") {
scoreLetter.style.backgroundColor = "";
scoreLetter.style.color = "";
continue;
}
// console.log(diemSo);
// Tô màu điểm
scoreLetter.style.backgroundColor = scoresBoxColor[Number(score4Text)];
scoreLetter.style.color = "#FFFFFF";
}
}
// Tô màu điểm TX
function highlightStudyScores() {
let tx1Index = 4;
if (window.location.pathname.includes("student/result/viewstudyresult")) {
tx1Index = 3;
}
const kgrid = document.querySelector("div.kGrid");
const hocPhan = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
const regex = /FL\d{4}OT\.\d/;
for (const row of hocPhan) {
if (regex.test(row.children[2].textContent.trim())) continue;
// Tô những học phần chưa có điểm
if (row.children[tx1Index].textContent.trim() == "")
row.children[tx1Index].style.backgroundColor = "rgb(248,226,135)";
}
}
// Chuyển đổi giữa trang kết quả học tập và kết quả thi
function toggleStudyAndExam() {
const title = document.querySelector("div.panel-heading");
const toggleLinkContainer = document.createElement("p");
toggleLinkContainer.id = "toggle-link-container";
const toggleLink = document.createElement("a");
toggleLink.style.color = "gray";
toggleLink.style.fontSize = "14px";
toggleLinkContainer.appendChild(toggleLink);
if (window.location.pathname === "/student/result/studyresults") {
toggleLink.href = "https://sv.haui.edu.vn/student/result/examresult";
toggleLink.textContent = "➡️🎯Kết quả thi";
} else if (window.location.pathname === "/student/result/examresult") {
toggleLink.href = "https://sv.haui.edu.vn/student/result/studyresults";
toggleLink.textContent = "➡️🎯Kết quả học tập";
} else {
if (window.location.pathname.includes("exam")) {
toggleLink.href = window.location.href.replace("exam", "study");
toggleLink.textContent = "➡️🎯Kết quả học tập";
} else {
toggleLink.href = window.location.href.replace("study", "exam");
toggleLink.textContent = "➡️🎯Kết quả thi";
}
}
title.appendChild(toggleLinkContainer);
}
// Chuyển đổi giữa trang chi tiết học phần theo CDIO và theo ngành
function toggleCourseInfo() {
const title = document.querySelector("div.panel-heading");
const toggleLinkContainer = document.createElement("p");
toggleLinkContainer.id = "toggle-link-container";
const toggleLink = document.createElement("a");
toggleLink.style.color = "gray";
toggleLink.style.fontSize = "14px";
toggleLinkContainer.appendChild(toggleLink);
if (window.location.pathname === "/training/viewmodulescdiosv/xem-chi-tiet-hoc-phan.htm") {
toggleLink.href = window.location.href.replace(
"viewmodulescdiosv",
"viewcourseindustry2"
);
toggleLink.textContent = "➡️ Chi tiết học phần theo ngành";
} else if (
window.location.pathname === "/training/viewcourseindustry2/xem-chi-tiet-hoc-phan.htm"
) {
toggleLink.href = window.location.href.replace(
"viewcourseindustry2",
"viewmodulescdiosv"
);
toggleLink.textContent = "➡️ Chi tiết học phần theo CDIO";
}
title.appendChild(toggleLinkContainer);
}
// Thêm link đến trang chi tiết học phần
function gotoCourseInfoStudy() {
const courses = document.querySelectorAll("div.kGrid .table tr");
let courseCode2ID = GM_getValue("~~~courseCode2ID", undefined);
if (courseCode2ID == undefined) return;
for (const course of courses) {
const className = course.children[2];
if (!/\w{2}\d{4}/.test(className.textContent)) continue;
const courseCode = className.textContent.trim().match(/\w{2}\d{4}/)[0];
className.innerHTML = `${className.textContent}`;
}
}
function gotoCourseInfoExam() {
const courses = document.querySelectorAll("div.kGrid .table tr");
let courseCode2ID = GM_getValue("~~~courseCode2ID", {});
for (const course of courses) {
const courseID = course.children[2]?.textContent.trim() || "";
const courseCodeElement = course.children[1];
const courseCode = courseCodeElement?.textContent.trim() || "";
if (!/HP\d{4}/.test(courseID)) continue;
courseCode2ID[courseCode] = courseID;
courseCodeElement.innerHTML = `${courseCode}`;
}
if (Object.keys(courseCode2ID).length < courses.length)
GM_setValue("~~~courseCode2ID", courseCode2ID);
}
function gotoCourseInfoGPA() {
const courses = document.querySelectorAll(".table.table-condensed tr");
let courseCode2ID = {};
for (const course of courses) {
const courseID = course.children[1]?.textContent.trim() || "";
const courseCodeCell = course.children[2];
const courseCode = courseCodeCell?.textContent.trim() || "";
if (!/HP\d{4}/.test(courseID)) continue;
courseCode2ID[courseCode] = courseID;
courseCodeCell.innerHTML = `${courseCode}`;
}
GM_setValue("~~~courseCode2ID", courseCode2ID);
}
// Hiển thị hệ số điểm trong chi tiết học phần
function showScoreWeight() {
const title = document.querySelector("div.panel-heading");
const courseCode = title.textContent.match(/([A-Z]{2})\d{4}/)[0];
const scoresType = document.querySelectorAll(
"td.k-table-viewdetail > table > tbody:nth-child(2) > tr > td.tdTh1"
);
const scoreWeight = document.querySelectorAll(
"td.k-table-viewdetail > table > tbody:nth-child(2) > tr > td.tdTh2"
);
const elementContainer = document.createElement("p");
elementContainer.id = "he-so-diem";
elementContainer.style.fontSize = "14px";
// Lấy hệ số điểm
let saveScoreWeight = {};
saveScoreWeight = GM_getValue("~scoreWeight", {});
// reset hp hiện tại
saveScoreWeight[courseCode] = "";
let elementHtml = "";
for (let i = 0; i < scoresType.length; i++) {
const type = scoresType[i].textContent.trim();
const heSo = scoreWeight[i].textContent.trim();
elementHtml += `${type}: ${heSo}
`;
saveScoreWeight[courseCode] += heSo + " | ";
// console.log(`${type}: ${heSo}`);
}
elementContainer.innerHTML = elementHtml;
title.appendChild(elementContainer);
if (isnonCreditCourse) return;
// Xử lý lại chuỗi
saveScoreWeight[courseCode] = saveScoreWeight[courseCode].slice(0, -3);
saveScoreWeight[courseCode].replace(/\s+/g, "");
console.log("score Weight: ", saveScoreWeight[courseCode]);
// Lưu lại hệ số điểm
GM_setValue("~scoreWeight", saveScoreWeight);
}
// Tính điểm TX dựa trên hệ số điểm
function calculateStudyScores() {
let tx1Index = 4;
let gk1Index = 14;
if (window.location.pathname == "/student/result/viewstudyresult") {
tx1Index = 3;
gk1Index = 9;
}
const scoreWeight = GM_getValue("~scoreWeight", {});
const kgrid = document.querySelector("div.kGrid");
const courses = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
for (const course of courses) {
const courseCode = course.children[2].textContent.match(/([A-Z]{2})\d{4}/)[0];
// console.log("maHP: ", maHP);
if (scoreWeight[courseCode] != "" && scoreWeight[courseCode] != undefined) {
// Hiển thị hệ số điểm vào cột cuối cùng
course.querySelector("td:last-child").textContent = scoreWeight[courseCode];
// Nếu có điểm giữ kỳ thì bỏ qua
if (course.children[gk1Index].textContent.trim() != "") continue;
// Nếu có điểm tx thì tính
if (course.children[tx1Index].textContent.trim() != "") {
let courseScoreWeight = scoreWeight[courseCode].split(" | ");
let tongDiem = 0;
for (let i = 0; i < courseScoreWeight.length; i++) {
tongDiem +=
(Number(course.children[tx1Index + i].textContent.trim()) *
Number(courseScoreWeight[i])) /
100;
// console.log(row.children[tx1Index + i].textContent.trim());
}
course.querySelector("td:last-child").innerHTML += `🎯 ${tongDiem.toFixed(
2
)}`;
course.querySelector("td:last-child").style.backgroundColor =
"rgb(255, 249, 227)";
}
}
}
}
// Lấy tổng số tín chỉ
function getYourTotalCredits() {
let totalCredits = document.querySelector(
"#ctl02_dvList > tbody > tr:nth-child(7) > td.k-table-viewdetail"
).textContent;
totalCredits = totalCredits.match(/\d+/)[0];
const totalCreditsNumber = Number(totalCredits);
let yourInfo = GM_getValue("yourInfo", {});
yourInfo.totalCredits = totalCreditsNumber;
GM_setValue("yourInfo", yourInfo);
console.log("yourInfo: ", yourInfo);
}
// Lấy Thông tin, tiến trình học
function getYourLearningProgress() {
let yourInfo = GM_getValue("yourInfo", {});
const infoTable = document.querySelector("#frmMain div.panel-body > table");
yourInfo.name = infoTable
.querySelector("tbody > tr:nth-child(1) > td:nth-child(2) > strong")
.textContent.replace(/\s+/g, " ");
yourInfo.msv = infoTable
.querySelector("tbody > tr:nth-child(2) > td:nth-child(2) > strong")
.textContent.trim();
yourInfo.classCode = infoTable
.querySelector("tbody > tr:nth-child(3) > td:nth-child(2) > strong")
.textContent.trim();
const kgrid = document.querySelector("div.kGrid");
const currentCredits = kgrid.querySelector("tbody > tr:last-child > td:first-child");
const currentCreditsNumber = Number(
currentCredits.textContent.trim().match(/(\d+)(?:\.\d+)?/g)[0]
);
yourInfo.currentCredits = currentCreditsNumber;
const currentGPA = kgrid.querySelector("tbody > tr:nth-last-child(2) > td:nth-child(2)");
const currentGPAValue = Number(currentGPA.textContent.trim().match(/(\d+)(?:\.\d+)?/g)[0]);
yourInfo.currentGPA = currentGPAValue;
console.log(yourInfo);
GM_setValue("yourInfo", yourInfo);
let courseCodeMap = new Map();
const courses = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
for (const course of courses) {
const code = course.children[1].textContent.trim();
const scorse4 = Number(course.children[12].textContent.trim()) || "";
if (courseCodeMap.has(code)) {
const old = courseCodeMap.get(code);
if (scorse4 > old.scorse4) {
courseCodeMap.delete(code);
courseCodeMap.set(code, scorse4);
}
} else {
courseCodeMap.set(code, scorse4);
}
}
const yourStudyProcess = Object.fromEntries(
Array.from(courseCodeMap.entries()).map(([code, scorse4]) => [code, scorse4])
);
GM_setValue("~~yourStudyProcess", yourStudyProcess);
console.log(yourStudyProcess, courseCodeMap);
}
// Tính toàn bộ
function calculateStudyStats() {
const kgrid = document.querySelector("div.kGrid");
const courses = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
let courseCodeMap = new Map();
for (const course of courses) {
const code = course.children[1].textContent.trim();
const courseCredit = course.children[5];
const credit = Number(courseCredit.textContent.trim());
const scorse4 = Number(course.children[12].textContent.trim());
if (checkNonCreditCourse(courseCredit)) continue;
if (Number.isNaN(credit)) continue;
if (Number.isNaN(scorse4)) continue;
if (scorse4 == "") continue;
if (scorse4 == "0") continue;
if (courseCodeMap.has(code)) {
const old = courseCodeMap.get(code);
if (scorse4 > old.scorse4) {
courseCodeMap.delete(code);
courseCodeMap.set(code, { scorse4: scorse4, credit: credit });
}
} else {
courseCodeMap.set(code, { scorse4: scorse4, credit: credit });
}
}
let sumCredits = 0;
let sumScore = 0;
for (const { scorse4, credit } of courseCodeMap.values()) {
sumScore += scorse4 * credit;
sumCredits += credit;
}
const GPA = sumScore / sumCredits;
// console.log(courseCodeMap);
return { calculateGPA: GPA, calculateCredits: sumCredits };
}
// Chỉ tính học phần đã sửa, tính cả học phần F
function calculateStudyStatsEdited() {
const kgrid = document.querySelector("div.kGrid");
const courses = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
let courseCodeMap = new Map();
for (const course of courses) {
const score4Cell = course.children[12];
if (
!score4Cell.classList.contains("is-calculated") &&
!score4Cell.classList.contains("is-edited")
)
continue;
const code = course.children[1].textContent.trim();
const courseCredit = course.children[5];
const credit = Number(courseCredit.textContent.trim());
const scorse4 = Number(course.children[12].textContent.trim());
if (checkNonCreditCourse(courseCredit)) continue;
if (Number.isNaN(credit)) continue;
if (Number.isNaN(scorse4)) continue;
if (scorse4 == "") continue;
if (courseCodeMap.has(code)) {
const old = courseCodeMap.get(code);
if (scorse4 > old.scorse4) {
courseCodeMap.delete(code);
courseCodeMap.set(code, { scorse4: scorse4, credit: credit });
}
} else {
courseCodeMap.set(code, { scorse4: scorse4, credit: credit });
}
}
let sumCredits = 0;
let sumScore = 0;
for (const { scorse4, credit } of courseCodeMap.values()) {
sumScore += scorse4 * credit;
sumCredits += credit;
}
const GPA = sumScore / sumCredits;
// console.log(courseCodeMap);
return { editedGPA: GPA, editedCredits: sumCredits };
}
// Cho phép chỉnh sửa điểm
function editScoreBtn() {
const toggleBtn = document.createElement("span");
toggleBtn.id = "toggle-edit-score-btn";
toggleBtn.textContent = "✏️";
toggleBtn.title = "Bật/Tắt chỉnh sửa điểm";
const defaultEditScore = GM_getValue("defaultEditScore");
if (defaultEditScore == undefined)
GM_setValue(
"defaultEditScore",
confirm("Bạn có muốn mặc định bật chế độ chỉnh sửa điểm không?")
);
GM_addStyle(`
#toggle-edit-score-btn {
cursor: pointer;
font-size: 24px;
transition: transform 0.2s;
display: inline-block;
margin-left: 8px;
}
#toggle-edit-score-btn:hover {
transform: scale(1.2);
}
`);
if (defaultEditScore == true) setTimeout(() => toggleBtn.click(), 1000);
toggleBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (toggleBtn.textContent === "✏️") {
toggleBtn.textContent = "📝";
onEditScore(true);
notyf.success("Đã bật chỉnh sửa điểm");
} else {
toggleBtn.textContent = "✏️";
onEditScore(false);
notyf.error("Tắt chỉnh sửa điểm");
}
});
const kGrid = document.querySelector("div.kGrid");
const container = kGrid.querySelector("table > thead > tr:nth-child(1) > td:nth-child(10)");
container.appendChild(toggleBtn);
}
// Chuẩn hoá qua điểm chữ
function normalizeScore(score) {
score = score
.trim()
.toUpperCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/Đ/g, "D");
score = score.replace(/.+(?=[ABCDF].*)/, "");
if (/\d+/.test(score)) {
score = Math.abs(Number(score));
score = score.toExponential();
score = parseFloat(score.split("e")[0]);
score = Math.ceil(score * 2) / 2;
if (score > 0 && score <= 4.0) {
score = scoreToLetter(score);
}
} else if (/^[ABCDF].*/.test(score)) {
score = score.replace(/^A.*$/g, "A");
score = score.replace(/^B.+$/g, "B+");
score = score.replace(/^C.+$/g, "C+");
score = score.replace(/^D.+$/g, "D+");
score = score.replace(/^F.*$/g, "F");
}
// console.log("▶️ score: ", score);
return score;
}
// Xử lý chỉnh sửa điểm
function onEditScore(isEnable) {
console.log("onEditScore", isEnable);
const kgrid = document.querySelector("div.kGrid");
const courses = kgrid.querySelectorAll("tr.kTableAltRow, tr.kTableRow");
GM_addStyle(`
.is-edited { background-color: #fcefc3ff !important; }
.is-calculated { background-color: #d4eddaff !important; }
`);
if (isEnable) {
for (const course of courses) {
const courseCredit = course.children[5];
const scoreCell = course.children[13];
const score4Cell = course.children[12];
scoreCell.setAttribute("originalScore", scoreCell.textContent.trim());
const originalScore = scoreCell.textContent.trim();
if (checkNonCreditCourse(courseCredit)) {
scoreCell.setAttribute("contenteditable", "false");
} else {
scoreCell.setAttribute("contenteditable", "true");
}
scoreCell.title = `📌: ${originalScore}\n✨: A, B+, B, C+, C, D+, D, F, 0, 1, 1.5, 2, 2.5, 3, 3.5, 4`;
score4Cell.title = "📌: " + originalScore;
scoreCell.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
scoreCell.blur();
}
});
scoreCell.addEventListener("focus", () => {
scoreCell.textContent = "";
});
scoreCell.addEventListener("blur", (e) => {
let score = normalizeScore(scoreCell.textContent);
// console.log(score);
if (!["A", "B+", "B", "C+", "C", "D+", "D", "F"].includes(score)) {
notyf.error(
"Điểm không hợp lệ!
Vui lòng nhập lại (A, B+, B, C+, C, D+, D, F | 0, 1, 1.5, 2, 2.5, 3, 3.5, 4)"
);
score = originalScore;
}
score4Cell.textContent = letterTo4(score);
scoreCell.textContent = score;
if (score !== originalScore) {
score4Cell.classList.add("is-edited");
score4Cell.classList.remove("is-calculated");
} else score4Cell.classList.remove("is-edited");
onScoreCellUpdated(scoreCell);
});
score4Cell.addEventListener("click", (e) => {
if (
!score4Cell.classList.contains("is-calculated") &&
!score4Cell.classList.contains("is-edited") &&
scoreCell.textContent.trim() != ""
) {
score4Cell.classList.add("is-calculated");
} else {
score4Cell.classList.remove("is-calculated");
}
onScoreCellUpdated(scoreCell);
});
}
} else {
for (const course of courses) {
course.children[13].setAttribute("contenteditable", "false");
}
}
}
// Hiển thị thêm thông tin trong trang kết quả thi
function showMoreInfoInExamResult() {
let yourInfo = GM_getValue("yourInfo");
let isSameTotalCredits = true;
if (window.location.pathname.includes("/student/result/viewexamresult")) {
const classCode = document
.querySelector(
"div.kGrid > div > div > div > table > tbody > tr:nth-child(3) > td:nth-child(2) > strong"
)
.textContent.trim();
const major = classCode.match(/\d{4}\D+/)[0];
isSameTotalCredits = yourInfo.classCode.includes(major);
console.log("isSameTotalCredits: ", isSameTotalCredits, yourInfo.classCode, major);
}
// Selector
const kgrid = document.querySelector("div.kGrid");
const tables = kgrid.querySelectorAll("table");
const lastTable = tables[tables.length - 1];
// GPA
const currentGPAContainer = lastTable.querySelector(
"tbody > tr:nth-last-child(2) > td:nth-child(2)"
);
currentGPAContainer.setAttribute("colspan", "5");
const editedGPA = document.createElement("td");
editedGPA.setAttribute("colspan", "3");
editedGPA.innerHTML = `🎯: `;
currentGPAContainer.insertAdjacentElement("afterend", editedGPA);
// Học lực
const currentStuydyContainer = lastTable.querySelector(
"tbody > tr:last-child > td:nth-child(2)"
);
currentStuydyContainer.setAttribute("colspan", "5");
const editedStudy = document.createElement("td");
editedStudy.setAttribute("colspan", "3");
editedStudy.innerHTML = `🎯: `;
currentStuydyContainer.insertAdjacentElement("afterend", editedStudy);
// Tín chỉ
const currentCreditsContainer = lastTable.querySelector(
"tbody > tr:last-child > td:first-child"
);
const currentCredits = currentCreditsContainer.textContent.trim().match(/\d+/)[0];
console.log("currentCredits: ", currentCredits);
currentCreditsContainer.innerHTML = `Tín chỉ đã tích luỹ: ${currentCredits} / ???`;
setTimeout(() => {
if (document.getElementById("current-credits").textContent.trim() != currentCredits)
notyf.error(
"Một số môn có thể chưa được tính vào GPA, nhấp vào số tín chỉ của môn chưa tính GPA để tính lại."
);
}, 500);
const info = GM_getValue("yourInfo") || {};
const totalCredits = info.totalCredits;
if (isSameTotalCredits && totalCredits > 0) {
document.getElementById("total-credits").textContent = totalCredits;
} else {
const totalCreditsSpan = document.getElementById("total-credits");
totalCreditsSpan.setAttribute("contenteditable", "true");
GM_addStyle(`
#total-credits {
border-bottom: 2px dashed gray;
}
#total-credits:focus {
outline: none;
background-color: #fff5d1ff;
}
`);
totalCreditsSpan.title =
"Nhấp để chỉnh sửa tổng tín chỉ\nHoặc vào khung chương trình đào tạo để tự động lấy";
// Event khi sửa totalCreditsSpan
totalCreditsSpan.addEventListener("blur", (e) => {
e.target.textContent = e.target.textContent.replace(/[^\d]/g, "");
if (e.target.textContent == "") e.target.textContent = "142";
onScoreCellUpdated();
});
}
// Mục tiêu GPA
const requiredScoreContainer = lastTable.querySelector("tbody > tr:last-child");
const requiredScore = document.createElement("tr");
requiredScore.style.backgroundColor = "#ffffff";
requiredScore.innerHTML = `
✏️: 0 / 0
🎯: 0 / 0
Số tín tích luỹ còn lại: 0
|
3.6🎯: 0
3.2🎯: 0
2.5🎯: 0
| `;
requiredScoreContainer.insertAdjacentElement("afterend", requiredScore);
GM_addStyle(`
.study-info {
color: Red;
font-weight: bold;
float: left;
font-size: 12px;
padding-left: 5px;
}
#requiredScore {
background-color: #ffffff;
font-size: 14px;
}
`);
onScoreCellUpdated();
}
// Xử lý khi ô điểm được chỉnh sửa
function onScoreCellUpdated() {
highlightExamScores();
const { calculateGPA, calculateCredits } = calculateStudyStats();
const { editedGPA, editedCredits } = calculateStudyStatsEdited();
const totalCredits = Number(document.getElementById("total-credits").textContent.trim());
document.getElementById("current-gpa").textContent = calculateGPA.toFixed(3);
if (calculateGPA >= 3.6) document.getElementById("edited-study").textContent = "Xuất sắc";
else if (calculateGPA >= 3.2) document.getElementById("edited-study").textContent = "Giỏi";
else if (calculateGPA >= 2.5) document.getElementById("edited-study").textContent = "Khá";
else if (calculateGPA >= 2.0)
document.getElementById("edited-study").textContent = "Trung bình";
else if (calculateGPA < 2.0) document.getElementById("edited-study").textContent = "Yếu";
const remainingCredits = totalCredits - calculateCredits;
const scoresToGPA25 =
(2.5 * totalCredits - calculateGPA * calculateCredits) / remainingCredits;
const scoresToGPA32 =
(3.2 * totalCredits - calculateGPA * calculateCredits) / remainingCredits;
const scoresToGPA36 =
(3.6 * totalCredits - calculateGPA * calculateCredits) / remainingCredits;
document.getElementById("edited-gpa").textContent = isNaN(editedGPA)
? "0"
: editedGPA.toFixed(3);
document.getElementById("edited-credits").textContent = editedCredits;
document.getElementById("current-gpa1").textContent = calculateGPA.toFixed(3);
document.getElementById("calculate-credits").textContent = calculateCredits;
document.getElementById("remaining-credits").textContent = remainingCredits;
document.getElementById("target-2.5").textContent = scoresToGPA25.toFixed(3);
document.getElementById("target-3.2").textContent = scoresToGPA32.toFixed(3);
document.getElementById("target-3.6").textContent = scoresToGPA36.toFixed(3);
}
// Đánh dấu môn học
function markedCourse(courseCode, courseName, markedCell) {
let markedCourse = GM_getValue("markedCourse", {});
let flag = courseCode in markedCourse;
if (flag) {
markedCell.style.backgroundColor = "#fcefc3ff";
}
markedCell.addEventListener("click", () => {
markedCourse = GM_getValue("markedCourse", {});
flag = !flag;
markedCell.style.backgroundColor = flag ? "#fcefc3ff" : "";
if (flag) {
markedCourse[courseCode] = courseName;
} else {
delete markedCourse[courseCode];
}
GM_setValue("markedCourse", markedCourse);
notyf.success(`Đã${flag ? "" : " huỷ"} đánh dấu môn ${courseCode}
${courseName}`);
});
}
// Tô màu, đánh dấu môn học
function customizeGPA() {
let yourStudyProcess = GM_getValue("~~yourStudyProcess");
const courses = document.querySelectorAll(".table.table-condensed tr");
for (const course of courses) {
const courseCodeCell = course.children[2];
const courseCode = courseCodeCell?.textContent.trim() || "";
if (!/\w{2}\d{4}/.test(courseCode)) continue;
if (checkDefaultNonCreditCourse(courseCode)) continue;
// Tìm index td.tinchi
const creditCell = course.querySelector(".tinchi");
if (!creditCell) continue;
const tds = Array.from(course.querySelectorAll("td"));
const index = tds.indexOf(creditCell);
// Tô màu điểm
let score4Cell = course.children[index + 6];
if (score4Cell.textContent.trim() != "") {
score4Cell.style.backgroundColor = scoresBoxColor[Number(score4Cell.textContent)];
score4Cell.style.color = "#FFFFFF";
}
// Tô màu tín đang học
if (courseCode in yourStudyProcess)
if (yourStudyProcess[courseCode] === "") {
creditCell.style.backgroundColor =
creditsBoxColor[creditCell.textContent.trim()];
creditCell.style.color = "#FFFFFF";
}
const courseNameCell = course.children[3];
courseNameCell.setAttribute("title", "👆➡️🔖");
markedCourse(courseCode, courseNameCell.textContent.trim(), courseNameCell);
}
}
// Tô màu, đánh dấu môn học
function customizeProgramFramework() {
const yourStudyProcess = GM_getValue("~~yourStudyProcess", {});
const courses = document.querySelectorAll("table.table > tbody > tr");
const markerIndex = window.location.pathname == "/training/programmodulessemester" ? 2 : 0;
for (const course of courses) {
const courseCodeCell = course.children[1];
if (!courseCodeCell) continue;
const courseCode = courseCodeCell.textContent.trim();
if (!/\w{2}\d{4}/.test(courseCode) || checkDefaultNonCreditCourse(courseCode)) continue;
if (courseCode in yourStudyProcess)
if (!yourStudyProcess[courseCode] == "") {
courseCodeCell.style.backgroundColor =
scoresBoxColor[yourStudyProcess[courseCode]];
courseCodeCell.style.color = "#FFFFFF";
courseCodeCell.setAttribute(
"title",
`📌: ${scoreToLetter(yourStudyProcess[courseCode])}`
);
} else {
const creditCell = course.children[3];
creditCell.style.backgroundColor =
creditsBoxColor[creditCell.textContent.trim()];
creditCell.style.color = "#FFFFFF";
}
const markerCell = course.children[markerIndex];
const courseName = course.children[2].textContent.trim();
markerCell.setAttribute("title", "👆➡️🔖");
markedCourse(courseCode, courseName, markerCell);
}
GM_addStyle(`
.kTableRowBackground {
background-color: #f5f5f5 !important;
}
.kTableRowBackground:nth-of-type(odd) {
background-color: inherit !important;
}
.kTableRowBackground td:nth-child(12) {
background-color: yellow !important;
}
.kTableRowBackground:nth-of-type(odd) td:nth-child(12) {
background-color: yellow !important;
}
`);
}
// Hiển thị môn đã đánh dấu
function showmarkedCourse() {
let markedCourseList = GM_getValue("markedCourse", {});
// Tô vàng những học phần nằm trong dự định
const planningCourses = document.querySelectorAll("#tableorder > tbody > tr");
for (const planningCourse of planningCourses) {
const courseCodeCell = planningCourse.children[2];
if (!courseCodeCell) continue;
const courseCode = courseCodeCell.textContent.replace("[Hủy đăng ký]", "").trim();
if (/\w{2}\d{4}/.test(courseCode))
if (courseCode in markedCourseList) {
courseCodeCell.style.backgroundColor = "#fcefc3ff";
delete markedCourseList[courseCode];
}
}
console.log(markedCourseList);
// Hiển thị học phần còn lại
document.querySelector("#note")?.querySelector("p")?.remove();
const markedCourseContainer = document.createElement("p");
markedCourseContainer.className = "markedCourse";
markedCourseContainer.style.fontSize = "18px";
note.appendChild(markedCourseContainer);
markedCourseContainer.textContent = "🎯: ";
for (const [code, name] of Object.entries(markedCourseList)) {
const span = document.createElement("span");
span.textContent = code;
span.title = name;
markedCourseContainer.appendChild(span);
markedCourseContainer.append(" ");
}
setTimeout(showmarkedCourse, 5000);
}
// Export calender
function enhanceCalender() {
const btnContainer = document.querySelector(
"div.boxpanel-mc > .form-horizontal > .form-group:nth-child(3) > div.col-sm-4"
);
// Curent
const findTermBtn = document.createElement("input");
findTermBtn.type = "button";
findTermBtn.className = "btn btn-primary btn-space hover";
findTermBtn.value = "Lọc kì hiện tại";
btnContainer.appendChild(findTermBtn);
findTermBtn.addEventListener("click", () => {
const { year, term } = findCalender((data, date) => {
const match = data.match(/(\d{5})\w\w\d{7}/);
if (!match) return false;
const classCode = match[1];
return { year: classCode.slice(0, 4), term: classCode[4] };
});
console.log(year, term);
calendarFilterTerm(year, term);
});
// Select
const selectElement = document.createElement("select");
selectElement.id = "termSelector";
const currentYear = new Date().getFullYear();
const defaultOption = document.createElement("option");
defaultOption.text = "--- Chọn Kì/Năm Học ---";
defaultOption.value = "";
defaultOption.disabled = true;
defaultOption.selected = true;
selectElement.appendChild(defaultOption);
for (let year = currentYear; year >= currentYear - 4; year--) {
for (let term = 1; term <= 4; term++) {
const optionI = document.createElement("option");
const termName = ["1️⃣", "2️⃣", "🌸", "☀️"];
optionI.text = `${termName[term - 1]} : ${year} - ${year + 1}`;
if (term == 1) optionI.value = `${year}${term}`;
else optionI.value = `${year + 1}${term}`;
selectElement.appendChild(optionI);
}
}
btnContainer.appendChild(selectElement);
selectElement.addEventListener("change", (e) => {
const value = e.target.value;
const year = value.slice(0, 4);
const term = value[4];
calendarFilterTerm(year, term);
});
// Export
const exportBtn = document.createElement("input");
exportBtn.type = "button";
exportBtn.className = "btn btn-primary btn-space hover";
exportBtn.value = "Xuất lịch học (.ics)";
btnContainer.appendChild(exportBtn);
exportBtn.addEventListener("click", () => {
const { year, term } = findCalender((data, date) => {
const match = data.match(/(\d{5})\w\w\d{7}/);
if (!match) return false;
const classCode = match[1];
return { year: classCode.slice(0, 4), term: classCode[4] };
});
const calendarName = `Học kỳ ${term} ${year}-${Number(year) + 1}`;
const listCourseData = getCalendarData();
const events = processCalendarData(listCourseData);
createICSFile(events, calendarName);
});
}
function findCalender(callback) {
const rows = document.querySelectorAll(".panel-body > table > tbody tr:nth-child(n+2)");
for (const row of rows) {
const dateElement = row.children[2];
const date =
/(?\d+)\/(?\d+)\/(?\d+)/.exec(dateElement.textContent.trim())
?.groups || {};
const calenders = row.querySelectorAll("td:nth-child(n+4)");
for (const calendar of calenders) {
const data = calendar.textContent.trim();
const value = callback(data, date);
if (value) return value;
}
}
}
function calendarFilterTerm(year, term) {
document.querySelector("#ctl02_inpStartDate").value = year;
document.querySelector("#ctl02_inpEndDate").value = year;
const termTimeMap = [
{ startD: 1, startM: 9, endD: 30, endM: 12 },
{ startD: 1, startM: 3, endD: 30, endM: 6 },
{ startD: 1, startM: 1, endD: 31, endM: 3 },
{ startD: 1, startM: 7, endD: 31, endM: 8 },
];
const termTime = termTimeMap[term - 1];
document.querySelector("#ctl02_inpStartDate_d").value = termTime.startD;
document.querySelector("#ctl02_inpEndDate_d").value = termTime.endD;
document.querySelector("#ctl02_inpStartDate_m").value = termTime.startM;
document.querySelector("#ctl02_inpEndDate_m").value = termTime.endM;
document.querySelector("#ctl02_butGet").click();
}
function createICSFile(events, calendarName) {
let icsContent = `BEGIN:VCALENDAR
PRODID:-// VuQuan // svHaUI Helper //EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:${calendarName}
X-WR-TIMEZONE:Asia/Ho_Chi_Minh
X-WR-CALDESC:Lịch học sinh viên HAUI
BEGIN:VTIMEZONE
TZID:Asia/Ho_Chi_Minh
X-LIC-LOCATION:Asia/Ho_Chi_Minh
BEGIN:STANDARD
TZOFFSETFROM:+0700
TZOFFSETTO:+0700
TZNAME:GMT+7
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
`;
for (const event of [].concat(events || [])) {
icsContent += toICSEvent(event);
}
icsContent += `END:VCALENDAR`;
const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${calendarName}.ics`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Lấy thông tin sự kiện
function getCalendarData() {
const days = document.querySelectorAll(".panel-body > table > tbody tr:nth-child(n+2)");
const regex1 =
/\((?\d+),(?:\d*,)*?(?\d+)\)\s*-\s*(?.+?)\s*\(Lớp:\s*(?\d+\w+\d+)\)/;
const regex2 = /GV:\s*(?.+?)\s*\((?\d+)?\s*-\s*(?[^)]+)\)/;
const listCourseData = new Map();
let listErrorClassCode = [];
for (const day of days) {
const dateElement = day.children[2];
const date =
/(?\d+)\/(?\d+)\/(?\d+)/.exec(dateElement.textContent.trim())
?.groups || {};
const sessions = day.querySelectorAll(
"td:nth-child(4), td:nth-child(5), td:nth-child(6)"
);
for (const session of sessions) {
const sessionContent = session.innerText.trim();
if (sessionContent == "") continue;
const subjects = sessionContent.split(/\n?\d+\.\s/).filter(Boolean);
if (subjects.length == 0) continue;
for (const subject of subjects) {
const subjectLine = subject.split("\n");
const classCode = subjectLine[0].match(/\d{5}\w+\d{7}/)?.[0] || subject;
if (listErrorClassCode.includes(classCode)) continue;
if (listCourseData.has(classCode)) {
listCourseData.get(classCode).date.push(date);
continue;
}
const match1 = subjectLine[0].match(regex1);
const match2 = subjectLine[1].match(regex2);
const match3 = subjectLine[2]
.replaceAll(/[()]|( - Cơ sở \d* - Khu \w)/g, "")
.trim();
// Nếu không khớp thì lưu lại mã lớp bị lỗi và thông báo lỗi
if (!match1 || !match2) {
listErrorClassCode.push(classCode);
const courseName = subject.match(/\(.+?\)\s*-\s*(.+?)\s\(/)?.[1];
notyf.error(`Có lỗi khi lấy dữ liệu lớp:
${classCode}
${courseName}`);
console.error(
`Có lỗi khi lấy dữ liệu:\n${classCode}: ${courseName}\n${subject}`
);
continue;
}
const courseData = {
...match1.groups,
...match2.groups,
location: match3,
date: [date],
};
listCourseData.set(classCode, courseData);
}
}
}
return listCourseData;
}
function processCalendarData(listCourseData) {
const startPeriodToTime = {
1: "07:00",
2: "07:50",
3: "08:45",
4: "09:40",
5: "10:35",
6: "11:25",
7: "12:30",
8: "13:20",
9: "14:15",
10: "15:10",
11: "16:05",
12: "16:55",
13: "18:00",
14: "18:50",
15: "19:45",
16: "20:35",
};
const endPeriodToTime = {
1: "07:50",
2: "08:40",
3: "09:35",
4: "10:30",
5: "11:25",
6: "12:15",
7: "13:20",
8: "14:10",
9: "15:05",
10: "16:00",
11: "16:55",
12: "17:45",
13: "18:50",
14: "19:40",
15: "20:35",
16: "21:25",
};
let events = [];
for (const [classCode, data] of listCourseData) {
const rule = findRule(data.date);
console.log(data, " ", rule);
const date0 = `${data.date[0].year}${String(data.date[0].month).padStart(
2,
"0"
)}${String(data.date[0].day).padStart(2, "0")}`;
const startTime = startPeriodToTime[data.start].replace(":", "");
const endTime = endPeriodToTime[data.end].replace(":", "");
let alarms = GM_getValue("alamrCalender", [15, 30]);
const eventData = {
summary: data.course,
uid: `${data.class}@sv.haui.edu.vn`,
dtstamp: DTStamp,
startDate: date0,
startTime: startTime,
endTime: endTime,
description: `${data.lecturer} \\n${data.sdt} - ${data.khoa}\\n${data.class}`,
location: data.location,
byday: rule.byday.join(","),
interval: rule.interval,
total: rule.total,
exdate: rule.exday.map((d) => {
const day = String(d.getDate()).padStart(2, "0");
const month = String(d.getMonth() + 1).padStart(2, "0");
const year = d.getFullYear();
return `${year}${month}${day}`;
}),
alarms: alarms,
};
events.push(eventData);
}
return events;
}
function addAlarm(alarms) {
return alarms
.map(
(minutes) => `
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER:-P0DT0H${minutes}M0S
DESCRIPTION:Báo trước ${minutes} phút
END:VALARM`
)
.join("");
}
// Tạo sự kiện ICS
function toICSEvent(data) {
const DTStamp = new Date().toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
data.exdate = data.exdate || [];
let event = `
BEGIN:VEVENT
SUMMARY:${data.summary}
UID:${data.uid}
DTSTAMP:${DTStamp}
DTSTART;TZID=Asia/Ho_Chi_Minh:${data.startDate}T${data.startTime}00
DTEND;TZID=Asia/Ho_Chi_Minh:${data.startDate}T${data.endTime}00
DESCRIPTION:${data.description || ""}
LOCATION:${data.location || ""}`;
// Lặp
const isRecurring = data.byday && data.interval && data.total;
if (isRecurring) {
event += `
RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=${data.byday};INTERVAL=${data.interval};COUNT=${data.total}`;
// Thêm EXDATE nếu có
for (const exdate of data.exdate) {
event += `\nEXDATE;TZID=Asia/Ho_Chi_Minh:${exdate}T${data.startTime}00`;
}
}
event += addAlarm(data.alarms);
event += `
END:VEVENT
`;
// Các sự kiện EXDATE
for (const exdate of data.exdate) {
event += `
BEGIN:VEVENT
SUMMARY:${data.summary}
UID:${data.uid}
DTSTAMP:${DTStamp}
DTSTART;TZID=Asia/Ho_Chi_Minh:${exdate}T${data.startTime}00
DTEND;TZID=Asia/Ho_Chi_Minh:${exdate}T${data.endTime}00
DESCRIPTION:${data.description || ""}
LOCATION:${data.location || ""}
RECURRENCE-ID;TZID=Asia/Ho_Chi_Minh:${exdate}T${data.endTime}00`;
event += addAlarm(data.alarms);
event += `
END:VEVENT
`;
}
event += "\n";
return event;
}
// Tìm quy luật
function findRule(listDate) {
const groupedDates = groupDatesByDayOfWeek(listDate);
let groupedMissingDates = {};
for (const [byday, days] of Object.entries(groupedDates)) {
groupedMissingDates[byday] = findMissingDates(days);
}
let result = { byday: [], interval: null, total: listDate.length, exday: [] };
for (const [byday, days] of Object.entries(groupedMissingDates)) {
if (result.interval === null) result.interval = days.interval;
else if (result.interval !== days.interval) {
result.interval = 0;
console.warn("Different intervals found:", byday, result.interval, days.interval);
notyf.error("Không thể xuất lịch do các buổi học có khoảng cách không đều nhau.");
break;
}
result.byday.push(byday);
result.exday.push(...days.missingDates);
result.total += days.missingDates.length;
}
return result;
}
function groupDatesByDayOfWeek(dateArray) {
const daysOfWeekNames = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
const groupedDates = {};
dateArray.forEach((dateObj) => {
const year = parseInt(dateObj.year);
const month = parseInt(dateObj.month) - 1;
const day = parseInt(dateObj.day);
const date = new Date(year, month, day);
const dayIndex = date.getDay();
const dayName = daysOfWeekNames[dayIndex];
if (!groupedDates[dayName]) {
groupedDates[dayName] = [];
}
groupedDates[dayName].push(date);
});
return groupedDates;
}
function findMissingDates(dateStrings) {
const dates = dateStrings.map((d) => new Date(d)).sort((a, b) => a - b);
if (dates.length < 2) return [];
let minDiff = Infinity;
for (let i = 0; i < dates.length - 1; i++) {
const diff = dates[i + 1] - dates[i];
if (diff > 0 && diff < minDiff) {
minDiff = diff;
}
}
const oneDayMs = 24 * 60 * 60 * 1000;
const weeksInterval = Math.round(minDiff / oneDayMs) / 7;
const missingDates = [];
for (let i = 0; i < dates.length - 1; i++) {
let current = dates[i].getTime();
const next = dates[i + 1].getTime();
if (next - current > minDiff * 1.1) {
let tempTime = current + minDiff;
while (tempTime < next - minDiff * 0.5) {
missingDates.push(new Date(tempTime));
tempTime += minDiff;
}
}
}
return {
interval: weeksInterval,
missingDates: missingDates,
};
}
//===============================================================
function run() {
notyf = new Notyf({
duration: 3500,
dismissible: true,
});
runOnUrl(changeTitle, "");
runOnUrl(changeHomePagePath, "");
runOnUrl(displayGPA, "");
runOnUrl(fastSurvey, /\/survey\//);
runOnUrl(captchaHelperRegister, "/register");
runOnUrl(customizeHomePage, "/home");
runOnUrl(sortExamSchedule, "/student/schedulefees/transactionmodules");
runOnUrl(highlightExamSchedule, "/student/schedulefees/transactionmodules");
runOnUrl(showExamPlan, "/student/schedulefees/examplant");
runOnUrl(
highlightCreditsCourse,
"/student/result/examresult",
"/student/result/viewexamresult"
);
runOnUrl(
highlightExamScores,
"/student/result/examresult",
"/student/result/viewexamresult"
);
runOnUrl(
highlightStudyScores,
"/student/result/studyresults",
"/student/result/viewstudyresult"
);
runOnUrl(
toggleStudyAndExam,
"/student/result/examresult",
"/student/result/viewexamresult",
"/student/result/viewexamresultclass",
"/student/result/studyresults",
"/student/result/viewstudyresult",
"/student/result/viewstudyresultclass"
);
runOnUrl(
toggleCourseInfo,
"/training/viewmodulescdiosv/xem-chi-tiet-hoc-phan.htm",
"/training/viewcourseindustry2/xem-chi-tiet-hoc-phan.htm"
);
runOnUrl(
gotoCourseInfoStudy,
"/student/result/studyresults",
"/student/result/viewstudyresult"
);
runOnUrl(
gotoCourseInfoExam,
"/student/result/examresult",
"/student/result/viewexamresult"
);
runOnUrl(gotoCourseInfoGPA, "/student/result/viewmodules");
runOnUrl(showScoreWeight, "/training/viewmodulescdiosv/xem-chi-tiet-hoc-phan.htm");
runOnUrl(
calculateStudyScores,
"/student/result/studyresults",
"/student/result/viewstudyresult"
);
runOnUrl(getYourTotalCredits, "/training/viewcourseindustry");
runOnUrl(getYourLearningProgress, "/student/result/examresult");
runOnUrl(editScoreBtn, "/student/result/examresult", "/student/result/viewexamresult");
runOnUrl(
showMoreInfoInExamResult,
"/student/result/examresult",
"/student/result/viewexamresult"
);
runOnUrl(customizeGPA, "/student/result/viewmodules");
runOnUrl(showmarkedCourse, "/register/dangkyhocphan");
runOnUrl(
customizeProgramFramework,
"/training/viewcourseindustry",
"/training/programmodulessemester"
);
runOnUrl(enhanceCalender, "/timestable/calendarcl");
}
waitForSelector("#frmMain", 5000, 100)
.then((el) => {
run();
})
.catch((err) => {
console.warn(err);
});
runOnUrl(() => {
waitForSelector("input#ctl00_txtimgcode").then(captchaHelperLogin);
}, "/sso");
runOnUrl(() => (window.location.href = "https://sv.haui.edu.vn/home"), "/");
// ================================================================
console.log("✅ svHaUI_Helper.user.js loaded: ", window.location);
})();