Scheduling Subject Registration

tool helping alot with subject registation

// ==UserScript==
// @name         Scheduling Subject Registration
// @namespace    http://tampermonkey.net/
// @version      2024-07-03
// @description  tool helping alot with subject registation
// @author       Minh Triet (Alex Ng)
// @match        https://uis.ptithcm.edu.vn/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require      http://code.jquery.com/jquery-3.6.0.min.js
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT



// ==/UserScript==


GM_addStyle(`
 .card-header {
        border-radius: 15px !important;
        border-bottom: 1px solid rgba(0, 0, 0, 0.125) !important;
        padding: 0.75rem 1.25rem !important;
        margin-bottom: 0 !important;
    }

    .container-fluid {
        border-radius: 15px !important;
        box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3) !important;

    }

    .card {
        border: 0px solid rgba(0, 0, 0, 0.125) !important;
        border-radius: 15px !important;
        box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.125) !important;
    }

    .btn-primary {
        border: 0px solid rgba(0, 0, 0, 0.125) !important;
        border-radius: 15px !important;
        box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.125) !important;
    }



    html {
        background: #f2f2f2;
    }
    body {
        background: inherit !important;
        margin: 0;
        font-family: "Inter", sans-serif;
    }

    .banner {
        background-color: #fff !important;
    }

    .banner {
        display: block;
        width: 100% !important;
        height: 728px !important;
        border-bottom-left-radius: 15px !important;
        border-bottom-right-radius: 15px !important;

    }

    .float {
        position: fixed;
        z-index: 1;
        width:60px;
        height:60px;
        bottom:40px;
        right:40px;
        color: #000000;
        background: #FFF;
        border-radius: 50%;
        border: none;
        transition: box-shadow 400ms cubic-bezier(0.2, 0, 0.7, 1), transform 200ms cubic-bezier(0.2, 0, 0.7, 1);
      }
    .float:after {
        font-size: 2.5em;
        line-height: 1.1em;
      }
    .float:hover {
        box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.22) rgba(0, 0, 0, 0.36) rgba(0, 0, 0, 0.3) !important;

        transform: scale(1.05);

    }

    .editTKB {
        background-color:#2fa4e7 !important;

    }

    .editTKB > i {
        color: #fff !important;
    }

    #tkb_div {
        height: auto;
        width: 80% !important;
        margin-left: 10% !important;
        margin-right: 10% !important;
        margin-top: 60px;
        align-items: center;
        justify-content: center;
        background-color: #fefefe;
    }

    .danhsachmonhoc_text {
        text-align: center;
        margin: auto;
    }

    .tkb_preview_table > thead {
        background-color: #2fa4e7;

    }

    .tkb_preview_table > thead > tr > th {
        color: #fff;
        padding: 0.5px;
        text-align: center;
    }

    .cellqh {
        border: 1px solid green;
        max-width: 14px;
        max-height: 6px;
        font-size: 13px;
        position: relative;
        border-bottom: 1px dotted black;
        padding: 2px;
        text-align: center;

    }

    .tooltiptext {
        font-size: 8px;
    }

    .cellqh .tooltiptext {
        visibility: hidden;
        width: 120px;
        background-color: black;
        color: #fff;
        text-align: center;
        padding: 5px 0;
        border-radius: 6px;
        position: absolute;
        z-index: 1;
        display: block;
        float: left;
      }
      .cellqh:hover .tooltiptext:not(:empty) {
        visibility: visible;
      }

      .cellqh:hover .tooltiptext {
        visibility: visible;
      }

    .starttimerow {
        padding: 1px;
        vertical-align: middle;
        border-top: 0.2px solid #2fa4e7;
        background-color: #2fa4e7;
        color: #fff;
        max-width: 30px;
        max-height: 10px;
        font-size: 8px;
    }

    .chonngaydiv {
        text-align: center;
        margin-bottom: 10px;
        background-color: #fefefe;
        border-collapse: collapse;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.02);
        align-items: center;
        justify-content: center;
        margin-left: auto;
        margin-right: auto;
        margin-top: 30px;

    }

    .danhsachmonhoc {
        display: flex;
        justify-content: center;
        align-items: center;
        margin-bottom: 10px;
        flex-wrap: wrap;
        background-color: #fefefe;
        border-collapse: collapse;
        flex-direction: column;
    }

    .dropdownlecture {
        text-align: center;
        margin-bottom: 10px;
        margin-top: 30px;
        background-color: #fefefe;
        border-collapse: collapse;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.02);
        background-color: #fefefe;
        margin-left: 30%;
        margin-right: 30%;
        width: 40%;
    }

    input[type="date"]::-webkit-datetime-edit, input[type="date"]::-webkit-inner-spin-button, input[type="date"]::-webkit-clear-button {
        color: #fff;
        position: relative;
    }

      input[type="date"]::-webkit-datetime-edit-year-field {
        position: absolute !important;
        border-left:1px solid #8c8c8c;
        padding: 2px;
        color:#000;
        left: 56px;
      }

      input[type="date"]::-webkit-datetime-edit-month-field {
        position: absolute !important;
        border-left:1px solid #8c8c8c;
        padding: 2px;
        color:#000;
        left: 26px;
      }


    input[type="date"]::-webkit-datetime-edit-day-field {
        position: absolute !important;
        color:#000;
        padding: 2px;
        left: 4px;
    }

    .timetables {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 20px;
        }

        .timetable-container {
            width: calc(100% / 5 - 20px);
            margin: 10px;
        }

        .timetable-container h2 {
            text-align: center;
        }

        .timetable {
            width: 100%;
            border-collapse: collapse;
        }

        .timetable th, .timetable td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: center;
        }

        .timetable th {
            background-color: #f2f2f2;
        }

        @media (max-width: 1200px) {
            .timetable-container {
                width: calc(100% / 4 - 20px);
            }
        }

        @media (max-width: 992px) {
            .timetable-container {
                width: calc(100% / 3 - 20px);
            }
        }

        @media (max-width: 768px) {
            .timetable-container {
                width: calc(100% / 2 - 20px);
            }
        }

        @media (max-width: 576px) {
            .timetable-container {
                width: 100%;
            }
        }
    `);



