Files
siop2/apps/mobile/app/(tabs)/menu.tsx
pr-daaif be5dfb6bc7 fix(r6): recette Gestionnaire — Menu non scrollable, BC sans confirmation
Deux bugs réels trouvés sur iPhone physique par le référent :

1. app/(tabs)/menu.tsx n'était pas scrollable (View, pas ScrollView) —
   avec les 4 groupes désormais pleinement câblés (12 liens + 4 en-têtes),
   le contenu dépasse la hauteur de l'écran et le dernier groupe
   (Pilotage, dont Personnes & équipes) était strictement inaccessible.
   Présent depuis R6.1, seulement révélé maintenant. Fix : ScrollView
   autour de la liste des groupes, en-tête fixe au-dessus.

2. app/stock/[id]/commander.tsx naviguait silencieusement en arrière
   après succès — le référent a créé deux BC (vérifiés corrects côté
   serveur) sans aucun retour dans l'app. Fix : écran de confirmation
   explicite (référence, fournisseur, total, statut) avant de revenir.

Amélioration sur retour direct : Tiers gagne une fiche détail
(app/tiers/[id].tsx — identité, contact, BC en cours, sites rattachés,
aucune nouvelle requête) — le lecture seule sans réaction au tap se
lisait comme un écran cassé plutôt que délibérément simple.

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 12:59:40 +01:00

142 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { router } from 'expo-router';
import { Pressable, ScrollView, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import type { ObjectCategory, PermissionRight } from '@siop/shared';
import { useMe } from '@/auth/session';
import { estRoleTerrain } from '@/auth/roles';
import { usePermissions } from '@/auth/use-permissions';
import { EnteteTabs } from '@/composants/ui';
import { useTokens, type Tokens } from '@/theme/tokens';
/** Menu — maquette « mobile ouvert à tous les rôles », écran 3 : reprend à
* l'identique les 4 groupes et l'ordre de la sidebar web (Exploitation /
* Parc / Ressources / Pilotage, coquille.tsx), filtrés par LA MÊME matrice
* (D2 — aucune règle de droit nouvelle). Un groupe sans aucun lien visible
* est masqué en entier (D3) : l'écran est petit, un intitulé vide n'aide
* personne. Ce que « Ma journée »/« Préventif » couvrent déjà pour le
* Technicien n'est pas dupliqué ici. */
interface LienMenu {
libelle: string;
permission: [ObjectCategory, PermissionRight];
ico: string;
cacheEnTerrain?: boolean; // déjà couvert par un onglet terrain dédié
route?: string;
aVenir?: string; // detail affiché sur l'écran « à venir »
}
const GROUPES: { titre: string; liens: LienMenu[] }[] = [
{
titre: 'Exploitation',
liens: [
{ libelle: 'Ordres de travail', ico: '☰', permission: ['WORK_ORDERS', 'view'], cacheEnTerrain: true, route: '/(tabs)/ot' },
{ libelle: 'Demandes', ico: '✎', permission: ['REQUESTS', 'view'], route: '/demandes' },
{ libelle: 'Préventif', ico: '✓', permission: ['WORK_ORDERS', 'view'], cacheEnTerrain: true, aVenir: 'Les grilles préventives pour les rôles de gestion arrivent dans une prochaine étape — le Technicien les a déjà dans son onglet dédié.' },
],
},
{
titre: 'Parc',
liens: [
{ libelle: 'Ascenseurs', ico: '▣', permission: ['ASSETS', 'view'], route: '/ascenseurs' },
{ libelle: 'Sites', ico: '◫', permission: ['LOCATIONS', 'view'], route: '/sites' },
{ libelle: 'Catégories', ico: '▤', permission: ['SETTINGS', 'view'], aVenir: 'Ladministration des catégories reste sur le grand écran pour linstant.' },
],
},
{
titre: 'Ressources',
liens: [
{ libelle: 'Stock & achats', ico: '◔', permission: ['PARTS', 'view'], route: '/stock' },
{ libelle: 'Tiers', ico: '⇄', permission: ['PURCHASE_ORDERS', 'view'], route: '/tiers' },
{ libelle: 'Fichiers', ico: '▧', permission: ['ASSETS', 'view'], route: '/bibliotheque' },
],
},
{
titre: 'Pilotage',
liens: [
{ libelle: 'Statistiques', ico: '◈', permission: ['ANALYTICS', 'view'], route: '/statistiques' },
{ libelle: 'Assistant', ico: '✦', permission: ['WORK_ORDERS', 'view'], route: '/assistant' },
{ libelle: 'Personnes & équipes', ico: '◎', permission: ['PEOPLE_TEAMS', 'view'], route: '/personnes' },
],
},
];
export default function PageMenu() {
const t = useTokens();
const { data: me } = useMe();
const { can } = usePermissions();
const terrain = estRoleTerrain(me?.role.name);
const groupesVisibles = GROUPES.map((g) => ({
...g,
liens: g.liens.filter(
({ permission, cacheEnTerrain }) => can(...permission) && !(cacheEnTerrain && terrain),
),
})).filter((g) => g.liens.length > 0);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
<View style={{ padding: 14, gap: 10, flex: 1 }}>
<EnteteTabs />
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>Menu</Text>
<ScrollView contentContainerStyle={{ gap: 14, paddingBottom: 16 }} showsVerticalScrollIndicator={false}>
{groupesVisibles.length === 0 ? (
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', textAlign: 'center', padding: 24 }}>
Rien d'autre accessible à votre rôle ici.
</Text>
) : null}
{groupesVisibles.map((g) => (
<View key={g.titre} style={{ gap: 6 }}>
<Text
style={{
fontFamily: 'Manrope_800ExtraBold',
fontSize: 10.5,
letterSpacing: 0.8,
textTransform: 'uppercase',
color: t.encre3,
}}
>
{g.titre}
</Text>
{g.liens.map((lien) => (
<LigneMenu key={lien.libelle} lien={lien} t={t} />
))}
</View>
))}
</ScrollView>
</View>
</SafeAreaView>
);
}
function LigneMenu({ lien, t }: { lien: LienMenu; t: Tokens }) {
const aller = () => {
if (lien.route) {
router.push(lien.route as never);
} else {
router.push({ pathname: '/a-venir', params: { titre: lien.libelle, detail: lien.aVenir ?? '' } });
}
};
return (
<Pressable
accessibilityRole="button"
onPress={aller}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
backgroundColor: t.surface,
borderColor: t.bordure,
borderWidth: 1,
borderRadius: 10,
padding: 11,
}}
>
<Text style={{ width: 22, textAlign: 'center', fontSize: 14, color: t.primaire }}>{lien.ico}</Text>
<Text style={{ flex: 1, fontFamily: 'Manrope_700Bold', fontSize: 12.5, color: t.encre }}>
{lien.libelle}
</Text>
<Text style={{ color: t.encre3, fontSize: 13 }}></Text>
</Pressable>
);
}