mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
This commit is contained in:
94
apps/mobile/app/personnes/index.tsx
Normal file
94
apps/mobile/app/personnes/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user