Files
siop2/apps/mobile/app/(tabs)/_layout.tsx
pr-daaif ab0f260d0f feat(r6): mobile ouvert à tous les rôles — maquette + socle R6.1
R4 avait délibérément fermé le mobile au Technicien. Le référent demande
l'ouverture à tous les rôles avec les mêmes droits qu'au web — nouvelle
release R6 (R4 est close, R4.1/4.2/4.3 déjà pris par le socle mobile
technicien d'origine, pas de réouverture ni de collision de numérotation).

Maquette (docs/02-design/maquettes/maquette-mobile-tous-roles.html, 7
écrans) validée par le référent : D1 barre d'onglets adaptative par rôle
(Technicien/Technicien limité inchangés + onglet Menu ajouté) ; D2 le Menu
reprend à l'identique les 4 groupes du web, même matrice de permissions ;
D3 groupe sans lien visible masqué en entier ; D4 le mobile porte les
actions courantes par famille, pas les flux de gestion les plus denses ;
D5 aucune logique de permission propre au mobile.

R6.1 — socle : usePermissions() mobile calqué sur le web ; barre d'onglets
adaptative (ongletsVisibles par rôle, href:null masque sans retirer du
navigateur) ; Accueil (dashboard réel pour Administrateur/Gestionnaire/
Dispatcher/Vue seule, aucun chiffre inventé) ; OT (liste complète,
viewOther déjà géré serveur, réutilise la fiche OT R4 telle quelle) ; Menu
(groupes filtrés, vide masqué) ; Demandes (PanneauDemandes — un seul
composant pour tous les rôles : création/suivi Demandeur, approbation/
rejet Gestionnaire/Dispatcher/Administrateur, lecture Vue seule).
Familles pas encore portées : écran « à venir » plutôt qu'un lien mort.

Typecheck propre, 17 tests Jest, lint 5/5 paquets.

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

124 lines
4.2 KiB
TypeScript

import { Tabs } from 'expo-router';
import { Text, type ColorValue } from 'react-native';
import { useAssets, useReferenceValues } from '@/api/exploitation';
import { useMe } from '@/auth/session';
import { ongletsVisibles } from '@/auth/roles';
import { usePermissions } from '@/auth/use-permissions';
import { useFile } from '@/file/store';
import { useTokens } from '@/theme/tokens';
/** Barre d'onglets ADAPTATIVE (maquette « mobile ouvert à tous les rôles »,
* D1) : le Technicien/Technicien limité gardent exactement leurs 4 onglets
* terrain R4, inchangés. Les autres rôles reçoivent Accueil/OT/Menu (ou une
* variante plus étroite — Demandeur, Vue seule — selon leurs droits,
* `ongletsVisibles`). Tous les écrans sont toujours déclarés : `href: null`
* masque un onglet de la barre sans le retirer du navigateur (le Menu peut
* toujours y pousser directement). */
function Pic({ glyphe, couleur }: { glyphe: string; couleur: ColorValue }) {
return <Text style={{ fontSize: 17, color: couleur, lineHeight: 20 }}>{glyphe}</Text>;
}
export default function CoquilleTabs() {
const t = useTokens();
const file = useFile();
const conflits = file.some((s) => s.statut === 'CONFLIT');
const { data: me } = useMe();
const { can } = usePermissions();
const visibles = ongletsVisibles(me?.role.name);
// D1 « lecture locale » : le parc (scan D4) et les référentiels du bilan
// se préchargent dès l'entrée — le sous-sol n'attend pas qu'on y pense.
// Le Demandeur n'a pas ASSETS.view (seulement /assets/options) : inutile
// d'appeler /assets pour lui, l'API répondrait 403.
const peutAssets = can('ASSETS', 'view');
useAssets({ enabled: peutAssets });
useReferenceValues({ enabled: peutAssets });
const cacher = (nom: string) => (visibles.has(nom) ? undefined : null);
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: t.primaire,
tabBarInactiveTintColor: t.encre3,
tabBarStyle: { backgroundColor: t.surface, borderTopColor: t.bordure },
tabBarLabelStyle: { fontFamily: 'Manrope_700Bold', fontSize: 10 },
sceneStyle: { backgroundColor: t.fond },
}}
>
<Tabs.Screen
name="journee"
options={{
href: cacher('journee'),
title: 'Ma journée',
tabBarIcon: ({ color }) => <Pic glyphe="☰" couleur={color} />,
}}
/>
<Tabs.Screen
name="scanner"
options={{
href: cacher('scanner'),
title: 'Scanner',
tabBarIcon: ({ color }) => <Pic glyphe="▣" couleur={color} />,
}}
/>
<Tabs.Screen
name="preventif"
options={{
href: cacher('preventif'),
title: 'Préventif',
tabBarIcon: ({ color }) => <Pic glyphe="✓" couleur={color} />,
}}
/>
<Tabs.Screen
name="synchro"
options={{
href: cacher('synchro'),
title: 'Synchro',
tabBarIcon: ({ color }) => <Pic glyphe="⇅" couleur={color} />,
tabBarBadge: file.length || undefined,
tabBarBadgeStyle: {
backgroundColor: conflits ? t.prioBloque : t.stAttente,
color: '#fff',
fontFamily: 'Manrope_800ExtraBold',
fontSize: 10,
},
}}
/>
<Tabs.Screen
name="accueil"
options={{
href: cacher('accueil'),
title: 'Accueil',
tabBarIcon: ({ color }) => <Pic glyphe="⌂" couleur={color} />,
}}
/>
<Tabs.Screen
name="ot"
options={{
href: cacher('ot'),
title: 'OT',
tabBarIcon: ({ color }) => <Pic glyphe="☰" couleur={color} />,
}}
/>
<Tabs.Screen
name="nouvelle-demande"
options={{
href: cacher('nouvelle-demande'),
title: 'Nouvelle demande',
tabBarIcon: ({ color }) => <Pic glyphe="+" couleur={color} />,
}}
/>
<Tabs.Screen
name="menu"
options={{
href: cacher('menu'),
title: 'Menu',
tabBarIcon: ({ color }) => <Pic glyphe="☷" couleur={color} />,
}}
/>
</Tabs>
);
}