Files
siop2/apps/mobile/app/personnes/inviter.tsx
pr-daaif 4c1a2d67a0 feat(r6): R6.4 — Pilotage sur mobile (Statistiques, Personnes)
EXPO_PUBLIC_WEB_URL/WEB_URL pour le lien d'activation (pointe la page web
/activation?token=…, aucun équivalent mobile). api/pilotage.ts
(useAnalyticsSummary, useUsers, useRoles, useInviteUser).

Écran Statistiques (maquette écran 7) : période 3/6/12 mois, coût du mois,
taux préventif, OT clôturés, pannes par organe, top équipements — cartes
plutôt que graphes denses (D4).

Écran Personnes & équipes : liste + statut, Inviter (nom/e-mail/rôle),
lien d'activation en texte sélectionnable (Text selectable, RN natif) —
expo-clipboard est une dépendance native absente, différée comme
expo-sharing en R6.3 plutôt qu'ajoutée à la légère. Taux horaire/rôles/
équipes restent gérés au web (D4).

Assistant reste "à venir" dans le Menu : un chat sourcé est un nouveau
patron d'écran jamais maquetté sur mobile, contrairement aux autres
familles qui réutilisaient des patrons déjà validés — mérite son propre
tour de design-first plutôt qu'être exécuté par réflexe en fin de release.

R6 quasi close (Exploitation/Parc/Ressources/Pilotage livrés pour tous les
rôles, hors Assistant) — reste la confirmation du référent sur iPhone
(R6.1 + correctif déconnexion) avant clôture réelle.

Typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:43:57 +01:00

100 lines
3.6 KiB
TypeScript

import { router } from 'expo-router';
import { useState } from 'react';
import { ScrollView, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useInviteUser, useRoles } from '@/api/pilotage';
import { BoutonTel, ChoixTel, EnteteFiche } from '@/composants/ui';
import { useTokens } from '@/theme/tokens';
/** Invitation — R6.4. Jamais de mot de passe créé pour autrui (décision R1) :
* seul un lien d'activation valable 7 j est émis, à transmettre à la main. */
export default function PageInviter() {
const t = useTokens();
const { data: roles } = useRoles();
const invitation = useInviteUser();
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [roleId, setRoleId] = useState<string | null>(null);
const role = (roles ?? []).find((r) => r.id === roleId) ?? null;
const valide = displayName.trim().length >= 2 && /\S+@\S+\.\S+/.test(email) && !!roleId;
return (
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
<ScrollView contentContainerStyle={{ padding: 14, gap: 12 }}>
<EnteteFiche titre="Inviter" />
<View style={{ gap: 4 }}>
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 11, color: t.encre2 }}>
Nom complet <Text style={{ color: t.danger }}>*</Text>
</Text>
<TextInput
accessibilityLabel="Nom complet"
value={displayName}
onChangeText={setDisplayName}
style={{
borderWidth: 1.5,
borderColor: t.bordureForte,
borderRadius: 9,
paddingHorizontal: 10,
paddingVertical: 9,
color: t.encre,
fontFamily: 'Manrope_600SemiBold',
fontSize: 13,
}}
/>
</View>
<View style={{ gap: 4 }}>
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 11, color: t.encre2 }}>
E-mail <Text style={{ color: t.danger }}>*</Text>
</Text>
<TextInput
accessibilityLabel="E-mail"
autoCapitalize="none"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
style={{
borderWidth: 1.5,
borderColor: t.bordureForte,
borderRadius: 9,
paddingHorizontal: 10,
paddingVertical: 9,
color: t.encre,
fontFamily: 'Manrope_600SemiBold',
fontSize: 13,
}}
/>
</View>
<ChoixTel
libelle="Rôle"
requis
valeur={role ? { id: role.id, label: role.name } : null}
options={(roles ?? []).map((r) => ({ id: r.id, label: r.name }))}
surChoix={setRoleId}
/>
{invitation.isError ? (
<Text style={{ color: t.danger, fontFamily: 'Manrope_600SemiBold', fontSize: 12.5 }}>
{invitation.error.message}
</Text>
) : null}
<BoutonTel
libelle="Émettre le lien d'activation"
desactive={!valide || invitation.isPending}
surAppui={() =>
invitation.mutate(
{ displayName: displayName.trim(), email: email.trim(), roleId: roleId! },
{
onSuccess: (r) =>
router.replace({
pathname: '/personnes/lien',
params: { token: r.activationToken, expiresAt: r.expiresAt },
}),
},
)
}
/>
</ScrollView>
</SafeAreaView>
);
}