Référent aussi bénévole : onglets Mon équipe / Mon planning
Un référent rattaché à une fiche bénévole voit aussi ses propres créneaux (vue bénévole) dans un second onglet de son écran, ouvert d'office quand il tient un créneau de bénévole hors de ses postes ; « TOI » dans l'équipe pointée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
16d8fb44a7
commit
a37f6c2c41
@@ -132,7 +132,13 @@ les tables sauf les tables d'association.
|
||||
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.
|
||||
(`VolunteerHomeView`) plutôt qu'un écran référent vide. Un référent peut
|
||||
aussi être affecté comme bénévole (même soirée comprise) : si son compte
|
||||
est rattaché à une fiche bénévole, `/mobile-referent` a deux onglets,
|
||||
« Mon équipe » et « Mon planning » (sa vue bénévole), ouvert sur « Mon
|
||||
planning » quand il tient en ce moment un créneau de bénévole hors de ses
|
||||
postes sans qu'aucun de ses postes ait un créneau en cours ; dans « Mon
|
||||
équipe », il est marqué « TOI » s'il fait partie de l'équipe pointée.
|
||||
- **`Position.message`** / `message_updated_at` : **message du poste**
|
||||
(consignes : tenue, matériel, rendez-vous…), saisi par le référent depuis
|
||||
son écran (`/mobile-referent`, « Message à ton équipe ») ou par la
|
||||
@@ -1008,6 +1014,7 @@ Aucune dépendance locale requise hors Docker.
|
||||
| 54 | Badges : ordre de superposition des champs (`BadgeField.z_order`, migration `0028` — champs existants renumérotés photo dessous) : boutons « Premier plan / Avancer / Reculer / Arrière-plan » dans l'inspecteur, aperçu et PDF dessinés dans cet ordre — la photo recouvrait le texte | à valider |
|
||||
| 55 | Écrans bénévole et référent redessinés d'après la maquette (`design/Planning bénévole mobile.dc.html`, pensée pour le téléphone — tablette et desktop gardent la même colonne, centrée) : bénévole — créneau en cours ou prochain mis en avant (compte à rebours, lieu, chevauchement, bouton « Appeler » le référent), planning par journée de festival avec frise horaire et statut de chaque créneau (terminé / en cours), message de fin de festival ; référent — onglets de postes, choix du créneau (en cours ou prochain par défaut), compteurs présents / attendus / absents, alerte d'absence, pointage (toujours local à l'écran) avec appel direct. Pas encore repris de la maquette, faute de donnée : téléphone de la coordination, bouton « Demander un renfort » (consigne du poste et message de l'organisation : étape 56) | à valider |
|
||||
| 56 | Messages aux bénévoles : message de l'organisation, le même pour tous (`Edition.volunteer_message`, onglet « Message aux bénévoles » de la configuration), et message de chaque poste (`Position.message`, saisi par le référent sur son écran ou par la coordination), migration `0029` — affichés sur l'écran bénévole (bloc « Message de l'organisation » daté, consignes du poste sur le créneau mis en avant et dans la liste) | à valider |
|
||||
| 57 | Référent aussi bénévole : onglets « Mon équipe » / « Mon planning » sur l'écran référent (compte rattaché à une fiche bénévole), ouverture sur celui qui correspond à ce qu'il fait en ce moment, pastille sur l'onglet dont un créneau est en cours, mention « TOI » dans l'équipe pointée | à valider |
|
||||
|
||||
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
|
||||
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
|
||||
|
||||
@@ -174,6 +174,9 @@
|
||||
"mobileLead.message.audience": "{count} bénévole(s) le verront",
|
||||
"mobileLead.message.help": "Consignes du poste (tenue, matériel, point de rendez-vous…), affichées sur l'écran de chaque bénévole affecté au poste.",
|
||||
"mobileLead.message.placeholder": "Ex. Gilet jaune fourni à la base bénévoles. Rendez-vous 10 min avant le début.",
|
||||
"mobileLead.view.team": "Mon équipe",
|
||||
"mobileLead.view.planning": "Mon planning · {count}",
|
||||
"mobileLead.you": "TOI",
|
||||
"mobileLead.noPosition": "Vous n'êtes actuellement référent d'aucun poste.",
|
||||
"mobileLead.noShift": "Aucun créneau sur ce poste.",
|
||||
"mobileLead.badgePresent": "✓ PRÉSENT",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useShifts,
|
||||
useUpdatePosition,
|
||||
useVolunteer,
|
||||
useVolunteerPlanning,
|
||||
useVolunteers,
|
||||
} from '@/api/hooks';
|
||||
import { useAuth } from '@/auth/auth-context';
|
||||
@@ -35,10 +36,18 @@ import { VolunteerHomeView } from '@/pages/VolunteerHomeView';
|
||||
// courant : « Donner un accès de connexion » promu en référent), il voit
|
||||
// alors sa vue de bénévole classique (`VolunteerHomeView`).
|
||||
//
|
||||
// Un référent peut aussi être affecté comme bénévole, y compris la même
|
||||
// soirée : s'il est rattaché à une fiche bénévole, deux onglets — « Mon
|
||||
// équipe » (cet écran) et « Mon planning » (`VolunteerHomeView`, ses
|
||||
// propres créneaux). On ouvre sur « Mon planning » quand il tient en ce
|
||||
// moment un créneau de bénévole hors de ses postes et qu'aucun de ses
|
||||
// postes n'a de créneau en cours ; sinon sur « Mon équipe ».
|
||||
//
|
||||
// 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<string, 'present' | 'absent'>;
|
||||
type View = 'team' | 'planning';
|
||||
|
||||
const STATUS_KEY: Record<SlotStatus, string> = {
|
||||
done: 'mobileLead.status.done',
|
||||
@@ -54,6 +63,8 @@ export function MobileLeadPage() {
|
||||
const assignments = useAssignments();
|
||||
const dayParts = useDayParts();
|
||||
const me = useVolunteer(account?.volunteer_id);
|
||||
const myPlanning = useVolunteerPlanning(account?.volunteer_id);
|
||||
const [view, setView] = useState<View | null>(null);
|
||||
const updatePosition = useUpdatePosition();
|
||||
const now = useNowMinutes();
|
||||
const [presence, setPresence] = useState<Presence>({});
|
||||
@@ -158,7 +169,38 @@ export function MobileLeadPage() {
|
||||
: null;
|
||||
const myName = me.data ? `${me.data.first_name} ${me.data.last_name}` : null;
|
||||
|
||||
const inProgress = (start: string, end: string) =>
|
||||
slotStatus(naiveMinutes(start), naiveMinutes(end), now) === 'now';
|
||||
const ledShiftIds = new Set(myPositions.flatMap((p) => (shiftsByPosition.get(p.id) ?? []).map((s) => s.id)));
|
||||
const leadingNow = (shifts.data ?? []).some((s) => ledShiftIds.has(s.id) && inProgress(s.starts_at, s.ends_at));
|
||||
const myEntries = myPlanning.data ?? [];
|
||||
const volunteeringNow = myEntries.some(
|
||||
(e) => !ledShiftIds.has(e.shift_id) && inProgress(e.starts_at, e.ends_at),
|
||||
);
|
||||
const currentView: View = view ?? (volunteeringNow && !leadingNow ? 'planning' : 'team');
|
||||
const tabs = account?.volunteer_id ? (
|
||||
<div className="mx-auto grid w-full max-w-[640px] grid-cols-2 border-b-[3px] border-ink">
|
||||
<ViewTab active={currentView === 'team'} live={leadingNow} onClick={() => setView('team')}>
|
||||
{t('mobileLead.view.team')}
|
||||
</ViewTab>
|
||||
<ViewTab active={currentView === 'planning'} live={volunteeringNow} onClick={() => setView('planning')}>
|
||||
{t('mobileLead.view.planning', { count: myEntries.length })}
|
||||
</ViewTab>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (currentView === 'planning' && account?.volunteer_id) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{tabs}
|
||||
<VolunteerHomeView volunteerId={account.volunteer_id} fallbackName={account.email.split('@')[0]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{tabs}
|
||||
<div className="mx-auto flex w-full max-w-[640px] flex-1 flex-col">
|
||||
<div className="bg-ink px-4 pb-4 pt-[15px] text-cream sm:px-6">
|
||||
{myPositions.length > 1 ? (
|
||||
@@ -291,6 +333,11 @@ export function MobileLeadPage() {
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-words font-display text-[16px] font-extrabold leading-tight">
|
||||
{member.name}
|
||||
{member.volunteerId === account?.volunteer_id ? (
|
||||
<span className="ml-[6px] inline-block border-2 border-ink bg-blue px-[5px] py-[2px] align-middle font-display text-[9.5px] font-black leading-none">
|
||||
{t('mobileLead.you')}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{member.phone ? (
|
||||
<span className="block font-mono text-[11px] font-medium leading-snug opacity-70">
|
||||
@@ -368,6 +415,7 @@ export function MobileLeadPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -379,3 +427,31 @@ function Counter({ value, label, className }: { value: number; label: string; cl
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewTab({
|
||||
active,
|
||||
live,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
/** Un créneau de cette vue est en cours : pastille orange. */
|
||||
live: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={cx(
|
||||
'flex min-h-[46px] cursor-pointer items-center justify-center gap-[7px] border-r-[3px] border-ink px-2 font-display text-[12px] font-black uppercase tracking-wide last:border-r-0',
|
||||
active ? 'bg-ink text-yellow' : 'bg-paper text-ink hover:bg-yellow-soft',
|
||||
)}
|
||||
>
|
||||
{live ? <span className="h-[9px] w-[9px] flex-none border-2 border-ink bg-orange" aria-hidden="true" /> : null}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user