Files
siop2/apps/mobile/app/(tabs)/menu.tsx
pr-daaif 21dd6cf034 feat(r6): R6.2 — Parc sur mobile (Sites, Ascenseurs)
useLocations() mobile ; écran Sites (premier niveau) ; fiche site
(identité, zones, ascenseurs du site — consultation seule, D4, pas de
carte ni d'édition sur mobile pour l'instant) ; écran Ascenseurs (parc
complet déjà préchargé, D1) ouvrant la fiche appareil R4 telle quelle
(déjà générique, aucune modification nécessaire). Menu : Ascenseurs/Sites
routent réellement au lieu de « à venir » ; Catégories reste à venir
(admin, hors périmètre R6.2).

Exécution directe de la maquette R6 déjà validée, patrons visuels
réutilisés (Carte/LigneInfo/EnteteFiche) — pas de nouveau tour de design.

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

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

140 lines
5.7 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, 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'], aVenir: 'Le stock et les bons de commande en mobilité arrivent dans une prochaine étape.' },
{ libelle: 'Tiers', ico: '⇄', permission: ['PURCHASE_ORDERS', 'view'], aVenir: 'Fournisseurs et clients restent sur le grand écran pour linstant.' },
{ libelle: 'Fichiers', ico: '▧', permission: ['ASSETS', 'view'], aVenir: 'La bibliothèque documentaire reste sur le grand écran pour linstant.' },
],
},
{
titre: 'Pilotage',
liens: [
{ libelle: 'Statistiques', ico: '◈', permission: ['ANALYTICS', 'view'], aVenir: 'Les statistiques en mobilité arrivent dans une prochaine étape.' },
{ libelle: 'Assistant', ico: '✦', permission: ['WORK_ORDERS', 'view'], aVenir: 'Lassistant IA reste sur le grand écran et la clôture mobile pour linstant.' },
{ libelle: 'Personnes & équipes', ico: '◎', permission: ['PEOPLE_TEAMS', 'view'], aVenir: 'La gestion des personnes et équipes en mobilité arrive dans une prochaine étape.' },
],
},
];
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: 14, flex: 1 }}>
<EnteteTabs />
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>Menu</Text>
{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>
))}
</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>
);
}