window.onload = function() {
    unsafeWindow.scraptSubjects = scraptSubjects;
    unsafeWindow.drawTable = drawTable;
    unsafeWindow.process = process;


    const floatingButton = document.createElement('button');
    floatingButton.innerHTML = `<button class="float"><i class="fa fa-cogs"></i></button>`;
    floatingButton.onclick = function() {
        var float = $(".float");
        float.addClass("editTKB");

        process()
        ensureDropdownEventListener()
        ensureCheckBoxEventListener()
    }

    document.body.appendChild(floatingButton)
    // we have input's class name, so how to combine with?
}


class Subject {
    constructor(subjectCode, subjectName, groupCode, teamCode, classCode, timeTable) {
        this.subjectCode = subjectCode
        this.subjectName = subjectName
        this.groupCode = groupCode
        this.teamCode = teamCode.trim().length >= 1 ? teamCode : ''
        this.classCode = classCode
        this.timeTable = reformatRoutine(timeTable)
    }
}



const registeredSubjects = new Map();
const subjects = new Map();
const nameStoring = new Map();
const storeCheckedSubjects = new Set();
let dateStart = new Date( '2024-08-12' );

const tables = Array.from({ length: 28 }, () => Array.from({ length: 14 }, () => Array.from({ length: 7 }, () => [])));


function scraptSubjects() {
    let nameStoring = new Map();
    unsafeWindow.subjects = subjects;
    $(".custom-control").attr("style", "display :none !important");
    const tableSubjects = $('tbody')[28]
    const rows = tableSubjects.children

    for (let i = 0; i < rows.length; i++) {
        // enable checked for this input
        const subject = new Subject(
            rows[i].children[1].innerText,
            rows[i].children[2].innerText,
            rows[i].children[3].innerText,
            rows[i].children[4].innerText,
            rows[i].children[6].innerText,
            rows[i].children[9].innerText
        )


        nameStoring.set(rows[i].children[1].innerText, rows[i].children[2].innerText)


        const key = subject.subjectCode + subject.groupCode + subject.teamCode
        subjects.set
        (
            key,
            subject
        ); // key -value

        // modify checkbox
        let form = $(rows[i]).find("form")
        let tag = `<input type='checkbox' class='editCheckBoxInput' value="${key}">`
        form.append(tag)
    }

    subjects.forEach((value, key) => {
        console.log(key, value)
    })

    // add dropdown
    var dropDown = $(".dropdownlecture");
    dropDown.empty();
    dropDown.append('<option value ="all" class ="">All</option>');

    for (let [key, value] of nameStoring) {
        dropDown.append(`<option value ="${key}">${key} ${value}</option>`);
    }

    alert("Đã tải xong dữ liệu môn học");

}



