Esprit Moyenne

Better moyenne experience

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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