Vue par bénévole : tri par total d'heures
ci-cd / build-backend (push) Successful in 6s
ci-cd / build-frontend (push) Successful in 19s
ci-cd / deploy (push) Successful in 12s

Tri des lignes par nom ou par total d'heures affectées, croissant ou
décroissant (boutons « Trier par » et clic sur l'en-tête « TOTAL H ») ;
à total égal, l'ordre par nom est conservé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 19:23:22 +01:00
co-authored by Claude Opus 5.5
parent ced0855d47
commit 4f86398ccb
3 changed files with 36 additions and 4 deletions
+1
View File
@@ -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 | | 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 | | 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 | | 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 **Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
+5
View File
@@ -109,6 +109,11 @@
"volunteers.legend.assigned": "AFFECTATION", "volunteers.legend.assigned": "AFFECTATION",
"volunteers.count": "{count} BÉNÉVOLES", "volunteers.count": "{count} BÉNÉVOLES",
"volunteers.totalHours": "TOTAL H", "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.name": "NOM",
"volunteers.cumulative": "CUMUL", "volunteers.cumulative": "CUMUL",
"volunteers.badge.unassigned": "○ OUBLIÉ", "volunteers.badge.unassigned": "○ OUBLIÉ",
+30 -4
View File
@@ -10,6 +10,9 @@ import { t } from '@/i18n';
import { cx } from '@/lib/cx'; import { cx } from '@/lib/cx';
type Filter = 'all' | 'unassigned'; 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() { export function VolunteerViewPage() {
const volunteers = useVolunteers(); const volunteers = useVolunteers();
@@ -18,6 +21,7 @@ export function VolunteerViewPage() {
const positions = usePositions(); const positions = usePositions();
const dayParts = useDayParts(); const dayParts = useDayParts();
const [filter, setFilter] = useState<Filter>('all'); const [filter, setFilter] = useState<Filter>('all');
const [sort, setSort] = useState<Sort>('name');
const shiftsById = useMemo( const shiftsById = useMemo(
() => new Map((shifts.data ?? []).map((s) => [s.id, s])), () => new Map((shifts.data ?? []).map((s) => [s.id, s])),
@@ -45,7 +49,7 @@ export function VolunteerViewPage() {
list.push(a); list.push(a);
byVolunteer.set(a.volunteer_id, list); byVolunteer.set(a.volunteer_id, list);
} }
return (volunteers.data ?? []) const built = (volunteers.data ?? [])
.filter((v) => { .filter((v) => {
const own = byVolunteer.get(v.id) ?? []; const own = byVolunteer.get(v.id) ?? [];
if (filter === 'unassigned') return own.length === 0; if (filter === 'unassigned') return own.length === 0;
@@ -69,7 +73,11 @@ export function VolunteerViewPage() {
empty: own.length === 0, 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 ( if (
volunteers.isLoading || volunteers.isLoading ||
@@ -104,6 +112,18 @@ export function VolunteerViewPage() {
{t('volunteers.filter.unassigned')} {t('volunteers.filter.unassigned')}
</Chip> </Chip>
</div> </div>
<div className="flex items-center gap-[6px]">
<span className="font-mono text-[9px] opacity-60">{t('volunteers.sort.label')}</span>
<Chip active={sort === 'name'} onClick={() => setSort('name')}>
{t('volunteers.sort.name')}
</Chip>
<Chip active={sort === 'hoursAsc'} onClick={() => setSort('hoursAsc')}>
{t('volunteers.sort.hoursAsc')}
</Chip>
<Chip active={sort === 'hoursDesc'} onClick={() => setSort('hoursDesc')}>
{t('volunteers.sort.hoursDesc')}
</Chip>
</div>
<div className="ml-auto flex items-center gap-3 border-2 border-ink bg-cream px-[9px] py-[5px]"> <div className="ml-auto flex items-center gap-3 border-2 border-ink bg-cream px-[9px] py-[5px]">
<span className="font-mono text-[8.5px] opacity-60">{t('legend.title')}</span> <span className="font-mono text-[8.5px] opacity-60">{t('legend.title')}</span>
<LegendItem swatch="bg-blue border-ink">{t('volunteers.legend.assigned')}</LegendItem> <LegendItem swatch="bg-blue border-ink">{t('volunteers.legend.assigned')}</LegendItem>
@@ -130,9 +150,15 @@ export function VolunteerViewPage() {
</span> </span>
</div> </div>
))} ))}
<div className="sticky right-0 top-0 z-[7] flex items-center border-b-2 border-l-[3px] border-ink bg-yellow px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider"> <button
type="button"
onClick={() => setSort(sort === 'hoursDesc' ? 'hoursAsc' : 'hoursDesc')}
title={t('volunteers.sort.toggle')}
className="sticky right-0 top-0 z-[7] flex cursor-pointer items-center gap-1 border-b-2 border-l-[3px] border-ink bg-yellow px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider hover:bg-yellow-soft"
>
{t('volunteers.totalHours')} {t('volunteers.totalHours')}
</div> {sort === 'hoursAsc' ? ' ↑' : sort === 'hoursDesc' ? ' ↓' : null}
</button>
<div className="sticky left-0 top-[27px] z-[7] flex items-center border-b-[3px] border-r-[3px] border-ink bg-sand px-2 font-mono text-[8.5px]"> <div className="sticky left-0 top-[27px] z-[7] flex items-center border-b-[3px] border-r-[3px] border-ink bg-sand px-2 font-mono text-[8.5px]">
{t('volunteers.name')} {t('volunteers.name')}