function ensureDropdownEventListener() {
    let inputEle;

    var timer = setInterval(() => {
        if ($('.dropdownlecture').length == 0) {
            return;
        }
        else {
            $(".dropdownlecture").change(function(event) {


                if (inputEle == undefined) {
                    inputEle = document.getElementsByClassName("form-control form-control-sm small text-secondary ng-untouched ng-pristine ng-valid")[0]
                }
                if (event.target.value == "all") {
                    inputEle.value = "";
                }
                else {
                    inputEle.value = event.target.value
                }
                const inputEvent = new Event('input', {
                    bubbles: true,
                    cancelable: true,
                });
                inputEle.dispatchEvent(inputEvent);

                reinitializeCheckboxInput()

            })
            clearInterval(timer);
        }
    }, 200)
    }

function ensureCheckBoxEventListener() {
    var time = setInterval(() => {
        if ($('.editCheckBoxInput').length == 0) {
            return;
        }
        else {
            $(".editCheckBoxInput").change(function(event) {
                const key = event.target.value
                console.log(key);
                const subject = subjects.get(key)

                if (event.target.checked) {
                    if (registeredSubjects.has(subject.subjectCode)) {
                        alert("Môn học đã được đăng ký");
                        $(event.target).prop('checked', false);
                    }
                    else {
                        console.log(subject)
                        addSubjectToTable(subject);

                        registeredSubjects.set(subject.subjectCode, subject);
                        storeCheckedSubjects.add(key);
                        $(".danhsachmonhoc").empty();

                        for (let [key, value] of registeredSubjects) {
                            $(".danhsachmonhoc").append(`<div class="${value.subjectCode}">${value.subjectCode} | ${value.subjectName} | ${value.classCode}</div>`);
                        }



                    }
                }
                else {
                    registeredSubjects.delete(subject.subjectCode);
                    storeCheckedSubjects.delete(key);
                    removeSubjectFromTable(subject);

                    $(".danhsachmonhoc").empty();

                    for (let [key, value] of registeredSubjects) {
                        $(".danhsachmonhoc").append(`<div class="${value.subjectCode}">${value.subjectCode} | ${value.subjectName} | ${value.classCode}</div>`);
                    }
                }
            })

            clearInterval(time);
        }
    }, 200)
    }




function addSubjectToTable(subject) {
    // add to table times
    subject.timeTable.forEach((item) => {
        console.log(item);
        let distanceStart = caculateDistanceTwoDays(item.start, dateStart);
        let distanceEnd = caculateDistanceTwoDays(item.end, dateStart);
        let day = item.day;

        let start = Math.floor(distanceStart / 7);
        let end = Math.floor(distanceEnd / 7);

        console.log(start, end);

        while (start <= end) {
            for (let i = item.time[0] - 1; i <= item.time[1] - 1; i++) {
                tables[start][i][day - 2].push({
                    subjectCode: subject.subjectCode,
                    subjectName: subject.subjectName,
                    classCode: subject.classCode
                });
                // console.log(`start: ${start} i: ${i} day: ${day} subject: ${subject.subjectCode}  tables: ${tables[start][i][day]}`)
            }
            start++;
        }
    })
    updateTable(tables)
}


