feat(mail): restauration de l'envoie de mail
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user