refactor(ui): unify frontend around compact, desktop-first design
All checks were successful
Build and Publish Docker Images / Build Frontend Image (push) Successful in 3m3s
Build and Publish Docker Images / Build Backend Image (push) Successful in 3m14s
Build and Publish Docker Images / Build Summary (push) Successful in 3s

Extract shared utilities (color functions, icon registry), replace hero
banners with compact PageHeader, add TrimesterSelector/ConfirmDialog/
Breadcrumb components, consolidate off-palette colors to design tokens,
convert AssessmentListView to table layout, compress ResultsView stats
into horizontal bar, and inline ClassFormView as a modal in ClassListView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:37:46 +01:00
parent bb15933e69
commit 6cca179346
31 changed files with 754 additions and 843 deletions

View File

@@ -1,74 +1,22 @@
<template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="px-4 sm:px-6 lg:px-8 py-8">
<LoadingSpinner v-if="loading" text="Chargement des résultats..." fullPage />
<template v-else-if="results">
<!-- Header amélioré avec métadonnées -->
<div class="bg-gradient-to-r from-success-500 to-emerald-600 rounded-2xl p-8 mb-8 text-white">
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold mb-2">{{ results.assessment_title }}</h1>
<p class="text-success-100 mb-4">Résultats de l'évaluation</p>
<!-- Métadonnées avec icônes -->
<div class="flex flex-wrap gap-4 text-sm text-success-100">
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{{ gradedStudents.length }}/{{ results.students_scores?.length || 0 }} élèves évalués</span>
</div>
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span>Moy: {{ calculatedStats?.mean?.toFixed(1) || '-' }}/20</span>
</div>
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<span>Max: {{ calculatedStats?.max?.toFixed(1) || '-' }}</span>
</div>
</div>
</div>
<!-- Bouton retour -->
<router-link
:to="`/assessments/${results.assessment_id}`"
class="btn bg-white/20 hover:bg-white/30 text-white"
>
Retour
</router-link>
</div>
</div>
<PageHeader
:title="results.assessment_title"
:subtitle="`Résultats \u00b7 ${gradedStudents.length}/${results.students_scores?.length || 0} élèves évalués`"
:breadcrumbs="[{ label: 'Évaluations', to: '/assessments' }, { label: results.assessment_title, to: `/assessments/${results.assessment_id}` }, { label: 'Résultats' }]"
/>
<!-- Statistics -->
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8">
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Évalués</p>
<p class="text-2xl font-bold">{{ gradedStudents.length }}<span class="text-sm text-gray-400">/{{ results.students_scores?.length }}</span></p>
</div>
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Moyenne</p>
<p class="text-2xl font-bold">{{ calculatedStats?.mean?.toFixed(2) || '-' }}</p>
</div>
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Médiane</p>
<p class="text-2xl font-bold">{{ calculatedStats?.median?.toFixed(2) || '-' }}</p>
</div>
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Écart-type</p>
<p class="text-2xl font-bold">{{ calculatedStats?.std_dev?.toFixed(2) || '-' }}</p>
</div>
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Min</p>
<p class="text-2xl font-bold">{{ calculatedStats?.min?.toFixed(2) || '-' }}</p>
</div>
<div class="card card-body text-center">
<p class="text-sm text-gray-500">Max</p>
<p class="text-2xl font-bold">{{ calculatedStats?.max?.toFixed(2) || '-' }}</p>
</div>
<!-- Statistics bar -->
<div class="flex items-center gap-6 px-4 py-3 bg-white rounded-lg border border-gray-200 mb-6 text-sm">
<div><span class="text-gray-500">Évalués</span> <span class="font-bold">{{ gradedStudents.length }}<span class="text-gray-400">/{{ results.students_scores?.length }}</span></span></div>
<div><span class="text-gray-500">Moy</span> <span class="font-bold">{{ calculatedStats?.mean?.toFixed(2) || '-' }}</span></div>
<div><span class="text-gray-500">Med</span> <span class="font-bold">{{ calculatedStats?.median?.toFixed(2) || '-' }}</span></div>
<div><span class="text-gray-500">E-T</span> <span class="font-bold">{{ calculatedStats?.std_dev?.toFixed(2) || '-' }}</span></div>
<div><span class="text-gray-500">Min</span> <span class="font-bold">{{ calculatedStats?.min?.toFixed(2) || '-' }}</span></div>
<div><span class="text-gray-500">Max</span> <span class="font-bold">{{ calculatedStats?.max?.toFixed(2) || '-' }}</span></div>
</div>
<!-- Avertissement si élèves non évalués -->
@@ -368,6 +316,7 @@ 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'
import PageHeader from '@/components/common/PageHeader.vue'
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
@@ -379,86 +328,7 @@ const configStore = useConfigStore()
const selectedStudents = ref([])
const showSendModal = ref(false)
// Color interpolation functions
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 }
}
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255
const max = Math.max(r, g, b), min = Math.min(r, g, b)
let h, s, l = (max + min) / 2
if (max === min) {
h = s = 0
} else {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break
case g: h = (b - r) / d + 2; break
case b: h = (r - g) / d + 4; break
}
h /= 6
}
return { h: h * 360, s: s * 100, l: l * 100 }
}
function hslToRgb(h, s, l) {
h /= 360; s /= 100; l /= 100
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1/6) return p + (q - p) * 6 * t
if (t < 1/2) return q
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
return p
}
let r, g, b
if (s === 0) {
r = g = b = l
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
}
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
}
function interpolateColorHSL(color1, color2, factor) {
const rgb1 = hexToRgb(color1)
const rgb2 = hexToRgb(color2)
const hsl1 = rgbToHsl(rgb1.r, rgb1.g, rgb1.b)
const hsl2 = rgbToHsl(rgb2.r, rgb2.g, rgb2.b)
let deltaH = hsl2.h - hsl1.h
if (deltaH > 180) hsl2.h -= 360
else if (deltaH < -180) hsl2.h += 360
const h = ((hsl1.h + (hsl2.h - hsl1.h) * factor) % 360 + 360) % 360
const s = hsl1.s + (hsl2.s - hsl1.s) * factor
const l = hsl1.l + (hsl2.l - hsl1.l) * factor
const rgb = hslToRgb(h, s, l)
return rgbToHex(rgb.r, rgb.g, rgb.b)
}
function getTextColorForBg(bgColor) {
const rgb = hexToRgb(bgColor)
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
return brightness > 128 ? '#000000' : '#ffffff'
}
import { interpolateColorHSL, getTextColorForBg } from '@/utils/colors'
// Get gradient color based on percentage
function getGradientColor(percentage) {