function removeSubjectFromTable(subject) {
    // remove from table times

    subject.timeTable.forEach((item) => {
        let distanceStart = caculateDistanceTwoDays(item.start, dateStart);
        let distanceEnd = caculateDistanceTwoDays(item.end, dateStart);
        let day = item.day;

        let start = Math.floor(distanceStart / 7);
        let end = Math.floor(distanceEnd / 7);


        while (start <= end) {
            for (let i = item.time[0] - 1; i <= item.time[1] - 1; i++) {
                let index = tables[start][i][day - 2].findIndex((item) => item.subjectCode == subject.subjectCode)
                tables[start][i][day - 2].splice(index, 1);
            }
            start++;
        }

    })
    updateTable(tables)

}
function updateTable(tables) {
    const states = {
        available: {
            color: "white",
            icon: "+",
            colorText: "black"
        },

        full: {
            // don't have any subject
            color: 'green',
            icon: "o",
            colorText: "white"
        },

        // have one subject
        // have more than one subject
        over: {
            color: 'red',
            icon: "x",
            colorText: "white"
        }
    }

    for (let i = 0; i < 28; i++) {
        for (let r = 0; r < 14; r++) {
            for (let c = 0; c < 7; c++) {
                let id = "tkb" + i + r + c;
                let cell = $(`#${id}`);

                let state = states.available;
                if (tables[i][r][c].length == 1) {
                    state = states.full;
                }
                else if (tables[i][r][c].length > 1) {
                    state = states.over;
                }

                let hoverText = tables[i][r][c].map((item) => {
                    return `${item.subjectCode} - ${item.subjectName} - ${item.classCode}`
                }).join("\n")
                cell.css('background-color', state.color);
                cell.css('color', state.colorText);
                cell.text(state.icon);
                cell.attr('title', hoverText );

                // hover event for cell
            }
        }

    }
}






function reformatRoutine(str) {
    try {
        if (str.trim() == "")
            return [];

        str = str.toLowerCase();

        const dayInWeek = new Map([
            ["thứ 2", 2],
            ["thứ 3", 3],
            ["thứ 4", 4],
            ["thứ 5", 5],
            ["thứ 6", 6],
            ["thứ 7", 7],
            ["chủ nhật", 8]
        ]);


        return str.split("\n").map((item) => {
            if (item.indexOf("đến") == -1) {
                let [day, time, date] = item.split(",");
                let [startDay, startMonth, startYear] = date.split("/");
                let [start, end] = time.replace("tiết", "").trim().split("->").map((item) => parseInt(item));
                startYear = startYear.replace(/\D/g, "");

                return {
                    day: dayInWeek.get(day),
                    time: [start, end],
                    start: new Date(`20${startYear.trim()}-${startMonth.trim()}-${startDay.trim()}`),
                    end: new Date(`20${startYear.trim()}-${startMonth.trim()}-${startDay.trim()}`)
                };
            }

            let [day, time, date] = item.split(",");
            let [start, end] = date.split("đến");
            let [startDay, startMonth, startYear] = start.split("/");
            let [endDay, endMonth, endYear] = end.split("/");
            endYear = endYear.replace(/\D/g, "");

            time = time.replace("tiết", "").trim().split("->").map((item) => parseInt(item));

            return {
                day: dayInWeek.get(day),
                time: time,
                start: new Date(`20${startYear.trim()}-${startMonth.trim()}-${startDay.trim()}`),
                end: new Date(`20${endYear.trim()}-${endMonth.trim()}-${endDay.trim()}`)
            };


        });

    } catch (e) {
        console.log(e);
        return [];
    }
}




