Files
WalletTracker/src/screens/TransactionScreen.tsx
Arthur Lempereur 0db3832282 Fix: Resolve UI/UX issues and improve user experience
- Configure Firebase Auth with AsyncStorage persistence
- Fix 'Text strings must be rendered within <Text>' error in navigation
- Improve bottom tab bar: iOS style with blur effect, better height, rounded corners
- Fix Dashboard quick action buttons to open transaction modal directly
- Add auto-open modal when navigating from Dashboard
- Improve selection visibility in modals (type selector and categories)
- Add amount validation: only positive numbers, max 2 decimals
- Add padding to Dashboard content to avoid tab bar overlap
- Apply same fixes to both Transaction and Subscription screens
2025-10-23 15:21:48 +02:00

445 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('');
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={(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,
{ 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: 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: 12,
marginBottom: 24
},
categoryItem: {
width: '30%',
aspectRatio: 1,
borderRadius: 12,
borderWidth: 2,
borderColor: '#E0E0E0',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F8F9FA'
},
categoryItemActive: {
backgroundColor: '#FFF',
borderWidth: 3,
transform: [{ scale: 1.05 }],
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4
},
categoryIcon: {
fontSize: 28,
marginBottom: 4
},
categoryName: {
fontSize: 11,
color: '#666',
fontWeight: '600',
textAlign: 'center'
},
submitButton: {
marginTop: 8,
marginBottom: Platform.OS === 'ios' ? 20 : 0
}
});