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:
414
src/screens/TransactionScreen.tsx
Normal file
414
src/screens/TransactionScreen.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
Alert,
|
||||
Platform
|
||||
} from 'react-native';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { transactionService } from '../services/transactionService';
|
||||
import { categoryService } from '../services/categoryService';
|
||||
import { Transaction, Category, TransactionType } from '../types';
|
||||
import { TransactionCard } from '../components/TransactionCard';
|
||||
import { InputText } from '../components/InputText';
|
||||
import { Button } from '../components/Button';
|
||||
|
||||
export const TransactionScreen = ({ route }: any) => {
|
||||
const { user } = useAuth();
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [type, setType] = useState<TransactionType>(route?.params?.type || 'expense');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
// Charger les catégories
|
||||
loadCategories();
|
||||
|
||||
// Écouter les transactions
|
||||
const unsubscribe = transactionService.subscribeToTransactions(
|
||||
user.uid,
|
||||
(newTransactions) => {
|
||||
setTransactions(newTransactions);
|
||||
}
|
||||
);
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [user]);
|
||||
|
||||
const loadCategories = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
let userCategories = await categoryService.getCategories(user.uid);
|
||||
|
||||
// Si l'utilisateur n'a pas de catégories, initialiser les catégories par défaut
|
||||
if (userCategories.length === 0) {
|
||||
await categoryService.initializeDefaultCategories(user.uid);
|
||||
userCategories = await categoryService.getCategories(user.uid);
|
||||
}
|
||||
|
||||
setCategories(userCategories);
|
||||
|
||||
// Sélectionner la première catégorie du type approprié
|
||||
const defaultCategory = userCategories.find((c) => c.type === type);
|
||||
if (defaultCategory) {
|
||||
setSelectedCategory(defaultCategory.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des catégories:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTransaction = async () => {
|
||||
if (!user) return;
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
Alert.alert('Erreur', 'Veuillez entrer un montant valide');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedCategory) {
|
||||
Alert.alert('Erreur', 'Veuillez sélectionner une catégorie');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await transactionService.addTransaction(
|
||||
user.uid,
|
||||
type,
|
||||
parseFloat(amount),
|
||||
selectedCategory,
|
||||
date,
|
||||
note
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
setAmount('');
|
||||
setNote('');
|
||||
setDate(new Date());
|
||||
setModalVisible(false);
|
||||
|
||||
Alert.alert('Succès', 'Transaction ajoutée avec succès');
|
||||
} catch (error: any) {
|
||||
Alert.alert('Erreur', error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCategories = categories.filter((c) => c.type === type);
|
||||
|
||||
const getCategoryInfo = (categoryName: string) => {
|
||||
const category = categories.find((c) => c.name === categoryName);
|
||||
return category || { icon: '📦', color: '#95A5A6' };
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Transactions</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Text style={styles.addButtonText}>+ Ajouter</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content}>
|
||||
{transactions.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>
|
||||
) : (
|
||||
transactions.map((transaction) => {
|
||||
const categoryInfo = getCategoryInfo(transaction.category);
|
||||
return (
|
||||
<TransactionCard
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
categoryIcon={categoryInfo.icon}
|
||||
categoryColor={categoryInfo.color}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Nouvelle transaction</Text>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<Text style={styles.closeButton}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView>
|
||||
<View style={styles.typeSelector}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.typeButton,
|
||||
type === 'expense' && styles.typeButtonActive,
|
||||
styles.expenseButton
|
||||
]}
|
||||
onPress={() => {
|
||||
setType('expense');
|
||||
const defaultCategory = filteredCategories.find((c) => c.type === 'expense');
|
||||
if (defaultCategory) setSelectedCategory(defaultCategory.name);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.typeButtonText,
|
||||
type === 'expense' && styles.typeButtonTextActive
|
||||
]}
|
||||
>
|
||||
Dépense
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.typeButton,
|
||||
type === 'income' && styles.typeButtonActive,
|
||||
styles.incomeButton
|
||||
]}
|
||||
onPress={() => {
|
||||
setType('income');
|
||||
const defaultCategory = filteredCategories.find((c) => c.type === 'income');
|
||||
if (defaultCategory) setSelectedCategory(defaultCategory.name);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.typeButtonText,
|
||||
type === 'income' && styles.typeButtonTextActive
|
||||
]}
|
||||
>
|
||||
Revenu
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<InputText
|
||||
label="Montant (€)"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChangeText={setAmount}
|
||||
keyboardType="decimal-pad"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Catégorie</Text>
|
||||
<View style={styles.categoryGrid}>
|
||||
{filteredCategories.map((category) => (
|
||||
<TouchableOpacity
|
||||
key={category.id}
|
||||
style={[
|
||||
styles.categoryItem,
|
||||
selectedCategory === category.name && styles.categoryItemActive,
|
||||
{ borderColor: category.color }
|
||||
]}
|
||||
onPress={() => setSelectedCategory(category.name)}
|
||||
>
|
||||
<Text style={styles.categoryIcon}>{category.icon}</Text>
|
||||
<Text style={styles.categoryName}>{category.name}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<InputText
|
||||
label="Note (optionnel)"
|
||||
placeholder="Ajouter une note..."
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Ajouter la transaction"
|
||||
onPress={handleAddTransaction}
|
||||
loading={loading}
|
||||
style={styles.submitButton}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F8F9FA'
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
paddingTop: 60,
|
||||
backgroundColor: '#FFF'
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#333'
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: '#4A90E2',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8
|
||||
},
|
||||
addButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600'
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 24
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 64,
|
||||
marginBottom: 16
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginBottom: 8
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
textAlign: 'center'
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: '#FFF',
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 24,
|
||||
maxHeight: '90%'
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333'
|
||||
},
|
||||
closeButton: {
|
||||
fontSize: 24,
|
||||
color: '#999'
|
||||
},
|
||||
typeSelector: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 24
|
||||
},
|
||||
typeButton: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E0E0E0',
|
||||
alignItems: 'center'
|
||||
},
|
||||
typeButtonActive: {
|
||||
borderWidth: 2
|
||||
},
|
||||
expenseButton: {
|
||||
borderColor: '#FF6B6B'
|
||||
},
|
||||
incomeButton: {
|
||||
borderColor: '#52C41A'
|
||||
},
|
||||
typeButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#666'
|
||||
},
|
||||
typeButtonTextActive: {
|
||||
color: '#333'
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 12
|
||||
},
|
||||
categoryGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
marginBottom: 24
|
||||
},
|
||||
categoryItem: {
|
||||
width: '30%',
|
||||
aspectRatio: 1,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E0E0E0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F8F9FA'
|
||||
},
|
||||
categoryItemActive: {
|
||||
backgroundColor: '#FFF',
|
||||
borderWidth: 2
|
||||
},
|
||||
categoryIcon: {
|
||||
fontSize: 28,
|
||||
marginBottom: 4
|
||||
},
|
||||
categoryName: {
|
||||
fontSize: 11,
|
||||
color: '#666',
|
||||
textAlign: 'center'
|
||||
},
|
||||
submitButton: {
|
||||
marginTop: 8,
|
||||
marginBottom: Platform.OS === 'ios' ? 20 : 0
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user