diff --git a/CLAUDE.md b/CLAUDE.md index fb0a673..a3167f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -981,6 +981,7 @@ Aucune dépendance locale requise hors Docker. | 50 | Badges : les créneaux affectés s'affichent en tableau — le jour de festival en en-tête, puis une colonne horaire et une colonne poste (le référent n'est plus imprimé sur le badge), filets à la couleur du champ, cellules repliées dans leur colonne ; même tableau dans l'aperçu de l'éditeur | à valider | | 51 | Refonte du créateur de badges d'après la maquette (`design/`) : champs sur le recto **et le verso** (`BadgeField.face`, verso en miroir dans le PDF), fond de zone, réduction automatique du texte, photo ovale, initiales sans photo, nom sur deux lignes (migration `0026`) ; éditeur en 3 colonnes (champs, aperçu recto/verso avec de vrais bénévoles, inspecteur), formats prédéfinis, annuler/flèches/zoom/repères, dépôt d'image sur l'aperçu, contrôle de tous les badges avant génération | à valider | | 52 | Nombre de badges par bénévole (`Volunteer.badge_count`, 1 par défaut, migration `0027`) : champ « Nombre de badges » sur la fiche, autant d'exemplaires sur les planches, total à imprimer affiché à la génération, colonne dans l'export Excel | à valider | +| 53 | Vue par bénévole : tri des lignes par nom ou par total d'heures affectées, croissant / décroissant (boutons « Trier par » et clic sur l'en-tête « TOTAL H ») — à total égal, l'ordre par nom est conservé | à valider | **Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 2a2e43d..4a3480e 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -109,6 +109,11 @@ "volunteers.legend.assigned": "AFFECTATION", "volunteers.count": "{count} BÉNÉVOLES", "volunteers.totalHours": "TOTAL H", + "volunteers.sort.label": "TRIER PAR", + "volunteers.sort.name": "NOM", + "volunteers.sort.hoursAsc": "HEURES ↑", + "volunteers.sort.hoursDesc": "HEURES ↓", + "volunteers.sort.toggle": "Trier par total d'heures (croissant / décroissant)", "volunteers.name": "NOM", "volunteers.cumulative": "CUMUL", "volunteers.badge.unassigned": "○ OUBLIÉ", diff --git a/frontend/src/pages/VolunteerViewPage.tsx b/frontend/src/pages/VolunteerViewPage.tsx index 35449b8..13bcd5b 100644 --- a/frontend/src/pages/VolunteerViewPage.tsx +++ b/frontend/src/pages/VolunteerViewPage.tsx @@ -10,6 +10,9 @@ import { t } from '@/i18n'; import { cx } from '@/lib/cx'; type Filter = 'all' | 'unassigned'; +/** Ordre des lignes : celui de la liste des bénévoles (par nom), ou par + * total d'heures affectées, croissant ou décroissant. */ +type Sort = 'name' | 'hoursAsc' | 'hoursDesc'; export function VolunteerViewPage() { const volunteers = useVolunteers(); @@ -18,6 +21,7 @@ export function VolunteerViewPage() { const positions = usePositions(); const dayParts = useDayParts(); const [filter, setFilter] = useState('all'); + const [sort, setSort] = useState('name'); const shiftsById = useMemo( () => new Map((shifts.data ?? []).map((s) => [s.id, s])), @@ -45,7 +49,7 @@ export function VolunteerViewPage() { list.push(a); byVolunteer.set(a.volunteer_id, list); } - return (volunteers.data ?? []) + const built = (volunteers.data ?? []) .filter((v) => { const own = byVolunteer.get(v.id) ?? []; if (filter === 'unassigned') return own.length === 0; @@ -69,7 +73,11 @@ export function VolunteerViewPage() { empty: own.length === 0, }; }); - }, [volunteers.data, assignments.data, shiftsById, hours, filter]); + if (sort === 'name') return built; + const direction = sort === 'hoursAsc' ? 1 : -1; + // `sort` est stable : à total égal, l'ordre par nom est conservé. + return built.sort((a, b) => direction * (a.total - b.total)); + }, [volunteers.data, assignments.data, shiftsById, hours, filter, sort]); if ( volunteers.isLoading || @@ -104,6 +112,18 @@ export function VolunteerViewPage() { {t('volunteers.filter.unassigned')} +
+ {t('volunteers.sort.label')} + setSort('name')}> + {t('volunteers.sort.name')} + + setSort('hoursAsc')}> + {t('volunteers.sort.hoursAsc')} + + setSort('hoursDesc')}> + {t('volunteers.sort.hoursDesc')} + +
{t('legend.title')} {t('volunteers.legend.assigned')} @@ -130,9 +150,15 @@ export function VolunteerViewPage() {
))} -
+
+ {sort === 'hoursAsc' ? ' ↑' : sort === 'hoursDesc' ? ' ↓' : null} +
{t('volunteers.name')}