- Fix infinite loading: properly reset loading state in both success and error cases - Fix form not resetting: clear all fields including selectedCategory after submission - Improve category display: smaller icons (24px), better sizing, no movement on selection - Remove colorful borders: use neutral colors for better readability - Remove scale/shadow effects that caused layout shifts - Use consistent styling: minHeight instead of aspectRatio for stable layout - Apply fixes to both TransactionScreen and SubscriptionScreen
441 lines
12 KiB
TypeScript
441 lines
12 KiB
TypeScript
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, navigation }: 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]);
|
|
|
|
// Ouvrir le modal automatiquement si on vient du Dashboard
|
|
useEffect(() => {
|
|
if (route?.params?.openModal) {
|
|
setModalVisible(true);
|
|
if (route.params.type) {
|
|
setType(route.params.type);
|
|
}
|
|
// Réinitialiser le paramètre
|
|
navigation.setParams({ openModal: false });
|
|
}
|
|
}, [route?.params]);
|
|
|
|
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('');
|
|
setSelectedCategory('');
|
|
setNote('');
|
|
setDate(new Date());
|
|
setLoading(false);
|
|
setModalVisible(false);
|
|
|
|
Alert.alert('Succès', 'Transaction ajoutée avec succès');
|
|
} catch (error: any) {
|
|
setLoading(false);
|
|
Alert.alert('Erreur', error.message);
|
|
}
|
|
};
|
|
|
|
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={(text) => {
|
|
// Permettre uniquement les chiffres et un point décimal
|
|
const cleaned = text.replace(/[^0-9.]/g, '');
|
|
// Empêcher plusieurs points
|
|
const parts = cleaned.split('.');
|
|
if (parts.length > 2) return;
|
|
// Limiter à 2 décimales
|
|
if (parts[1] && parts[1].length > 2) return;
|
|
setAmount(cleaned);
|
|
}}
|
|
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
|
|
]}
|
|
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: 3,
|
|
backgroundColor: '#F0F7FF'
|
|
},
|
|
expenseButton: {
|
|
borderColor: '#FF6B6B'
|
|
},
|
|
incomeButton: {
|
|
borderColor: '#52C41A'
|
|
},
|
|
typeButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#999'
|
|
},
|
|
typeButtonTextActive: {
|
|
color: '#333',
|
|
fontWeight: '700'
|
|
},
|
|
label: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: '#333',
|
|
marginBottom: 12
|
|
},
|
|
categoryGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
marginBottom: 24
|
|
},
|
|
categoryItem: {
|
|
width: '31%',
|
|
paddingVertical: 12,
|
|
borderRadius: 8,
|
|
borderWidth: 1.5,
|
|
borderColor: '#E0E0E0',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#FFF',
|
|
minHeight: 70
|
|
},
|
|
categoryItemActive: {
|
|
backgroundColor: '#F0F7FF',
|
|
borderWidth: 2,
|
|
borderColor: '#4A90E2'
|
|
},
|
|
categoryIcon: {
|
|
fontSize: 24,
|
|
marginBottom: 4
|
|
},
|
|
categoryName: {
|
|
fontSize: 10,
|
|
color: '#666',
|
|
fontWeight: '500',
|
|
textAlign: 'center'
|
|
},
|
|
submitButton: {
|
|
marginTop: 8,
|
|
marginBottom: Platform.OS === 'ios' ? 20 : 0
|
|
}
|
|
});
|