mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
fix(mobile): déconnexion accessible depuis tous les onglets
Bug remonté par le référent : une fois authentifié, impossible de revenir en arrière ou de se déconnecter. Seule « Ma journée » portait ce contrôle (onLongPress non découvrable) ; les 3 autres onglets n'avaient rien. EnteteTabs() factorise l'entête (composants/ui.tsx) : tap simple + confirmation Alert, rôle affiché = celui du compte connecté (plus de libellé "Technicien" figé — le mobile n'est plus réservé aux techniciens). Réutilisée dans les 4 onglets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ import {
|
|||||||
type WorkOrderStatus,
|
type WorkOrderStatus,
|
||||||
} from '@siop/shared';
|
} from '@siop/shared';
|
||||||
import { api, unwrap } from '@/api/client';
|
import { api, unwrap } from '@/api/client';
|
||||||
import { useHorsLigne, useLogout, useMe } from '@/auth/session';
|
import { useHorsLigne } from '@/auth/session';
|
||||||
|
import { EnteteTabs } from '@/composants/ui';
|
||||||
import { triJournee } from '@/lib/journee';
|
import { triJournee } from '@/lib/journee';
|
||||||
import { useTokens, type Tokens } from '@/theme/tokens';
|
import { useTokens, type Tokens } from '@/theme/tokens';
|
||||||
|
|
||||||
@@ -43,9 +44,7 @@ const STRIE_PRIORITE: Record<WorkOrderPriority, (t: Tokens) => string> = {
|
|||||||
export default function PageJournee() {
|
export default function PageJournee() {
|
||||||
const t = useTokens();
|
const t = useTokens();
|
||||||
const horsLigne = useHorsLigne();
|
const horsLigne = useHorsLigne();
|
||||||
const { data: me } = useMe();
|
|
||||||
const { data: workOrders, refetch, isFetching, dataUpdatedAt } = useWorkOrders();
|
const { data: workOrders, refetch, isFetching, dataUpdatedAt } = useWorkOrders();
|
||||||
const logout = useLogout();
|
|
||||||
|
|
||||||
const ots = triJournee(workOrders ?? []);
|
const ots = triJournee(workOrders ?? []);
|
||||||
const urgences = ots.filter((o) => o.priority === 'PERSON_TRAPPED');
|
const urgences = ots.filter((o) => o.priority === 'PERSON_TRAPPED');
|
||||||
@@ -55,57 +54,7 @@ export default function PageJournee() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
<View style={{ padding: 14, gap: 10, flex: 1 }}>
|
<View style={{ padding: 14, gap: 10, flex: 1 }}>
|
||||||
{/* Entête app : marque + pastille synchro + compte */}
|
<EnteteTabs />
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Text style={{ color: t.safran, fontSize: 15 }}>♦</Text>
|
|
||||||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 17, color: t.encre }}>
|
|
||||||
SIOP
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 10.5, color: t.encre3 }}>
|
|
||||||
Technicien
|
|
||||||
</Text>
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginLeft: 'auto' }}>
|
|
||||||
<Text
|
|
||||||
accessibilityLabel="État de synchronisation"
|
|
||||||
style={{
|
|
||||||
fontFamily: 'Manrope_700Bold',
|
|
||||||
fontSize: 10.5,
|
|
||||||
color: horsLigne ? t.stAttente : t.stTermine,
|
|
||||||
backgroundColor: horsLigne ? t.stAttenteFond : t.stTermineFond,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 999,
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{horsLigne ? 'Hors-ligne' : 'Synchro à jour'}
|
|
||||||
</Text>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Se déconnecter"
|
|
||||||
onLongPress={() => {
|
|
||||||
void logout().then(() => router.replace('/connexion'));
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: 26,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 13,
|
|
||||||
backgroundColor: t.stEncours,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: '#fff', fontFamily: 'Manrope_700Bold', fontSize: 10 }}>
|
|
||||||
{(me?.displayName ?? '·')
|
|
||||||
.split(' ')
|
|
||||||
.map((m) => m[0])
|
|
||||||
.join('')
|
|
||||||
.slice(0, 2)
|
|
||||||
.toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
||||||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { router } from 'expo-router';
|
|||||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useWorkOrders } from '@/api/exploitation';
|
import { useWorkOrders } from '@/api/exploitation';
|
||||||
import { ChipStatut } from '@/composants/ui';
|
import { ChipStatut, EnteteTabs } from '@/composants/ui';
|
||||||
import { useTokens } from '@/theme/tokens';
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
/** Onglet Préventif : mes grilles du moment — chaque tuile mène à la
|
/** Onglet Préventif : mes grilles du moment — chaque tuile mène à la
|
||||||
@@ -18,6 +18,7 @@ export default function PagePreventif() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||||||
|
<EnteteTabs />
|
||||||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
Préventif
|
Préventif
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Platform, Text, TextInput, View } from 'react-native';
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useAssets } from '@/api/exploitation';
|
import { useAssets } from '@/api/exploitation';
|
||||||
import { useHorsLigne } from '@/auth/session';
|
import { useHorsLigne } from '@/auth/session';
|
||||||
import { BoutonTel } from '@/composants/ui';
|
import { BoutonTel, EnteteTabs } from '@/composants/ui';
|
||||||
import { analyseScan } from '@/lib/scan';
|
import { analyseScan } from '@/lib/scan';
|
||||||
import { useTokens } from '@/theme/tokens';
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ export default function PageScanner() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||||||
|
<EnteteTabs />
|
||||||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
Scanner
|
Scanner
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { router } from 'expo-router';
|
|||||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useHorsLigne } from '@/auth/session';
|
import { useHorsLigne } from '@/auth/session';
|
||||||
import { BoutonTel } from '@/composants/ui';
|
import { BoutonTel, EnteteTabs } from '@/composants/ui';
|
||||||
import { useFile, type Saisie } from '@/file/store';
|
import { useFile, type Saisie } from '@/file/store';
|
||||||
import { abandonnerSaisie, rejouer, rejouerSurVersionAJour } from '@/file/synchro';
|
import { abandonnerSaisie, rejouer, rejouerSurVersionAJour } from '@/file/synchro';
|
||||||
import { useTokens } from '@/theme/tokens';
|
import { useTokens } from '@/theme/tokens';
|
||||||
@@ -33,6 +33,7 @@ export default function PageSynchro() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||||||
|
<EnteteTabs />
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}>
|
||||||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
Synchro
|
Synchro
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useState, type ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { Modal, Pressable, ScrollView, Text, View } from 'react-native';
|
import { Alert, Modal, Pressable, ScrollView, Text, View } from 'react-native';
|
||||||
import { WORK_ORDER_STATUS_LABELS, type WorkOrderStatus } from '@siop/shared';
|
import { WORK_ORDER_STATUS_LABELS, type WorkOrderStatus } from '@siop/shared';
|
||||||
|
import { useHorsLigne, useLogout, useMe } from '@/auth/session';
|
||||||
import { useTokens, type Tokens } from '@/theme/tokens';
|
import { useTokens, type Tokens } from '@/theme/tokens';
|
||||||
|
|
||||||
/** Briques d'écran de la maquette R4 — cartes, chips, boutons, sélecteur. */
|
/** Briques d'écran de la maquette R4 — cartes, chips, boutons, sélecteur. */
|
||||||
@@ -263,3 +264,76 @@ export function ChoixTel({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Entête commune à tous les onglets (D'où qu'on parte, on peut se
|
||||||
|
* déconnecter — trouvée manquante en recette : seule « Ma journée » avait
|
||||||
|
* ce contrôle, ailleurs l'utilisateur se sentait bloqué). Le rôle affiché
|
||||||
|
* est CELUI du compte connecté, pas un libellé figé — le mobile n'est plus
|
||||||
|
* réservé aux techniciens (voir décision élargissant le périmètre). */
|
||||||
|
export function EnteteTabs() {
|
||||||
|
const t = useTokens();
|
||||||
|
const horsLigne = useHorsLigne();
|
||||||
|
const { data: me } = useMe();
|
||||||
|
const logout = useLogout();
|
||||||
|
|
||||||
|
const seDeconnecter = () => {
|
||||||
|
Alert.alert('Se déconnecter ?', me?.displayName ? `Compte : ${me.displayName}` : undefined, [
|
||||||
|
{ text: 'Annuler', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Se déconnecter',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => void logout().then(() => router.replace('/connexion')),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Text style={{ color: t.safran, fontSize: 15 }}>♦</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 17, color: t.encre }}>SIOP</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 10.5, color: t.encre3 }}>
|
||||||
|
{me?.role.name ?? '·'}
|
||||||
|
</Text>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginLeft: 'auto' }}>
|
||||||
|
<Text
|
||||||
|
accessibilityLabel="État de synchronisation"
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: horsLigne ? t.stAttente : t.stTermine,
|
||||||
|
backgroundColor: horsLigne ? t.stAttenteFond : t.stTermineFond,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{horsLigne ? 'Hors-ligne' : 'Synchro à jour'}
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Compte et déconnexion"
|
||||||
|
onPress={seDeconnecter}
|
||||||
|
hitSlop={8}
|
||||||
|
style={{
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 13,
|
||||||
|
backgroundColor: t.stEncours,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#fff', fontFamily: 'Manrope_700Bold', fontSize: 10 }}>
|
||||||
|
{(me?.displayName ?? '·')
|
||||||
|
.split(' ')
|
||||||
|
.map((m) => m[0])
|
||||||
|
.join('')
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user