Esprit Moyenne

Better moyenne experience

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