Initial commit: WalletTracker app with Firebase integration
- 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)
This commit is contained in:
317
src/screens/DashboardScreen.tsx
Normal file
317
src/screens/DashboardScreen.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
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'
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user