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, 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; export function MobileLeadPage() { const { account } = useAuth(); const positions = usePositions(); const shifts = useShifts(); const volunteers = useVolunteers(); const assignments = useAssignments(); const nowMinutes = useNowMinutes(); const [presence, setPresence] = useState({}); const myPositions = (positions.data ?? []).filter( (p) => account && p.lead_account_id === account.id, ); const shiftsByPosition = useMemo(() => { const map = new Map(); for (const s of shifts.data ?? []) { const list = map.get(s.position_id) ?? []; 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]); const volunteersById = useMemo( () => new Map((volunteers.data ?? []).map((v) => [v.id, v])), [volunteers.data], ); if (positions.isLoading || shifts.isLoading) return ; 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 (
{t('mobileLead.kicker')}
{t('mobileLead.positionsCount', { count: myPositions.length })}
{myPositions.map((position) => { const positionShifts = shiftsByPosition.get(position.id) ?? []; const nowIndex = positionShifts.findIndex((s) => naiveMinutes(s.starts_at) > nowMinutes); const nowInsertAt = nowIndex === -1 ? positionShifts.length : nowIndex; return (
{position.name.toUpperCase()}
{position.location ? (
{position.location}
) : null}
{positionShifts.length === 0 ? (
{t('mobileLead.noShift')}
) : null} {positionShifts.map((shift, i) => { const team = (assignments.data ?? []) .filter((a) => a.shift_id === shift.id) .map((a) => { const volunteer = volunteersById.get(a.volunteer_id); return { id: a.id, name: volunteer ? `${volunteer.first_name} ${volunteer.last_name}` : a.volunteer_id, initials: volunteer ? `${volunteer.first_name[0]}${volunteer.last_name[0]}` : '?', phone: volunteer?.phone ?? '', present: presence[a.id] === 'present', absent: presence[a.id] === 'absent', }; }); const presentCount = team.filter((m) => m.present).length; return ( {i === nowInsertAt ? : null}
{longDayLabel(shift.starts_at)} ·{' '} {hourRangeLabel(shift.starts_at, shift.ends_at)}
{t('mobileLead.present', { present: presentCount, total: team.length })}
{team.map((member) => (
{member.initials} {member.name} {member.phone} {member.present ? ( {t('mobileLead.badgePresent')} ) : null} {member.absent ? ( {t('mobileLead.badgeAbsent')} ) : null}
))} {team.length === 0 ? (
{t('mobileLead.emptyTeam')}
) : null}
); })} {nowInsertAt === positionShifts.length && positionShifts.length > 0 ? ( ) : null}
); })}
); }