Vue par bénévole : tri par total d'heures
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:
co-authored by
Claude Opus 5.5
parent
ced0855d47
commit
4f86398ccb
@@ -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 +
|
||||||
|
|||||||
@@ -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É",
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
Reference in New Issue
Block a user