Esprit Moyenne

Better moyenne experience

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Esprit Moyenne
// @namespace    http://tampermonkey.net/
// @version      2026-02-18
// @description  Better moyenne experience
// @author       NotSoHealthy
// @match        https://esprit-tn.com/esponline/Etudiants/Resultat2021.aspx
// @icon         https://www.google.com/s2/favicons?sz=64&domain=esprit-tn.com
// @grant        none
// @license MIT
// ==/UserScript==

const matieres = [
	{
		name: "Advanced Big Data",
		cc: 0.4,
		tp: 0,
		exam: 0.6
	},
	{
		name: "Architecture des SI I",
		cc: 0.6,
		tp: 0,
		exam: 0.4
	},
	{
		name: "Architecture des SI II",
		cc: 0.6,
		tp: 0,
		exam: 0.4
	},
	{
		name: 'Architecture Orientée Services "SOA"',
		cc: 0,
		tp: 0.2,
		exam: 0.8
	},
	{
		name: "Communication, Culture et Citoyenneté F4"
	},
	{
		name: "Complexité appliquée à la RO",
		cc: 0.4,
		tp: 0,
		exam: 0.6
	},
	{
		name: "DEVOPS",
		cc: 0.6,
		tp: 0,
		exam: 0.4
	},
	{
		name: "Droit de la propriété intellectuelle",
		cc: 0,
		tp: 0,
		exam: 1
	},
	{
		name: "Gestion de projet",
		cc: 0.4,
		tp: 0,
		exam: 0.6
	},
	{
		name: "Graphes et applications",
		cc: 0,
		tp: 0.2,
		exam: 0.8
	},
	{
		name: "Initiation application côté client",
		cc: 0,
		tp: 0.2,
		exam: 0.8
	},
	{
		name: "Programmation linéaire",
		cc: 0,
		tp: 0.2,
		exam: 0.8

	}
];

