- Setup Expo project with TypeScript - Implement authentication (Login/Signup/Logout) - Create Dashboard, Transactions, Subscriptions, and Analysis screens - Add Firebase services (Auth, Firestore, Storage) - Implement real-time synchronization - Add charts and analytics - Create reusable components (Button, InputText, TransactionCard, SubscriptionCard) - Configure React Navigation with bottom tabs - Add Firestore security rules - Create comprehensive documentation (README, FIREBASE_SETUP, TESTING)
318 lines
7.8 KiB
TypeScript
318 lines
7.8 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
ScrollView,
|
||
RefreshControl,
|
||
TouchableOpacity
|
||
} from 'react-native';
|
||
import { useAuth } from '../hooks/useAuth';
|
||
import { transactionService } from '../services/transactionService';
|
||
import { Transaction } from '../types';
|
||
import { TransactionCard } from '../components/TransactionCard';
|
||
|
||
export const DashboardScreen = ({ navigation }: any) => {
|
||
const { user, logout } = useAuth();
|
||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!user) return;
|
||
|
||
const unsubscribe = transactionService.subscribeToTransactions(
|
||
user.uid,
|
||
(newTransactions) => {
|
||
setTransactions(newTransactions);
|
||
setLoading(false);
|
||
setRefreshing(false);
|
||
}
|
||
);
|
||
|
||
return () => unsubscribe();
|
||
}, [user]);
|
||
|
||
const onRefresh = () => {
|
||
setRefreshing(true);
|
||
};
|
||
|
||
const getCurrentMonthStats = () => {
|
||
const now = new Date();
|
||
const currentMonth = now.getMonth();
|
||
const currentYear = now.getFullYear();
|
||
|
||
const monthlyTransactions = transactions.filter((t) => {
|
||
const transactionDate = new Date(t.date);
|
||
return (
|
||
transactionDate.getMonth() === currentMonth &&
|
||
transactionDate.getFullYear() === currentYear
|
||
);
|
||
});
|
||
|
||
const totalIncome = monthlyTransactions
|
||
.filter((t) => t.type === 'income')
|
||
.reduce((sum, t) => sum + t.amount, 0);
|
||
|
||
const totalExpenses = monthlyTransactions
|
||
.filter((t) => t.type === 'expense')
|
||
.reduce((sum, t) => sum + t.amount, 0);
|
||
|
||
const balance = totalIncome - totalExpenses;
|
||
|
||
return { totalIncome, totalExpenses, balance };
|
||
};
|
||
|
||
const stats = getCurrentMonthStats();
|
||
const recentTransactions = transactions.slice(0, 5);
|
||
|
||
const getMonthName = () => {
|
||
return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format(
|
||
new Date()
|
||
);
|
||
};
|
||
|
||
return (
|
||
<ScrollView
|
||
style={styles.container}
|
||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||
>
|
||
<View style={styles.header}>
|
||
<View>
|
||
<Text style={styles.greeting}>Bonjour 👋</Text>
|
||
<Text style={styles.monthLabel}>{getMonthName()}</Text>
|
||
</View>
|
||
<TouchableOpacity onPress={logout} style={styles.logoutButton}>
|
||
<Text style={styles.logoutText}>Déconnexion</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.balanceCard}>
|
||
<Text style={styles.balanceLabel}>Solde du mois</Text>
|
||
<Text
|
||
style={[
|
||
styles.balanceAmount,
|
||
stats.balance >= 0 ? styles.positiveBalance : styles.negativeBalance
|
||
]}
|
||
>
|
||
{stats.balance >= 0 ? '+' : ''}{stats.balance.toFixed(2)} €
|
||
</Text>
|
||
<View style={styles.statsRow}>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statLabel}>Revenus</Text>
|
||
<Text style={styles.incomeText}>+{stats.totalIncome.toFixed(2)} €</Text>
|
||
</View>
|
||
<View style={styles.statDivider} />
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statLabel}>Dépenses</Text>
|
||
<Text style={styles.expenseText}>-{stats.totalExpenses.toFixed(2)} €</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.section}>
|
||
<View style={styles.sectionHeader}>
|
||
<Text style={styles.sectionTitle}>Transactions récentes</Text>
|
||
{transactions.length > 5 && (
|
||
<TouchableOpacity onPress={() => navigation.navigate('Transactions')}>
|
||
<Text style={styles.seeAllText}>Voir tout</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
|
||
{loading ? (
|
||
<Text style={styles.emptyText}>Chargement...</Text>
|
||
) : recentTransactions.length === 0 ? (
|
||
<View style={styles.emptyState}>
|
||
<Text style={styles.emptyIcon}>📊</Text>
|
||
<Text style={styles.emptyText}>Aucune transaction</Text>
|
||
<Text style={styles.emptySubtext}>
|
||
Ajoutez votre première transaction pour commencer
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
recentTransactions.map((transaction) => (
|
||
<TransactionCard key={transaction.id} transaction={transaction} />
|
||
))
|
||
)}
|
||
</View>
|
||
|
||
<View style={styles.quickActions}>
|
||
<TouchableOpacity
|
||
style={[styles.actionButton, styles.addExpenseButton]}
|
||
onPress={() => navigation.navigate('Transactions', { type: 'expense' })}
|
||
>
|
||
<Text style={styles.actionIcon}>➖</Text>
|
||
<Text style={styles.actionText}>Dépense</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.actionButton, styles.addIncomeButton]}
|
||
onPress={() => navigation.navigate('Transactions', { type: 'income' })}
|
||
>
|
||
<Text style={styles.actionIcon}>➕</Text>
|
||
<Text style={styles.actionText}>Revenu</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</ScrollView>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#F8F9FA'
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: 24,
|
||
paddingTop: 60
|
||
},
|
||
greeting: {
|
||
fontSize: 28,
|
||
fontWeight: 'bold',
|
||
color: '#333'
|
||
},
|
||
monthLabel: {
|
||
fontSize: 14,
|
||
color: '#666',
|
||
marginTop: 4,
|
||
textTransform: 'capitalize'
|
||
},
|
||
logoutButton: {
|
||
padding: 8
|
||
},
|
||
logoutText: {
|
||
color: '#4A90E2',
|
||
fontSize: 14,
|
||
fontWeight: '600'
|
||
},
|
||
balanceCard: {
|
||
backgroundColor: '#4A90E2',
|
||
margin: 24,
|
||
marginTop: 0,
|
||
padding: 24,
|
||
borderRadius: 20,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.2,
|
||
shadowRadius: 8,
|
||
elevation: 5
|
||
},
|
||
balanceLabel: {
|
||
fontSize: 14,
|
||
color: '#FFF',
|
||
opacity: 0.9,
|
||
marginBottom: 8
|
||
},
|
||
balanceAmount: {
|
||
fontSize: 36,
|
||
fontWeight: 'bold',
|
||
marginBottom: 20
|
||
},
|
||
positiveBalance: {
|
||
color: '#FFF'
|
||
},
|
||
negativeBalance: {
|
||
color: '#FFE0E0'
|
||
},
|
||
statsRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-around'
|
||
},
|
||
statItem: {
|
||
flex: 1,
|
||
alignItems: 'center'
|
||
},
|
||
statDivider: {
|
||
width: 1,
|
||
backgroundColor: '#FFF',
|
||
opacity: 0.3
|
||
},
|
||
statLabel: {
|
||
fontSize: 12,
|
||
color: '#FFF',
|
||
opacity: 0.8,
|
||
marginBottom: 4
|
||
},
|
||
incomeText: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: '#FFF'
|
||
},
|
||
expenseText: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: '#FFF'
|
||
},
|
||
section: {
|
||
padding: 24,
|
||
paddingTop: 0
|
||
},
|
||
sectionHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: 16
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 20,
|
||
fontWeight: 'bold',
|
||
color: '#333'
|
||
},
|
||
seeAllText: {
|
||
fontSize: 14,
|
||
color: '#4A90E2',
|
||
fontWeight: '600'
|
||
},
|
||
emptyState: {
|
||
alignItems: 'center',
|
||
paddingVertical: 40
|
||
},
|
||
emptyIcon: {
|
||
fontSize: 48,
|
||
marginBottom: 12
|
||
},
|
||
emptyText: {
|
||
fontSize: 16,
|
||
color: '#666',
|
||
textAlign: 'center'
|
||
},
|
||
emptySubtext: {
|
||
fontSize: 14,
|
||
color: '#999',
|
||
textAlign: 'center',
|
||
marginTop: 8
|
||
},
|
||
quickActions: {
|
||
flexDirection: 'row',
|
||
padding: 24,
|
||
paddingTop: 0,
|
||
gap: 12
|
||
},
|
||
actionButton: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 16,
|
||
borderRadius: 12,
|
||
gap: 8
|
||
},
|
||
addExpenseButton: {
|
||
backgroundColor: '#FF6B6B'
|
||
},
|
||
addIncomeButton: {
|
||
backgroundColor: '#52C41A'
|
||
},
|
||
actionIcon: {
|
||
fontSize: 20
|
||
},
|
||
actionText: {
|
||
color: '#FFF',
|
||
fontSize: 16,
|
||
fontWeight: '600'
|
||
}
|
||
});
|