feat(mail): restauration de l'envoie de mail
All checks were successful
Build and Publish Docker Images / Build Frontend Image (push) Successful in 2m56s
Build and Publish Docker Images / Build Backend Image (push) Successful in 3m5s
Build and Publish Docker Images / Build Summary (push) Successful in 3s

This commit is contained in:
2025-12-04 06:04:13 +01:00
parent 08c8ee4931
commit f76b033d55
11 changed files with 1189 additions and 184 deletions

View File

@@ -202,30 +202,126 @@
</div>
</div>
<!-- Toolbar de sélection (mode sélection activé) -->
<div v-if="selectionMode && gradedStudentsWithEmail.length > 0" class="card mb-6 border-2 border-blue-500 bg-blue-50">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-semibold text-blue-900">Mode Sélection Activé</h3>
<p class="text-sm text-blue-700">
Cliquez sur les cases pour sélectionner les élèves qui recevront leur bilan
<span v-if="selectedStudents.length > 0" class="font-semibold">
({{ selectedStudents.length }} sélectionné{{selectedStudents.length > 1 ? 's' : ''}})
</span>
</p>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button
@click="toggleSelectAll"
class="btn btn-sm btn-secondary"
>
{{ allSelected ? 'Tout désélectionner' : 'Tout sélectionner' }}
</button>
<button
@click="cancelSelectionMode"
class="btn btn-sm btn-secondary"
>
<svg class="w-4 h-4 mr-1 inline" 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>
Annuler
</button>
<button
@click="openSendModal"
:disabled="selectedStudents.length === 0"
class="btn btn-primary shadow-lg"
:class="{ 'opacity-50 cursor-not-allowed': selectedStudents.length === 0 }"
>
<svg class="w-5 h-5 mr-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.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 à {{ selectedStudents.length }} élève{{selectedStudents.length > 1 ? 's' : ''}}
</button>
</div>
</div>
</div>
</div>
<!-- Student scores table -->
<div class="card">
<div class="card-header">
<div class="card-header flex justify-between items-center">
<h2 class="text-lg font-semibold">Détail par élève</h2>
<!-- Mode Normal : Bouton pour activer la sélection -->
<button
v-if="!selectionMode && gradedStudentsWithEmail.length > 0"
@click="activateSelectionMode"
class="btn btn-primary"
>
<svg class="w-5 h-5 mr-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.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 des bilans
</button>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th v-if="selectionMode && gradedStudentsWithEmail.length > 0" class="w-16">
<div class="flex items-center gap-1">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</th>
<th>Élève</th>
<th>Email</th>
<th class="text-center">Score</th>
<th class="text-center">%</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr
v-for="student in results.students_scores"
:key="student.student_id"
:class="{ 'bg-gray-50 opacity-60': !isStudentGraded(student) }"
:class="{
'bg-gray-50 opacity-60': !isStudentGraded(student),
'bg-blue-50 border-l-4 border-blue-500': selectionMode && isSelected(student.student_id)
}"
>
<td v-if="selectionMode && gradedStudentsWithEmail.length > 0" class="w-16">
<input
v-if="isStudentGraded(student) && student.email"
type="checkbox"
:value="student.student_id"
v-model="selectedStudents"
class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
/>
<span
v-else-if="isStudentGraded(student) && !student.email"
class="text-amber-500"
title="Pas d'adresse email"
>
</span>
</td>
<td class="font-medium">
{{ student.student_name }}
<span v-if="!isStudentGraded(student)" class="ml-2 text-xs text-amber-600">(non évalué)</span>
</td>
<td class="text-sm text-gray-600">
<span v-if="student.email">{{ student.email }}</span>
<span v-else class="text-gray-400 italic">Pas d'email</span>
</td>
<td class="text-center font-bold">
<template v-if="isStudentGraded(student)">
<span
@@ -266,12 +362,31 @@
</template>
<span v-else class="text-gray-400">-</span>
</td>
<td class="text-center">
<button
v-if="isStudentGraded(student)"
@click="openReportInNewTab(student.student_id)"
class="btn btn-sm btn-secondary"
title="Ouvrir le bilan dans un nouvel onglet"
>
👁️ Aperçu
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- Modal d'envoi de bilans -->
<SendReportsModal
v-if="showSendModal"
:assessment-id="results.assessment_id"
:selected-students="selectedStudentsData"
@close="showSendModal = false"
@sent="handleReportsSent"
/>
</div>
</template>
@@ -283,6 +398,7 @@ import { useConfigStore } from '@/stores/config'
import { Bar } from 'vue-chartjs'
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import SendReportsModal from '@/components/assessment/SendReportsModal.vue'
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
@@ -290,6 +406,11 @@ const route = useRoute()
const assessmentsStore = useAssessmentsStore()
const configStore = useConfigStore()
// État pour la sélection d'élèves et l'envoi d'emails
const selectedStudents = ref([])
const showSendModal = ref(false)
const selectionMode = ref(false) // Mode sélection activé/désactivé
// Color interpolation functions
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
@@ -586,6 +707,88 @@ function isStudentGraded(student) {
return student.has_grades === true
}
// Computed pour les élèves corrigés avec email
const gradedStudentsWithEmail = computed(() => {
return gradedStudents.value.filter(s => s.email)
})
// Vérifie si tous les élèves sont sélectionnés
const allSelected = computed(() => {
if (gradedStudentsWithEmail.value.length === 0) return false
return gradedStudentsWithEmail.value.every(s => selectedStudents.value.includes(s.student_id))
})
// Vérifie s'il y a au moins un élève avec email
const hasStudentsWithEmail = computed(() => {
return gradedStudentsWithEmail.value.length > 0
})
// Données des élèves sélectionnés pour le modal
const selectedStudentsData = computed(() => {
if (!results.value) return []
return results.value.students_scores
.filter(s => selectedStudents.value.includes(s.student_id))
.map(s => ({
id: s.student_id,
name: s.student_name,
email: s.email
}))
})
// Activer le mode sélection
function activateSelectionMode() {
selectionMode.value = true
selectedStudents.value = [] // Réinitialiser la sélection
}
// Annuler le mode sélection
function cancelSelectionMode() {
selectionMode.value = false
selectedStudents.value = []
}
// Vider la sélection
function clearSelection() {
selectedStudents.value = []
}
// Vérifier si un élève est sélectionné
function isSelected(studentId) {
return selectedStudents.value.includes(studentId)
}
// Toggle sélection de tous les élèves
function toggleSelectAll() {
if (allSelected.value) {
selectedStudents.value = []
} else {
selectedStudents.value = gradedStudentsWithEmail.value.map(s => s.student_id)
}
}
// Ouvrir le modal d'envoi
function openSendModal() {
if (selectedStudents.value.length === 0) return
showSendModal.value = true
}
// Gérer le résultat de l'envoi
function handleReportsSent(result) {
showSendModal.value = false
selectedStudents.value = []
selectionMode.value = false // Désactiver le mode sélection après envoi
// Le modal affiche déjà les résultats, pas besoin de notification supplémentaire
}
// Ouvrir l'aperçu d'un bilan dans un nouvel onglet
function openReportInNewTab(studentId) {
const assessmentId = results.value.assessment_id
// Construire l'URL complète avec le préfixe API
const baseUrl = window.location.origin // Ex: http://localhost:5173
const url = `${baseUrl}/api/v2/assessments/${assessmentId}/preview-report/${studentId}`
window.open(url, '_blank')
}
onMounted(async () => {
try {
// Charger la config du dégradé si pas déjà chargée