Files
cesizen-individuel/mobile/app/(tabs)/index.tsx
T
Azmog 7fd27d8dc2 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>
2026-06-19 15:22:15 +02:00

161 lines
7.0 KiB
TypeScript

import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { useRapport } from '@/hooks/useRapport';
import { useSaisies } from '@/hooks/useTracker';
import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
export default function DashboardScreen() {
const router = useRouter();
const { user } = useAuthStore();
const { data: rapport } = useRapport('month');
const { data: saisies, isLoading } = useSaisies();
const recentSaisies = (saisies || []).slice(0, 3);
return (
<View style={styles.screen}>
<AppBar
title={`Bonjour ${user?.prenom || ''}`.trim()}
subtitle="Comment allez-vous aujourd'hui ?"
/>
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Actions rapides */}
<View style={styles.actionsRow}>
<Card onPress={() => router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}>
<View style={[styles.actionIcon, styles.actionIconPrimary]}>
<Ionicons name="add-circle" size={28} color={Colors.white} />
</View>
<Text style={styles.actionTextPrimary}>Nouvelle saisie</Text>
</Card>
<Card onPress={() => router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}>
<View style={[styles.actionIcon, styles.actionIconSecondary]}>
<Ionicons name="bar-chart" size={28} color={Colors.primary} />
</View>
<Text style={styles.actionText}>Mes rapports</Text>
</Card>
</View>
{/* Stats du mois */}
{rapport && (
<View style={styles.block}>
<SectionTitle>Ce mois-ci</SectionTitle>
<Card padding={Spacing.lg}>
<View style={styles.statsRow}>
<View style={styles.statCell}>
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<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.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel} numberOfLines={1}>
{rapport.stats.emotion_dominante?.nom || 'Aucune'}
</Text>
</View>
</View>
</Card>
</View>
)}
{/* Dernières saisies */}
<View style={styles.block}>
<View style={styles.sectionHeader}>
<SectionTitle>Dernières saisies</SectionTitle>
{recentSaisies.length > 0 && (
<Text style={styles.seeAll} onPress={() => router.push('/(tabs)/journal')}>
Tout voir
</Text>
)}
</View>
{isLoading ? (
<Loader label="Chargement…" />
) : recentSaisies.length === 0 ? (
<EmptyState
icon="sparkles-outline"
title="Aucune saisie pour l'instant"
message="Commencez par enregistrer votre première émotion du jour."
actionLabel="Nouvelle saisie"
onAction={() => router.push('/(tabs)/journal/new')}
/>
) : (
recentSaisies.map((saisie) => (
<Card
key={saisie.id}
onPress={() => router.push('/(tabs)/journal')}
padding={Spacing.md}
style={styles.saisieCard}
>
<View style={[styles.emotionIcon, { backgroundColor: `${saisie.emotion?.couleur}20` }]}>
<Text style={styles.emotionEmoji}>{saisie.emotion?.icone || '🔵'}</Text>
</View>
<View style={styles.saisieContent}>
{saisie.emotion?.couleur ? (
<Badge label={saisie.emotion?.nom || ''} color={saisie.emotion.couleur} />
) : (
<Badge label={saisie.emotion?.nom || '—'} tone="neutral" />
)}
<Text style={styles.saisieDate}>
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensité {saisie.intensite}/10
</Text>
</View>
</Card>
))
)}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
actionsRow: { flexDirection: 'row', gap: Spacing.md, marginBottom: Spacing.xl },
actionCard: { flex: 1, alignItems: 'center', gap: Spacing.md },
actionPrimary: {
flex: 1,
alignItems: 'center',
gap: Spacing.md,
backgroundColor: Colors.primary,
borderColor: Colors.primary,
},
actionIcon: { width: 56, height: 56, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
actionIconPrimary: { backgroundColor: Colors.primaryDark },
actionIconSecondary: { backgroundColor: '#E3E3FD' },
actionText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.black },
actionTextPrimary: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.white },
block: { 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.primary },
statLabel: { fontSize: FontSize.tiny, color: Colors.gray[500], textAlign: 'center' },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
seeAll: {
fontSize: FontSize.sm,
fontWeight: FontWeight.semibold,
color: Colors.primary,
textDecorationLine: 'underline',
minHeight: MIN_TOUCH,
paddingTop: Spacing.xs,
},
saisieCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm },
emotionIcon: { width: 44, height: 44, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
emotionEmoji: { fontSize: 22 },
saisieContent: { flex: 1, gap: Spacing.xs, alignItems: 'flex-start' },
saisieDate: { fontSize: FontSize.xs, color: Colors.gray[500] },
});