feat: add mailing and bilan to send

This commit is contained in:
2025-09-10 09:04:32 +02:00
parent 2c549c7234
commit 844d4d6bba
14 changed files with 2334 additions and 3 deletions

View File

@@ -52,6 +52,17 @@
</div>
</div>
<!-- Bouton d'envoi des bilans -->
<div class="flex justify-end mb-6">
<button id="sendReportsBtn" onclick="openSendReportsModal()"
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
📧 Envoyer les bilans par email
</button>
</div>
<!-- Histogramme des notes -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
@@ -764,5 +775,284 @@ document.addEventListener('DOMContentLoaded', function() {
createGradingElementsHeatmap('gradingElementsHeatmap', gradingElementsData);
{% endif %}
});
// === FONCTIONNALITÉ D'ENVOI DE BILANS ===
let eligibleStudents = [];
let selectedStudents = [];
// Charger les élèves éligibles au chargement de la page
document.addEventListener('DOMContentLoaded', function() {
loadEligibleStudents();
});
function loadEligibleStudents() {
fetch(`/assessments/{{ assessment.id }}/eligible-students`)
.then(response => response.json())
.then(data => {
if (data.success) {
eligibleStudents = data.students;
updateSendReportsButton(data.with_email_count, data.total_count);
} else {
console.error('Erreur lors du chargement des élèves:', data.error);
}
})
.catch(error => {
console.error('Erreur réseau:', error);
});
}
function updateSendReportsButton(withEmailCount, totalCount) {
const btn = document.getElementById('sendReportsBtn');
if (withEmailCount === 0) {
btn.disabled = true;
btn.classList.add('opacity-50', 'cursor-not-allowed');
btn.classList.remove('hover:bg-blue-700');
btn.innerHTML = `
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Aucune adresse email configurée
`;
} else {
btn.innerHTML = `
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
📧 Envoyer les bilans (${withEmailCount}/${totalCount} avec email)
`;
}
}
function openSendReportsModal() {
if (eligibleStudents.length === 0) {
alert('Aucun élève éligible trouvé');
return;
}
document.getElementById('sendReportsModal').classList.remove('hidden');
populateStudentsList();
}
function closeSendReportsModal() {
document.getElementById('sendReportsModal').classList.add('hidden');
selectedStudents = [];
}
function populateStudentsList() {
const container = document.getElementById('studentsList');
const studentsWithEmail = eligibleStudents.filter(s => s.has_email);
if (studentsWithEmail.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">Aucun élève avec adresse email configurée</p>';
return;
}
container.innerHTML = studentsWithEmail.map(student => `
<label class="flex items-center p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
<input type="checkbox"
value="${student.id}"
onchange="updateSelectedStudents()"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3">
<div class="flex-1">
<div class="font-medium text-gray-900">${student.full_name}</div>
<div class="text-sm text-gray-500">${student.email}</div>
</div>
</label>
`).join('');
// Sélectionner tous par défaut
selectAllStudents();
}
function selectAllStudents() {
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = true);
updateSelectedStudents();
}
function unselectAllStudents() {
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = false);
updateSelectedStudents();
}
function updateSelectedStudents() {
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]:checked');
selectedStudents = Array.from(checkboxes).map(cb => parseInt(cb.value));
const count = selectedStudents.length;
document.getElementById('selectedCount').textContent = count;
document.getElementById('sendReportsSubmitBtn').disabled = count === 0;
}
function sendReports() {
if (selectedStudents.length === 0) {
alert('Veuillez sélectionner au moins un élève');
return;
}
const customMessage = document.getElementById('customMessage').value.trim();
const submitBtn = document.getElementById('sendReportsSubmitBtn');
const originalText = submitBtn.innerHTML;
// Désactiver le bouton et afficher le loading
submitBtn.disabled = true;
submitBtn.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Envoi en cours...
`;
// Envoyer la requête
fetch(`/assessments/{{ assessment.id }}/send-reports`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
student_ids: selectedStudents,
custom_message: customMessage
})
})
.then(response => response.json())
.then(data => {
// Restaurer le bouton
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
if (data.success) {
// Afficher le résultat
showSendResult(data);
// Fermer la modal après un délai
setTimeout(() => {
closeSendReportsModal();
}, 3000);
} else {
alert(`Erreur lors de l'envoi: ${data.error}`);
}
})
.catch(error => {
// Restaurer le bouton en cas d'erreur
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
console.error('Erreur:', error);
alert('Erreur réseau lors de l\'envoi des bilans');
});
}
function showSendResult(result) {
const resultDiv = document.getElementById('sendResult');
let content = `<div class="mb-4">
<h3 class="text-lg font-medium ${result.success ? 'text-green-600' : 'text-red-600'} mb-2">
${result.message}
</h3>
</div>`;
if (result.sent_count > 0) {
content += `<div class="mb-3">
<p class="text-green-600">✅ ${result.sent_count} bilan(s) envoyé(s) avec succès</p>
</div>`;
}
if (result.error_count > 0) {
content += `<div class="mb-3">
<p class="text-red-600 font-medium">❌ ${result.error_count} erreur(s):</p>
<ul class="text-sm text-red-600 mt-2 max-h-32 overflow-y-auto">`;
result.errors.forEach(error => {
content += `<li class="py-1">• ${error}</li>`;
});
content += `</ul></div>`;
}
resultDiv.innerHTML = content;
resultDiv.classList.remove('hidden');
// Cacher les autres sections
document.getElementById('studentsSelection').classList.add('hidden');
document.getElementById('modalActions').classList.add('hidden');
}
// Fermer la modal en cliquant à côté
document.addEventListener('click', function(e) {
const modal = document.getElementById('sendReportsModal');
if (e.target === modal) {
closeSendReportsModal();
}
});
</script>
<!-- Modal d'envoi des bilans -->
<div id="sendReportsModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-2xl shadow-lg rounded-md bg-white">
<div class="mt-3">
<!-- En-tête -->
<div class="flex justify-between items-center pb-4 border-b">
<h3 class="text-lg font-medium text-gray-900">📧 Envoyer les bilans par email</h3>
<button onclick="closeSendReportsModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Sélection des élèves -->
<div id="studentsSelection" class="py-4">
<div class="flex justify-between items-center mb-4">
<h4 class="font-medium text-gray-900">Sélectionner les élèves</h4>
<div class="flex space-x-2">
<button onclick="selectAllStudents()"
class="text-sm text-blue-600 hover:text-blue-800">Tout sélectionner</button>
<span class="text-gray-300">|</span>
<button onclick="unselectAllStudents()"
class="text-sm text-blue-600 hover:text-blue-800">Tout désélectionner</button>
</div>
</div>
<div id="studentsList" class="max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-2 space-y-1">
<!-- Les élèves seront ajoutés ici par JavaScript -->
</div>
<p class="text-sm text-gray-600 mt-2">
<span id="selectedCount">0</span> élève(s) sélectionné(s)
</p>
</div>
<!-- Message personnalisé -->
<div class="py-4 border-t">
<label for="customMessage" class="block text-sm font-medium text-gray-700 mb-2">
Message personnalisé (optionnel)
</label>
<textarea id="customMessage"
rows="3"
placeholder="Message du professeur à ajouter dans les bilans..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<!-- Résultat de l'envoi -->
<div id="sendResult" class="hidden py-4 border-t">
<!-- Le résultat sera affiché ici -->
</div>
<!-- Actions -->
<div id="modalActions" class="flex justify-end space-x-3 pt-4 border-t">
<button onclick="closeSendReportsModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Annuler
</button>
<button id="sendReportsSubmitBtn" onclick="sendReports()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled>
📧 Envoyer les bilans
</button>
</div>
</div>
</div>
</div>
{% endblock %}

