Esprit Moyenne

Better moyenne experience

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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