(function() {
	'use strict';

	const style = document.createElement('style');
	style.innerHTML = `
    .row{
        margin: 0;
    }

    .table-header {
        color: white;
        background-color: #A80000;
        font-weight: bold;
    }

    .moyenne-high {
        background-color: green;
        color: white;
    }

    .moyenne-low {
        background-color: red;
        color: white;
    }

    #ContentPlaceHolder1_GridView1 {
        width: 100%;
        border-collapse: collapse;
    }

    #ContentPlaceHolder1_GridView1 th,
    #ContentPlaceHolder1_GridView1 td {
        border: 1px solid #ddd;
        padding: 8px;
    }

    #ContentPlaceHolder1_GridView1 tr:nth-child(even) {
        background-color: #f2f2f2;
    }

    #ContentPlaceHolder1_GridView1 tr:hover {
        background-color: #ddd;
    }

    #ContentPlaceHolder1_GridView1 .table-header:hover {
        background-color: #A80000;;
    }

    #downloadButton {
        margin-bottom: 10px;
        padding: 10px 20px;
        font-size: 16px;
        cursor: pointer;
        background-color: #A80000;
        color: white;
        border: none;
        border-radius: 5px;
        transition: transform 0.2s ease;
    }

    #downloadButton:hover {
        transform: scale(1.05);
        background-color: #CC0000;
    }
    `;
	document.head.appendChild(style);

	function loadScript(src) {
		return new Promise(function(resolve, reject) {
			if (document.querySelector(`script[src="${src}"]`)) {
				resolve();
				return;
			}
			const script = document.createElement('script');
			script.src = src;
			script.onload = () => resolve();
			script.onerror = () => reject(new Error(`Failed to load script ${src}`));
			document.head.appendChild(script);
		});
	}

	function getStudentDetails() {
		const nameElement = document.getElementById('Label2');
		const classElement = document.getElementById('Label3');
		const studentName = nameElement ? nameElement.textContent.trim() : 'Unknown Student';
		const studentClass = classElement ? classElement.textContent.trim() : 'Unknown Class';
		return { studentName, studentClass };
	}

	function getLogoImage() {
		const logoElement = document.querySelector('.img-responsive img');
		return logoElement ? logoElement.src : null;
	}

	function tableToJson(table) {
		const data = [];
		const headers = Array.from(table.rows[0].cells).map(cell =>
			cell.innerText.toLowerCase().replace(/ /g, '')
		);
		for (let i = 1; i < table.rows.length; i++) {
			const row = table.rows[i];
			const rowData = {};
			headers.forEach((header, index) => {
				rowData[header] = row.cells[index].innerText;
			});
			data.push(rowData);
		}
		data.forEach(item => {
			item.coef = parseFloat(item.coef.replace(',', '.'));
			item.note_exam = parseFloat(item.note_exam.replace(',', '.'));
			item.note_cc = parseFloat(item.note_cc.replace(',', '.'));
			item.note_tp = parseFloat(item.note_tp.replace(',', '.'));
		});
		return data;
	}

	function normalizeMatiereName(value) {
		return String(value ?? '')
			.trim()
			.toLowerCase()
			.normalize('NFD')
			.replace(/\p{Diacritic}/gu, '')
			.replace(/[“”"'’]/g, '')
			.replace(/[^a-z0-9]+/g, ' ')
			.replace(/\s+/g, ' ')
			.trim();
	}

	function isFiniteNumber(n) {
		return typeof n === 'number' && Number.isFinite(n);
	}

	function buildMatieresIndex(list) {
		const index = new Map();
		(list || []).forEach(m => {
			if (!m || !m.name) return;
			index.set(normalizeMatiereName(m.name), m);
		});
		return index;
	}

	const matieresIndex = buildMatieresIndex(matieres);

	function getWeightedMoyenne({ note_exam, note_cc, note_tp }, weights) {
		let wExam = isFiniteNumber(weights.exam) ? weights.exam : 0;
		let wCc = isFiniteNumber(weights.cc) ? weights.cc : 0;
		let wTp = isFiniteNumber(weights.tp) ? weights.tp : 0;

		// If a note is missing, drop its weight and renormalize to the remaining notes.
		if (Number.isNaN(note_exam)) wExam = 0;
		if (Number.isNaN(note_cc)) wCc = 0;
		if (Number.isNaN(note_tp)) wTp = 0;

		const sum = wExam + wCc + wTp;
		if (sum <= 0) return null;

		wExam /= sum;
		wCc /= sum;
		wTp /= sum;

		const safeExam = Number.isNaN(note_exam) ? 0 : note_exam;
		const safeCc = Number.isNaN(note_cc) ? 0 : note_cc;
		const safeTp = Number.isNaN(note_tp) ? 0 : note_tp;

		return safeExam * wExam + safeCc * wCc + safeTp * wTp;
	}

	function getFallbackMoyenne(item) {
		const { note_exam, note_cc, note_tp } = item;
		let moyenne = 0;
		if (Number.isNaN(note_tp)) {
			if (Number.isNaN(note_cc)) {
				moyenne = note_exam;
			} else {
				moyenne = note_exam * 0.6 + note_cc * 0.4;
				if (item.designation === 'Génie logiciel & atelier GL') {
					moyenne = note_exam * 0.4 + note_cc * 0.6;
				}
			}
		} else if (Number.isNaN(note_cc)) {
			moyenne = note_exam * 0.8 + note_tp * 0.2;
		} else {
			moyenne = note_exam * 0.5 + note_cc * 0.3 + note_tp * 0.2;
		}
		return moyenne;
	}

	function calculMoyenne(dataSet, totalCoef) {
		let total = 0;
		dataSet.forEach(item => {
			const { note_exam, note_cc, note_tp, coef } = item;

			// Try to use explicit weights from matieres ("when possible").
			const key = normalizeMatiereName(item.designation);
			const matiere = matieresIndex.get(key);
			const hasAnyWeights = !!matiere && (isFiniteNumber(matiere.exam) || isFiniteNumber(matiere.cc) || isFiniteNumber(matiere.tp));

			let moyenne = null;
			if (hasAnyWeights) {
				moyenne = getWeightedMoyenne(
					{ note_exam, note_cc, note_tp },
					{ exam: matiere.exam, cc: matiere.cc, tp: matiere.tp }
				);
			}

			// If we can't compute from matieres (no match / no weights / weights don't apply), use existing rules.
			if (moyenne === null) {
				moyenne = getFallbackMoyenne(item);
			}

			item.moyenne = moyenne;
			total += moyenne * coef;
		});
		dataSet.push({
			designation: 'Moyenne',
			coef: totalCoef,
			nom_ens: '',
			note_cc: '',
			note_tp: '',
			note_exam: '',
			moyenne: total / totalCoef
		});
		return dataSet;
	}

	function populateTable(table, dataSet) {
		let title = document.querySelector('.col-xs-10 > center > h1');
		title.innerHTML = title.innerHTML + ': ' + (document.querySelectorAll('tr').length - 1).toString();
		document.querySelectorAll('.row ~ br').forEach((e) => e.remove());

		let tableContent = `
            <tr class="table-header">
                <th scope="col">DESIGNATION</th>
                <th scope="col">COEF</th>
                <th scope="col">NOM_ENS</th>
                <th scope="col">NOTE_CC</th>
                <th scope="col">NOTE_TP</th>
                <th scope="col">NOTE_EXAM</th>
                <th scope="col">Moyenne</th>
            </tr>`;
		dataSet.forEach(item => {
			const moyenneClass = !Number.isNaN(item.moyenne)
				? item.moyenne >= 8 ? 'moyenne-high' : 'moyenne-low'
				: '';
			tableContent += `
                <tr>
                    <td>${item.designation}</td>
                    <td>${item.coef}</td>
                    <td>${item.nom_ens}</td>
                    <td>${Number.isNaN(item.note_cc) ? '' : item.note_cc}</td>
                    <td>${Number.isNaN(item.note_tp) ? '' : item.note_tp}</td>
                    <td>${Number.isNaN(item.note_exam) ? '' : item.note_exam}</td>
                    <td class="${moyenneClass}">${!Number.isNaN(item.moyenne) ? item.moyenne.toFixed(2) : ''}</td>
                </tr>`;
		});

		const tbody = table.querySelector('tbody') || table;
		tbody.innerHTML = tableContent;
		addDownloadButton(table);
	}

	function addDownloadButton(table) {
		const existing = document.getElementById('downloadButton');
		if (existing) return;

		const downloadButton = document.createElement('button');
		downloadButton.textContent = 'Download as PDF';
		downloadButton.id = 'downloadButton';
		downloadButton.type = 'button';
		table.parentNode.insertBefore(downloadButton, table);
		downloadButton.addEventListener('click', function(event) {
			event.preventDefault();
			generatePDF();
		});
	}

	async function generatePDF() {
		try {
			const { studentName, studentClass } = getStudentDetails();
			const logoSrc = getLogoImage();
			const element = document.getElementById('ContentPlaceHolder1_GridView1');
			const canvas = await html2canvas(element, {
				useCORS: true,
				allowTaint: true
			});
			const imgData = canvas.toDataURL('image/png');
			const pdf = new window.jspdf.jsPDF('p', 'pt', 'a4');
			pdf.setFontSize(16);
			pdf.text(`Student Name: ${studentName}`, 20, 20);
			pdf.setTextColor(204, 0, 0);
			pdf.text(`Class: ${studentClass}`, 20, 40);
			if (logoSrc) {
				const logoImage = new Image();
				logoImage.src = logoSrc;
				pdf.addImage(logoImage, 'PNG', pdf.internal.pageSize.getWidth() - 80, 10, 60, 60);
			}
			pdf.setTextColor(0, 0, 0);
			const pdfWidth = pdf.internal.pageSize.getWidth();
			const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
			pdf.addImage(imgData, 'PNG', 0, 70, pdfWidth, pdfHeight);
			pdf.save(`${studentName} Grades.pdf`);
		} catch (error) {
			console.error('Could not generate PDF:', error);
			alert('Could not generate PDF: ' + error.message);
		}
	}

	function newDropCheck(dataset) {
		let tableList = Array.from(document.querySelectorAll('#ContentPlaceHolder1_GridView1 tr td:nth-child(1)'));
		tableList.pop();
		let oldDrop = JSON.parse(localStorage.getItem('oldDrop')) || [];
		dataset.pop();
		let newDrop = dataset.map((e) => e.designation);
		localStorage.setItem('oldDrop', JSON.stringify(newDrop));
		let newItems = newDrop.filter(n => !oldDrop.includes(n));
		newItems.forEach((item) => {
			tableList.forEach((element) => {
				if (element.textContent === item) {
					element.textContent += ' 🔴';
				}
			});
		});
	}

	(async function main() {
		await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
		await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');

		const table = document.getElementById('ContentPlaceHolder1_GridView1');
		if (!table) return;

		const data = tableToJson(table);
		const sumCoef = data.reduce((sum, item) => sum + item.coef, 0);
		const newData = calculMoyenne(data, sumCoef);
		populateTable(table, newData);
		newDropCheck(newData);
	})();
})();