mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
100 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|