Files
siop2/apps/mobile/app/(tabs)/menu.tsx
pr-daaif f210d4f801 feat(r6): R6.3 — Ressources sur mobile (Stock, Tiers, Fichiers)
api/ressources.ts (usePartners, useParts/usePart, useCreatePurchaseOrder,
useDocuments). Écran Stock (sous-seuil en tête) + fiche pièce + "Commander"
pré-rempli en une ligne (fournisseur figé, quantité = manquant jusqu'au
seuil, prix = dernier connu — BC multi-lignes détaillé réservé au web, D4).
Tiers en lecture seule (création/édition réservées au web). Fichiers en
métadonnées seules — l'ouverture demande expo-sharing (dépendance native
absente, donc un nouveau build natif) : différée explicitement plutôt
qu'ajoutée à la légère au milieu de cette passe.

Menu : Stock & achats / Tiers / Fichiers routent réellement.

Typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché,
toutes les opérations utilisées existaient déjà depuis R3.

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

140 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, 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'], 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>
);
}