258
templates/config/email.html Normal file
View File

@@ -0,0 +1,258 @@
{% extends "base.html" %}
{% block title %}Configuration Email - Gestion Scolaire{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<a href="{{ url_for('config.index') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour à la configuration
</a>
<h1 class="text-2xl font-bold text-gray-900">Configuration Email</h1>
<p class="text-gray-600">Configurez les paramètres SMTP pour l'envoi des bilans d'évaluation</p>
</div>
</div>
<!-- Formulaire de configuration email -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Paramètres SMTP</h2>
<p class="text-sm text-gray-600 mt-1">
Configurez votre serveur SMTP pour envoyer les bilans d'évaluation par email
</p>
</div>
<form method="POST" action="{{ url_for('config.update_email') }}" class="px-6 py-4 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Serveur SMTP -->
<div>
<label for="smtp_host" class="block text-sm font-medium text-gray-700 mb-1">
Serveur SMTP
</label>
<input type="text"
id="smtp_host"
name="smtp_host"
value="{{ email_config.smtp_host }}"
placeholder="smtp.gmail.com"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Ex: smtp.gmail.com, smtp.outlook.com</p>
</div>
<!-- Port SMTP -->
<div>
<label for="smtp_port" class="block text-sm font-medium text-gray-700 mb-1">
Port SMTP
</label>
<input type="text"
id="smtp_port"
name="smtp_port"
value="{{ email_config.smtp_port }}"
placeholder="587"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">587 (TLS) ou 465 (SSL) généralement</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Nom d'utilisateur -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
Nom d'utilisateur
</label>
<input type="text"
id="username"
name="username"
value="{{ email_config.username }}"
placeholder="votre.email@domain.com"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Votre adresse email de connexion SMTP</p>
</div>
<!-- Mot de passe -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
Mot de passe
</label>
<input type="password"
id="password"
name="password"
value="{{ email_config.password }}"
placeholder="••••••••"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Mot de passe ou mot de passe d'application</p>
</div>
</div>
<!-- Options de sécurité -->
<div class="flex items-center">
<input type="checkbox"
id="use_tls"
name="use_tls"
{% if email_config.use_tls %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="use_tls" class="ml-2 text-sm text-gray-700">
Utiliser TLS (recommandé)
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Nom d'expéditeur -->
<div>
<label for="from_name" class="block text-sm font-medium text-gray-700 mb-1">
Nom d'expéditeur
</label>
<input type="text"
id="from_name"
name="from_name"
value="{{ email_config.from_name }}"
placeholder="Notytex"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Nom qui apparaîtra comme expéditeur</p>
</div>
<!-- Adresse d'expéditeur -->
<div>
<label for="from_address" class="block text-sm font-medium text-gray-700 mb-1">
Adresse d'expéditeur
</label>
<input type="email"
id="from_address"
name="from_address"
value="{{ email_config.from_address }}"
placeholder="professeur@etablissement.fr"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Adresse qui apparaîtra comme expéditeur</p>
</div>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('config.index') }}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Annuler
</a>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Sauvegarder
</button>
</div>
</form>
</div>
<!-- Test de configuration -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Test de configuration</h2>
<p class="text-sm text-gray-600 mt-1">
Testez votre configuration en envoyant un email de test
</p>
</div>
<form method="POST" action="{{ url_for('config.test_email') }}" class="px-6 py-4">
<div class="space-y-4">
<div>
<label for="test_email" class="block text-sm font-medium text-gray-700 mb-1">
Adresse email de test
</label>
<input type="email"
id="test_email"
name="test_email"
placeholder="test@example.com"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Un email de test sera envoyé à cette adresse</p>
</div>
<div class="flex justify-end">
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
📧 Envoyer un test
</button>
</div>
</div>
</form>
</div>
<!-- Aide configuration -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h3 class="text-lg font-medium text-blue-900 mb-3">💡 Aide à la configuration</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 class="font-medium text-blue-800 mb-2">Gmail</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li><strong>Serveur:</strong> smtp.gmail.com</li>
<li><strong>Port:</strong> 587 (TLS)</li>
<li><strong>Sécurité:</strong> Utiliser un mot de passe d'application</li>
</ul>
</div>
<div>
<h4 class="font-medium text-blue-800 mb-2">Outlook/Hotmail</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li><strong>Serveur:</strong> smtp-mail.outlook.com</li>
<li><strong>Port:</strong> 587 (TLS)</li>
<li><strong>Sécurité:</strong> Authentification moderne requise</li>
</ul>
</div>
</div>
<div class="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<p class="text-sm text-yellow-800">
<strong>⚠️ Sécurité:</strong> Pour Gmail, créez un mot de passe d'application dans votre compte Google.
N'utilisez pas votre mot de passe principal.
</p>
</div>
</div>
<!-- Serveur SMTP de test local -->
<div class="bg-green-50 border border-green-200 rounded-lg p-6">
<h3 class="text-lg font-medium text-green-900 mb-3">🧪 Tests en local - Serveur SMTP factice</h3>
<p class="text-sm text-green-700 mb-3">
Pour tester l'envoi d'emails sans configuration réelle, utilisez le serveur SMTP de débogage Python :
</p>
<div class="bg-green-100 p-4 rounded-md mb-4">
<h4 class="font-medium text-green-800 mb-2">1. Lancer le serveur de débogage</h4>
<p class="text-sm text-green-700 mb-2"><strong>Option A:</strong> Script inclus dans Notytex (recommandé)</p>
<code class="block text-sm bg-gray-900 text-green-400 p-2 rounded mb-3">
python debug_smtp_server.py
</code>
<p class="text-sm text-green-700 mb-2"><strong>Option B:</strong> Avec aiosmtpd</p>
<code class="block text-sm bg-gray-900 text-blue-400 p-2 rounded mb-1">
pip install aiosmtpd
</code>
<code class="block text-sm bg-gray-900 text-blue-400 p-2 rounded mb-3">
python -m aiosmtpd -n -l localhost:1025
</code>
<p class="text-xs text-green-600">💡 Le script inclus affiche les emails avec un formatage amélioré</p>
<p class="text-xs text-green-600">⚠️ Laissez ce terminal ouvert pendant les tests</p>
</div>
<div class="bg-green-100 p-4 rounded-md mb-4">
<h4 class="font-medium text-green-800 mb-2">2. Configuration pour les tests</h4>
<ul class="text-sm text-green-700 space-y-1">
<li><strong>Serveur SMTP:</strong> localhost</li>
<li><strong>Port:</strong> 1025</li>
<li><strong>Utilisateur/Mot de passe:</strong> Laisser vides</li>
<li><strong>TLS:</strong> ❌ Décocher</li>
<li><strong>Adresse expéditeur:</strong> test@notytex.local</li>
</ul>
</div>
<div class="bg-green-100 p-4 rounded-md">
<h4 class="font-medium text-green-800 mb-2">3. Résultat</h4>
<p class="text-sm text-green-700">
✅ Tous les emails s'afficheront dans le terminal (contenu HTML complet)<br>
✅ Aucun email réellement envoyé<br>
✅ Parfait pour tester les bilans d'évaluation
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -103,6 +103,43 @@
Configurer l'échelle
</a>
</div>
<!-- Configuration email -->
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center mr-4">
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Configuration Email</h3>
</div>
<p class="text-gray-600 mb-4">Paramètres SMTP pour l'envoi des bilans d'évaluation</p>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-sm text-gray-500">Serveur SMTP :</span>
<span class="text-sm font-medium text-gray-900">
{% set smtp_host = app_config.get('email.smtp_host', '') %}
{{ smtp_host if smtp_host else 'Non configuré' }}
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-500">Statut :</span>
<span class="text-sm font-medium">
{% set smtp_host = app_config.get('email.smtp_host', '') %}
{% set from_address = app_config.get('email.from_address', '') %}
{% if smtp_host and from_address %}
<span class="text-green-600">✅ Configuré</span>
{% else %}
<span class="text-red-600">⚠️ À configurer</span>
{% endif %}
</span>
</div>
</div>
<a href="{{ url_for('config.email') }}" class="mt-4 block w-full bg-orange-600 text-white py-2 px-4 rounded-md hover:bg-orange-700 transition-colors text-center">
Configurer Email
</a>
</div>
</div>
<!-- Actions globales -->

