diff --git a/frontend/src/components/state.tsx b/frontend/src/components/state.tsx
index 8e63103..9a4c0b3 100644
--- a/frontend/src/components/state.tsx
+++ b/frontend/src/components/state.tsx
@@ -1,6 +1,17 @@
+import type { ReactNode } from 'react';
+
import { ApiError } from '@/api';
import { t } from '@/i18n';
+export function EmptyNote({ children }: { children: ReactNode }) {
+ return (
+
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
index 5f327cd..48a4770 100644
--- a/frontend/src/i18n/locales/fr.json
+++ b/frontend/src/i18n/locales/fr.json
@@ -90,6 +90,7 @@
"grille.positionsCount": "{count} POSTES",
"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.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.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",
@@ -119,6 +120,7 @@
"volunteers.badge.overloaded": "▲ SURCHARGE",
"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.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.",
"design.noteTitle": "CHOIX DE CONCEPTION",
diff --git a/frontend/src/pages/GrillePage.tsx b/frontend/src/pages/GrillePage.tsx
index 2b6aa26..4fabeaa 100644
--- a/frontend/src/pages/GrillePage.tsx
+++ b/frontend/src/pages/GrillePage.tsx
@@ -3,7 +3,7 @@ import { useMemo, useState } from 'react';
import { useMatrix } from '@/api/hooks';
import type { MatrixPosition, MatrixShift, PositionCategory } from '@/api/schema';
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 { categoryLabel, latticeFromMatrix, type LatticeColumn } from '@/lib/planning';
import { datePart, shortDayLabel, timePart } from '@/lib/time';
@@ -25,6 +25,15 @@ export function GrillePage() {
if (matrix.error) return
;
if (!matrix.data) return null;
+ const hasShifts = matrix.data.positions.some((p) => p.shifts.length > 0);
+ if (!hasShifts) {
+ return (
+
+ {t('grille.empty')}
+
+ );
+ }
+
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 shownColumns = shownDays.flatMap((d) => d.columns);
diff --git a/frontend/src/pages/VolunteerViewPage.tsx b/frontend/src/pages/VolunteerViewPage.tsx
index 9c46856..3c1ad45 100644
--- a/frontend/src/pages/VolunteerViewPage.tsx
+++ b/frontend/src/pages/VolunteerViewPage.tsx
@@ -3,7 +3,7 @@ import { useMemo, useState } from 'react';
import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/hooks';
import type { AssignmentRead, ShiftRead, VolunteerRead } from '@/api/schema';
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 { datePart, shortDayLabel, timePart } from '@/lib/time';
import { t } from '@/i18n';
@@ -67,10 +67,24 @@ export function VolunteerViewPage() {
});
}, [volunteers.data, assignments.data, shiftsById, hours, filter]);
- if (volunteers.isLoading || shifts.isLoading || assignments.isLoading) return
;
- const error = volunteers.error ?? shifts.error ?? assignments.error;
+ if (
+ volunteers.isLoading ||
+ shifts.isLoading ||
+ assignments.isLoading ||
+ positions.isLoading
+ )
+ return
;
+ const error = volunteers.error ?? shifts.error ?? assignments.error ?? positions.error;
if (error) return
;
+ if ((volunteers.data ?? []).length === 0) {
+ return (
+
+ {t('volunteers.noData')}
+
+ );
+ }
+
const gridTemplateColumns = `230px repeat(${lattice.columns.length}, 62px) 84px`;
const abbr = (positionId: string) => (positionsById.get(positionId)?.name ?? positionId).slice(0, 4).toUpperCase();
@@ -96,6 +110,9 @@ export function VolunteerViewPage() {