Vue par bénévole : recherche sur le bénévole ou le poste
Barre de recherche (sans accents ni casse) sur le nom du bénévole ou d'un de ses postes, postes trouvés cernés d'orange, cumulable avec les filtres et le tri. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cc8c2e61ab
commit
749c7e0917
@@ -110,6 +110,9 @@
|
||||
"volunteers.legend.now": "MAINTENANT",
|
||||
"volunteers.bar.title": "{position} · {time}",
|
||||
"volunteers.bar.overlapTitle": "{position} · {time} — en même temps qu'un autre poste",
|
||||
"volunteers.search.placeholder": "Rechercher un bénévole ou un poste…",
|
||||
"volunteers.search.label": "Rechercher un bénévole ou un poste",
|
||||
"volunteers.search.empty": "Aucun bénévole ni poste ne correspond à « {query} ».",
|
||||
"volunteers.count": "{count} BÉNÉVOLES",
|
||||
"volunteers.totalHours": "TOTAL H",
|
||||
"volunteers.sort.label": "TRIER PAR",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
columnWidthPx,
|
||||
dayBoundaryFromDayParts,
|
||||
festivalDay,
|
||||
foldAccents,
|
||||
hoursByVolunteer,
|
||||
placeInTimeGrid,
|
||||
type TimeGridDay,
|
||||
@@ -43,6 +44,8 @@ interface Bar {
|
||||
lane: number;
|
||||
/** Chevauche un autre créneau du même bénévole. */
|
||||
overlap: boolean;
|
||||
/** Le poste correspond à la recherche (barre mise en avant). */
|
||||
match: boolean;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -63,6 +66,10 @@ export function VolunteerViewPage() {
|
||||
const now = useNowMinutes();
|
||||
const [filter, setFilter] = useState<Filter>('all');
|
||||
const [sort, setSort] = useState<Sort>('name');
|
||||
// Recherche sur le nom du bénévole ou le nom d'un de ses postes, sans
|
||||
// tenir compte des accents ni de la casse.
|
||||
const [search, setSearch] = useState('');
|
||||
const query = foldAccents(search.trim());
|
||||
|
||||
const dayBoundary = useMemo(() => dayBoundaryFromDayParts(dayParts.data ?? []), [dayParts.data]);
|
||||
const shiftsById = useMemo(
|
||||
@@ -120,6 +127,7 @@ export function VolunteerViewPage() {
|
||||
width: p.columnSpan * l.slotW,
|
||||
lane: 0,
|
||||
overlap: false,
|
||||
match: false,
|
||||
label: `${timePart(shift.starts_at)}–${timePart(shift.ends_at)}`,
|
||||
});
|
||||
}
|
||||
@@ -134,19 +142,23 @@ export function VolunteerViewPage() {
|
||||
bar.lane = lane;
|
||||
bar.overlap = bars.some((o) => o !== bar && o.start < bar.end && bar.start < o.end);
|
||||
}
|
||||
const nameMatch = !query || foldAccents(`${v.first_name} ${v.last_name}`).includes(query);
|
||||
for (const bar of bars) bar.match = Boolean(query) && foldAccents(bar.positionName).includes(query);
|
||||
return {
|
||||
volunteer: v,
|
||||
visible: nameMatch || bars.some((b) => b.match),
|
||||
bars,
|
||||
lanes: Math.max(1, laneEnds.length),
|
||||
total: Math.round((hours.get(v.id) ?? 0) * 10) / 10,
|
||||
empty: items.length === 0,
|
||||
};
|
||||
});
|
||||
if (sort === 'name') return built;
|
||||
const shown = built.filter((r) => r.visible);
|
||||
if (sort === 'name') return shown;
|
||||
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, positionsById, hours, axis, dayBoundary, filter, sort]);
|
||||
return shown.sort((a, b) => direction * (a.total - b.total));
|
||||
}, [volunteers.data, assignments.data, shiftsById, positionsById, hours, axis, dayBoundary, filter, sort, query]);
|
||||
|
||||
// Position (px) de l'heure actuelle dans la piste, `null` hors grille.
|
||||
const nowX = useMemo(() => nowPosition(axis.layout, now, dayBoundary), [axis.layout, now, dayBoundary]);
|
||||
@@ -167,6 +179,16 @@ export function VolunteerViewPage() {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex flex-wrap items-center gap-[14px] border-b-2 border-ink bg-sand px-4 py-[9px]">
|
||||
<div className="relative w-full sm:w-[260px]">
|
||||
<input
|
||||
type="search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t('volunteers.search.placeholder')}
|
||||
aria-label={t('volunteers.search.label')}
|
||||
className="w-full border-2 border-ink bg-cream px-[9px] py-[6px] font-mono text-[12px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span className="font-mono text-[9px] opacity-60">{t('volunteers.filter.show')}</span>
|
||||
<Chip active={filter === 'all'} onClick={() => setFilter('all')}>
|
||||
@@ -198,7 +220,7 @@ export function VolunteerViewPage() {
|
||||
|
||||
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto">
|
||||
{rows.length === 0 ? (
|
||||
<EmptyNote>{t('volunteers.empty')}</EmptyNote>
|
||||
<EmptyNote>{query ? t('volunteers.search.empty', { query: search.trim() }) : t('volunteers.empty')}</EmptyNote>
|
||||
) : (
|
||||
<div className="w-max border-b-[3px] border-ink">
|
||||
{/* En-tête : jours, puis tranches de 30 min. */}
|
||||
@@ -355,6 +377,8 @@ function VolunteerRow({
|
||||
className={cx(
|
||||
'absolute z-[2] flex items-center overflow-hidden border-2 border-ink px-[5px] font-display text-[10px] font-extrabold leading-none',
|
||||
bar.overlap ? 'bg-yellow' : 'bg-blue',
|
||||
// Poste trouvé par la recherche : cerné d'orange.
|
||||
bar.match && 'outline outline-[3px] outline-offset-0 outline-orange',
|
||||
)}
|
||||
style={{
|
||||
left: bar.left + 1,
|
||||
|
||||
Reference in New Issue
Block a user