Grille & vue bénévole : message quand il n'y a pas de données
- components/state : composant EmptyNote (encadré pointillés). - GrillePage : si aucun créneau, message au lieu de la grille vide. - VolunteerViewPage : si aucun bénévole, message ; si un filtre ne renvoie rien, message à la place du tableau (avant, l'en-tête collant restait affiché). typecheck + lint + build + smoke OK. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
385c46c241
commit
06f8736c33
@@ -1,6 +1,17 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { ApiError } from '@/api';
|
import { ApiError } from '@/api';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
export function EmptyNote({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="m-4 flex max-w-[640px] items-center gap-[14px] border-[3px] border-dashed border-ink bg-paper p-[22px]">
|
||||||
|
<span className="h-9 w-9 flex-none border-[3px] border-ink bg-yellow" />
|
||||||
|
<span className="font-display text-[14px] font-extrabold leading-snug">{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Loading() {
|
export function Loading() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 p-6 font-mono text-[11px] uppercase tracking-widest opacity-60">
|
<div className="flex items-center gap-3 p-6 font-mono text-[11px] uppercase tracking-widest opacity-60">
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"grille.positionsCount": "{count} POSTES",
|
"grille.positionsCount": "{count} POSTES",
|
||||||
"grille.assignedRequired": "AFFECTÉS / IDÉAL",
|
"grille.assignedRequired": "AFFECTÉS / IDÉAL",
|
||||||
"grille.footnote": "Chaque cellule est un créneau. Nombre = bénévoles affectés, / = effectif idéal. Cliquer ouvre le panneau d'affectation à droite.",
|
"grille.footnote": "Chaque cellule est un créneau. Nombre = bénévoles affectés, / = effectif idéal. Cliquer ouvre le panneau d'affectation à droite.",
|
||||||
|
"grille.empty": "Aucun créneau pour le moment. Créez des postes dans Configuration → « Postes & créneaux » — un créneau est généré par jour et par créneau type.",
|
||||||
"grille.panel.empty.title": "Cliquez une cellule de la grille pour affecter des bénévoles.",
|
"grille.panel.empty.title": "Cliquez une cellule de la grille pour affecter des bénévoles.",
|
||||||
"grille.panel.empty.body": "Le panneau s'ouvre ici, sans quitter la grille : candidats triés par charge, alertes bloquantes et avertissements du moteur de validation.",
|
"grille.panel.empty.body": "Le panneau s'ouvre ici, sans quitter la grille : candidats triés par charge, alertes bloquantes et avertissements du moteur de validation.",
|
||||||
"grille.panel.slotDuration": "CRÉNEAU DE {hours} H",
|
"grille.panel.slotDuration": "CRÉNEAU DE {hours} H",
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
"volunteers.badge.overloaded": "▲ SURCHARGE",
|
"volunteers.badge.overloaded": "▲ SURCHARGE",
|
||||||
"volunteers.badge.unassigned": "○ OUBLIÉ",
|
"volunteers.badge.unassigned": "○ OUBLIÉ",
|
||||||
"volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l'ensemble de l'équipe.",
|
"volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l'ensemble de l'équipe.",
|
||||||
|
"volunteers.noData": "Aucun bénévole pour le moment. Ajoutez-les dans Configuration → « Bénévoles & import ».",
|
||||||
"volunteers.footnote": "Surcharge = plus de 8 h sur une même journée ou moins de 8 h entre deux créneaux. Oublié = aucune affectation.",
|
"volunteers.footnote": "Surcharge = plus de 8 h sur une même journée ou moins de 8 h entre deux créneaux. Oublié = aucune affectation.",
|
||||||
|
|
||||||
"design.noteTitle": "CHOIX DE CONCEPTION",
|
"design.noteTitle": "CHOIX DE CONCEPTION",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useMatrix } from '@/api/hooks';
|
import { useMatrix } from '@/api/hooks';
|
||||||
import type { MatrixPosition, MatrixShift, PositionCategory } from '@/api/schema';
|
import type { MatrixPosition, MatrixShift, PositionCategory } from '@/api/schema';
|
||||||
import { Chip } from '@/components/ui';
|
import { Chip } from '@/components/ui';
|
||||||
import { ErrorNote, Loading } from '@/components/state';
|
import { EmptyNote, ErrorNote, Loading } from '@/components/state';
|
||||||
import { AssignmentPanel } from '@/pages/grille/AssignmentPanel';
|
import { AssignmentPanel } from '@/pages/grille/AssignmentPanel';
|
||||||
import { categoryLabel, latticeFromMatrix, type LatticeColumn } from '@/lib/planning';
|
import { categoryLabel, latticeFromMatrix, type LatticeColumn } from '@/lib/planning';
|
||||||
import { datePart, shortDayLabel, timePart } from '@/lib/time';
|
import { datePart, shortDayLabel, timePart } from '@/lib/time';
|
||||||
@@ -25,6 +25,15 @@ export function GrillePage() {
|
|||||||
if (matrix.error) return <ErrorNote error={matrix.error} />;
|
if (matrix.error) return <ErrorNote error={matrix.error} />;
|
||||||
if (!matrix.data) return null;
|
if (!matrix.data) return null;
|
||||||
|
|
||||||
|
const hasShifts = matrix.data.positions.some((p) => p.shifts.length > 0);
|
||||||
|
if (!hasShifts) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
|
<EmptyNote>{t('grille.empty')}</EmptyNote>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const categories = [...new Set(matrix.data.positions.map((p) => p.category))].sort();
|
const categories = [...new Set(matrix.data.positions.map((p) => p.category))].sort();
|
||||||
const shownDays = dayFilter === 'all' ? lattice.days : lattice.days.filter((d) => d.date === dayFilter);
|
const shownDays = dayFilter === 'all' ? lattice.days : lattice.days.filter((d) => d.date === dayFilter);
|
||||||
const shownColumns = shownDays.flatMap((d) => d.columns);
|
const shownColumns = shownDays.flatMap((d) => d.columns);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/hooks';
|
import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/hooks';
|
||||||
import type { AssignmentRead, ShiftRead, VolunteerRead } from '@/api/schema';
|
import type { AssignmentRead, ShiftRead, VolunteerRead } from '@/api/schema';
|
||||||
import { Chip } from '@/components/ui';
|
import { Chip } from '@/components/ui';
|
||||||
import { ErrorNote, Loading } from '@/components/state';
|
import { EmptyNote, ErrorNote, Loading } from '@/components/state';
|
||||||
import { buildLattice, hoursByVolunteer, isOverloaded } from '@/lib/planning';
|
import { buildLattice, hoursByVolunteer, isOverloaded } from '@/lib/planning';
|
||||||
import { datePart, shortDayLabel, timePart } from '@/lib/time';
|
import { datePart, shortDayLabel, timePart } from '@/lib/time';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
@@ -67,10 +67,24 @@ export function VolunteerViewPage() {
|
|||||||
});
|
});
|
||||||
}, [volunteers.data, assignments.data, shiftsById, hours, filter]);
|
}, [volunteers.data, assignments.data, shiftsById, hours, filter]);
|
||||||
|
|
||||||
if (volunteers.isLoading || shifts.isLoading || assignments.isLoading) return <Loading />;
|
if (
|
||||||
const error = volunteers.error ?? shifts.error ?? assignments.error;
|
volunteers.isLoading ||
|
||||||
|
shifts.isLoading ||
|
||||||
|
assignments.isLoading ||
|
||||||
|
positions.isLoading
|
||||||
|
)
|
||||||
|
return <Loading />;
|
||||||
|
const error = volunteers.error ?? shifts.error ?? assignments.error ?? positions.error;
|
||||||
if (error) return <ErrorNote error={error} />;
|
if (error) return <ErrorNote error={error} />;
|
||||||
|
|
||||||
|
if ((volunteers.data ?? []).length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
|
<EmptyNote>{t('volunteers.noData')}</EmptyNote>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const gridTemplateColumns = `230px repeat(${lattice.columns.length}, 62px) 84px`;
|
const gridTemplateColumns = `230px repeat(${lattice.columns.length}, 62px) 84px`;
|
||||||
const abbr = (positionId: string) => (positionsById.get(positionId)?.name ?? positionId).slice(0, 4).toUpperCase();
|
const abbr = (positionId: string) => (positionsById.get(positionId)?.name ?? positionId).slice(0, 4).toUpperCase();
|
||||||
|
|
||||||
@@ -96,6 +110,9 @@ export function VolunteerViewPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto">
|
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto">
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<EmptyNote>{t('volunteers.empty')}</EmptyNote>
|
||||||
|
) : (
|
||||||
<div className="grid w-max border-b-[3px] border-ink" style={{ gridTemplateColumns }}>
|
<div className="grid w-max border-b-[3px] border-ink" style={{ gridTemplateColumns }}>
|
||||||
<div className="sticky left-0 top-0 z-[7] flex items-center border-b-2 border-r-[3px] border-ink border-b-cream bg-ink px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider text-cream">
|
<div className="sticky left-0 top-0 z-[7] flex items-center border-b-2 border-r-[3px] border-ink border-b-cream bg-ink px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider text-cream">
|
||||||
{t('volunteers.count', { count: rows.length })}
|
{t('volunteers.count', { count: rows.length })}
|
||||||
@@ -139,18 +156,13 @@ export function VolunteerViewPage() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{rows.length === 0 ? (
|
{rows.length > 0 ? (
|
||||||
<div className="m-[18px] flex max-w-[640px] items-center gap-[14px] border-[3px] border-dashed border-ink bg-paper p-[22px]">
|
<div className="px-[14px] py-[10px] font-mono text-[9.5px] leading-relaxed opacity-60">
|
||||||
<span className="h-9 w-9 flex-none border-[3px] border-ink bg-yellow" />
|
{t('volunteers.footnote')}
|
||||||
<span className="font-display text-[14px] font-extrabold leading-snug">
|
|
||||||
{t('volunteers.empty')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="px-[14px] py-[10px] font-mono text-[9.5px] leading-relaxed opacity-60">
|
|
||||||
{t('volunteers.footnote')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user