diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..f7c5b7b --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,149 @@ +# 🚀 DĂ©marrage rapide - WalletTracker + +Guide pour lancer l'application en 5 minutes. + +## Étape 1 : VĂ©rifier les prĂ©requis + +```bash +# VĂ©rifier Node.js (v16+) +node --version + +# VĂ©rifier npm +npm --version +``` + +## Étape 2 : Installer les dĂ©pendances + +```bash +cd WalletTracker +npm install +``` + +## Étape 3 : Configurer Firebase + +### Option A : Configuration rapide (pour tester) + +1. Allez sur https://console.firebase.google.com/ +2. CrĂ©ez un nouveau projet "WalletTracker" +3. Ajoutez une application Web +4. Copiez les identifiants dans `src/config/firebase.ts` + +### Option B : Configuration complĂšte + +Suivez le guide dĂ©taillĂ© dans `FIREBASE_SETUP.md` + +## Étape 4 : Lancer l'application + +```bash +npm start +``` + +Vous verrez un QR code s'afficher dans le terminal. + +## Étape 5 : Tester sur votre tĂ©lĂ©phone + +### Sur iOS ou Android : + +1. TĂ©lĂ©chargez **Expo Go** depuis l'App Store ou Google Play +2. Ouvrez Expo Go +3. Scannez le QR code affichĂ© dans le terminal +4. L'application se chargera automatiquement + +### Sur Ă©mulateur : + +**iOS (Mac uniquement)** : +```bash +npm run ios +``` + +**Android** : +```bash +npm run android +``` + +## 🎉 C'est prĂȘt ! + +Vous devriez voir l'Ă©cran de connexion de WalletTracker. + +### PremiĂšre utilisation : + +1. Cliquez sur **"CrĂ©er un compte"** +2. Remplissez le formulaire : + - Nom : Votre nom + - Email : votre@email.com + - Mot de passe : minimum 6 caractĂšres +3. Cliquez sur **"CrĂ©er mon compte"** +4. Vous ĂȘtes redirigĂ© vers le Dashboard ! + +### Ajouter votre premiĂšre transaction : + +1. Cliquez sur le bouton **"DĂ©pense"** ou **"Revenu"** +2. Entrez le montant +3. SĂ©lectionnez une catĂ©gorie +4. Ajoutez une note (optionnel) +5. Cliquez sur **"Ajouter la transaction"** + +## đŸ“± Commandes utiles + +```bash +# Lancer l'application +npm start + +# Lancer sur iOS +npm run ios + +# Lancer sur Android +npm run android + +# Lancer sur le web +npm run web + +# Nettoyer le cache +npm start -- --clear +``` + +## 🐛 ProblĂšmes courants + +### L'application ne se lance pas + +```bash +# Nettoyer et rĂ©installer +rm -rf node_modules +npm install +npm start -- --clear +``` + +### Erreur Firebase + +VĂ©rifiez que vous avez bien : +- CopiĂ© les identifiants Firebase dans `src/config/firebase.ts` +- ActivĂ© Authentication (Email/Password) dans Firebase Console +- Créé la base de donnĂ©es Firestore + +### QR code ne fonctionne pas + +- Assurez-vous que votre tĂ©lĂ©phone et ordinateur sont sur le mĂȘme rĂ©seau Wi-Fi +- Essayez de scanner avec l'appareil photo puis ouvrir avec Expo Go +- Utilisez le mode tunnel : `npm start -- --tunnel` + +## 📚 Prochaines Ă©tapes + +- Lisez le `README.md` pour comprendre l'architecture +- Consultez `FIREBASE_SETUP.md` pour la configuration complĂšte +- Utilisez `TESTING.md` pour tester toutes les fonctionnalitĂ©s + +## 💡 Conseils + +- **DĂ©veloppement** : Utilisez `npm start` et Expo Go pour un rechargement rapide +- **Production** : Utilisez EAS Build pour crĂ©er des binaires iOS/Android +- **DĂ©bogage** : Secouez votre tĂ©lĂ©phone pour ouvrir le menu de dĂ©veloppement + +## 🆘 Besoin d'aide ? + +- Documentation Expo : https://docs.expo.dev/ +- Documentation Firebase : https://firebase.google.com/docs +- React Native : https://reactnative.dev/docs/getting-started + +--- + +**Bon dĂ©veloppement ! đŸ’Ș** diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..3383af9 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,161 @@ +/** + * Constantes utilisĂ©es dans l'application + */ + +// Couleurs principales +export const COLORS = { + primary: '#4A90E2', + secondary: '#6C757D', + success: '#52C41A', + danger: '#FF6B6B', + warning: '#FFA07A', + info: '#13C2C2', + light: '#F8F9FA', + dark: '#333', + white: '#FFF', + gray: '#999', + border: '#E0E0E0' +}; + +// Couleurs des catĂ©gories par dĂ©faut +export const CATEGORY_COLORS = { + // DĂ©penses + courses: '#FF6B6B', + logement: '#4ECDC4', + transport: '#45B7D1', + loisirs: '#FFA07A', + restaurant: '#98D8C8', + sante: '#F7DC6F', + vetements: '#BB8FCE', + education: '#85C1E2', + abonnements: '#F8B739', + autre: '#95A5A6', + + // Revenus + salaire: '#52C41A', + freelance: '#13C2C2', + investissement: '#1890FF', + cadeau: '#EB2F96', + autreRevenu: '#52C41A' +}; + +// Tailles de police +export const FONT_SIZES = { + xs: 11, + sm: 12, + md: 14, + lg: 16, + xl: 18, + xxl: 24, + xxxl: 32 +}; + +// Espacements +export const SPACING = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 24, + xxl: 32 +}; + +// Rayons de bordure +export const BORDER_RADIUS = { + sm: 8, + md: 12, + lg: 16, + xl: 20, + round: 999 +}; + +// Formats de date +export const DATE_FORMATS = { + short: 'DD/MM/YYYY', + medium: 'DD MMM YYYY', + long: 'DD MMMM YYYY', + full: 'dddd DD MMMM YYYY' +}; + +// Messages d'erreur +export const ERROR_MESSAGES = { + network: 'Erreur de connexion. VĂ©rifiez votre connexion Internet.', + auth: { + invalidEmail: 'Adresse email invalide', + weakPassword: 'Le mot de passe doit contenir au moins 6 caractĂšres', + emailInUse: 'Cette adresse email est dĂ©jĂ  utilisĂ©e', + userNotFound: 'Aucun compte trouvĂ© avec cette adresse email', + wrongPassword: 'Mot de passe incorrect', + tooManyRequests: 'Trop de tentatives. Veuillez rĂ©essayer plus tard.' + }, + transaction: { + invalidAmount: 'Montant invalide', + missingCategory: 'Veuillez sĂ©lectionner une catĂ©gorie', + addFailed: 'Impossible d\'ajouter la transaction', + updateFailed: 'Impossible de mettre Ă  jour la transaction', + deleteFailed: 'Impossible de supprimer la transaction' + }, + subscription: { + invalidName: 'Nom invalide', + invalidAmount: 'Montant invalide', + invalidDate: 'Date invalide', + addFailed: 'Impossible d\'ajouter l\'abonnement', + updateFailed: 'Impossible de mettre Ă  jour l\'abonnement', + deleteFailed: 'Impossible de supprimer l\'abonnement' + } +}; + +// Messages de succĂšs +export const SUCCESS_MESSAGES = { + auth: { + signupSuccess: 'Compte créé avec succĂšs', + loginSuccess: 'Connexion rĂ©ussie', + logoutSuccess: 'DĂ©connexion rĂ©ussie' + }, + transaction: { + addSuccess: 'Transaction ajoutĂ©e avec succĂšs', + updateSuccess: 'Transaction mise Ă  jour', + deleteSuccess: 'Transaction supprimĂ©e' + }, + subscription: { + addSuccess: 'Abonnement ajoutĂ© avec succĂšs', + updateSuccess: 'Abonnement mis Ă  jour', + deleteSuccess: 'Abonnement supprimĂ©' + } +}; + +// Limites +export const LIMITS = { + maxTransactionsPerPage: 50, + maxCategoriesPerUser: 50, + maxSubscriptionsPerUser: 100, + maxNoteLength: 500, + minPasswordLength: 6, + maxImageSize: 5 * 1024 * 1024 // 5 MB +}; + +// FrĂ©quences d'abonnement +export const SUBSCRIPTION_FREQUENCIES = [ + { value: 'daily', label: 'Quotidien' }, + { value: 'weekly', label: 'Hebdomadaire' }, + { value: 'monthly', label: 'Mensuel' }, + { value: 'yearly', label: 'Annuel' } +]; + +// Jours de rappel par dĂ©faut +export const DEFAULT_REMINDER_DAYS = 3; + +// ClĂ©s de stockage AsyncStorage +export const STORAGE_KEYS = { + user: '@wallettracker_user', + theme: '@wallettracker_theme', + language: '@wallettracker_language' +}; + +// URLs utiles +export const URLS = { + privacyPolicy: 'https://example.com/privacy', + termsOfService: 'https://example.com/terms', + support: 'https://example.com/support', + github: 'https://github.com/yourusername/wallettracker' +}; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..1604c92 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,251 @@ +/** + * Fonctions utilitaires pour l'application + */ + +/** + * Formate un montant en euros + */ +export const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: 'EUR' + }).format(amount); +}; + +/** + * Formate une date + */ +export const formatDate = ( + date: Date, + format: 'short' | 'medium' | 'long' = 'medium' +): string => { + const options: Intl.DateTimeFormatOptions = { + short: { day: '2-digit', month: '2-digit', year: 'numeric' }, + medium: { day: '2-digit', month: 'short', year: 'numeric' }, + long: { day: '2-digit', month: 'long', year: 'numeric' } + }[format]; + + return new Intl.DateTimeFormat('fr-FR', options).format(date); +}; + +/** + * Formate une date relative (il y a X jours) + */ +export const formatRelativeDate = (date: Date): string => { + const now = new Date(); + const diffInMs = now.getTime() - date.getTime(); + const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); + + if (diffInDays === 0) return "Aujourd'hui"; + if (diffInDays === 1) return 'Hier'; + if (diffInDays < 7) return `Il y a ${diffInDays} jours`; + if (diffInDays < 30) { + const weeks = Math.floor(diffInDays / 7); + return `Il y a ${weeks} semaine${weeks > 1 ? 's' : ''}`; + } + if (diffInDays < 365) { + const months = Math.floor(diffInDays / 30); + return `Il y a ${months} mois`; + } + const years = Math.floor(diffInDays / 365); + return `Il y a ${years} an${years > 1 ? 's' : ''}`; +}; + +/** + * Obtient le nom du mois en français + */ +export const getMonthName = (date: Date, format: 'long' | 'short' = 'long'): string => { + return new Intl.DateTimeFormat('fr-FR', { + month: format, + year: 'numeric' + }).format(date); +}; + +/** + * Calcule le nombre de jours entre deux dates + */ +export const daysBetween = (date1: Date, date2: Date): number => { + const diffInMs = Math.abs(date2.getTime() - date1.getTime()); + return Math.ceil(diffInMs / (1000 * 60 * 60 * 24)); +}; + +/** + * VĂ©rifie si une date est dans le mois en cours + */ +export const isCurrentMonth = (date: Date): boolean => { + const now = new Date(); + return ( + date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() + ); +}; + +/** + * Obtient le premier jour du mois + */ +export const getFirstDayOfMonth = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth(), 1); +}; + +/** + * Obtient le dernier jour du mois + */ +export const getLastDayOfMonth = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59); +}; + +/** + * Valide une adresse email + */ +export const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +/** + * Valide un montant + */ +export const isValidAmount = (amount: string | number): boolean => { + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; + return !isNaN(numAmount) && numAmount > 0; +}; + +/** + * Tronque un texte + */ +export const truncate = (text: string, maxLength: number): string => { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; +}; + +/** + * Capitalise la premiĂšre lettre + */ +export const capitalize = (text: string): string => { + if (!text) return ''; + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); +}; + +/** + * GĂ©nĂšre une couleur alĂ©atoire + */ +export const generateRandomColor = (): string => { + const colors = [ + '#FF6B6B', + '#4ECDC4', + '#45B7D1', + '#FFA07A', + '#98D8C8', + '#F7DC6F', + '#BB8FCE', + '#85C1E2', + '#F8B739', + '#52C41A', + '#13C2C2', + '#1890FF', + '#EB2F96' + ]; + return colors[Math.floor(Math.random() * colors.length)]; +}; + +/** + * Calcule le pourcentage + */ +export const calculatePercentage = (value: number, total: number): number => { + if (total === 0) return 0; + return (value / total) * 100; +}; + +/** + * Arrondit un nombre Ă  N dĂ©cimales + */ +export const roundTo = (num: number, decimals: number = 2): number => { + return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals); +}; + +/** + * Groupe les transactions par date + */ +export const groupByDate = (items: T[]): Map => { + const grouped = new Map(); + + items.forEach((item) => { + const dateKey = formatDate(item.date, 'short'); + const existing = grouped.get(dateKey) || []; + grouped.set(dateKey, [...existing, item]); + }); + + return grouped; +}; + +/** + * Groupe les transactions par mois + */ +export const groupByMonth = (items: T[]): Map => { + const grouped = new Map(); + + items.forEach((item) => { + const monthKey = getMonthName(item.date); + const existing = grouped.get(monthKey) || []; + grouped.set(monthKey, [...existing, item]); + }); + + return grouped; +}; + +/** + * Trie les transactions par date (plus rĂ©centes en premier) + */ +export const sortByDateDesc = (items: T[]): T[] => { + return [...items].sort((a, b) => b.date.getTime() - a.date.getTime()); +}; + +/** + * Filtre les transactions par pĂ©riode + */ +export const filterByDateRange = ( + items: T[], + startDate: Date, + endDate: Date +): T[] => { + return items.filter((item) => item.date >= startDate && item.date <= endDate); +}; + +/** + * Calcule la somme des montants + */ +export const sumAmounts = (items: T[]): number => { + return items.reduce((sum, item) => sum + item.amount, 0); +}; + +/** + * Attend X millisecondes (pour les animations) + */ +export const wait = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +/** + * Debounce une fonction + */ +export const debounce = any>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout; + + return (...args: Parameters) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func(...args), delay); + }; +}; + +/** + * VĂ©rifie si l'objet est vide + */ +export const isEmpty = (obj: any): boolean => { + if (obj === null || obj === undefined) return true; + if (typeof obj === 'string') return obj.trim().length === 0; + if (Array.isArray(obj)) return obj.length === 0; + if (typeof obj === 'object') return Object.keys(obj).length === 0; + return false; +}; diff --git a/src/utils/sampleData.ts b/src/utils/sampleData.ts new file mode 100644 index 0000000..670e920 --- /dev/null +++ b/src/utils/sampleData.ts @@ -0,0 +1,237 @@ +/** + * DonnĂ©es d'exemple pour faciliter les tests et le dĂ©veloppement + * Ces fonctions peuvent ĂȘtre utilisĂ©es pour peupler la base de donnĂ©es avec des donnĂ©es de test + */ + +import { transactionService } from '../services/transactionService'; +import { subscriptionService } from '../services/subscriptionService'; +import { TransactionType, SubscriptionFrequency } from '../types'; + +/** + * GĂ©nĂšre des transactions d'exemple pour un utilisateur + */ +export const generateSampleTransactions = async (userId: string) => { + const transactions = [ + // Revenus + { + type: 'income' as TransactionType, + amount: 2500, + category: 'Salaire', + date: new Date(2025, 9, 1), + note: 'Salaire mensuel' + }, + { + type: 'income' as TransactionType, + amount: 500, + category: 'Freelance', + date: new Date(2025, 9, 15), + note: 'Projet web client XYZ' + }, + + // DĂ©penses - Courses + { + type: 'expense' as TransactionType, + amount: 85.50, + category: 'Courses', + date: new Date(2025, 9, 5), + note: 'SupermarchĂ© Carrefour' + }, + { + type: 'expense' as TransactionType, + amount: 42.30, + category: 'Courses', + date: new Date(2025, 9, 12), + note: 'MarchĂ© local' + }, + { + type: 'expense' as TransactionType, + amount: 67.80, + category: 'Courses', + date: new Date(2025, 9, 19), + note: 'SupermarchĂ© Leclerc' + }, + + // DĂ©penses - Logement + { + type: 'expense' as TransactionType, + amount: 850, + category: 'Logement', + date: new Date(2025, 9, 1), + note: 'Loyer mensuel' + }, + { + type: 'expense' as TransactionType, + amount: 120, + category: 'Logement', + date: new Date(2025, 9, 10), + note: 'ÉlectricitĂ© et gaz' + }, + + // DĂ©penses - Transport + { + type: 'expense' as TransactionType, + amount: 60, + category: 'Transport', + date: new Date(2025, 9, 3), + note: 'Essence' + }, + { + type: 'expense' as TransactionType, + amount: 75, + category: 'Transport', + date: new Date(2025, 9, 8), + note: 'Pass Navigo' + }, + + // DĂ©penses - Restaurant + { + type: 'expense' as TransactionType, + amount: 45, + category: 'Restaurant', + date: new Date(2025, 9, 6), + note: 'DĂźner au restaurant italien' + }, + { + type: 'expense' as TransactionType, + amount: 28, + category: 'Restaurant', + date: new Date(2025, 9, 13), + note: 'DĂ©jeuner avec collĂšgues' + }, + { + type: 'expense' as TransactionType, + amount: 15, + category: 'Restaurant', + date: new Date(2025, 9, 20), + note: 'Fast food' + }, + + // DĂ©penses - Loisirs + { + type: 'expense' as TransactionType, + amount: 60, + category: 'Loisirs', + date: new Date(2025, 9, 7), + note: 'CinĂ©ma et pop-corn' + }, + { + type: 'expense' as TransactionType, + amount: 35, + category: 'Loisirs', + date: new Date(2025, 9, 14), + note: 'Jeu vidĂ©o Steam' + }, + + // DĂ©penses - SantĂ© + { + type: 'expense' as TransactionType, + amount: 25, + category: 'SantĂ©', + date: new Date(2025, 9, 9), + note: 'Pharmacie - mĂ©dicaments' + }, + + // DĂ©penses - VĂȘtements + { + type: 'expense' as TransactionType, + amount: 89, + category: 'VĂȘtements', + date: new Date(2025, 9, 16), + note: 'Nouvelle paire de chaussures' + } + ]; + + try { + for (const transaction of transactions) { + await transactionService.addTransaction( + userId, + transaction.type, + transaction.amount, + transaction.category, + transaction.date, + transaction.note + ); + } + console.log('✅ Transactions d\'exemple créées avec succĂšs'); + } catch (error) { + console.error('❌ Erreur lors de la crĂ©ation des transactions d\'exemple:', error); + } +}; + +/** + * GĂ©nĂšre des abonnements d'exemple pour un utilisateur + */ +export const generateSampleSubscriptions = async (userId: string) => { + const subscriptions = [ + { + name: 'Netflix', + amount: 15.99, + category: 'Abonnements', + frequency: 'monthly' as SubscriptionFrequency, + dayOfMonth: 15 + }, + { + name: 'Spotify', + amount: 9.99, + category: 'Abonnements', + frequency: 'monthly' as SubscriptionFrequency, + dayOfMonth: 1 + }, + { + name: 'Amazon Prime', + amount: 6.99, + category: 'Abonnements', + frequency: 'monthly' as SubscriptionFrequency, + dayOfMonth: 10 + }, + { + name: 'Salle de sport', + amount: 35, + category: 'SantĂ©', + frequency: 'monthly' as SubscriptionFrequency, + dayOfMonth: 5 + }, + { + name: 'Assurance tĂ©lĂ©phone', + amount: 120, + category: 'Autre', + frequency: 'yearly' as SubscriptionFrequency, + dayOfMonth: 1 + } + ]; + + try { + for (const subscription of subscriptions) { + const now = new Date(); + const nextPaymentDate = new Date(now.getFullYear(), now.getMonth(), subscription.dayOfMonth); + + // Si la date est dĂ©jĂ  passĂ©e ce mois-ci, passer au mois suivant + if (nextPaymentDate < now) { + nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1); + } + + await subscriptionService.addSubscription( + userId, + subscription.name, + subscription.amount, + subscription.category, + subscription.frequency, + nextPaymentDate, + 3 + ); + } + console.log('✅ Abonnements d\'exemple créés avec succĂšs'); + } catch (error) { + console.error('❌ Erreur lors de la crĂ©ation des abonnements d\'exemple:', error); + } +}; + +/** + * GĂ©nĂšre toutes les donnĂ©es d'exemple + */ +export const generateAllSampleData = async (userId: string) => { + console.log('🔄 GĂ©nĂ©ration des donnĂ©es d\'exemple...'); + await generateSampleTransactions(userId); + await generateSampleSubscriptions(userId); + console.log('✅ Toutes les donnĂ©es d\'exemple ont Ă©tĂ© gĂ©nĂ©rĂ©es !'); +};