BetterUCAM

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);
    }

})();