Files
siop2/apps/mobile/app/personnes/index.tsx
pr-daaif 4c1a2d67a0 feat(r6): R6.4 — Pilotage sur mobile (Statistiques, Personnes)
EXPO_PUBLIC_WEB_URL/WEB_URL pour le lien d'activation (pointe la page web
/activation?token=…, aucun équivalent mobile). api/pilotage.ts
(useAnalyticsSummary, useUsers, useRoles, useInviteUser).

Écran Statistiques (maquette écran 7) : période 3/6/12 mois, coût du mois,
taux préventif, OT clôturés, pannes par organe, top équipements — cartes
plutôt que graphes denses (D4).

Écran Personnes & équipes : liste + statut, Inviter (nom/e-mail/rôle),
lien d'activation en texte sélectionnable (Text selectable, RN natif) —
expo-clipboard est une dépendance native absente, différée comme
expo-sharing en R6.3 plutôt qu'ajoutée à la légère. Taux horaire/rôles/
équipes restent gérés au web (D4).

Assistant reste "à venir" dans le Menu : un chat sourcé est un nouveau
patron d'écran jamais maquetté sur mobile, contrairement aux autres
familles qui réutilisaient des patrons déjà validés — mérite son propre
tour de design-first plutôt qu'être exécuté par réflexe en fin de release.

R6 quasi close (Exploitation/Parc/Ressources/Pilotage livrés pour tous les
rôles, hors Assistant) — reste la confirmation du référent sur iPhone
(R6.1 + correctif déconnexion) avant clôture réelle.

Typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché.

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

95 lines
3.1 KiB
TypeScript

import { router } from 'expo-router';
import { FlatList, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useUsers } from '@/api/pilotage';
import { usePermissions } from '@/auth/use-permissions';
import { BoutonTel, EnteteFiche } from '@/composants/ui';
import { useTokens, type Tokens } from '@/theme/tokens';
const LABEL_STATUT: Record<'active' | 'invited' | 'disabled', string> = {
active: 'Actif',
invited: 'Invitation envoyée',
disabled: 'Désactivé',
};
/** Personnes & équipes — R6.4 (Pilotage). Consultation + « Inviter » (D4) —
* taux horaire, équipes, rôles restent gérés depuis le web pour l'instant. */
export default function PagePersonnes() {
const t = useTokens();
const { data: users } = useUsers();
const { can } = usePermissions();
const personnes = [...(users ?? [])].sort((a, b) => a.displayName.localeCompare(b.displayName));
return (
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
<View style={{ flex: 1, padding: 14, gap: 10 }}>
<EnteteFiche titre="Personnes" />
{can('PEOPLE_TEAMS', 'create') ? (
<BoutonTel libelle="+ Inviter" surAppui={() => router.push('/personnes/inviter')} />
) : null}
<FlatList
data={personnes}
keyExtractor={(u) => u.id}
contentContainerStyle={{ gap: 8, paddingBottom: 12 }}
ListEmptyComponent={
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', padding: 16, textAlign: 'center' }}>
Personne accessible.
</Text>
}
renderItem={({ item: u }) => <LignePersonne utilisateur={u} t={t} />}
/>
</View>
</SafeAreaView>
);
}
function LignePersonne({
utilisateur: u,
t,
}: {
utilisateur: { id: string; displayName: string; email: string; role: { name: string }; teams: { name: string }[]; status: 'active' | 'invited' | 'disabled' };
t: Tokens;
}) {
const [enc, fond] =
u.status === 'active'
? [t.stTermine, t.stTermineFond]
: u.status === 'invited'
? [t.stAttente, t.stAttenteFond]
: [t.stAnnule, t.stAnnuleFond];
return (
<View
style={{
backgroundColor: t.surface,
borderColor: t.bordure,
borderWidth: 1,
borderRadius: 12,
padding: 12,
gap: 2,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Text style={{ flex: 1, fontFamily: 'Manrope_700Bold', fontSize: 14, color: t.encre }}>
{u.displayName}
</Text>
<Text
style={{
fontFamily: 'Manrope_700Bold',
fontSize: 10.5,
color: enc,
backgroundColor: fond,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 999,
}}
>
{LABEL_STATUT[u.status]}
</Text>
</View>
<Text style={{ fontFamily: 'Manrope_400Regular', fontSize: 12, color: t.encre2 }}>
{u.role.name}
{u.teams.length ? ` · ${u.teams.map((eq) => eq.name).join(', ')}` : ''}
</Text>
</View>
);
}