mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
This commit is contained in:
99
apps/mobile/app/personnes/inviter.tsx
Normal file
99
apps/mobile/app/personnes/inviter.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user