feat: add mobile application structure with emotion picker and API integration
- Added favicon and icon assets for the mobile app. - Implemented EmotionPicker component for selecting emotions with child options. - Created hooks for fetching emotions, feeds, and rapport data. - Established API service for handling requests and authentication. - Integrated Zustand for state management of user authentication. - Defined color palette for the application. - Set up TypeScript types for various entities including User, Emotion, and Feed. - Configured Babel and TypeScript for the mobile project. - Added package.json with necessary dependencies for Expo and React Native.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export default function JournalLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: Colors.white },
|
||||
headerTitleStyle: { fontWeight: '700', color: Colors.black },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ title: 'Mon journal' }} />
|
||||
<Stack.Screen name="new" options={{ title: 'Nouvelle saisie', presentation: 'modal' }} />
|
||||
<Stack.Screen name="edit" options={{ title: 'Modifier la saisie', presentation: 'modal' }} />
|
||||
<Stack.Screen name="reports" options={{ title: 'Mes rapports' }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSaisies, useUpdateSaisie } from '@/hooks/useTracker';
|
||||
import EmotionPicker from '@/components/EmotionPicker';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export default function EditEntryScreen() {
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const { data: saisies } = useSaisies();
|
||||
const updateSaisie = useUpdateSaisie();
|
||||
|
||||
const saisie = saisies?.find((s) => s.id === Number(id));
|
||||
|
||||
const [emotionId, setEmotionId] = useState<number | undefined>();
|
||||
const [intensite, setIntensite] = useState(5);
|
||||
const [note, setNote] = useState('');
|
||||
const [dateSaisie, setDateSaisie] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (saisie) {
|
||||
setEmotionId(saisie.emotion_id);
|
||||
setIntensite(saisie.intensite);
|
||||
setNote(saisie.note || '');
|
||||
setDateSaisie(saisie.date_saisie.split('T')[0]);
|
||||
}
|
||||
}, [saisie]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!emotionId || !id) return;
|
||||
try {
|
||||
await updateSaisie.mutateAsync({
|
||||
id: Number(id),
|
||||
emotion_id: emotionId,
|
||||
intensite,
|
||||
note: note.trim() || undefined,
|
||||
date_saisie: dateSaisie,
|
||||
});
|
||||
Alert.alert('Succes', 'Saisie modifiee !', [
|
||||
{ text: 'OK', onPress: () => router.back() },
|
||||
]);
|
||||
} catch {
|
||||
Alert.alert('Erreur', 'Impossible de modifier la saisie');
|
||||
}
|
||||
};
|
||||
|
||||
if (!saisie) {
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<Text style={styles.loadingText}>Chargement...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.title}>Modifier la saisie</Text>
|
||||
|
||||
{/* Emotion */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Emotion</Text>
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
</View>
|
||||
|
||||
{/* Intensite */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Intensite</Text>
|
||||
<View style={styles.intensityContainer}>
|
||||
<Text style={styles.intensityValue}>{intensite}</Text>
|
||||
<Text style={styles.intensityUnit}>/10</Text>
|
||||
</View>
|
||||
<View style={styles.sliderButtons}>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
|
||||
<TouchableOpacity
|
||||
key={v}
|
||||
style={[styles.sliderDot, intensite === v && styles.sliderDotActive]}
|
||||
onPress={() => setIntensite(v)}
|
||||
>
|
||||
<Text style={[styles.sliderDotText, intensite === v && styles.sliderDotTextActive]}>{v}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.sliderLabels}>
|
||||
<Text style={styles.sliderLabel}>Faible</Text>
|
||||
<Text style={styles.sliderLabel}>Forte</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Note */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Note (optionnel)</Text>
|
||||
<TextInput
|
||||
style={styles.textArea}
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
placeholder="Decrivez ce que vous ressentez..."
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Actions */}
|
||||
<View style={styles.actionsRow}>
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => router.back()}>
|
||||
<Text style={styles.cancelBtnText}>Annuler</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn, updateSaisie.isPending && styles.btnDisabled]}
|
||||
onPress={handleSubmit}
|
||||
disabled={updateSaisie.isPending || !emotionId}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color={Colors.black} />
|
||||
<Text style={styles.submitBtnText}>
|
||||
{updateSaisie.isPending ? 'Envoi...' : 'Enregistrer'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
loadingText: { color: Colors.gray[400], fontSize: 16 },
|
||||
title: { fontSize: 24, fontWeight: '800', color: Colors.black, marginBottom: 24 },
|
||||
section: { marginBottom: 24 },
|
||||
label: { fontSize: 15, fontWeight: '700', color: Colors.gray[700], marginBottom: 10 },
|
||||
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 16 },
|
||||
intensityValue: { fontSize: 48, fontWeight: '800', color: Colors.black },
|
||||
intensityUnit: { fontSize: 20, color: Colors.gray[400], marginLeft: 4 },
|
||||
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
|
||||
sliderDot: {
|
||||
width: 28, height: 28, borderRadius: 14,
|
||||
backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
sliderDotActive: { backgroundColor: Colors.primary },
|
||||
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
|
||||
sliderDotTextActive: { color: Colors.black },
|
||||
sliderLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 },
|
||||
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
|
||||
textArea: {
|
||||
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
|
||||
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black, minHeight: 100,
|
||||
},
|
||||
actionsRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, gap: 12 },
|
||||
cancelBtn: {
|
||||
flex: 1, paddingVertical: 16, borderRadius: 14,
|
||||
backgroundColor: Colors.gray[100], alignItems: 'center',
|
||||
},
|
||||
cancelBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
|
||||
submitBtn: {
|
||||
flex: 1, flexDirection: 'row', gap: 8, paddingVertical: 16, borderRadius: 14,
|
||||
backgroundColor: Colors.secondary, alignItems: 'center', justifyContent: 'center',
|
||||
},
|
||||
submitBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
|
||||
btnDisabled: { opacity: 0.5 },
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert, RefreshControl } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSaisies, useDeleteSaisie } from '@/hooks/useTracker';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import type { SaisieTracker } from '@/types';
|
||||
|
||||
export default function JournalScreen() {
|
||||
const router = useRouter();
|
||||
const { data: saisies, isLoading, refetch } = useSaisies();
|
||||
const deleteSaisie = useDeleteSaisie();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await refetch();
|
||||
setRefreshing(false);
|
||||
}, [refetch]);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
Alert.alert('Supprimer', 'Supprimer cette saisie ?', [
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Supprimer',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await deleteSaisie.mutateAsync(id);
|
||||
} catch {
|
||||
Alert.alert('Erreur', 'Impossible de supprimer');
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: SaisieTracker }) => (
|
||||
<View style={styles.saisieCard}>
|
||||
<View style={[styles.emotionIcon, { backgroundColor: `${item.emotion?.couleur}20` }]}>
|
||||
<Text style={styles.emotionEmoji}>{item.emotion?.icone || '🔵'}</Text>
|
||||
</View>
|
||||
<View style={styles.saisieContent}>
|
||||
<View style={styles.saisieHeader}>
|
||||
<View style={[styles.badge, { backgroundColor: `${item.emotion?.couleur}20` }]}>
|
||||
<Text style={[styles.badgeText, { color: item.emotion?.couleur }]}>{item.emotion?.nom}</Text>
|
||||
</View>
|
||||
<Text style={styles.intensityText}>Intensite: {item.intensite}/10</Text>
|
||||
</View>
|
||||
<View style={styles.intensityBar}>
|
||||
<View style={[styles.intensityFill, { width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary }]} />
|
||||
</View>
|
||||
{item.note && <Text style={styles.note} numberOfLines={2}>{item.note}</Text>}
|
||||
<Text style={styles.date}>{new Date(item.date_saisie).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}</Text>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}>
|
||||
<Ionicons name="create-outline" size={18} color={Colors.gray[400]} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item.id)}>
|
||||
<Ionicons name="trash-outline" size={18} color={Colors.gray[400]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={saisies || []}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary} />}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="book-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyTitle}>Aucune saisie</Text>
|
||||
<Text style={styles.emptyText}>Commencez par enregistrer votre premiere emotion</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity style={styles.fab} onPress={() => router.push('/(tabs)/journal/new')}>
|
||||
<Ionicons name="add" size={28} color={Colors.black} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
listContent: { padding: 16, paddingBottom: 80 },
|
||||
saisieCard: {
|
||||
flexDirection: 'row', alignItems: 'flex-start', gap: 12, padding: 16,
|
||||
backgroundColor: Colors.gray[50], borderRadius: 16, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[100],
|
||||
},
|
||||
emotionIcon: { width: 48, height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionEmoji: { fontSize: 24 },
|
||||
saisieContent: { flex: 1 },
|
||||
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 6 },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 8 },
|
||||
badgeText: { fontSize: 13, fontWeight: '600' },
|
||||
intensityText: { fontSize: 12, color: Colors.gray[400] },
|
||||
intensityBar: { height: 4, backgroundColor: Colors.gray[200], borderRadius: 2, marginBottom: 6 },
|
||||
intensityFill: { height: 4, borderRadius: 2 },
|
||||
note: { fontSize: 13, color: Colors.gray[600], marginBottom: 4 },
|
||||
date: { fontSize: 12, color: Colors.gray[400] },
|
||||
actions: { gap: 4 },
|
||||
actionBtn: { padding: 6, borderRadius: 8 },
|
||||
empty: { alignItems: 'center', paddingTop: 80 },
|
||||
emptyTitle: { fontSize: 18, fontWeight: '600', color: Colors.gray[400], marginTop: 12 },
|
||||
emptyText: { fontSize: 14, color: Colors.gray[400], marginTop: 4 },
|
||||
fab: {
|
||||
position: 'absolute', right: 20, bottom: 20,
|
||||
width: 56, height: 56, borderRadius: 28,
|
||||
backgroundColor: Colors.primary, justifyContent: 'center', alignItems: 'center',
|
||||
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8,
|
||||
elevation: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useCreateSaisie } from '@/hooks/useTracker';
|
||||
import EmotionPicker from '@/components/EmotionPicker';
|
||||
import { Colors } from '@/lib/colors';
|
||||
export default function NewEntryScreen() {
|
||||
const router = useRouter();
|
||||
const createSaisie = useCreateSaisie();
|
||||
const [step, setStep] = useState(1);
|
||||
const [emotionId, setEmotionId] = useState<number | undefined>();
|
||||
const [intensite, setIntensite] = useState(5);
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!emotionId) return;
|
||||
try {
|
||||
await createSaisie.mutateAsync({
|
||||
emotion_id: emotionId,
|
||||
intensite,
|
||||
note: note.trim() || undefined,
|
||||
date_saisie: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
Alert.alert('Succes', 'Saisie enregistree !', [
|
||||
{ text: 'OK', onPress: () => router.back() },
|
||||
]);
|
||||
} catch {
|
||||
Alert.alert('Erreur', 'Impossible d\'enregistrer la saisie');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
{/* Stepper */}
|
||||
<View style={styles.stepper}>
|
||||
{[1, 2, 3].map((s) => (
|
||||
<View key={s} style={styles.stepRow}>
|
||||
<View style={[styles.stepCircle, step >= s && styles.stepActive]}>
|
||||
{step > s ? (
|
||||
<Ionicons name="checkmark" size={16} color={Colors.white} />
|
||||
) : (
|
||||
<Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text>
|
||||
)}
|
||||
</View>
|
||||
{s < 3 && <View style={[styles.stepLine, step > s && styles.stepLineActive]} />}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Step 1: Emotion */}
|
||||
{step === 1 && (
|
||||
<View>
|
||||
<Text style={styles.stepTitle}>Comment vous sentez-vous ?</Text>
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
<TouchableOpacity
|
||||
style={[styles.nextBtn, !emotionId && styles.btnDisabled]}
|
||||
onPress={() => emotionId && setStep(2)}
|
||||
disabled={!emotionId}
|
||||
>
|
||||
<Text style={styles.nextBtnText}>Suivant</Text>
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 2: Intensity */}
|
||||
{step === 2 && (
|
||||
<View>
|
||||
<Text style={styles.stepTitle}>A quel point ressentez-vous cette emotion ?</Text>
|
||||
<View style={styles.intensityContainer}>
|
||||
<Text style={styles.intensityValue}>{intensite}</Text>
|
||||
<Text style={styles.intensityLabel}>/10</Text>
|
||||
</View>
|
||||
<View style={styles.sliderContainer}>
|
||||
<Text style={styles.sliderLabel}>Faible</Text>
|
||||
<View style={styles.sliderWrapper}>
|
||||
<View style={styles.nativeSlider}>
|
||||
<View style={styles.sliderTrack}>
|
||||
<View style={[styles.sliderFill, { width: `${(intensite - 1) * 11.11}%` }]} />
|
||||
</View>
|
||||
<View style={styles.sliderButtons}>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
|
||||
<TouchableOpacity
|
||||
key={v}
|
||||
style={[styles.sliderDot, intensite === v && styles.sliderDotActive]}
|
||||
onPress={() => setIntensite(v)}
|
||||
>
|
||||
<Text style={[styles.sliderDotText, intensite === v && styles.sliderDotTextActive]}>{v}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.sliderLabel}>Forte</Text>
|
||||
</View>
|
||||
<View style={styles.navRow}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(1)}>
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
|
||||
<Text style={styles.backBtnText}>Retour</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.nextBtn} onPress={() => setStep(3)}>
|
||||
<Text style={styles.nextBtnText}>Suivant</Text>
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 3: Note */}
|
||||
{step === 3 && (
|
||||
<View>
|
||||
<Text style={styles.stepTitle}>Souhaitez-vous ajouter un commentaire ?</Text>
|
||||
<TextInput
|
||||
style={styles.textArea}
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
placeholder="Decrivez ce que vous ressentez, ce qui a declenche cette emotion..."
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<View style={styles.navRow}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(2)}>
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
|
||||
<Text style={styles.backBtnText}>Retour</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn, createSaisie.isPending && styles.btnDisabled]}
|
||||
onPress={handleSubmit}
|
||||
disabled={createSaisie.isPending}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color={Colors.black} />
|
||||
<Text style={styles.nextBtnText}>{createSaisie.isPending ? 'Envoi...' : 'Enregistrer'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: 32 },
|
||||
stepRow: { flexDirection: 'row', alignItems: 'center' },
|
||||
stepCircle: {
|
||||
width: 32, height: 32, borderRadius: 16, backgroundColor: Colors.gray[200],
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
stepActive: { backgroundColor: Colors.secondary },
|
||||
stepNumber: { fontSize: 14, fontWeight: '700', color: Colors.gray[500] },
|
||||
stepNumberActive: { color: Colors.white },
|
||||
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: 4 },
|
||||
stepLineActive: { backgroundColor: Colors.secondary },
|
||||
stepTitle: { fontSize: 20, fontWeight: '700', color: Colors.black, marginBottom: 20 },
|
||||
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 24 },
|
||||
intensityValue: { fontSize: 64, fontWeight: '800', color: Colors.black },
|
||||
intensityLabel: { fontSize: 24, color: Colors.gray[400], marginLeft: 4 },
|
||||
sliderContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 32 },
|
||||
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
|
||||
sliderWrapper: { flex: 1 },
|
||||
nativeSlider: {},
|
||||
sliderTrack: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 8 },
|
||||
sliderFill: { height: 6, backgroundColor: Colors.primary, borderRadius: 3 },
|
||||
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
sliderDot: { width: 28, height: 28, borderRadius: 14, backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center' },
|
||||
sliderDotActive: { backgroundColor: Colors.primary },
|
||||
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
|
||||
sliderDotTextActive: { color: Colors.black },
|
||||
textArea: {
|
||||
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
|
||||
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black,
|
||||
minHeight: 120, marginBottom: 24,
|
||||
},
|
||||
navRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 16 },
|
||||
backBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 20, borderRadius: 12, backgroundColor: Colors.gray[100] },
|
||||
backBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
|
||||
nextBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.primary },
|
||||
submitBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.secondary },
|
||||
nextBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
|
||||
btnDisabled: { opacity: 0.5 },
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRapport } from '@/hooks/useRapport';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
type Period = 'week' | 'month' | 'quarter' | 'year';
|
||||
|
||||
const PERIODS: { value: Period; label: string }[] = [
|
||||
{ value: 'week', label: 'Semaine' },
|
||||
{ value: 'month', label: 'Mois' },
|
||||
{ value: 'quarter', label: 'Trimestre' },
|
||||
{ value: 'year', label: 'Annee' },
|
||||
];
|
||||
|
||||
export default function ReportsScreen() {
|
||||
const [period, setPeriod] = useState<Period>('month');
|
||||
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>
|
||||
) : 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}>Intensite moy.</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Ionicons name="heart" size={20} color="#FF69B4" />
|
||||
<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}>Repartition des emotions</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>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="bar-chart-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyText}>Pas de donnees pour cette periode</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 12, padding: 4, marginBottom: 20 },
|
||||
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 10 },
|
||||
periodBtnActive: { backgroundColor: Colors.white, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 },
|
||||
periodText: { fontSize: 13, fontWeight: '600', color: Colors.gray[500] },
|
||||
periodTextActive: { color: Colors.black },
|
||||
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: 16, padding: 16,
|
||||
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[100],
|
||||
},
|
||||
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 },
|
||||
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.gray[700] },
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user