From 06f8736c3310370f458a07fb2f168c349632e7a0 Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Thu, 10 Sep 2026 21:22:10 +0200 Subject: [PATCH] =?UTF-8?q?Grille=20&=20vue=20b=C3=A9n=C3=A9vole=20:=20mes?= =?UTF-8?q?sage=20quand=20il=20n'y=20a=20pas=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM --- frontend/src/components/state.tsx | 11 ++++++++ frontend/src/i18n/locales/fr.json | 2 ++ frontend/src/pages/GrillePage.tsx | 11 +++++++- frontend/src/pages/VolunteerViewPage.tsx | 36 ++++++++++++++++-------- 4 files changed, 47 insertions(+), 13 deletions(-) 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 ( +
+ + {children} +
+ ); +} + export function Loading() { 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() {
+ {rows.length === 0 ? ( + {t('volunteers.empty')} + ) : (
{t('volunteers.count', { count: rows.length })} @@ -139,18 +156,13 @@ export function VolunteerViewPage() { /> ))}
+ )} - {rows.length === 0 ? ( -
- - - {t('volunteers.empty')} - + {rows.length > 0 ? ( +
+ {t('volunteers.footnote')}
) : null} -
- {t('volunteers.footnote')} -
);