function drawTable() {
    let rows = 14; // Number of rows representing the periods
    let cols = 7; // Number of columns representing the days
    let tables = [];

    // Creating the main div container for the timetable
    let tkb_div = $("<div id='tkb_div' class='flex flex-wrap justify-center flex-row items-center'></div>");


    // draw tables that have 20 tables 1 table represent 1 week, and 1 day have 14 rows

    for (let i = 0; i < 28; i++) {
        let table_id = "tkbPreview" + i + 1;
        tables[i] = $('<table style="text-align:center;border-collapse: collapse;" class="tkb_preview_table" id="' + table_id + '"><thead> <th></th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th></th></thead><tbody>');

        for (let r = 0; r < rows; r++) {
            let start_time_in_hr = r +1;
            let tkb_separator = r % 2 ? 'border-bottom:2px solid #2fa4e7;' : '';
            let tr = $('<tr style="height:1px;' + tkb_separator + '"><td class="starttimerow">' + start_time_in_hr + '</td>');

            for (let c = 0; c < cols; c++) {
                let id = "tkb" + i + r + c;
                $('<td class="cellqh" id="' + (id) + '">+</td>').appendTo(tr);
            }



            tr.appendTo(tables[i]);
        }

        $('</tbody></table>').appendTo(tables[i]);
        tkb_div.append(tables[i]);



    }
    tkb_div.attr("style", "display:flex; flex-wrap: wrap; padding: 10px; justify-content: center; align-items: center;")
    // Adding date picker and dropdown for lectures
    let date_pickerSection = $("<div id= 'datepickersection'></div>");
    date_pickerSection.append('<div class="chonngaydiv"><label for="datetimepicker">Chọn ngày bắt đầu tuần đầu tiên (Xem trong TKB tuần)</label><input class="inputdate" id="datetimepicker" type="date" value="2024-08-12"></input><br></div>');
    date_pickerSection.append('<select class="dropdownlecture"><option value="" class="label_dropdownlecture">Chọn môn</option></select>');
    date_pickerSection.append('<div class="danhsachmonhoc_text"><strong>Các môn đã đăng ký</strong></div>');
    date_pickerSection.append('<div class="danhsachmonhoc"></div>');



    // Adding the timetable to the page
    $("div.card-body.p-0 div.row.d-flex.justify-content-center.text-nowrap.pt-1").prepend(date_pickerSection);
    $("div.card-body.p-0 div.row.d-flex.justify-content-center.text-nowrap.pt-1").prepend(tkb_div);

    let introduce = $("<div class='introduce'></div>")

    introduce.append("<h3>Made by Minh Triet(Alex Ng)</h3>")
    introduce.append("<h3>Tool hỗ trợ sắp xếp thời gian biểu PTITHCM</h3>")
    introduce.append("<h3>1. Chọn thời gian bắt đầu của kì học mới</h3>")
    introduce.append("<h3>2. Chọn môn học muốn đăng ký</h3>")
    introduce.append("<h3>Note: Ô màu trắng thì chưa có môn nào chiếm chỗ</h3>")
    introduce.append("<h3>Ô màu xanh thì đang có 1 môn, Ô màu đỏ là ô không hợp lệ(chứa 2 môn)</h3>")
    introduce.append("<h3>Không chịu trách nhiệm dưới bất kì hình thức nào</h3>")
    introduce.append("<h3>Mình mong sẽ nhận được feedbacks từ các bạn, facebook: https://fb.com/triet.nguyen.39904181 </h3>")
    introduce.attr("style", "display:flex; ; padding: 10px; justify-content: center; align-items: center; flex-direction: column")



    $("div.card-body.p-0 div.row.d-flex.justify-content-center.text-nowrap.pt-1").prepend(introduce);



}





function process() {
    if ($("#tkb_div").length == 0) {
        drawTable();
    }

    if (subjects.size == 0) {
        scraptSubjects();
    }

}





function reinitializeCheckboxInput() {
    // wait for the input to be loaded

    var timer = setInterval(() => {
        if ($('.editCheckBoxInput').length == 0) {
            return;
        }

        else {
            const tableSubjects = $('tbody')[28]
            const rows = tableSubjects.children
            // delete all checkbox
            // delete all checkbox, consist old or new
            //
            for (let i = 0; i < rows.length; i++) {
                let form = $(rows[i]).find("form")
                // replace this input with new input
                let key = rows[i].children[1].innerText + rows[i].children[3].innerText + rows[i].children[4].innerText;

                let currTag = $(form).find("input");

                currTag.attr("value", key);
                if (storeCheckedSubjects.has(key)) {
                    currTag.prop('checked', true);
                }
                else {
                    currTag.prop('checked', false);
                }
            }
            clearInterval(timer);
        }
    }, 200)
    }





function caculateDistanceTwoDays(day1, day2) {
    const parsedDate1 = new Date(day1);
    const parsedDate2 = new Date(day2);

    // Calculate the difference in milliseconds
    const differenceInMilliseconds = parsedDate2 - parsedDate1;

    // Convert milliseconds to days
    const millisecondsInOneDay = 1000 * 60 * 60 * 24;
    const differenceInDays = differenceInMilliseconds / millisecondsInOneDay;

    return Math.abs(differenceInDays);
}



// listener for choosing date in date tag
$(document).ready(function() {
    $(document).on('change', '.inputdate', function(event) {
        dateStart = new Date(event.target.value);

    })
})