// ==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 = `
GPA: ${ info.currentGPA || "..." }
${info.currentCredits || "..."} / ${ info.totalCredits || "..." }
`; 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 = `

Chức năng chính

Kế hoạch thi

STT Mã lớp độc lập Tên học phần Ngày thi Ca thi Lần thi Lớp ưu tiên Khoa

Lịch thi

STT Môn thi Ngày thi Ca thi SBD Lần thi Vị trí thi Phòng thi Tòa nhà Cơ sở Tiền VP PVT Tham gia thi Tình trạng
`; 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); })();