import { useState, type FormEvent } from 'react'; import type { InvitationResponse, UserAdmin } from '@siop/shared'; import { useCreateTeam, useInviteUser, useLocations, useResendInvitation, useRoles, useTeams, useUpdateUser, useUsers, } from '@/api/referentiel'; import { usePermissions } from '@/auth/use-permissions'; import { Avatar, initialsOf } from '@/components/avatar'; import { Button } from '@/components/ui/button'; import { Modale } from '@/components/ui/modale'; /** Écran 6 des maquettes R1 : personnes (invitation par lien 7 j, jamais de * mot de passe créé pour autrui) et équipes par zone. */ export default function PagePersonnes() { const { data: users } = useUsers(); const { data: teams } = useTeams(); const { can } = usePermissions(); const [modaleInvitation, setModaleInvitation] = useState(false); const [modaleEquipe, setModaleEquipe] = useState(false); const [lien, setLien] = useState(null); const renvoi = useResendInvitation(); return ( <>

Personnes & équipes

{can('PEOPLE_TEAMS', 'create') ? ( ) : null}
{(users ?? []).map((u) => ( ))}
Personne Rôle Équipe Taux horaire Sites Statut
{u.displayName} {u.email}
{u.role.name} {u.teams.map((t) => t.name).join(', ') || '—'} {u.role.name === 'Demandeur' ? ( ) : ( '—' )} {u.status === 'active' ? ( Actif ) : u.status === 'invited' ? ( Invitation envoyée ) : ( Désactivé )} {u.status === 'invited' && can('PEOPLE_TEAMS', 'edit') ? ( ) : null}

Équipes

{(teams ?? []).map((t) => (
{t.name} {t.description ?? '—'}
{t.members.map((m) => ( ))} {t.members.length === 0 ? ( aucun membre ) : null}
))} {can('PEOPLE_TEAMS', 'create') ? ( ) : null}
setLien(null)} /> ); } /** Taux horaire courant (R3) — appliqué aux futures saisies de temps ; les * temps déjà valorisés gardent leur taux figé. */ function CelluleTaux({ userId, taux, editable, }: { userId: string; taux: number | null; editable: boolean; }) { const maj = useUpdateUser(); const [edition, setEdition] = useState(false); if (!edition) { return ( {taux != null ? `${new Intl.NumberFormat('fr-FR').format(taux)} MAD/h` : '—'} {editable ? ( ) : null} ); } return ( { const valeur = e.target.value.trim(); const nombre = Number(valeur); if (valeur && nombre > 0 && nombre !== taux) { maj.mutate({ id: userId, hourlyRate: nombre }, { onSettled: () => setEdition(false) }); } else { setEdition(false); } }} onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Escape') setEdition(false); }} /> ); } /** Sites autorisés en signalement (R6.6) — un Demandeur ne choisit son * équipement que parmi ceux de ses sites rattachés (web + mobile, même * filtre côté API : `/assets/options`). */ function CelluleSites({ utilisateur, editable, }: { utilisateur: UserAdmin; editable: boolean; }) { const [modale, setModale] = useState(false); return ( <> {utilisateur.assignedSites.map((s) => s.name).join(', ') || '—'} {editable ? ( ) : null} ); } function ModaleSites({ utilisateur, ouverte, surFermeture, }: { utilisateur: UserAdmin; ouverte: boolean; surFermeture: (o: boolean) => void; }) { const { data: locations } = useLocations(); const sites = (locations ?? []).filter((l) => l.parentId === null); const maj = useUpdateUser(); const [coches, setCoches] = useState(() => utilisateur.assignedSites.map((s) => s.id)); const surEnvoi = (e: FormEvent) => { e.preventDefault(); maj.mutate( { id: utilisateur.id, locationIds: coches }, { onSuccess: () => surFermeture(false) }, ); }; return (
{sites.map((s) => ( ))}

Le signalement se limite aux sites cochés ci-dessus. Aucun site coché = aucune restriction (accès au parc complet, comportement historique).

{maj.isError ?

{maj.error.message}

: null}
); } function ModaleInvitation({ ouverte, surFermeture, surLien, }: { ouverte: boolean; surFermeture: (o: boolean) => void; surLien: (l: InvitationResponse) => void; }) { const { data: roles } = useRoles(); const { data: teams } = useTeams(); const { data: locations } = useLocations(); const invitation = useInviteUser(); const [roleId, setRoleId] = useState(''); const [sitesCoches, setSitesCoches] = useState([]); const roleName = roles?.find((r) => r.id === roleId)?.name; const sites = (locations ?? []).filter((l) => l.parentId === null); const surEnvoi = (e: FormEvent) => { e.preventDefault(); const donnees = new FormData(e.currentTarget); const teamId = String(donnees.get('teamId')); invitation.mutate( { email: String(donnees.get('email')), displayName: String(donnees.get('displayName')), roleId: String(donnees.get('roleId')), teamIds: teamId ? [teamId] : undefined, locationIds: sitesCoches.length ? sitesCoches : undefined, }, { onSuccess: (reponse) => { surFermeture(false); setRoleId(''); setSitesCoches([]); surLien(reponse); }, }, ); }; return (
{roleName === 'Demandeur' ? (
{sites.map((s) => ( ))}

Aucun site coché = aucune restriction (accès au parc complet).

) : null}

La personne recevra un lien d'activation valable 7 jours pour choisir son mot de passe. Aucun compte n'est actif avant cela.

{invitation.isError ? (

{invitation.error.message}

) : null}
); } /** L'envoi d'email n'existe pas encore : le lien se copie ici (décision R1). */ function ModaleLien({ lien, surFermeture, }: { lien: InvitationResponse | null; surFermeture: () => void; }) { const [copie, setCopie] = useState(false); if (!lien) return null; const url = `${window.location.origin}/activation?token=${lien.activationToken}`; return (

Transmettez ce lien à la personne (valable jusqu'au{' '} {new Intl.DateTimeFormat('fr-FR', { dateStyle: 'long' }).format(new Date(lien.expiresAt))}) :

{url}
); } function ModaleEquipe({ ouverte, surFermeture, }: { ouverte: boolean; surFermeture: (o: boolean) => void; }) { const creation = useCreateTeam(); const surEnvoi = (e: FormEvent) => { e.preventDefault(); const donnees = new FormData(e.currentTarget); creation.mutate( { name: String(donnees.get('name')), description: String(donnees.get('description')) || undefined, }, { onSuccess: () => surFermeture(false) }, ); }; return (
{creation.isError ? (

{creation.error.message}

) : null}
); }