View File

@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Notytex - Bilan d'évaluation{% endblock %}</title>
<style>
/* Reset styles pour compatibilité email */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #374151;
background-color: #f9fafb;
padding: 20px;
}
.email-container {
max-width: 800px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.header {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 8px;
font-weight: 700;
}
.header p {
font-size: 16px;
opacity: 0.9;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
background-color: #f8fafc;
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.section h2 {
color: #1f2937;
font-size: 20px;
margin-bottom: 15px;
font-weight: 600;
}
.section h3 {
color: #374151;
font-size: 16px;
margin-bottom: 10px;
font-weight: 600;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-item {
text-align: center;
padding: 15px;
background-color: white;
border-radius: 8px;
border: 2px solid #e5e7eb;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #3b82f6;
}
.stat-label {
font-size: 12px;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 5px;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin: 8px 0;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background-color: white;
border-radius: 8px;
overflow: hidden;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.table th {
background-color: #f3f4f6;
font-weight: 600;
color: #374151;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
}
.table tr:hover {
background-color: #f9fafb;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-success {
background-color: #dcfce7;
color: #166534;
}
.badge-warning {
background-color: #fef3c7;
color: #92400e;
}
.badge-info {
background-color: #dbeafe;
color: #1e40af;
}
.competence-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
margin: 8px 0;
background-color: white;
border-radius: 6px;
border: 1px solid #e5e7eb;
}
.competence-name {
font-weight: 500;
}
.competence-score {
font-weight: 600;
color: #3b82f6;
}
.footer {
background-color: #f3f4f6;
padding: 20px 30px;
text-align: center;
border-top: 1px solid #e5e7eb;
}
.footer p {
color: #6b7280;
font-size: 12px;
margin: 5px 0;
}
.highlight {
background-color: #fef3c7;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
/* Responsive pour mobile */
@media only screen and (max-width: 600px) {
body {
padding: 10px;
}
.header {
padding: 20px;
}
.header h1 {
font-size: 24px;
}
.content {
padding: 20px;
}
.section {
padding: 15px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.table th,
.table td {
padding: 8px 10px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>{% block header_title %}🎓 Notytex{% endblock %}</h1>
<p>{% block header_subtitle %}Système de gestion scolaire{% endblock %}</p>
</div>
<div class="content">
{% block content %}
<!-- Contenu principal de l'email -->
{% endblock %}
</div>
<div class="footer">
<p><strong>{% block school_name %}Établissement scolaire{% endblock %}</strong></p>
<p>Email généré automatiquement par Notytex</p>
<p>Pour toute question, contactez votre professeur.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,239 @@
{% extends "email/base_email.html" %}
{% block title %}{{ report.assessment.title }} - {{ report.student.full_name }}{% endblock %}
{% block header_title %}📊 Bilan d'Évaluation{% endblock %}
{% block header_subtitle %}{{ report.assessment.title }} - {{ report.student.full_name }}{% endblock %}
{% block content %}
<!-- Informations sur l'évaluation -->
<div class="section">
<h2>📋 Informations sur l'évaluation</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ report.assessment.class_name }}</div>
<div class="stat-label">Classe</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ report.assessment.date.strftime('%d/%m/%Y') }}</div>
<div class="stat-label">Date</div>
</div>
<div class="stat-item">
<div class="stat-value">T{{ report.assessment.trimester }}</div>
<div class="stat-label">Trimestre</div>
</div>
<div class="stat-item">
<div class="stat-value">×{{ report.assessment.coefficient }}</div>
<div class="stat-label">Coefficient</div>
</div>
</div>
{% if report.assessment.description %}
<p style="margin-top: 15px; padding: 15px; background-color: #e0f2fe; border-radius: 6px; border-left: 3px solid #0288d1;">
<strong>Description :</strong> {{ report.assessment.description }}
</p>
{% endif %}
</div>
<!-- Note globale -->
<div class="section" style="border-left-color: #10b981;">
<h2>🎯 Note globale</h2>
<div style="text-align: center; padding: 30px;">
<div style="font-size: 48px; font-weight: bold; color: #10b981; margin-bottom: 10px;">
{{ "%.1f"|format(report.results.total_score) }}/{{ "%.1f"|format(report.results.total_max_points) }}
</div>
<div style="font-size: 16px; color: #6b7280; margin-bottom: 20px;">
Note obtenue sur cette évaluation
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ report.results.percentage }}%;
background: linear-gradient(90deg,
{% if report.results.percentage < 50 %}#ef4444{% elif report.results.percentage < 75 %}#f59e0b{% else %}#10b981{% endif %},
{% if report.results.percentage < 50 %}#dc2626{% elif report.results.percentage < 75 %}#d97706{% else %}#059669{% endif %});"></div>
</div>
</div>
</div>
<!-- Résultats par exercice -->
{% if report.exercises %}
<div class="section" style="border-left-color: #8b5cf6;">
<h2>📝 Résultats par exercice</h2>
{% for exercise in report.exercises %}
<div style="margin: 20px 0; padding: 15px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb;">
<h3 style="color: #8b5cf6;">{{ exercise.title }}</h3>
{% if exercise.description %}
<p style="font-size: 14px; color: #6b7280; margin-bottom: 10px;">{{ exercise.description }}</p>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center; margin: 10px 0;">
<span style="font-weight: 600;">Score :</span>
<span style="font-size: 18px; font-weight: 700; color: #8b5cf6;">
{{ "%.1f"|format(exercise.score) }}/{{ "%.1f"|format(exercise.max_points) }}
</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ exercise.percentage }}%; background-color: #8b5cf6;"></div>
</div>
{% if exercise.elements %}
<div style="margin-top: 15px;">
<h4 style="font-size: 14px; color: #374151; margin-bottom: 8px;">Détail des questions :</h4>
<table class="table" style="font-size: 14px;">
<thead>
<tr>
<th>Question</th>
<th>Compétence</th>
<th>Domaine</th>
<th>Résultat</th>
</tr>
</thead>
<tbody>
{% for element in exercise.elements %}
<tr>
<td>
<strong>{{ element.label }}</strong>
{% if element.description %}
<br><small style="color: #6b7280;">{{ element.description }}</small>
{% endif %}
</td>
<td>
{% if element.skill %}
<span class="badge badge-info">{{ element.skill }}</span>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td>
{% if element.domain %}
<small style="color: #6b7280;">{{ element.domain }}</small>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td style="text-align: center;">
{% if element.raw_value == '.' %}
<span style="color: #9ca3af; font-size: 20px;"></span>
<br><small style="color: #9ca3af;">Pas de réponse</small>
{% elif element.grading_type == 'score' and element.raw_value %}
{% set score_value = element.raw_value|int %}
<div style="font-size: 18px;">
{% for i in range(3) %}
{% if i < score_value %}{% else %}{% endif %}
{% endfor %}
</div>
<small style="color: #6b7280;">
{{ element.score_label if element.score_label else 'Score ' + score_value|string }}
</small>
{% elif element.raw_value %}
<strong style="font-size: 16px;">{{ element.raw_value }}/{{ element.max_points }}</strong>
{% else %}
<span style="color: #9ca3af; font-size: 20px;"></span>
<br><small style="color: #9ca3af;">Non noté</small>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Performances par compétence -->
{% if report.competences %}
<div class="section" style="border-left-color: #f59e0b;">
<h2>⭐ Performances par compétence</h2>
<p style="font-size: 14px; color: #6b7280; margin-bottom: 15px;">
Analyse de vos performances sur chaque compétence évaluée
</p>
{% for competence in report.competences %}
<div class="competence-item">
<div>
<div class="competence-name">{{ competence.name }}</div>
<div style="font-size: 12px; color: #6b7280;">
{{ competence.elements_count }} élément{{ 's' if competence.elements_count > 1 else '' }} évalué{{ 's' if competence.elements_count > 1 else '' }}
</div>
</div>
<div style="text-align: right;">
{% set competence_percentage = (competence.score / competence.max_points * 100) if competence.max_points > 0 else 0 %}
{% if competence_percentage < 20 %}
{% set star_count = 0 %}
{% elif competence_percentage < 50 %}
{% set star_count = 1 %}
{% elif competence_percentage < 80 %}
{% set star_count = 2 %}
{% else %}
{% set star_count = 3 %}
{% endif %}
<div style="font-size: 18px; margin-bottom: 4px;">
{% for i in range(3) %}
{% if i < star_count %}{% else %}{% endif %}
{% endfor %}
</div>
<div style="font-size: 12px; color: #6b7280;">
{{ "%.1f"|format(competence.score) }}/{{ "%.1f"|format(competence.max_points) }}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Performances par domaine -->
{% if report.domains %}
<div class="section" style="border-left-color: #06b6d4;">
<h2>🏷️ Performances par domaine</h2>
<p style="font-size: 14px; color: #6b7280; margin-bottom: 15px;">
Analyse de vos performances par thème/domaine
</p>
{% for domain in report.domains %}
<div class="competence-item">
<div>
<div class="competence-name">{{ domain.name }}</div>
<div style="font-size: 12px; color: #6b7280;">
{{ domain.elements_count }} élément{{ 's' if domain.elements_count > 1 else '' }} évalué{{ 's' if domain.elements_count > 1 else '' }}
</div>
</div>
<div style="text-align: right;">
{% set domain_percentage = (domain.score / domain.max_points * 100) if domain.max_points > 0 else 0 %}
{% if domain_percentage < 20 %}
{% set star_count = 0 %}
{% elif domain_percentage < 50 %}
{% set star_count = 1 %}
{% elif domain_percentage < 80 %}
{% set star_count = 2 %}
{% else %}
{% set star_count = 3 %}
{% endif %}
<div style="font-size: 18px; margin-bottom: 4px;">
{% for i in range(3) %}
{% if i < star_count %}{% else %}{% endif %}
{% endfor %}
</div>
<div style="font-size: 12px; color: #6b7280;">
{{ "%.1f"|format(domain.score) }}/{{ "%.1f"|format(domain.max_points) }}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Message personnalisé si fourni -->
{% if custom_message %}
<div class="section" style="border-left-color: #10b981; background-color: #f0fdf4;">
<h2>💬 Message du professeur</h2>
<p style="font-style: italic; color: #166534;">{{ custom_message }}</p>
</div>
{% endif %}
{% endblock %}