BUP UCAM Playground

Calculates In-Course marks and guide you to achieve better result.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         BUP UCAM Playground
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Calculates In-Course marks and guide you to achieve better result.
// @author       mdtiTAHSIN
// @match        https://ucam.bup.edu.bd/miu/result/StudentExamMarkSummary.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);
    }

    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);

})();