feat(mobile): refonte design DSFR complète + partie admin

- écrans auth/dashboard/journal/info/urgence/profil refondus sur le design system
- AppBar institutionnel République Française sur tous les écrans
- partie admin (utilisateurs, contenus, émotions) avec AdminGuard
- entrée Espace administration dans le profil (admins uniquement)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-19 15:22:15 +02:00
parent 2e96e07c7e
commit 7fd27d8dc2
22 changed files with 2102 additions and 910 deletions
+130 -119
View File
@@ -3,6 +3,8 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-nati
import { Ionicons } from '@expo/vector-icons';
import { useRapport } from '@/hooks/useRapport';
import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
import EmotionPieChart from '@/components/EmotionPieChart';
type Period = 'week' | 'month' | 'quarter' | 'year';
@@ -19,130 +21,139 @@ export default function ReportsScreen() {
const { data: rapport, isLoading } = useRapport(period);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Period selector */}
<View style={styles.periodRow}>
{PERIODS.map((p) => (
<TouchableOpacity
key={p.value}
style={[styles.periodBtn, period === p.value && styles.periodBtnActive]}
onPress={() => setPeriod(p.value)}
>
<Text style={[styles.periodText, period === p.value && styles.periodTextActive]}>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
{isLoading ? (
<View style={styles.loading}>
<Text style={styles.loadingText}>Chargement...</Text>
<View style={styles.screen}>
<AppBar title="Mes rapports" subtitle="Analyse de vos émotions" />
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Sélecteur de période — boutons segmentés */}
<View style={styles.segment}>
{PERIODS.map((p) => {
const active = period === p.value;
return (
<TouchableOpacity
key={p.value}
style={[styles.segmentBtn, active && styles.segmentBtnActive]}
onPress={() => setPeriod(p.value)}
accessibilityLabel={p.label}
>
<Text style={[styles.segmentText, active && styles.segmentTextActive]}>{p.label}</Text>
</TouchableOpacity>
);
})}
</View>
) : rapport ? (
<>
{/* Stats cards */}
<View style={styles.statsRow}>
<View style={styles.statCard}>
<Ionicons name="bar-chart" size={20} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
<View style={styles.statCard}>
<Ionicons name="trending-up" size={20} color={Colors.secondary} />
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensité moy.</Text>
</View>
<View style={styles.statCard}>
<Ionicons name="heart" size={20} color={rapport.stats.emotion_dominante?.couleur || Colors.blueFranceSun} />
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel}>{rapport.stats.emotion_dominante?.nom || 'Aucune'}</Text>
</View>
</View>
{/* Period info */}
<View style={styles.periodInfo}>
<Text style={styles.periodInfoText}>
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au {new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
</Text>
</View>
{/* Repartition */}
{rapport.repartition.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Répartition des émotions</Text>
{/* Camembert (donut) — équivalent mobile du graphique Recharts du web */}
<EmotionPieChart
data={rapport.repartition.map((item) => ({
nom: item.nom,
couleur: item.couleur,
count: item.count,
pourcentage: item.pourcentage,
}))}
/>
<Text style={styles.subSectionTitle}>Détail par émotion</Text>
{rapport.repartition.map((item) => (
<View key={item.nom} style={styles.repartitionItem}>
<View style={styles.repartitionHeader}>
<View style={[styles.colorDot, { backgroundColor: item.couleur }]} />
<Text style={styles.repartitionName}>{item.nom}</Text>
<Text style={styles.repartitionPct}>{item.pourcentage}%</Text>
</View>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${item.pourcentage}%`, backgroundColor: item.couleur }]} />
</View>
<View style={styles.repartitionFooter}>
<Text style={styles.repartitionDetail}>{item.count} saisies</Text>
<Text style={styles.repartitionDetail}>Moy: {item.intensite_moyenne.toFixed(1)}/10</Text>
</View>
{isLoading ? (
<Loader label="Chargement du rapport…" />
) : rapport && rapport.stats.total_saisies > 0 ? (
<>
{/* Stats */}
<Card padding={Spacing.lg} style={styles.statsCard}>
<View style={styles.statsRow}>
<View style={styles.statCell}>
<Ionicons name="bar-chart" size={20} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
))}
</View>
)}
</>
) : (
<View style={styles.empty}>
<Ionicons name="bar-chart-outline" size={48} color={Colors.gray[300]} />
<Text style={styles.emptyText}>Pas de données pour cette période</Text>
</View>
)}
</ScrollView>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Ionicons name="trending-up" size={20} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensité moy.</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Text style={styles.statEmoji}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel} numberOfLines={1}>
{rapport.stats.emotion_dominante?.nom || 'Aucune'}
</Text>
</View>
</View>
<Text style={styles.periodInfo}>
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au{' '}
{new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
</Text>
</Card>
{/* Répartition */}
{rapport.repartition.length > 0 && (
<View style={styles.block}>
<SectionTitle>Répartition des émotions</SectionTitle>
<Card padding={Spacing.lg}>
<EmotionPieChart
data={rapport.repartition.map((item) => ({
nom: item.nom,
couleur: item.couleur,
count: item.count,
pourcentage: item.pourcentage,
}))}
/>
</Card>
<View style={styles.detailHeader}>
<SectionTitle>Détail par émotion</SectionTitle>
</View>
<Card padding={Spacing.lg}>
{rapport.repartition.map((item, idx) => (
<View
key={item.nom}
style={[styles.repItem, idx > 0 && styles.repItemBorder]}
>
<View style={styles.repHeader}>
<Badge label={item.nom} color={item.couleur} />
<Text style={styles.repPct}>{item.pourcentage}%</Text>
</View>
<View style={styles.progressBar}>
<View
style={[styles.progressFill, { width: `${item.pourcentage}%`, backgroundColor: item.couleur }]}
/>
</View>
<View style={styles.repFooter}>
<Text style={styles.repDetail}>{item.count} saisies</Text>
<Text style={styles.repDetail}>Moy. {item.intensite_moyenne.toFixed(1)}/10</Text>
</View>
</View>
))}
</Card>
</View>
)}
</>
) : (
<EmptyState
icon="bar-chart-outline"
title="Aucune donnée"
message="Pas de saisie pour cette période. Changez de période ou ajoutez une émotion."
/>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 4, padding: 4, marginBottom: 20 },
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 4 },
periodBtnActive: { backgroundColor: Colors.primary },
periodText: { fontSize: 13, fontWeight: '600', color: Colors.gray[500] },
periodTextActive: { color: Colors.white },
loading: { padding: 40, alignItems: 'center' },
loadingText: { color: Colors.gray[400] },
statsRow: { flexDirection: 'row', gap: 10, marginBottom: 16 },
statCard: {
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 4, padding: 16,
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[200],
},
statValue: { fontSize: 22, fontWeight: '800', color: Colors.black },
statLabel: { fontSize: 11, color: Colors.gray[500] },
periodInfo: { marginBottom: 20 },
periodInfoText: { fontSize: 13, color: Colors.gray[400], textAlign: 'center' },
section: { marginBottom: 24 },
sectionTitle: { fontSize: 18, fontWeight: '700', color: Colors.black, marginBottom: 16 },
subSectionTitle: { fontSize: 14, fontWeight: '700', color: Colors.gray[600], marginBottom: 12, marginTop: 4 },
repartitionItem: { marginBottom: 16 },
repartitionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 6 },
colorDot: { width: 10, height: 10, borderRadius: 5, marginRight: 8 },
repartitionName: { flex: 1, fontSize: 14, fontWeight: '600', color: Colors.black },
repartitionPct: { fontSize: 14, fontWeight: '700', color: Colors.black },
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 4 },
progressFill: { height: 6, borderRadius: 3 },
repartitionFooter: { flexDirection: 'row', justifyContent: 'space-between' },
repartitionDetail: { fontSize: 12, color: Colors.gray[400] },
empty: { alignItems: 'center', paddingTop: 60 },
emptyText: { fontSize: 16, color: Colors.gray[400], marginTop: 12 },
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
segment: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: Radius.sm, padding: Spacing.xs, marginBottom: Spacing.lg },
segmentBtn: { flex: 1, paddingVertical: Spacing.sm, alignItems: 'center', borderRadius: Radius.sm, minHeight: 40, justifyContent: 'center' },
segmentBtnActive: { backgroundColor: Colors.primary },
segmentText: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.gray[500] },
segmentTextActive: { color: Colors.white },
statsCard: { marginBottom: Spacing.xl },
statsRow: { flexDirection: 'row', alignItems: 'center' },
statCell: { flex: 1, alignItems: 'center', gap: Spacing.xs },
statDivider: { width: 1, alignSelf: 'stretch', backgroundColor: Colors.gray[200], marginVertical: Spacing.xs },
statValue: { fontSize: FontSize.xxl, fontWeight: FontWeight.heavy, color: Colors.black },
statEmoji: { fontSize: FontSize.xxl },
statLabel: { fontSize: FontSize.tiny, color: Colors.gray[500], textAlign: 'center' },
periodInfo: { fontSize: FontSize.xs, color: Colors.gray[400], textAlign: 'center', marginTop: Spacing.md },
block: { marginBottom: Spacing.xl },
detailHeader: { marginTop: Spacing.lg },
repItem: { paddingVertical: Spacing.md },
repItemBorder: { borderTopWidth: 1, borderTopColor: Colors.gray[200] },
repHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.sm },
repPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.black },
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs },
progressFill: { height: 6, borderRadius: Radius.sm },
repFooter: { flexDirection: 'row', justifyContent: 'space-between' },
repDetail: { fontSize: FontSize.xs, color: Colors.gray[500] },
});