diff --git a/CLAUDE.md b/CLAUDE.md
index 8361ee8..222fceb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -101,7 +101,11 @@ les tables sauf les tables d'association.
poste y crée ou met à niveau (`PATCH /accounts/{id}` : voir plus bas) son
compte en `LEAD` — à sa prochaine connexion il voit l'écran référent
(`/mobile-referent`) plutôt que l'écran bénévole. Décocher ne fait que
- retirer le poste, sans jamais rétrograder le compte automatiquement.
+ retirer le poste, sans jamais rétrograder le compte automatiquement : un
+ compte `LEAD` qui ne référence plus aucun poste (tous décochés) reste donc
+ `LEAD`, mais `/mobile-referent` détecte ce cas et affiche alors, si le
+ compte est rattaché à une fiche bénévole, sa vue de bénévole classique
+ (`VolunteerHomeView`) plutôt qu'un écran référent vide.
- **`Shift`** : `position_id`, `day_part_id` (nullable ; le créneau type dont
il est issu, `SET NULL` si le type est supprimé), `starts_at`, `ends_at`
(peut finir le lendemain), `min_staff`, `ideal_staff`. À la création d'un
diff --git a/frontend/src/components/NowLine.tsx b/frontend/src/components/NowLine.tsx
new file mode 100644
index 0000000..1b5623d
--- /dev/null
+++ b/frontend/src/components/NowLine.tsx
@@ -0,0 +1,16 @@
+import { t } from '@/i18n';
+
+/** Repère horizontal « MAINTENANT », à insérer dans une liste triée
+ * chronologiquement (créneaux d'un bénévole, créneaux d'un poste…) au point
+ * correspondant à l'heure réelle. Purement visuel. */
+export function NowLine() {
+ return (
+
+
+
+ {t('common.now')}
+
+
+
+ );
+}
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
index 66d452d..1e32f84 100644
--- a/frontend/src/i18n/locales/fr.json
+++ b/frontend/src/i18n/locales/fr.json
@@ -5,6 +5,7 @@
"action.remove": "Retirer",
"action.add": "Ajouter",
"app.loading": "Chargement…",
+ "common.now": "MAINTENANT",
"app.error.generic": "Une erreur est survenue",
"error.generic": "Erreur inattendue",
"error.not_found": "Ressource introuvable",
@@ -123,7 +124,6 @@
"mobileVol.location": "📍 {location}",
"mobileVol.referent": "👤 Responsable : {name}",
"mobileVol.phone": "📞 {phone}",
- "mobileVol.now": "MAINTENANT",
"mobileVol.noSlots": "Aucun créneau pour l'instant. La coordination t'affectera bientôt.",
"mobileVol.noAccount": "Ce compte n'est rattaché à aucun bénévole. Contactez la coordination.",
"mobileLead.kicker": "RÉFÉRENT DE POSTE",
diff --git a/frontend/src/pages/MobileLeadPage.tsx b/frontend/src/pages/MobileLeadPage.tsx
index 95da0ec..89c9935 100644
--- a/frontend/src/pages/MobileLeadPage.tsx
+++ b/frontend/src/pages/MobileLeadPage.tsx
@@ -1,41 +1,33 @@
-import { useMemo, useState } from 'react';
+import { Fragment, useMemo, useState } from 'react';
import type { ShiftRead } from '@/api/schema';
import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/hooks';
import { useAuth } from '@/auth/auth-context';
import { ErrorNote, Loading } from '@/components/state';
+import { NowLine } from '@/components/NowLine';
import { hourRangeLabel, longDayLabel, naiveMinutes } from '@/lib/time';
import { useNowMinutes } from '@/lib/useNowMinutes';
import { t } from '@/i18n';
import { cx } from '@/lib/cx';
+import { VolunteerHomeView } from '@/pages/VolunteerHomeView';
// Page réellement consultée par le référent (accueil de son rôle LEAD, cf.
// `auth/roles.ts:homePath`) — page responsive (mobile d'abord, élargie à
// partir de `sm:`), plus de maquette de téléphone. Un compte peut être
// référent de plusieurs postes (`Position.lead_account_id`) : chacun a sa
-// propre carte, avec le créneau en cours ou à venir. Aucun repli vers « le
-// premier poste de l'édition » si le compte n'est référent d'aucun poste —
-// ce serait montrer un poste qui n'est pas le sien.
+// propre carte, listant TOUS ses créneaux (pas seulement le plus proche) avec
+// un repère « MAINTENANT » pour se situer. Aucun repli vers « le premier
+// poste de l'édition » si le compte n'est référent d'aucun poste — ce serait
+// montrer un poste qui n'est pas le sien ; s'il est par ailleurs rattaché à
+// une fiche bénévole (cas courant : « Donner un accès de connexion » promu
+// en référent), il voit alors sa vue de bénévole classique plutôt qu'un
+// écran référent vide (`VolunteerHomeView`).
//
// Le pointage présent/absent reste local à l'écran (état React, jamais
// envoyé au serveur) : le pointage sur site est hors périmètre (CLAUDE.md §5).
type Presence = Record;
-/** Créneau à afficher pour un poste : celui en cours, sinon le prochain à
- * venir, sinon (plus aucun à venir) le dernier passé. */
-function pickRelevantShift(shifts: ShiftRead[], nowMinutes: number): ShiftRead | null {
- const sorted = [...shifts].sort(
- (a, b) => naiveMinutes(a.starts_at) - naiveMinutes(b.starts_at),
- );
- const current = sorted.find(
- (s) => naiveMinutes(s.starts_at) <= nowMinutes && nowMinutes < naiveMinutes(s.ends_at),
- );
- if (current) return current;
- const next = sorted.find((s) => naiveMinutes(s.starts_at) > nowMinutes);
- return next ?? sorted[sorted.length - 1] ?? null;
-}
-
export function MobileLeadPage() {
const { account } = useAuth();
const positions = usePositions();
@@ -56,6 +48,9 @@ export function MobileLeadPage() {
list.push(s);
map.set(s.position_id, list);
}
+ for (const list of map.values()) {
+ list.sort((a, b) => naiveMinutes(a.starts_at) - naiveMinutes(b.starts_at));
+ }
return map;
}, [shifts.data]);
@@ -68,6 +63,24 @@ export function MobileLeadPage() {
const error = positions.error ?? shifts.error ?? volunteers.error ?? assignments.error;
if (error) return ;
+ if (myPositions.length === 0) {
+ if (account?.volunteer_id) {
+ return (
+
+ );
+ }
+ return (
+
+
+ {t('mobileLead.noPosition')}
+
+
+ );
+ }
+
return (
@@ -78,33 +91,14 @@ export function MobileLeadPage() {
diff --git a/frontend/src/pages/MobileVolunteerPage.tsx b/frontend/src/pages/MobileVolunteerPage.tsx
index ba30efd..b669bec 100644
--- a/frontend/src/pages/MobileVolunteerPage.tsx
+++ b/frontend/src/pages/MobileVolunteerPage.tsx
@@ -1,49 +1,19 @@
-import { Fragment, useMemo } from 'react';
-
-import { useVolunteer, useVolunteerPlanning } from '@/api/hooks';
import { useAuth } from '@/auth/auth-context';
-import { ErrorNote, Loading } from '@/components/state';
-import { hourRangeLabel, longDayLabel, naiveMinutes } from '@/lib/time';
-import { useNowMinutes } from '@/lib/useNowMinutes';
import { t } from '@/i18n';
+import { VolunteerHomeView } from '@/pages/VolunteerHomeView';
// Page réellement consultée par le bénévole (accueil de son rôle, cf.
// `auth/roles.ts:homePath`) — sur son propre téléphone la plupart du temps,
-// mais aussi possible depuis un ordinateur : le rendu ci-dessous s'adapte
-// (mobile d'abord, une seule colonne pleine largeur ; élargi et centré à
-// partir de `sm:`), sans jamais simuler un cadre de téléphone. Repères de
-// conception conservés : fonds opaques, texte noir sur jaune ou blanc (pas
-// de gris clair, lisibilité plein soleil), cibles tactiles ≥ 52 px de haut,
-// une seule action par créneau.
-
-function NowLine() {
- return (
-
-
-
- {t('mobileVol.now')}
-
-
-
- );
-}
+// mais aussi possible depuis un ordinateur : le rendu s'adapte (mobile
+// d'abord, une seule colonne pleine largeur ; élargi et centré à partir de
+// `sm:`), sans jamais simuler un cadre de téléphone. Le contenu lui-même vit
+// dans `VolunteerHomeView`, également montré à un compte RÉFÉRENT sans poste
+// actuellement assigné (voir `MobileLeadPage.tsx`).
export function MobileVolunteerPage() {
const { account } = useAuth();
const volunteerId = account?.volunteer_id ?? null;
- const meVolunteer = useVolunteer(volunteerId);
- const planning = useVolunteerPlanning(volunteerId);
- const nowMinutes = useNowMinutes();
- // Hooks appelés inconditionnellement avant les retours anticipés ci-dessous.
- const slots = useMemo(
- () =>
- [...(planning.data ?? [])].sort(
- (a, b) => naiveMinutes(a.starts_at) - naiveMinutes(b.starts_at),
- ),
- [planning.data],
- );
-
if (!volunteerId) {
return (
@@ -53,83 +23,8 @@ export function MobileVolunteerPage() {
);
}
- if (planning.isLoading) return ;
- if (planning.error) return ;
-
- const firstName = meVolunteer.data?.first_name ?? account?.email.split('@')[0] ?? '';
-
- const totalHours =
- Math.round(
- slots.reduce((sum, s) => sum + (naiveMinutes(s.ends_at) - naiveMinutes(s.starts_at)) / 60, 0) *
- 10,
- ) / 10;
-
- // Le repère « maintenant » se place juste avant le premier créneau pas
- // encore commencé (le prochain à venir) ; en fin de liste si tous sont déjà
- // passés.
- const nowIndex = slots.findIndex((s) => naiveMinutes(s.starts_at) > nowMinutes);
- const nowInsertAt = nowIndex === -1 ? slots.length : nowIndex;
return (
-