mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +00:00
apps/mobile (Expo SDK 57, TS strict, workspace pnpm) : connexion e-mail/mdp + sélecteur démo ADR-002 (uniquement si l'API l'expose), tabbar de la maquette validée (onglets futurs marqués R4.2/R4.3), « Ma journée » triée priorité puis échéance (fonction pure testée), urgence en tête, pastille de synchro. D1 en actes (lecture) : cache TanStack persisté dans AsyncStorage (7 jours), NetInfo → onlineManager + bandeau hors-ligne horodaté ; jeton en SecureStore ; tokens light/dark répliqués et testés ; client typé régénéré depuis docs/openapi.json (règle d'or). API : CORS_ORIGINS opt-in (vide par défaut) — Expo web/debug seulement, le web de prod reste derrière le proxy, les apps natives n'ont pas d'Origin. Vérifié 10/10 en Expo web piloté (connexion démo Ahmed → Ma journée scopée → hors-ligne servie du cache → retour) ; 6 tests jest-expo ; lint racine étendu (react-hooks) ; job CI mobile, deploy en dépend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
import { router } from 'expo-router';
|
|
import { useState } from 'react';
|
|
import {
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { useDemoAccounts, useDemoLogin, useLogin } from '@/auth/session';
|
|
import { useTokens } from '@/theme/tokens';
|
|
|
|
/** Connexion mobile — mêmes règles que le web : formulaire e-mail/mot de
|
|
* passe, et le sélecteur démo (< 3 s pour changer de rôle, ADR-002)
|
|
* UNIQUEMENT si l'API l'expose. */
|
|
export default function PageConnexion() {
|
|
const t = useTokens();
|
|
const [email, setEmail] = useState('');
|
|
const [motDePasse, setMotDePasse] = useState('');
|
|
const { data: comptes } = useDemoAccounts();
|
|
const login = useLogin();
|
|
const demo = useDemoLogin();
|
|
|
|
const entrer = () => router.replace('/(tabs)/journee');
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={{ flex: 1, backgroundColor: t.fond }}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<ScrollView contentContainerStyle={{ padding: 24, paddingTop: 80, gap: 12 }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}>
|
|
<Text style={{ color: t.safran, fontSize: 22 }}>♦</Text>
|
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 26, color: t.encre }}>
|
|
SIOP
|
|
</Text>
|
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 13, color: t.encre3 }}>
|
|
Technicien
|
|
</Text>
|
|
</View>
|
|
<Text style={{ fontFamily: 'Manrope_400Regular', color: t.encre2, marginBottom: 8 }}>
|
|
Vos ordres de travail, sur le terrain — même sans réseau.
|
|
</Text>
|
|
|
|
<View style={{ gap: 10 }}>
|
|
<TextInput
|
|
accessibilityLabel="E-mail"
|
|
placeholder="E-mail"
|
|
placeholderTextColor={t.encre3}
|
|
autoCapitalize="none"
|
|
keyboardType="email-address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
style={{
|
|
backgroundColor: t.surface,
|
|
borderColor: t.bordureForte,
|
|
borderWidth: 1.5,
|
|
borderRadius: 10,
|
|
padding: 13,
|
|
color: t.encre,
|
|
fontFamily: 'Manrope_600SemiBold',
|
|
}}
|
|
/>
|
|
<TextInput
|
|
accessibilityLabel="Mot de passe"
|
|
placeholder="Mot de passe"
|
|
placeholderTextColor={t.encre3}
|
|
secureTextEntry
|
|
value={motDePasse}
|
|
onChangeText={setMotDePasse}
|
|
style={{
|
|
backgroundColor: t.surface,
|
|
borderColor: t.bordureForte,
|
|
borderWidth: 1.5,
|
|
borderRadius: 10,
|
|
padding: 13,
|
|
color: t.encre,
|
|
fontFamily: 'Manrope_600SemiBold',
|
|
}}
|
|
/>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
disabled={!email || !motDePasse || login.isPending}
|
|
onPress={() =>
|
|
login.mutate({ email, password: motDePasse }, { onSuccess: entrer })
|
|
}
|
|
style={{
|
|
backgroundColor: !email || !motDePasse ? t.bordureForte : t.primaire,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Text style={{ color: '#fff', fontFamily: 'Manrope_700Bold', fontSize: 15 }}>
|
|
{login.isPending ? 'Connexion…' : 'Se connecter'}
|
|
</Text>
|
|
</Pressable>
|
|
{login.isError ? (
|
|
<Text style={{ color: t.danger, fontFamily: 'Manrope_600SemiBold', fontSize: 13 }}>
|
|
{login.error.message}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
|
|
{comptes?.length ? (
|
|
<View style={{ marginTop: 18, gap: 8 }}>
|
|
<Text
|
|
style={{
|
|
fontFamily: 'Manrope_700Bold',
|
|
fontSize: 11,
|
|
letterSpacing: 1,
|
|
textTransform: 'uppercase',
|
|
color: t.encre3,
|
|
}}
|
|
>
|
|
Mode démo — connexion 1 clic
|
|
</Text>
|
|
{comptes.map((c) => (
|
|
<Pressable
|
|
key={c.id}
|
|
accessibilityRole="button"
|
|
disabled={demo.isPending}
|
|
onPress={() => demo.mutate(c.id, { onSuccess: entrer })}
|
|
style={{
|
|
backgroundColor: t.surface,
|
|
borderColor: t.bordure,
|
|
borderWidth: 1,
|
|
borderRadius: 10,
|
|
padding: 12,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Text style={{ fontFamily: 'Manrope_700Bold', color: t.encre, fontSize: 14 }}>
|
|
{c.displayName}
|
|
</Text>
|
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', color: t.encre3, fontSize: 12 }}>
|
|
{c.roleName}
|
|
</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|