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:
@@ -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] },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user