feat(class): improve class/id
This commit is contained in:
@@ -79,59 +79,65 @@
|
||||
<div v-if="stats" class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Domaines -->
|
||||
<div v-if="stats.domains_stats?.length" class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Performance par domaine</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Évaluations par domaine</h2>
|
||||
<p class="text-xs text-gray-500 mb-4">Perspective enseignant : ce qui a été évalué</p>
|
||||
<div class="space-y-4">
|
||||
<div v-for="domain in stats.domains_stats" :key="domain.id" class="space-y-1">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="font-medium text-gray-700 truncate" :title="domain.name">{{ domain.name }}</span>
|
||||
<span class="font-bold" :style="{ color: domain.color || '#6B7280' }">
|
||||
{{ domain.mean?.toFixed(1) || '-' }}/20
|
||||
{{ domain.total_points_possible?.toFixed(1) || '0' }} points
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-1000 ease-out"
|
||||
:style="{
|
||||
width: `${domain.mean ? (domain.mean / 20) * 100 : 0}%`,
|
||||
width: `${getRelativeWidth(domain, stats.domains_stats)}%`,
|
||||
backgroundColor: domain.color || '#6B7280'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400">{{ domain.elements_count }} éléments évalués</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
{{ domain.evaluation_count }} élément(s) de notation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Performance par domaine</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Évaluations par domaine</h2>
|
||||
<p class="text-sm text-gray-500 italic">Aucune donnée de domaine disponible</p>
|
||||
</div>
|
||||
|
||||
<!-- Compétences -->
|
||||
<div v-if="stats.competences_stats?.length" class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Performance par compétence</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Évaluations par compétence</h2>
|
||||
<p class="text-xs text-gray-500 mb-4">Perspective enseignant : ce qui a été évalué</p>
|
||||
<div class="space-y-4">
|
||||
<div v-for="competence in stats.competences_stats" :key="competence.id" class="space-y-1">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="font-medium text-gray-700 truncate" :title="competence.name">{{ competence.name }}</span>
|
||||
<span class="font-bold" :style="{ color: competence.color || '#6B7280' }">
|
||||
{{ competence.mean?.toFixed(1) || '-' }}/3
|
||||
{{ competence.total_points_possible?.toFixed(1) || '0' }} points
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-1000 ease-out"
|
||||
:style="{
|
||||
width: `${competence.mean ? (competence.mean / 3) * 100 : 0}%`,
|
||||
width: `${getRelativeWidth(competence, stats.competences_stats)}%`,
|
||||
backgroundColor: competence.color || '#6B7280'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400">{{ competence.elements_count }} éléments évalués</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
{{ competence.evaluation_count }} élément(s) de notation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Performance par compétence</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Évaluations par compétence</h2>
|
||||
<p class="text-sm text-gray-500 italic">Aucune donnée de compétence disponible</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,32 +145,53 @@
|
||||
<!-- Students averages -->
|
||||
<div v-if="stats?.student_averages?.length" class="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Moyennes par élève</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800">Notes par élève</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">Cliquez sur les en-têtes pour trier</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Élève</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Moyenne</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Performance</th>
|
||||
<th
|
||||
@click="sortBy('name')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
Élève {{ getSortIcon('name') }}
|
||||
</th>
|
||||
<th
|
||||
@click="sortBy('average')"
|
||||
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
Moyenne {{ getSortIcon('average') }}
|
||||
</th>
|
||||
<th
|
||||
v-for="assessment in assessments"
|
||||
:key="assessment.assessment_id"
|
||||
@click="sortBy(`assessment_${assessment.assessment_id}`)"
|
||||
class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
:title="assessment.assessment_title"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="truncate max-w-[120px]">{{ assessment.assessment_title }}</span>
|
||||
<span class="text-[10px] font-normal">{{ getSortIcon(`assessment_${assessment.assessment_id}`) }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="student in stats.student_averages" :key="student.student_id" class="hover:bg-gray-50">
|
||||
<tr v-for="student in sortedStudents" :key="student.student_id" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ student.last_name }} {{ student.first_name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-bold">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-bold text-blue-600">
|
||||
{{ student.average?.toFixed(2) || '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="getPerformanceClass(student.average)"
|
||||
>
|
||||
{{ getPerformanceLabel(student.average) }}
|
||||
</span>
|
||||
<td
|
||||
v-for="assessment in assessments"
|
||||
:key="assessment.assessment_id"
|
||||
class="px-4 py-4 whitespace-nowrap text-sm text-center text-gray-700"
|
||||
>
|
||||
{{ getAssessmentScore(student, assessment.assessment_id) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -172,15 +199,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stats" class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Moyennes par élève</h2>
|
||||
<p class="text-sm text-gray-500 italic text-center py-4">Aucune moyenne disponible pour ce trimestre</p>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Notes par élève</h2>
|
||||
<p class="text-sm text-gray-500 italic text-center py-4">Aucune note disponible pour ce trimestre</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useClassesStore } from '@/stores/classes'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
@@ -192,6 +219,8 @@ const loading = ref(true)
|
||||
const classData = ref(null)
|
||||
const stats = ref(null)
|
||||
const trimester = ref(1)
|
||||
const sortColumn = ref('name')
|
||||
const sortDirection = ref('asc')
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
@@ -209,21 +238,71 @@ async function selectTrimester(t) {
|
||||
stats.value = await classesStore.fetchClassStats(route.params.id, t)
|
||||
}
|
||||
|
||||
// Fonctions pour les labels de performance
|
||||
function getPerformanceClass(average) {
|
||||
if (average === null || average === undefined) return 'bg-gray-100 text-gray-600'
|
||||
if (average >= 16) return 'bg-green-100 text-green-800'
|
||||
if (average >= 12) return 'bg-blue-100 text-blue-800'
|
||||
if (average >= 8) return 'bg-orange-100 text-orange-800'
|
||||
return 'bg-red-100 text-red-800'
|
||||
// Récupérer la liste des évaluations triée par date
|
||||
const assessments = computed(() => {
|
||||
if (!stats.value?.student_averages?.length) return []
|
||||
|
||||
// Extraire les évaluations depuis le premier élève
|
||||
const firstStudent = stats.value.student_averages[0]
|
||||
if (!firstStudent?.assessment_scores) return []
|
||||
|
||||
return Object.values(firstStudent.assessment_scores).sort((a, b) => a.assessment_id - b.assessment_id)
|
||||
})
|
||||
|
||||
// Fonction de tri des élèves
|
||||
const sortedStudents = computed(() => {
|
||||
if (!stats.value?.student_averages) return []
|
||||
|
||||
const students = [...stats.value.student_averages]
|
||||
|
||||
students.sort((a, b) => {
|
||||
let valA, valB
|
||||
|
||||
if (sortColumn.value === 'name') {
|
||||
valA = `${a.last_name} ${a.first_name}`.toLowerCase()
|
||||
valB = `${b.last_name} ${b.first_name}`.toLowerCase()
|
||||
} else if (sortColumn.value === 'average') {
|
||||
valA = a.average ?? -1
|
||||
valB = b.average ?? -1
|
||||
} else if (sortColumn.value.startsWith('assessment_')) {
|
||||
const assessmentId = parseInt(sortColumn.value.split('_')[1])
|
||||
valA = a.assessment_scores?.[assessmentId]?.score ?? -1
|
||||
valB = b.assessment_scores?.[assessmentId]?.score ?? -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
|
||||
const comparison = valA > valB ? 1 : valA < valB ? -1 : 0
|
||||
return sortDirection.value === 'asc' ? comparison : -comparison
|
||||
})
|
||||
|
||||
return students
|
||||
})
|
||||
|
||||
function sortBy(column) {
|
||||
if (sortColumn.value === column) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
sortColumn.value = column
|
||||
sortDirection.value = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
function getPerformanceLabel(average) {
|
||||
if (average === null || average === undefined) return '-'
|
||||
if (average >= 16) return 'Excellent'
|
||||
if (average >= 12) return 'Bon'
|
||||
if (average >= 8) return 'Moyen'
|
||||
return 'Insuffisant'
|
||||
function getAssessmentScore(student, assessmentId) {
|
||||
const score = student.assessment_scores?.[assessmentId]
|
||||
if (!score || score.score === null) return '-'
|
||||
return `${score.score.toFixed(1)}/${score.max_points.toFixed(0)}`
|
||||
}
|
||||
|
||||
function getSortIcon(column) {
|
||||
if (sortColumn.value !== column) return ''
|
||||
return sortDirection.value === 'asc' ? '▲' : '▼'
|
||||
}
|
||||
|
||||
function getRelativeWidth(item, allItems) {
|
||||
const maxPoints = Math.max(...allItems.map(d => d.total_points_possible || 0))
|
||||
if (maxPoints === 0) return 0
|
||||
return ((item.total_points_possible || 0) / maxPoints) * 100
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
|
||||
Reference in New Issue
Block a user