BetterUCAM

Calculates In-Course marks, attendance %, semester GPA, and guides you to achieve better results.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         BetterUCAM
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Calculates In-Course marks, attendance %, semester GPA, and guides you to achieve better results.
// @author       mdtiTAHSIN
// @match        https://ucam.bup.edu.bd/miu/result/StudentExamMarkSummary.aspx*
// @match        https://ucam.bup.edu.bd/miu/ClassAttendance/StudentClassAttendanceSummary.aspx*
// @match        https://ucam.bup.edu.bd/miu/student/StudentCourseHistory.aspx*
// @icon         https://ucam.bup.edu.bd/Images/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const TABLE_ID_PARTIAL = 'ExamMarkSummaryDetails';
    const RESULT_ROW_ID = 'userscript_calc_row';
    const PREDICTION_ID = 'userscript_prediction_card';

    // Grading Scale
    const GRADING_SCALE = [
        { grade: 'A+', point: '4.00', min: 80 },
        { grade: 'A', point: '3.75', min: 75 },
        { grade: 'A-', point: '3.50', min: 70 },
        { grade: 'B+', point: '3.25', min: 65 },
        { grade: 'B', point: '3.00', min: 60 },
        { grade: 'B-', point: '2.75', min: 55 },
        { grade: 'C+', point: '2.50', min: 50 },
        { grade: 'C', point: '2.25', min: 45 },
        { grade: 'D', point: '2.00', min: 40 }
    ];

    function parseMark(text) {
        if (!text) return 0;
        text = text.trim();
        if (text === '--' || text === '' || text.toLowerCase().includes('absent')) return 0;
        return parseFloat(text) || 0;
    }

    // --- 1. Grade Predictor UI ---
    function renderGradePredictor(targetTable, obtainedInCourse, maxInCourse, isLab) {
        if (maxInCourse <= 0) return;

        const container = targetTable.closest('.card-body') || targetTable.parentElement;
        let wrapper = document.getElementById(PREDICTION_ID);

        if (!wrapper) {
            wrapper = document.createElement('div');
            wrapper.id = PREDICTION_ID;
            wrapper.style.marginTop = '15px';
            wrapper.style.maxWidth = '500px';

            const toggleBtn = document.createElement('button');
            toggleBtn.innerText = "Show Grade Targets";
            Object.assign(toggleBtn.style, {
                backgroundColor: '#198754', color: 'white', border: 'none',
                padding: '8px 15px', borderRadius: '5px', fontSize: '13px',
                cursor: 'pointer', fontWeight: 'bold', display: 'flex', gap: '5px'
            });

            const contentDiv = document.createElement('div');
            contentDiv.id = 'userscript_prediction_content';
            contentDiv.style.display = 'none';
            contentDiv.style.marginTop = '10px';
            contentDiv.style.padding = '10px';
            contentDiv.style.border = '1px solid #d1e7dd';
            contentDiv.style.borderRadius = '5px';
            contentDiv.style.backgroundColor = '#f8fdfa';

            toggleBtn.onclick = (e) => {
                e.preventDefault();
                const isHidden = contentDiv.style.display === 'none';
                contentDiv.style.display = isHidden ? 'block' : 'none';
                toggleBtn.innerText = isHidden ? "Hide Grade Targets" : "Show Grade Targets";
                toggleBtn.style.backgroundColor = isHidden ? '#dc3545' : '#198754';
            };

            wrapper.appendChild(toggleBtn);
            wrapper.appendChild(contentDiv);
            container.appendChild(wrapper);
        }

        const contentDiv = wrapper.querySelector('#userscript_prediction_content');
        let multiplier = 1;
        let maxPaperMark = 100;

        if (isLab) {
            maxPaperMark = 40;
            multiplier = 1.0;
        }
        else {
            if (maxInCourse > 55) {
                multiplier = 2.5;
            } else {
                multiplier = 2.0;
            }
        }

        const headerText = `Final Exam Targets (Paper mark out of ${maxPaperMark})`;

        let tbodyRows = '';
        GRADING_SCALE.forEach((tier) => {
            let gap = tier.min - obtainedInCourse;
            let requiredPaperMark = gap * multiplier;
            let statusText = '', rowStyle = 'border-bottom: 1px solid #eee;', statusColor = '#333';

            if (gap <= 0) {
                statusText = "Done ✅";
                rowStyle += 'background-color: #d1e7dd; color: #0f5132; font-weight: bold;'; statusColor = '#0f5132';
            } else if (requiredPaperMark > maxPaperMark) {
                statusText = "-"; statusColor = '#ccc'; rowStyle += 'color: #aaa;';
            } else {
                statusText = `${requiredPaperMark.toFixed(2)}`; statusColor = '#000';
            }

            tbodyRows += `<tr style="${rowStyle}">
                <td style="padding: 4px;">${tier.grade}</td>
                <td style="padding: 4px; text-align: center;">${tier.point}</td>
                <td style="padding: 4px; text-align: center; color: ${statusColor}; font-weight: bold;">${statusText}</td>
            </tr>`;
        });

        contentDiv.innerHTML = `<div style="font-weight: bold; color: #0f5132; margin-bottom: 8px; border-bottom: 1px solid #ccc; padding-bottom: 5px; font-size: 13px;">${headerText}</div>
        <table style="width:100%; border-collapse:collapse; font-size:12px;">
            <thead><tr style="background-color: #eee; color: #333;"><th style="padding:5px;">Grade</th><th style="padding:5px; text-align:center;">GPA</th><th style="padding:5px; text-align:center;">Need</th></tr></thead>
            <tbody>${tbodyRows}</tbody>
        </table>`;
    }

    // --- 2. Smart Calculation Engine ---
    function updateCalculations(table) {
        const rows = Array.from(table.querySelectorAll('tbody tr')).filter(tr => !tr.querySelector('th') && tr.id !== RESULT_ROW_ID);
        const rowText = table.innerText.toLowerCase();
        const isLab = rowText.includes('quiz');

        let totalObtained = 0;
        let totalMax = 0;

        if (rows.length === 0 || rowText.includes('no data found')) {
        }
        else if (isLab) {
            // === LAB LOGIC ===
            let quiz = { o: 0, t: 0 }, othersO = 0, othersM = 0;
            rows.forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 4) return;
                const name = cells[1].innerText.trim().toLowerCase();
                const total = parseMark(cells[2].innerText);
                const obtained = parseMark(cells[3].innerText);

                if (name.includes('quiz')) { quiz.o += obtained; quiz.t += total; }
                else { othersO += obtained; othersM += total; }
            });

            let quizScore = (quiz.t > 0) ? (quiz.o / quiz.t) * 20 : 0;
            let quizMax = (quiz.t > 0) ? 20 : 0;
            totalObtained = quizScore + othersO;
            totalMax = quizMax + othersM;

        } else {
            // === THEORY LOGIC ===
            let cts = [], mid = { o: 0, t: 0 }, othersO = 0, othersM = 0;

            rows.forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 4) return;
                const name = cells[1].innerText.trim().toLowerCase();
                const total = parseMark(cells[2].innerText);
                const obtained = parseMark(cells[3].innerText);

                if (name.includes('class test')) {
                    cts.push({ o: obtained, t: total });
                } else if (name.includes('mid term')) {
                    mid = { o: obtained, t: total };
                } else {
                    othersO += obtained;
                    othersM += total;
                }
            });

            let ctScore = 0;
            let ctMaxWeighted = 0;
            if (cts.length > 0) {
                ctMaxWeighted = 10.00;
                let sumCtMax = cts.reduce((acc, curr) => acc + curr.t, 0);
                if (sumCtMax <= 15) {
                    ctScore = cts.reduce((acc, curr) => acc + curr.o, 0);
                } else {
                    let normalizedCTs = cts.map(ct => (ct.t > 0) ? (ct.o / ct.t) * 10 : 0);
                    normalizedCTs.sort((a, b) => b - a);
                    const top3 = normalizedCTs.slice(0, 3);
                    if (top3.length > 0) {
                        ctScore = top3.reduce((a, b) => a + b, 0) / 3;
                    }
                }
            }

            let midScore = 0;
            let midMaxWeighted = 0;
            if (mid.t > 0) {
                midMaxWeighted = 20.00;
                midScore = (mid.o / mid.t) * 20;
            }

            totalObtained = ctScore + midScore + othersO;
            totalMax = ctMaxWeighted + midMaxWeighted + othersM;
            if (totalMax > 49 && totalMax < 51) totalMax = 50.00;
            if (totalMax > 59 && totalMax < 61) totalMax = 60.00;
        }

        const resultRow = table.querySelector('#' + RESULT_ROW_ID);
        if (resultRow) {
            resultRow.cells[2].innerText = totalMax.toFixed(2);
            resultRow.cells[3].innerText = totalObtained.toFixed(2);
        } else {
            createResultRow(table, totalObtained, totalMax);
        }

        renderGradePredictor(table, totalObtained, totalMax, isLab);
    }

    // --- 3. Editable Cells ---
    function makeCellsEditable(table) {
        const rows = Array.from(table.querySelectorAll('tbody tr')).filter(tr => !tr.querySelector('th') && tr.id !== RESULT_ROW_ID);
        rows.forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length < 4) return;
            const targetCell = cells[3];
            const maxCell = cells[2];

            if (targetCell.getAttribute('data-processed') === 'true') return;
            targetCell.setAttribute('data-processed', 'true');

            if (targetCell.innerText.trim() !== '--') return;

            Object.assign(targetCell.style, {
                cursor: 'pointer', backgroundColor: '#fff3cd', border: '1px dashed #ffc107', title: "Click to Simulate"
            });

            targetCell.onclick = function () {
                if (this.querySelector('input')) return;
                const val = (this.innerText.trim() === '--') ? '' : this.innerText.trim();
                const maxVal = parseFloat(maxCell.innerText.trim()) || 100;

                const input = document.createElement('input');
                input.type = 'number';
                input.value = val;
                Object.assign(input.style, { width: '60px', padding: '2px', textAlign: 'center' });

                const save = () => {
                    let newVal = parseFloat(input.value);
                    if (isNaN(newVal)) { this.innerText = '--'; }
                    else {
                        if (newVal < 0) newVal = 0;
                        if (newVal > maxVal) newVal = maxVal;
                        this.innerText = newVal.toFixed(2);
                    }
                    updateCalculations(table);
                };
                input.onblur = save;
                input.onkeydown = (e) => { if (e.key === 'Enter') save(); };
                this.innerText = '';
                this.appendChild(input);
                input.focus();
            };
        });
    }

    function createResultRow(table, totalObtained, maxMarks) {
        if (table.querySelector('#' + RESULT_ROW_ID)) return;
        const tr = document.createElement('tr');
        tr.id = RESULT_ROW_ID;
        Object.assign(tr.style, { backgroundColor: '#d1e7dd', fontWeight: 'bold', color: '#0f5132', borderTop: '2px solid #0f5132' });
        tr.innerHTML = `<td></td><td align="right" style="padding-right:15px;">Total Obtained</td>
                        <td align="center">${maxMarks.toFixed(2)}</td><td align="center">${totalObtained.toFixed(2)}</td>`;
        table.querySelector('tbody').appendChild(tr);
    }

    // --- 4. Attendance Percentage Column ---
    const ATTENDANCE_TABLE_ID = 'ctl00_MainContainer_gvClassAttendanceSummary';
    const ATTENDANCE_PROCESSED_ATTR = 'data-attendance-pct';

    function addAttendancePercentage() {
        const table = document.getElementById(ATTENDANCE_TABLE_ID);
        if (!table || table.getAttribute(ATTENDANCE_PROCESSED_ATTR) === 'true') return;

        const rows = table.querySelectorAll('tbody tr');
        if (rows.length === 0) return;

        rows.forEach((row, idx) => {
            const headerCells = row.querySelectorAll('th');
            const dataCells = row.querySelectorAll('td');

            if (headerCells.length > 0) {
                const th = document.createElement('th');
                th.scope = 'col';
                th.textContent = 'Attendance %';
                const lastTh = headerCells[headerCells.length - 1];
                row.insertBefore(th, lastTh);
            } else if (dataCells.length >= 9) {
                const presentSpan = dataCells[6].querySelector('span');
                const absentSpan = dataCells[7].querySelector('span');
                const present = presentSpan ? parseInt(presentSpan.textContent.trim(), 10) || 0 : 0;
                const absent = absentSpan ? parseInt(absentSpan.textContent.trim(), 10) || 0 : 0;
                const total = present + absent;
                const pct = total > 0 ? (present / total) * 100 : 0;
                const td = document.createElement('td');
                td.align = 'center';

                const span = document.createElement('span');
                span.style.fontWeight = 'bold';
                span.textContent = total > 0 ? pct.toFixed(1) + '%' : 'N/A';

                if (pct >= 80) {
                    span.style.color = 'Green';
                } else if (pct >= 60) {
                    span.style.color = '#CC8400';
                } else {
                    span.style.color = 'Red';
                }

                td.appendChild(span);
                const lastTd = dataCells[dataCells.length - 1];
                row.insertBefore(td, lastTd);
            }
        });

        table.setAttribute(ATTENDANCE_PROCESSED_ATTR, 'true');
    }

    // --- 5. Semester GPA on Course History page ---
    const SEMESTER_GPA_ATTR = 'data-semester-gpa-processed';

    function addSemesterGPA() {
        const table = document.getElementById('markTable');
        if (!table || table.getAttribute(SEMESTER_GPA_ATTR) === 'true') return;

        const tbody = table.querySelector('tbody');
        if (!tbody) return;

        const allRows = Array.from(tbody.querySelectorAll('tr'));
        let currentSemesterCourses = [];
        let cumulativeCredits = 0;
        let cumulativeWeightedPoints = 0;
        let semesterInsertions = [];

        allRows.forEach((row) => {
            const cells = row.querySelectorAll('td');
            if (cells.length === 0) return;

            const firstCell = cells[0];
            if (firstCell.colSpan >= 4 && firstCell.textContent.includes('Total Credit')) {
                let semCredits = 0;
                let semWeightedPoints = 0;
                let hasGrades = false;

                currentSemesterCourses.forEach(course => {
                    if (course.gradePoint !== null) {
                        semCredits += course.credit;
                        semWeightedPoints += course.credit * course.gradePoint;
                        hasGrades = true;
                    }
                });

                let semGPA = hasGrades ? (semWeightedPoints / semCredits) : null;

                if (hasGrades) {
                    cumulativeCredits += semCredits;
                    cumulativeWeightedPoints += semWeightedPoints;
                }

                const gpaRow = document.createElement('tr');
                const labelCell = document.createElement('td');
                labelCell.colSpan = 4;
                labelCell.style.cssText = 'border:1px solid #008080;text-align:center';
                labelCell.innerHTML = '<b>GPA</b>';

                const valueCell = document.createElement('td');
                valueCell.colSpan = 3;
                valueCell.style.cssText = 'border:1px solid #008080;';

                if (semGPA !== null) {
                    valueCell.innerHTML = `<b style="margin-left:25px">${semGPA.toFixed(2)}</b>`;
                } else {
                    valueCell.innerHTML = '<i style="margin-left:25px">Results Pending</i>';
                }

                gpaRow.appendChild(labelCell);
                gpaRow.appendChild(valueCell);

                semesterInsertions.push({ afterRow: row, gpaRow: gpaRow });
                currentSemesterCourses = [];
                return;
            }

            if (cells.length >= 7) {
                const creditText = cells[4] ? cells[4].textContent.trim() : '';
                const gpText = cells[6] ? cells[6].textContent.trim() : '';
                const credit = parseFloat(creditText);
                const gradePoint = gpText !== '' ? parseFloat(gpText) : null;

                if (!isNaN(credit)) {
                    currentSemesterCourses.push({ credit, gradePoint: isNaN(gradePoint) ? null : gradePoint });
                }
            }
            else if (cells.length >= 5) {
                const creditCell = cells[3] ? cells[3].textContent.trim() : '';
                const gpCell = cells[5] ? cells[5].textContent.trim() : '';
                const credit = parseFloat(creditCell);
                const gradePoint = gpCell !== '' ? parseFloat(gpCell) : null;

                if (!isNaN(credit)) {
                    currentSemesterCourses.push({ credit, gradePoint: isNaN(gradePoint) ? null : gradePoint });
                }
            }
        });

        semesterInsertions.forEach(({ afterRow, gpaRow }) => {
            afterRow.parentNode.insertBefore(gpaRow, afterRow.nextSibling);
        });

        if (cumulativeCredits > 0) {
            const cgpaRow = document.createElement('tr');
            cgpaRow.style.cssText = 'font-weight: bold; background-color: aliceblue;';

            const cgpaLabel = document.createElement('td');
            cgpaLabel.colSpan = 4;
            cgpaLabel.style.cssText = 'text-align: center; border:1px solid #008080;';
            cgpaLabel.innerHTML = '<b>CGPA</b>';

            const cgpaValue = document.createElement('td');
            cgpaValue.colSpan = 3;
            cgpaValue.style.cssText = 'border:1px solid #008080;';

            const cgpa = cumulativeWeightedPoints / cumulativeCredits;
            cgpaValue.innerHTML = `<b style="margin-left:25px;">${cgpa.toFixed(2)}</b> <span style="margin-left: 10px;">(${cumulativeCredits.toFixed(2)} credits)</span>`;

            cgpaRow.appendChild(cgpaLabel);
            cgpaRow.appendChild(cgpaValue);
            tbody.appendChild(cgpaRow);
        }

        table.setAttribute(SEMESTER_GPA_ATTR, 'true');
    }

    // --- 6. Page-specific initialization ---
    const currentUrl = window.location.href;
    const isAttendancePage = currentUrl.includes('ClassAttendance/StudentClassAttendanceSummary');
    const isCourseHistoryPage = currentUrl.includes('student/StudentCourseHistory');

    if (isAttendancePage) {
        addAttendancePercentage();
        if (typeof Sys !== 'undefined' && Sys.WebForms && Sys.WebForms.PageRequestManager) {
            Sys.WebForms.PageRequestManager.getInstance().add_endRequest(function () {
                const table = document.getElementById(ATTENDANCE_TABLE_ID);
                if (table && !table.getAttribute(ATTENDANCE_PROCESSED_ATTR)) {
                    addAttendancePercentage();
                }
            });
        }
    } else if (isCourseHistoryPage) {
        const waitForTable = setInterval(() => {
            const table = document.getElementById('markTable');
            if (table && table.querySelectorAll('tbody tr').length > 0) {
                clearInterval(waitForTable);
                addSemesterGPA();
            }
        }, 500);
    } else {
        setInterval(() => {
            const table = document.querySelector(`table[id$="${TABLE_ID_PARTIAL}"]`);
            if (table) {
                if (table.innerText.toLowerCase().includes('no data found')) return;
                makeCellsEditable(table);
                if (!table.querySelector('#' + RESULT_ROW_ID)) updateCalculations(table);
            }
        }, 1000);
    }

})();