From f6156e51146738082195972bf97eab8639911dbb Mon Sep 17 00:00:00 2001 From: Farid-Efrei <128361230+Farid-Efrei@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:26:33 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Bug:=20Corrections=20des=20bugs=20des=20ass?= =?UTF-8?q?ignations=20et=20de=20l'UPDATE=20des=20t=C3=A2ches.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/assign_users_dialog.dart | 87 ++++++++++++++++++- .../presentation/widgets/task_modal.dart | 11 ++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart index c6054de..9e16017 100644 --- a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart +++ b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart @@ -26,10 +26,17 @@ class _AssignUsersDialogState extends State late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; + + // Sauvegarder l'état initial pour permettre l'annulation + late List _initialAssignedUsers; @override void initState() { super.initState(); + + // Sauvegarder l'état initial des assignations + _initialAssignedUsers = List.from(widget.task.assignedTo); + _loadUsers(); // Animations d'entrée @@ -96,12 +103,79 @@ class _AssignUsersDialogState extends State }); } + /// Annule toutes les modifications et restaure l'état initial + Future _cancelChanges() async { + try { + final provider = context.read(); + final currentTask = provider.allTasks.firstWhere((t) => t.id == widget.task.id); + final currentAssignedUsers = currentTask.assignedTo; + + // Trouver les utilisateurs à retirer (qui ont été ajoutés) + final usersToRemove = currentAssignedUsers + .where((userId) => !_initialAssignedUsers.contains(userId)) + .toList(); + + // Trouver les utilisateurs à rajouter (qui ont été retirés) + final usersToAdd = _initialAssignedUsers + .where((userId) => !currentAssignedUsers.contains(userId)) + .toList(); + + // Effectuer les modifications pour restaurer l'état initial + for (final userId in usersToRemove) { + await provider.unassignUserFromTask(widget.task.id, userId); + } + + for (final userId in usersToAdd) { + await provider.assignUserToTask(widget.task.id, userId); + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.undo, color: Colors.white), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Modifications annulées', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: AppColors.getOnSurface(context).withOpacity(0.8), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + + // Fermer le dialog après avoir annulé + Navigator.of(context).pop(); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de l\'annulation: $e'), + backgroundColor: AppColors.error, + ), + ); + } + } + } + Future _toggleUserAssignment(String userId, bool isAssigned) async { try { final provider = context.read(); if (isAssigned) { + // L'utilisateur EST déjà assigné → on le RETIRE await provider.unassignUserFromTask(widget.task.id, userId); } else { + // L'utilisateur N'EST PAS assigné → on l'AJOUTE await provider.assignUserToTask(widget.task.id, userId); } @@ -562,6 +636,17 @@ class _AssignUsersDialogState extends State ), ), ), + // Bouton Annuler - Restaure l'état initial + TextButton( + onPressed: _cancelChanges, + style: TextButton.styleFrom( + foregroundColor: AppColors.getOnSurface(context).withOpacity(0.7), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + // Bouton Modifier - Ferme le dialog (changements déjà appliqués) ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( @@ -572,7 +657,7 @@ class _AssignUsersDialogState extends State borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Terminé'), + child: const Text('Modifier'), ), ], ), diff --git a/lib/features/tasks/presentation/widgets/task_modal.dart b/lib/features/tasks/presentation/widgets/task_modal.dart index bf925e9..4d77180 100644 --- a/lib/features/tasks/presentation/widgets/task_modal.dart +++ b/lib/features/tasks/presentation/widgets/task_modal.dart @@ -99,8 +99,14 @@ class _TaskModalState extends State { } if (_isEditing) { - // Modifier la tâche existante - final updatedTask = widget.task!.copyWith( + // Récupérer la tâche actuelle depuis le provider pour avoir les assignations à jour + final currentTask = taskProvider.allTasks.firstWhere( + (t) => t.id == widget.task!.id, + orElse: () => widget.task!, + ); + + // Modifier la tâche existante en gardant les assignations actuelles + final updatedTask = currentTask.copyWith( title: _titleController.text.trim(), description: _descriptionController.text.trim(), priority: _selectedPriority, @@ -108,6 +114,7 @@ class _TaskModalState extends State { tags: _selectedTags, ); print('📝 TaskModal - Modification tâche: ${updatedTask.id}'); + print(' - assignedTo conservé: ${updatedTask.assignedTo}'); taskProvider.updateTask(updatedTask); } else { // Créer une nouvelle tâche avec ownerId et ownerName From d4849896b335e30bf7d8ad93ac67e280f4a1b141 Mon Sep 17 00:00:00 2001 From: Farid-Efrei <128361230+Farid-Efrei@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:29:28 +0100 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20Corriger=20l'annulation=20des=20assi?= =?UTF-8?q?gnations=20lors=20de=20la=20modification=20de=20t=C3=A2che?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Récupérer la tâche actuelle depuis le provider pour préserver les assignations - Ajouter méthode _cancelChanges dans assign_users_dialog pour annuler les modifications - Les assignations faites en temps réel sont maintenant conservées lors de la sauvegarde --- .../presentation/widgets/assign_users_dialog.dart | 12 +++++++----- .../tasks/presentation/widgets/task_modal.dart | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart index 9e16017..5c538d1 100644 --- a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart +++ b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart @@ -26,17 +26,17 @@ class _AssignUsersDialogState extends State late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; - + // Sauvegarder l'état initial pour permettre l'annulation late List _initialAssignedUsers; @override void initState() { super.initState(); - + // Sauvegarder l'état initial des assignations _initialAssignedUsers = List.from(widget.task.assignedTo); - + _loadUsers(); // Animations d'entrée @@ -107,7 +107,9 @@ class _AssignUsersDialogState extends State Future _cancelChanges() async { try { final provider = context.read(); - final currentTask = provider.allTasks.firstWhere((t) => t.id == widget.task.id); + final currentTask = provider.allTasks.firstWhere( + (t) => t.id == widget.task.id, + ); final currentAssignedUsers = currentTask.assignedTo; // Trouver les utilisateurs à retirer (qui ont été ajoutés) @@ -152,7 +154,7 @@ class _AssignUsersDialogState extends State ), ), ); - + // Fermer le dialog après avoir annulé Navigator.of(context).pop(); } diff --git a/lib/features/tasks/presentation/widgets/task_modal.dart b/lib/features/tasks/presentation/widgets/task_modal.dart index 4d77180..683f336 100644 --- a/lib/features/tasks/presentation/widgets/task_modal.dart +++ b/lib/features/tasks/presentation/widgets/task_modal.dart @@ -104,7 +104,7 @@ class _TaskModalState extends State { (t) => t.id == widget.task!.id, orElse: () => widget.task!, ); - + // Modifier la tâche existante en gardant les assignations actuelles final updatedTask = currentTask.copyWith( title: _titleController.text.trim(), From d7861c4f968b2c27e2fb92185f634895e86eb6b1 Mon Sep 17 00:00:00 2001 From: Loris Date: Thu, 6 Nov 2025 11:57:41 +0100 Subject: [PATCH 3/4] Clean du code - suppression des commentaires useless --- CREATE_FIRESTORE_INDEXES.md | 155 ------------ DEBUG_ASSIGNATION.md | 162 ------------ FIREBASE_SETUP.md | 141 ----------- FIRESTORE_DEBUG.md | 156 ------------ GUIDE_ASSIGNATION_UTILISATEURS.md | 230 ------------------ QUICKSTART.md | 145 ----------- SOLUTION_ASSIGNATION.md | 226 ----------------- SOLUTION_FINALE_ASSIGNATION.md | 214 ---------------- VERIFIER_ASSIGNATION.md | 177 -------------- integration_test/app_flow_test.dart | 27 -- lib/app.dart | 21 +- .../tasks/data/firestore_task_service.dart | 27 -- lib/features/tasks/data/task_service.dart | 86 +------ .../presentation/providers/task_provider.dart | 38 --- lib/main.dart | 22 +- 15 files changed, 5 insertions(+), 1822 deletions(-) delete mode 100644 CREATE_FIRESTORE_INDEXES.md delete mode 100644 DEBUG_ASSIGNATION.md delete mode 100644 FIREBASE_SETUP.md delete mode 100644 FIRESTORE_DEBUG.md delete mode 100644 GUIDE_ASSIGNATION_UTILISATEURS.md delete mode 100644 QUICKSTART.md delete mode 100644 SOLUTION_ASSIGNATION.md delete mode 100644 SOLUTION_FINALE_ASSIGNATION.md delete mode 100644 VERIFIER_ASSIGNATION.md diff --git a/CREATE_FIRESTORE_INDEXES.md b/CREATE_FIRESTORE_INDEXES.md deleted file mode 100644 index 2c61bfd..0000000 --- a/CREATE_FIRESTORE_INDEXES.md +++ /dev/null @@ -1,155 +0,0 @@ -# 🔧 Création des Index Firestore - GUIDE RAPIDE - -## ⚠️ Erreur Actuelle - -Vous voyez cette erreur dans la console : - -``` -[cloud_firestore/failed-precondition] The query requires an index. -``` - -C'est **NORMAL** ! Notre nouveau système de privacy nécessite des index composites. - -## ✅ Solution Rapide (2 minutes) - -### Étape 1 : Cliquer sur les Liens - -Dans votre terminal, vous voyez deux liens qui commencent par : - -``` -https://console.firebase.google.com/v1/r/project/flutter-todo-web-305fb/... -``` - -**Action** : - -1. Copiez le **premier lien** (celui avec `userId`) -2. Collez-le dans votre navigateur -3. Cliquez sur **"Créer l'index"** -4. Attendez ~2-5 minutes (Firebase crée l'index en arrière-plan) - -5. Répétez avec le **second lien** (celui avec `assignedTo`) -6. Cliquez sur **"Créer l'index"** -7. Attendez ~2-5 minutes - -### Étape 2 : Vérifier la Création - -1. Allez dans **Firebase Console** → **Firestore Database** → **Index** -2. Vous devriez voir 2 nouveaux index : - - `tasks` : `userId (Ascending) + createdAt (Descending)` - - `tasks` : `assignedTo (Array) + createdAt (Descending)` -3. Statut doit passer de **"Building"** à **"Enabled"** - -### Étape 3 : Relancer l'App - -Une fois les index créés (statut **Enabled**) : - -```bash -# Appuyez sur 'R' dans le terminal Flutter pour Hot Restart -# OU relancez complètement -flutter run -d edge -``` - -## 🎯 Les Deux Index Nécessaires - -### Index 1 : Tâches Créées (userId) - -``` -Collection : tasks -Fields indexed: - - userId (Ascending) - - createdAt (Descending) - - __name__ (Descending) -Query scope: Collection -``` - -**Pourquoi ?** Pour récupérer rapidement toutes les tâches créées par un utilisateur, triées par date. - -### Index 2 : Tâches Assignées (assignedTo) - -``` -Collection : tasks -Fields indexed: - - assignedTo (Array-contains) - - createdAt (Descending) - - __name__ (Descending) -Query scope: Collection -``` - -**Pourquoi ?** Pour récupérer rapidement toutes les tâches où l'utilisateur est assigné, triées par date. - -## 🚀 Après Création des Index - -Une fois les index créés, votre application : - -- ✅ Affichera uniquement VOS tâches -- ✅ Affichera les tâches où vous êtes assigné -- ✅ Sera rapide même avec des milliers de tâches -- ✅ Respectera la privacy (règles Firestore) - -## 🔍 Vérification que Tout Fonctionne - -1. **Connectez-vous** avec votre compte -2. **Créez une tâche** → elle s'affiche immédiatement -3. **Créez un second compte** dans un autre navigateur (mode incognito) -4. **Vérifiez** que les tâches du premier compte ne sont PAS visibles -5. **Retournez au premier compte** → Modifiez une tâche → "Assigner des utilisateurs" → Sélectionnez le second compte -6. **Vérifiez dans le second compte** → La tâche assignée est maintenant visible - -## ⏱️ Temps de Création des Index - -- **Petite base** (< 100 documents) : ~30 secondes - 2 minutes -- **Base moyenne** (100-1000 documents) : ~2-5 minutes -- **Grande base** (> 1000 documents) : ~5-15 minutes - -⚠️ **IMPORTANT** : Ne fermez pas la page pendant la création ! - -## 🆘 Dépannage - -**Erreur persiste après création ?** - -- Vérifiez que le statut est **"Enabled"** (pas "Building") -- Faites un **Hot Restart** (R) ou relancez l'app -- Videz le cache du navigateur - -**Les index ne se créent pas ?** - -- Vérifiez votre quota Firebase (plan gratuit limité) -- Essayez de créer manuellement depuis Console → Firestore → Index - -**Je ne vois pas les liens dans la console ?** -Créez manuellement : - -1. Firebase Console → Firestore → Index -2. Cliquez sur **"Créer un index composite"** -3. Utilisez les configurations ci-dessus - -## 📝 Création Manuelle (Alternative) - -Si les liens ne marchent pas, voici les étapes manuelles : - -### Index 1 (userId) - -1. Console Firebase → Firestore Database → Index -2. Cliquer sur **"Créer un index composite"** -3. Remplir : - - **Collection ID** : `tasks` - - **Champs** : - - `userId` → Ascending - - `createdAt` → Descending - - **Query scope** : Collection -4. Créer - -### Index 2 (assignedTo) - -1. Cliquer à nouveau sur **"Créer un index composite"** -2. Remplir : - - **Collection ID** : `tasks` - - **Champs** : - - `assignedTo` → Array-contains - - `createdAt` → Descending - - **Query scope** : Collection -3. Créer - ---- - -✅ **Après ces étapes, votre système de privacy et d'assignation sera pleinement opérationnel !** diff --git a/DEBUG_ASSIGNATION.md b/DEBUG_ASSIGNATION.md deleted file mode 100644 index d7b1877..0000000 --- a/DEBUG_ASSIGNATION.md +++ /dev/null @@ -1,162 +0,0 @@ -# 🔍 Guide de débogage - Problème d'assignation - -## 📋 Symptômes - -1. ✅ Les utilisateurs disponibles s'affichent correctement dans le dialog -2. ✅ Le bouton "Assigner" fonctionne (notification verte) -3. ❌ Le badge "X assigné(s)" n'apparaît PAS dans la TaskTile -4. ❌ Les tâches assignées n'apparaissent PAS chez l'utilisateur assigné - -## 🎯 Points de vérification - -### 1. Vérifier que les règles Firestore sont déployées - -**CRITIQUE** : Sans règles déployées, les mises à jour Firestore échoueront silencieusement. - -1. Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules -2. Vérifiez que les règles contiennent : - ``` - // Règles pour la collection 'users' - match /users/{userId} { - allow read: if request.auth != null; // DOIT ÊTRE COMME ÇA - } - ``` -3. Cliquez sur "Publier" si ce n'est pas déjà fait - -### 2. Vérifier les données dans Firestore - -1. Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data -2. Ouvrez la collection `tasks` -3. Sélectionnez une tâche -4. **Vérifiez que le champ `assignedTo` existe et contient un tableau d'UIDs** - - ✅ Bon exemple : `assignedTo: ["riXsDCyTOVZi0gyr3pKZUxAkjT02"]` - - ❌ Mauvais : Champ absent ou vide `[]` - -### 3. Analyser les logs de l'application - -Lors de l'assignation, vous devriez voir dans la console : - -``` -📌 TaskService.assignUserToTask: taskId=abc123, userIdToAssign=xyz456 -✅ TaskService.assignUserToTask: Succès -🎨 TaskTile: Affichage badge assignés - task.id=abc123, assignedCount=1, assignedTo=[xyz456] -``` - -**Si vous voyez ❌ erreurs** : - -- Vérifiez les règles Firestore -- Vérifiez que la tâche existe -- Vérifiez les permissions - -**Si vous NE voyez PAS les logs 📌** : - -- L'assignation n'est pas appelée -- Vérifiez le code du dialog - -**Si vous NE voyez PAS les logs 🎨** : - -- La TaskTile ne reçoit pas la mise à jour -- Le Provider ne notifie pas les changements - -### 4. Tester l'isolation des utilisateurs - -Pour vérifier que les tâches assignées apparaissent bien : - -1. **Compte A (créateur)** : Créez une tâche -2. **Compte A** : Assignez la tâche à Compte B -3. **Déconnectez-vous du Compte A** -4. **Connectez-vous au Compte B** -5. **Vérifiez** : La tâche doit apparaître dans la liste du Compte B - -**Si la tâche n'apparaît pas** : - -- Vérifiez que `assignedTo` contient bien l'UID du Compte B dans Firestore -- Vérifiez les logs du stream Firestore : `TaskService.tasksStream()` -- Vérifiez les règles de lecture : `request.auth.uid in resource.data.get('assignedTo', [])` - -## 🛠️ Actions correctives - -### Si l'assignation échoue silencieusement - -1. **Ajoutez des try-catch** dans le dialog : - - ```dart - try { - await provider.assignUserToTask(widget.task.id, userId); - print('✅ Assignation réussie'); - } catch (e) { - print('❌ Erreur assignation: $e'); - } - ``` - -2. **Vérifiez les permissions Firestore** : - - L'utilisateur connecté doit être le créateur de la tâche - - Règle : `allow update: if resource.data.userId == request.auth.uid` - -### Si le badge n'apparaît pas - -1. **Vérifiez que TaskTile reçoit la tâche mise à jour** : - - - Le Provider doit émettre `notifyListeners()` après l'assignation - - Le stream Firestore doit émettre la nouvelle version de la tâche - -2. **Forcez un rebuild** du widget après assignation : - - Le dialog utilise `context.watch()` ✅ - - Le TaskListScreen écoute le Provider ✅ - -### Si la tâche n'apparaît pas chez l'utilisateur assigné - -1. **Vérifiez la requête Firestore** : - - ```dart - // Dans tasksStream(), vérifiez que cette requête existe : - final assignedTasksStream = col - .where('assignedTo', arrayContains: uid) - .orderBy('createdAt', descending: true) - .snapshots() - ``` - -2. **Vérifiez l'index Firestore** : - - Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/indexes - - Index requis : Collection `tasks`, Champs `assignedTo` (Array-contains) + `createdAt` (Descending) - -## 📊 Checklist de test - -- [ ] Règles Firestore déployées -- [ ] Index Firestore créés (userId+createdAt, assignedTo+createdAt) -- [ ] Logs d'assignation visibles (📌 et ✅) -- [ ] Champ `assignedTo` visible dans Firestore Data -- [ ] Badge "X assigné(s)" visible dans la tâche -- [ ] Tâche assignée visible chez l'utilisateur B -- [ ] Désassignation fonctionne (icône X dans le chip) -- [ ] Compteur se met à jour en temps réel - -## 🎓 Commandes utiles - -### Hot reload - -```bash -r # Dans le terminal Flutter -``` - -### Redémarrage complet - -```bash -R # Dans le terminal Flutter -``` - -### Voir les logs Firestore - -Ajoutez dans `task_service.dart` : - -```dart -_tasksSub = _taskService.tasksStream().listen( - (list) { - debugPrint('🔄 Stream Firestore: ${list.length} tâches reçues'); - for (var task in list) { - debugPrint(' - ${task.title}: assignedTo=${task.assignedTo}'); - } - // ... - } -); -``` diff --git a/FIREBASE_SETUP.md b/FIREBASE_SETUP.md deleted file mode 100644 index 69f868e..0000000 --- a/FIREBASE_SETUP.md +++ /dev/null @@ -1,141 +0,0 @@ -# 🔥 Configuration Firebase - Guide étape par étape - -## 📧 Compte Firebase -Email : eric.amour2022@gmail.com - ---- - -## 🚀 Étapes de configuration - -### 1️⃣ Créer/Accéder au projet Firebase - -1. Aller sur [Firebase Console](https://console.firebase.google.com/) -2. Se connecter avec : **eric.amour2022@gmail.com** -3. Cliquer sur **"Ajouter un projet"** ou sélectionner un projet existant -4. Nom du projet suggéré : **flutter-todolist-app** (ou votre choix) - ---- - -### 2️⃣ Configurer la plateforme Web - -1. Dans la console Firebase, cliquer sur **⚙️ Paramètres du projet** -2. Descendre jusqu'à **"Vos applications"** -3. Cliquer sur l'icône ** Web** -4. Enregistrer l'app : - - Nom : **FlutterProject Web** - - ✅ Cocher : "Configurer également Firebase Hosting" -5. Copier les valeurs affichées : - -```javascript -const firebaseConfig = { - apiKey: "VOTRE_API_KEY", - authDomain: "VOTRE_PROJECT_ID.firebaseapp.com", - projectId: "VOTRE_PROJECT_ID", - storageBucket: "VOTRE_PROJECT_ID.appspot.com", - messagingSenderId: "VOTRE_SENDER_ID", - appId: "VOTRE_APP_ID" -}; -``` - ---- - -### 3️⃣ Activer Firestore Database - -1. Dans le menu latéral, cliquer sur **"Firestore Database"** -2. Cliquer sur **"Créer une base de données"** -3. Choisir le mode : **Mode test** (pour le développement) -4. Sélectionner la région : **europe-west1** (Belgique) ou **us-central1** -5. Cliquer sur **"Activer"** - ---- - -### 4️⃣ Configurer les règles Firestore - -Dans **Firestore Database → Règles**, remplacer par : - -```javascript -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - // Collection des tâches - accès public en mode développement - match /tasks/{taskId} { - allow read, write: if true; - } - } -} -``` - -⚠️ **Important** : Ces règles sont ouvertes pour le développement. -En production, sécuriser avec l'authentification. - -Cliquer sur **"Publier"**. - ---- - -### 5️⃣ Mettre à jour firebase_options.dart - -Remplacer les valeurs dans `lib/firebase_options.dart` : - -```dart -static const FirebaseOptions web = FirebaseOptions( - apiKey: 'VOTRE_API_KEY', - appId: 'VOTRE_APP_ID', - messagingSenderId: 'VOTRE_SENDER_ID', - projectId: 'VOTRE_PROJECT_ID', - authDomain: 'VOTRE_PROJECT_ID.firebaseapp.com', - storageBucket: 'VOTRE_PROJECT_ID.appspot.com', -); -``` - ---- - -### 6️⃣ Tester l'application - -```bash -flutter pub get -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -``` - -Vérifier dans la console : -``` -✅ Firebase initialisé avec succès -``` - ---- - -### 7️⃣ Vérifier Firestore - -1. Retourner dans Firebase Console → Firestore Database -2. Vous devriez voir une collection **"tasks"** se créer automatiquement -3. Les tâches créées dans l'app apparaîtront ici en temps réel - ---- - -## 📝 Commandes utiles - -```bash -# Reconfigurer Firebase automatiquement (si FlutterFire CLI configuré) -flutterfire configure - -# Voir les logs Firebase -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -v - -# Nettoyer et relancer -flutter clean -flutter pub get -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -``` - ---- - -## 🆘 Aide - -Si vous rencontrez des problèmes : -1. Vérifier que toutes les clés sont correctement copiées -2. Vérifier que Firestore est activé -3. Vérifier les règles de sécurité Firestore -4. Regarder la console du navigateur (F12) pour les erreurs - ---- - -✅ Une fois configuré, l'application sera connectée à Firebase et les tâches seront synchronisées en temps réel ! diff --git a/FIRESTORE_DEBUG.md b/FIRESTORE_DEBUG.md deleted file mode 100644 index 496eaae..0000000 --- a/FIRESTORE_DEBUG.md +++ /dev/null @@ -1,156 +0,0 @@ -# 🔧 Guide de débogage Firestore - -## Problème actuel -Les tâches ne sont pas créées dans la console Firebase malgré une connexion réussie. - -## ✅ Checklist de diagnostic - -### 1. Vérifier l'authentification Firebase -- [ ] Ouvrir l'application : http://127.0.0.1:8082 -- [ ] Se connecter avec : eric.amour2022@gmail.com -- [ ] Vérifier dans la console : https://console.firebase.google.com/project/flutter-todo-web-305fb/authentication/users - -### 2. Vérifier les règles Firestore -- [ ] Ouvrir : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules -- [ ] Vérifier que les règles permettent l'écriture - -**Règles recommandées pour le développement :** -```javascript -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - // Règles pour la collection tasks - match /tasks/{taskId} { - // Autoriser lecture/écriture uniquement pour utilisateurs authentifiés - allow read, write: if request.auth != null; - } - } -} -``` - -**Règles pour le test (TEMPORAIRE UNIQUEMENT) :** -```javascript -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - match /tasks/{taskId} { - // ⚠️ ATTENTION : Règles ouvertes pour TEST uniquement ! - allow read, write: if true; - } - } -} -``` - -### 3. Utiliser la page de debug -- [ ] Dans l'application, cliquer sur l'icône 🐛 en haut à droite -- [ ] Cliquer sur "Test écriture" pour créer une tâche de test -- [ ] Observer les logs pour identifier l'erreur exacte - -### 4. Vérifier la console navigateur -- [ ] Ouvrir les DevTools du navigateur (F12) -- [ ] Aller dans l'onglet "Console" -- [ ] Essayer de créer une tâche -- [ ] Noter les erreurs affichées - -## 🔍 Erreurs courantes - -### Erreur : "Missing or insufficient permissions" -**Cause :** Les règles Firestore bloquent l'écriture - -**Solution :** -1. Aller dans https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules -2. Mettre à jour les règles (voir ci-dessus) -3. Cliquer sur **"Publier"** (en haut à droite) -4. Attendre 1-2 minutes pour que les règles se propagent -5. Réessayer - -### Erreur : "No user signed in" -**Cause :** Utilisateur non connecté - -**Solution :** -1. Se déconnecter de l'application -2. Se reconnecter avec eric.amour2022@gmail.com -3. Réessayer de créer une tâche - -### Collection "tasks" n'apparaît pas -**Cause :** La collection n'est créée qu'après la première écriture réussie - -**Solution :** -1. Vérifier que les règles Firestore sont correctes -2. Créer une première tâche avec succès -3. Rafraîchir la console Firebase : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - -## 📊 Vérification des données - -### Voir les données dans Firestore -1. Ouvrir : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data -2. Chercher la collection "tasks" -3. Si elle existe, vérifier les documents à l'intérieur - -### Structure attendue d'un document task -```json -{ - "title": "Ma tâche", - "description": "Description de la tâche", - "isCompleted": false, - "priority": 2, - "createdAt": "Timestamp", - "dueDate": null, - "tags": [] -} -``` - -## 🛠️ Actions de dépannage - -### Si les règles sont correctes mais ça ne fonctionne toujours pas - -1. **Vérifier la connexion Firebase dans la console navigateur :** - ```javascript - // Dans la console navigateur (F12) - firebase.apps.length // Doit retourner 1 ou plus - ``` - -2. **Tester manuellement dans la console navigateur :** - ```javascript - // Dans la console navigateur (F12) - firebase.firestore().collection('tasks').add({ - title: 'Test manuel', - description: 'Test depuis console', - isCompleted: false, - priority: 2, - createdAt: firebase.firestore.Timestamp.now(), - tags: [] - }).then(doc => console.log('Créé:', doc.id)) - ``` - -3. **Vérifier les quotas Firebase :** - - Ouvrir : https://console.firebase.google.com/project/flutter-todo-web-305fb/usage - - Vérifier que vous n'avez pas atteint les limites - -## 📝 Logs utiles - -Pour voir les logs détaillés dans l'application : -1. Aller sur la page de debug (icône 🐛) -2. Les logs apparaîtront avec des codes couleur : - - 🟢 Vert : Succès - - 🔴 Rouge : Erreur - - 🟠 Orange : Avertissement - - 🔵 Cyan : Information - -## 🎯 Prochaines étapes - -Une fois que le test d'écriture fonctionne dans la page de debug : - -1. ✅ La collection "tasks" devrait apparaître dans Firebase -2. Retourner à la page des tâches -3. Essayer de créer une tâche normale -4. Vérifier qu'elle apparaît dans la liste ET dans Firebase - -## 📞 Besoin d'aide ? - -Si après toutes ces vérifications ça ne fonctionne toujours pas : - -1. Copier les logs de la page de debug -2. Copier les erreurs de la console navigateur (F12) -3. Vérifier une dernière fois les règles Firestore -4. Partager ces informations pour un diagnostic plus approfondi diff --git a/GUIDE_ASSIGNATION_UTILISATEURS.md b/GUIDE_ASSIGNATION_UTILISATEURS.md deleted file mode 100644 index b3102ad..0000000 --- a/GUIDE_ASSIGNATION_UTILISATEURS.md +++ /dev/null @@ -1,230 +0,0 @@ -# 🎨 Guide : Système d'Assignation d'Utilisateurs - -## ✨ Fonctionnalités Implémentées - -### 1. **Affichage du Créateur de la Tâche** ⭐ - -Chaque tâche affiche maintenant un badge élégant montrant qui l'a créée. - -**Emplacement** : `TaskTile` (liste des tâches) - -**Design** : - -- 🎨 Badge avec dégradé violet/indigo (couleurs primary/secondary) -- 👤 Avatar circulaire avec l'initiale du créateur -- ⭐ Icône étoile pour indiquer le créateur -- 📛 Nom du créateur affiché - -**Code** : Méthode `_buildOwnerBadge()` dans `task_tile.dart` - ---- - -### 2. **Affichage des Utilisateurs Assignés** 👥 - -Un badge compact montre combien d'utilisateurs sont assignés à la tâche. - -**Design** : - -- 🔵 Badge bleu avec icône "people" -- 📊 Compteur : "X assigné(s)" -- 📍 Positionné à côté du badge créateur - -**Code** : Méthode `_buildAssignedUsersBadge()` dans `task_tile.dart` - ---- - -### 3. **Dialog d'Assignation Moderne** 🚀 - -Un dialog complet et élégant pour gérer les assignations. - -#### **Fonctionnalités** : - -##### A. **En-tête avec Gradient** 🎨 - -- Dégradé violet/indigo -- Titre "Gérer l'équipe" -- Compteur de membres assignés -- Nom de la tâche en badge - -##### B. **Barre de Recherche** 🔍 - -- Recherche en temps réel -- Filtre par nom OU email -- Icône de recherche + bouton "clear" -- Design moderne avec bordures arrondies - -##### C. **Liste des Utilisateurs** 📋 - -- **Avatar coloré** : Couleur générée automatiquement basée sur le nom -- **Badge "Assigné"** : Chip vert pour les utilisateurs déjà assignés -- **Bouton "Assigner"** : Pour ajouter rapidement un utilisateur -- **Bordure colorée** : Violet pour les assignés, gris pour les autres -- **Effet de survol** : Animation au clic - -##### D. **États Vides** 🎭 - -- Message si aucun utilisateur disponible -- Message si aucun résultat de recherche -- Icons et textes adaptatifs - -##### E. **Pied de Page** 📊 - -- Compteur récapitulatif -- Bouton "Terminé" pour fermer - -##### F. **Animations** ✨ - -- Fade-in au chargement -- Slide-in depuis le bas -- Transitions fluides lors des assignations - ---- - -## 🎯 Expérience Utilisateur - -### **Scénario 1 : Voir qui a créé une tâche** - -1. Ouvrez la liste des tâches -2. Chaque tâche affiche un badge avec : - - Avatar du créateur - - Nom du créateur - - Icône étoile ⭐ - -**Résultat** : Vous savez immédiatement qui est responsable de chaque tâche. - ---- - -### **Scénario 2 : Assigner des utilisateurs à une tâche** - -1. **Cliquez** sur une tâche pour l'ouvrir -2. **Cliquez** sur le bouton "Gérer les utilisateurs assignés (X)" -3. Le dialog s'ouvre avec animations fluides -4. **Recherchez** un utilisateur (tapez son nom ou email) -5. **Cliquez** sur le bouton "Assigner" ou sur la ligne -6. L'utilisateur est immédiatement assigné avec : - - Badge "Assigné" vert - - Bordure violette autour de sa carte - - Checkmark sur l'avatar -7. **Notification** : SnackBar de confirmation en bas -8. **Compteur mis à jour** : "2 membres dans l'équipe" - -**Résultat** : Assignation ultra-rapide et visuelle ! - ---- - -### **Scénario 3 : Retirer un utilisateur** - -1. Ouvrez le dialog d'assignation -2. **Cliquez** sur un utilisateur déjà assigné (badge vert) -3. Il est immédiatement retiré -4. Le badge "Assigné" disparaît -5. La bordure redevient grise -6. Le compteur se met à jour - ---- - -## 🎨 Design System - -### **Couleurs** 🌈 - -| Élément | Couleur | Utilisation | -| ------------------- | -------------------------- | ---------------------- | -| Badge Créateur | Dégradé Primary/Secondary | Identifier le créateur | -| Badge Assignés | Info Blue | Compter les assignés | -| Utilisateur Assigné | Success Green | Confirmation visuelle | -| Bordure Active | Primary Violet | Sélection | -| En-tête Dialog | Gradient Primary/Secondary | Impact visuel | - -### **Avatars** 👤 - -- **Couleur automatique** : Basée sur le hash du nom (6 couleurs possibles) -- **Initiale** : Première lettre du nom en majuscule -- **Checkmark** : Badge vert en bas à droite si assigné - -### **Animations** ✨ - -| Action | Animation | Durée | -| ---------------- | ----------------------- | ----- | -| Ouverture dialog | Fade + Slide | 600ms | -| Assignation | Background color change | 300ms | -| Bouton → Chip | AnimatedSwitcher | 300ms | -| Hover sur carte | Scale transform | 150ms | - ---- - -## 📝 Code Principal - -### **Fichiers Modifiés** - -1. **`task_tile.dart`** ✅ - - - Ajout de `_buildOwnerBadge()` - - Ajout de `_buildAssignedUsersBadge()` - - Modification de `_buildMetadata()` pour afficher les badges - -2. **`assign_users_dialog.dart`** ✅ - - Refonte complète du dialog - - Ajout de la barre de recherche - - Amélioration du design - - Ajout des animations - ---- - -## 🚀 Prochaines Étapes (Optionnelles) - -### **Améliorations Possibles** : - -1. **Avatars Empilés** 📸 - - - Afficher plusieurs avatars superposés dans le badge assignés - - Limiter à 3 avatars + compteur "+X" - -2. **Notifications** 📧 - - - Notifier un utilisateur quand il est assigné - - Email ou push notification - -3. **Rôles et Permissions** 🔐 - - - Rôles : Créateur, Assigné, Observateur - - Permissions différentes selon le rôle - -4. **Historique des Assignations** 📊 - - - Voir qui a assigné qui et quand - - Timeline des changements - -5. **Filtres Avancés** 🔍 - - Filtrer les tâches par assigné - - "Mes tâches" vs "Tâches de l'équipe" - ---- - -## ✅ Checklist de Test - -- [ ] Les badges créateur s'affichent correctement -- [ ] Les badges assignés comptent bien le nombre d'utilisateurs -- [ ] La recherche fonctionne (nom ET email) -- [ ] L'assignation est immédiate (pas de délai) -- [ ] Les animations sont fluides -- [ ] Les couleurs d'avatar sont variées -- [ ] Le compteur se met à jour après assignation -- [ ] Le retrait d'utilisateur fonctionne -- [ ] Le dialog se ferme proprement -- [ ] Les SnackBars apparaissent avec les bons messages - ---- - -## 🎉 Résultat Final - -Vous avez maintenant un système d'assignation moderne et élégant avec : - -- ✅ **Visibilité claire** du créateur de chaque tâche -- ✅ **Compteur visuel** des utilisateurs assignés -- ✅ **Interface intuitive** pour assigner/retirer des utilisateurs -- ✅ **Recherche rapide** parmi tous les utilisateurs -- ✅ **Feedback instantané** avec animations et notifications -- ✅ **Design moderne** avec gradients et avatars colorés -- ✅ **Expérience fluide** avec transitions douces - -Profitez de votre nouvelle fonctionnalité ! 🚀 diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 74403e9..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,145 +0,0 @@ -# 🚀 Démarrage Rapide - FlutterProject avec Firebase - -## ✅ Ce qui a été fait - -### 📦 10 commits créés sur `feature/database-integration` - -1. ✅ Ajout dépendances SQLite (puis abandonné) -2. ✅ Retour à Firebase, suppression SQLite -3. ✅ Création du service Firestore pour les tâches -4. ✅ Création du repository des tâches -5. ✅ Intégration Firestore dans TaskProvider avec temps réel -6. ✅ Initialisation Firebase au démarrage -7. ✅ Documentation complète Firebase dans README -8. ✅ Fichier firebase_options.dart (template) -9. ✅ Guide de configuration Firebase pour eric.amour2022@gmail.com -10. ✅ Fix BuildContext async dans SplashPage - ---- - -## 🎯 Prochaines étapes (VOUS) - -### 1️⃣ Configurer Firebase (5 minutes) - -📖 Suivre le guide : **`FIREBASE_SETUP.md`** - -Résumé rapide : -```bash -1. Aller sur https://console.firebase.google.com/ -2. Se connecter avec : eric.amour2022@gmail.com -3. Créer un projet : "flutter-todolist-app" -4. Ajouter une application Web -5. Copier les clés Firebase -6. Activer Firestore Database (mode test) -7. Configurer les règles Firestore -``` - -### 2️⃣ Mettre à jour firebase_options.dart - -Éditer `lib/firebase_options.dart` et remplacer : -- `YOUR_WEB_API_KEY` -- `YOUR_WEB_APP_ID` -- `YOUR_MESSAGING_SENDER_ID` -- `YOUR_PROJECT_ID` - -Par les vraies valeurs obtenues dans Firebase Console. - -### 3️⃣ Lancer l'application - -```bash -flutter pub get -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -``` - -Puis ouvrir : **http://127.0.0.1:8081** - ---- - -## 🏗️ Architecture Firebase implémentée - -``` -lib/ -├── firebase_options.dart ✅ Configuration Firebase -├── main.dart ✅ Initialisation Firebase -└── features/ - └── tasks/ - ├── data/ - │ ├── firestore_task_service.dart ✅ Service Firestore - │ └── task_repository.dart ✅ Repository - ├── domain/models/ - │ └── task.dart ✅ Modèle Task - └── presentation/ - └── providers/ - └── task_provider.dart ✅ Provider avec temps réel -``` - ---- - -## 🎁 Fonctionnalités disponibles - -- ✅ **CRUD complet** : Créer, Lire, Mettre à jour, Supprimer -- ✅ **Temps réel** : Synchronisation automatique -- ✅ **Statistiques** : Total, complétées, en attente, priorité haute -- ✅ **Gestion d'erreurs** : Messages d'erreur dans l'UI -- ✅ **Données de test** : Charger des tâches de démo - ---- - -## 🔍 Vérifications - -### ✅ Dans le terminal Flutter -``` -✅ Firebase initialisé avec succès -``` - -### ✅ Dans Firebase Console -- Collection `tasks` créée automatiquement -- Tâches apparaissent en temps réel - -### ✅ Dans l'application -- Créer une tâche → apparaît immédiatement -- Modifier une tâche → mise à jour en temps réel -- Supprimer une tâche → disparaît instantanément - ---- - -## 🆘 En cas de problème - -### Erreur : Firebase not initialized -➡️ Vérifier que `firebase_options.dart` contient les bonnes clés - -### Erreur : Permission denied -➡️ Vérifier les règles Firestore (mode test activé) - -### L'app ne se lance pas -```bash -flutter clean -flutter pub get -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -``` - -### Voir les logs Firebase -```bash -flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8081 -v -``` - ---- - -## 📚 Documentation - -- 📖 **FIREBASE_SETUP.md** : Guide détaillé de configuration -- 📖 **README.md** : Documentation générale du projet -- 📖 Code commenté dans tous les fichiers - ---- - -## 🎉 Une fois configuré - -Vous aurez une application Flutter complète avec : -- ✅ Base de données temps réel -- ✅ Architecture propre (Service → Repository → Provider) -- ✅ Synchronisation automatique multi-appareils -- ✅ Gestion des erreurs -- ✅ Code prêt pour la production - -**Bon développement ! 🚀** diff --git a/SOLUTION_ASSIGNATION.md b/SOLUTION_ASSIGNATION.md deleted file mode 100644 index f851c62..0000000 --- a/SOLUTION_ASSIGNATION.md +++ /dev/null @@ -1,226 +0,0 @@ -# 🎯 SOLUTION COMPLÈTE - Problème d'Assignation - -## 📊 Diagnostic complet effectué - -### ✅ Code vérifié - TOUT est correct - -- ✅ `Task.toMap()` sérialise bien `assignedTo: []` -- ✅ `Task.fromMap()` désérialise correctement `assignedTo` -- ✅ `TaskService.addTask()` crée les tâches avec le champ `assignedTo` -- ✅ `TaskService.assignUserToTask()` utilise `arrayUnion` correctement -- ✅ `TaskProvider` appelle bien les bonnes méthodes -- ✅ `AssignUsersDialog` utilise `context.watch()` pour la mise à jour en temps réel -- ✅ `TaskTile` affiche le badge avec le bon compteur - -### ❌ SEUL PROBLÈME : Les règles Firestore - -## 🔴 CAUSE RACINE DU PROBLÈME - -**Les règles Firestore dans la console Firebase ne sont PAS synchronisées avec votre fichier local `firestore.rules`.** - -Preuve : Les logs montrent `✅ Succès` mais le tableau reste vide dans Firebase. - -Cela signifie que : - -1. Le code Flutter envoie bien la requête à Firestore -2. Firestore **REJETTE** la mise à jour côté serveur (règles de sécurité) -3. Le SDK Web ne renvoie PAS d'erreur au client (comportement normal) - -## 🚀 SOLUTION EN 3 ÉTAPES - -### ÉTAPE 1 : Vérifier les règles actuelles dans Firebase - -1. **Allez sur** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules - -2. **Regardez la ligne 36-39** dans l'éditeur en ligne - -3. **Si vous voyez ceci** : - ``` - allow update: if request.auth != null && - resource.data.userId == request.auth.uid && - request.resource.data.userId == resource.data.userId && - request.resource.data.ownerName == resource.data.ownerName; - ``` - **C'EST LE PROBLÈME !** Cette règle bloque la modification du champ `assignedTo`. - -### ÉTAPE 2 : Déployer les BONNES règles - -1. **Restez sur** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules - -2. **Sélectionnez TOUT le contenu** de l'éditeur (Ctrl+A) - -3. **SUPPRIMEZ** et **COLLEZ** ceci : - -```plaintext -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - - // Règles pour la collection 'users' - match /users/{userId} { - allow read: if request.auth != null; - allow create: if request.auth != null && request.auth.uid == userId; - allow update: if request.auth != null && request.auth.uid == userId; - allow delete: if false; - } - - // Règles pour la collection 'tasks' - match /tasks/{taskId} { - // Lecture : autorisée si créateur OU assigné - allow read: if request.auth != null && ( - resource.data.userId == request.auth.uid || - request.auth.uid in resource.data.get('assignedTo', []) - ); - - // Création : autorisée si authentifié - allow create: if request.auth != null && - request.resource.data.userId == request.auth.uid; - - // Mise à jour : autorisée si créateur (userId ne change pas) - allow update: if request.auth != null && - resource.data.userId == request.auth.uid && - request.resource.data.userId == resource.data.userId; - - // Suppression : autorisée si créateur - allow delete: if request.auth != null && - resource.data.userId == request.auth.uid; - } - } -} -``` - -4. **Cliquez sur "Publier"** (bouton bleu en haut à droite) - -5. **Attendez le message de confirmation** (3-5 secondes) - -### ÉTAPE 3 : Tester avec les nouveaux logs - -1. **Ouvrez votre application Flutter** (elle devrait déjà tourner) - -2. **Créez une NOUVELLE tâche** avec le compte `far@id.jp` - - - Titre : "Test assignation finale" - - N'importe quelle description - -3. **Ouvrez la tâche** → Cliquez sur "Gérer les utilisateurs assignés" - -4. **Assignez `mody@d.fr`** - -5. **Regardez les logs dans la console Flutter**. Vous devriez voir : - - ``` - 📌 TaskService.assignUserToTask: taskId=xxx, userIdToAssign=yyy - Current user UID: 2tVdkeWkrhhe3nuWx4YFvvYHjWE2 - Tâche actuelle: userId=2tVdkeWkrhhe3nuWx4YFvvYHjWE2, assignedTo=[] - ✅ TaskService.assignUserToTask: Succès - Tâche après update: assignedTo=[riXsDCyTOVZi0gyr3pKZUxAkjT02] - ``` - -6. **Vérifiez dans Firebase** : - - - https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - - Ouvrez la tâche "Test assignation finale" - - Le champ `assignedTo` doit contenir : `["riXsDCyTOVZi0gyr3pKZUxAkjT02"]` - -7. **Déconnectez-vous** de `far@id.jp` - -8. **Connectez-vous** avec `mody@d.fr` - -9. **La tâche "Test assignation finale" doit apparaître dans la liste !** - -## 🔍 Si ça ne fonctionne toujours pas - -### Scénario A : Erreur permission-denied - -Si vous voyez dans les logs : - -``` -❌ TaskService.assignUserToTask: Erreur permission-denied -``` - -**Solution** : Les règles ne sont pas encore déployées. Attendez 1 minute et réessayez. - -### Scénario B : assignedTo reste [] - -Si les logs montrent : - -``` -✅ Succès - Tâche après update: assignedTo=[] ← VIDE ! -``` - -**Solution** : - -1. Vérifiez que vous avez bien cliqué sur "Publier" dans Firebase Console -2. Rafraîchissez la page des règles pour voir si elles sont bien enregistrées -3. Attendez 30 secondes (propagation des règles) - -### Scénario C : La tâche n'apparaît pas chez mody@d.fr - -Si `assignedTo` contient bien l'UID mais la tâche n'apparaît pas : - -1. **Vérifiez l'UID** de `mody@d.fr` : - - - Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/authentication/users - - Copiez l'UID exact de Mody - - Vérifiez qu'il correspond à celui dans `assignedTo` - -2. **Vérifiez la requête Firestore** : - - - Les logs devraient montrer : `assignedTo array-contains UID_de_mody` - -3. **Vérifiez l'index Firestore** : - - https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/indexes - - Doit contenir un index : Collection `tasks`, Champs `assignedTo` (Array-contains) + `createdAt` (Descending) - -## 📋 Checklist de validation - -- [ ] Règles Firebase déployées (ligne 36 ne mentionne PAS `ownerName`) -- [ ] Nouvelle tâche créée APRÈS le déploiement des règles -- [ ] Logs montrent `Tâche après update: assignedTo=[UID]` -- [ ] Champ `assignedTo` visible dans Firebase Data -- [ ] Badge "1 assigné" visible dans l'UI -- [ ] Tâche apparaît chez l'utilisateur assigné après connexion - -## 🎓 Commandes de test - -### Voir les logs en temps réel - -Les logs s'affichent automatiquement dans le terminal Flutter. - -### Hot Restart - -Si besoin de redémarrer l'app : - -``` -R (dans le terminal Flutter) -``` - -### Nettoyer Firestore - -Si vous voulez repartir de zéro : - -1. Allez sur Firebase Data -2. Sélectionnez toutes les tâches (Shift+Click) -3. Cliquez sur "Supprimer" - -## ✅ RÉSULTAT ATTENDU - -Après avoir suivi ces 3 étapes : - -1. **Compte Far (créateur)** : - - - Voit toutes ses tâches - - Badge "1 assigné" sur la tâche partagée - - Peut assigner/désassigner - -2. **Compte Mody (assigné)** : - - - Voit la tâche assignée dans sa liste - - Badge "Créé par Far" visible - - NE PEUT PAS modifier la tâche (seul le créateur peut) - -3. **Firebase Data** : - - `assignedTo: ["UID_de_mody"]` - - `userId: "UID_de_far"` - - `ownerName: "Far"` diff --git a/SOLUTION_FINALE_ASSIGNATION.md b/SOLUTION_FINALE_ASSIGNATION.md deleted file mode 100644 index be41faf..0000000 --- a/SOLUTION_FINALE_ASSIGNATION.md +++ /dev/null @@ -1,214 +0,0 @@ -# 🚨 SOLUTION FINALE - Assignation ne fonctionne pas - -## 🔴 PROBLÈME CONFIRMÉ - -**Les logs mentent !** Ils montrent "✅ Succès" mais le tableau `assignedTo` reste VIDE dans Firebase. - -Cela signifie : **Firestore rejette silencieusement les mises à jour côté serveur**. - -## 🎯 CAUSE : Règles Firestore trop strictes - -La ligne problématique : - -``` -allow update: if request.auth != null && - resource.data.userId == request.auth.uid && - request.resource.data.userId == resource.data.userId; ← TOO STRICT! -``` - -Cette condition bloque peut-être la modification du champ `assignedTo`. - -## ✅ SOLUTION EN 3 ACTIONS - -### ACTION 1 : Simplifier les règles (1 minute) - -1. **Ouvrez** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules - -2. **Remplacez la ligne 35-37** par simplement : - - ``` - allow update: if request.auth != null && - resource.data.userId == request.auth.uid; - ``` - -3. Les règles complètes doivent être : - - ``` - rules_version = '2'; - service cloud.firestore { - match /databases/{database}/documents { - - match /users/{userId} { - allow read: if request.auth != null; - allow create: if request.auth != null && request.auth.uid == userId; - allow update: if request.auth != null && request.auth.uid == userId; - allow delete: if false; - } - - match /tasks/{taskId} { - allow read: if request.auth != null && ( - resource.data.userId == request.auth.uid || - request.auth.uid in resource.data.get('assignedTo', []) - ); - - allow create: if request.auth != null && - request.resource.data.userId == request.auth.uid; - - allow update: if request.auth != null && - resource.data.userId == request.auth.uid; - - allow delete: if request.auth != null && - resource.data.userId == request.auth.uid; - } - } - } - ``` - -4. **Cliquez sur "Publier"** (bouton bleu) - -5. **Attendez 10 secondes** (important!) - -### ACTION 2 : Vider le cache et tester (2 minutes) - -1. **Fermez complètement votre application** (cliquez sur X) - -2. **Dans le terminal Flutter, tapez `q`** pour quitter - -3. **Relancez** : - - ```bash - flutter run -d edge - ``` - -4. **Connectez-vous avec `far@id.jp`** - -5. **Créez une TOUTE NOUVELLE tâche** (titre: "Test Final") - -6. **Assignez `mody@d.fr`** - -7. **Regardez les logs** : - ``` - 📌 TaskService.assignUserToTask: ... - ✅ TaskService.assignUserToTask: Succès - Tâche après update: assignedTo=[riXsDCyTOVZi0gyr3pKZUxAkjT02] - ``` - -### ACTION 3 : Vérifier dans Firebase (30 secondes) - -1. **Allez sur** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - -2. **Cliquez sur la collection `tasks`** - -3. **Trouvez la tâche "Test Final"** - -4. **Regardez le champ `assignedTo`** : - - ✅ Doit contenir : `["riXsDCyTOVZi0gyr3pKZUxAkjT02"]` - - ❌ Si vide `[]`, passez à la section "Plan B" - -## 🔧 PLAN B : Si ça ne fonctionne toujours pas - -### Test avec set() au lieu de arrayUnion() - -Le problème peut venir de `FieldValue.arrayUnion()`. Testons avec `set()` : - -1. **Modifiez `task_service.dart`**, ligne ~153 : - - **REMPLACEZ** : - - ```dart - await _firestore.collection('tasks').doc(taskId).update({ - 'assignedTo': FieldValue.arrayUnion([userIdToAssign]), - }); - ``` - - **PAR** : - - ```dart - // Lire d'abord la tâche - final doc = await _firestore.collection('tasks').doc(taskId).get(); - final currentAssignedTo = (doc.data()?['assignedTo'] as List?) ?? []; - - // Ajouter l'UID s'il n'existe pas déjà - final newAssignedTo = List.from(currentAssignedTo); - if (!newAssignedTo.contains(userIdToAssign)) { - newAssignedTo.add(userIdToAssign); - } - - // Utiliser set() avec merge - await _firestore.collection('tasks').doc(taskId).set({ - 'assignedTo': newAssignedTo, - }, SetOptions(merge: true)); - ``` - -2. **Hot Reload** (`r` dans le terminal) - -3. **Testez l'assignation à nouveau** - -4. **Vérifiez dans Firebase Data** - -## 🧪 PLAN C : Test de diagnostic - -Si même `set()` ne fonctionne pas, utilisez le fichier de test : - -1. **Ouvrez `lib/features/tasks/presentation/widgets/task_modal.dart`** - -2. **Importez le tester** : - - ```dart - import '../../data/test_firestore_update.dart'; - ``` - -3. **Ajoutez un bouton de test temporaire** dans `_buildCollaborationSection()` : - - ```dart - FirestoreTestButton( - taskId: widget.task!.id, - userIdToAssign: 'riXsDCyTOVZi0gyr3pKZUxAkjT02', - ), - ``` - -4. **Cliquez sur "Test avec arrayUnion"** - -5. **Regardez les logs** - ils vous diront exactement si Firestore accepte ou rejette - -## 📊 Checklist de diagnostic - -- [ ] Règles Firestore publiées (vérifiez qu'elles sont bien enregistrées) -- [ ] Application complètement redémarrée (pas juste hot reload) -- [ ] Nouvelle tâche créée APRÈS le déploiement des règles -- [ ] Logs montrent `Tâche après update: assignedTo=[UID]` -- [ ] Firebase Data montre le tableau non vide - -## 🎓 Pourquoi les logs mentent ? - -Le SDK Web Firestore : - -1. Fait la mise à jour **localement** (dans le cache) -2. Renvoie "succès" immédiatement -3. Envoie la requête au serveur **en arrière-plan** -4. Si le serveur rejette (règles), **n'informe PAS le client** - -C'est pour ça que : - -- Les logs montrent ✅ Succès -- Le badge s'affiche (avec les données locales) -- Mais Firebase Data reste vide - -## ✅ RÉSULTAT ATTENDU - -Après ACTION 1 + ACTION 2 : - -1. **Firebase Data** : `assignedTo: ["riXsDCyTOVZi0gyr3pKZUxAkjT02"]` ✅ -2. **UI Far** : Badge "1 assigné" visible ✅ -3. **Compte Mody** : Tâche visible dans la liste ✅ -4. **Badge dans la tâche** : "Créé par Far" visible ✅ - -## 🆘 Si rien ne fonctionne - -Envoyez-moi : - -1. Capture d'écran des règles dans Firebase Console -2. Capture d'écran de la tâche dans Firebase Data (montrant assignedTo vide) -3. Les logs complets après avoir testé ACTION 2 - -Mais normalement, **ACTION 1 (simplifier les règles) devrait suffire** ! 🎯 diff --git a/VERIFIER_ASSIGNATION.md b/VERIFIER_ASSIGNATION.md deleted file mode 100644 index 718733b..0000000 --- a/VERIFIER_ASSIGNATION.md +++ /dev/null @@ -1,177 +0,0 @@ -# 🔍 Vérification et Correction de l'Assignation - -## Problème identifié - -Les logs montrent que l'assignation "réussit" côté client mais le tableau `assignedTo` reste vide dans Firestore. Cela indique que **Firestore rejette silencieusement la mise à jour**. - -## 🎯 Solution en 3 étapes - -### Étape 1 : Déployer les règles simplifiées - -1. **Allez sur** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/rules - -2. **Remplacez TOUT par** : - -```plaintext -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - - // Règles pour la collection 'users' - match /users/{userId} { - allow read: if request.auth != null; - allow create: if request.auth != null && request.auth.uid == userId; - allow update: if request.auth != null && request.auth.uid == userId; - allow delete: if false; - } - - // Règles pour la collection 'tasks' - match /tasks/{taskId} { - // Lecture : autorisée si créateur OU assigné - allow read: if request.auth != null && ( - resource.data.userId == request.auth.uid || - request.auth.uid in resource.data.get('assignedTo', []) - ); - - // Création : autorisée si authentifié - allow create: if request.auth != null && - request.resource.data.userId == request.auth.uid; - - // Mise à jour : autorisée si créateur (userId ne change pas) - allow update: if request.auth != null && - resource.data.userId == request.auth.uid && - request.resource.data.userId == resource.data.userId; - - // Suppression : autorisée si créateur - allow delete: if request.auth != null && - resource.data.userId == request.auth.uid; - } - } -} -``` - -3. **Cliquez sur "Publier"** - -4. **Attendez 5 secondes** - -### Étape 2 : Ajouter le champ assignedTo aux tâches existantes - -Les tâches créées avant la fonctionnalité d'assignation n'ont PAS le champ `assignedTo`. Il faut l'initialiser. - -**Option A : Via la console Firebase (RECOMMANDÉ)** - -1. **Allez sur** : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - -2. **Pour chaque tâche qui n'a PAS le champ `assignedTo`** : - - Cliquez sur la tâche - - Cliquez sur "Ajouter un champ" - - Nom : `assignedTo` - - Type : `array` (tableau) - - Valeur : Laissez vide `[]` - - Cliquez sur "Ajouter" - -**Option B : Script dans la console Firebase** - -1. Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - -2. Ouvrez la console JavaScript du navigateur (F12) - -3. Collez ce script : - -```javascript -// ATTENTION : Exécuter ce script UNIQUEMENT si vous savez ce que vous faites -// Il modifie toutes les tâches qui n'ont pas le champ assignedTo - -const db = firebase.firestore(); -const batch = db.batch(); - -db.collection("tasks") - .get() - .then((snapshot) => { - let count = 0; - snapshot.docs.forEach((doc) => { - if (!doc.data().assignedTo) { - batch.update(doc.ref, { assignedTo: [] }); - count++; - } - }); - - if (count > 0) { - batch.commit().then(() => { - console.log(`✅ ${count} tâches mises à jour avec assignedTo: []`); - }); - } else { - console.log("✅ Toutes les tâches ont déjà le champ assignedTo"); - } - }); -``` - -**Option C : Créer une nouvelle tâche pour tester** - -Si vous voulez juste tester, créez une NOUVELLE tâche (qui aura automatiquement `assignedTo: []`) et testez l'assignation dessus. - -### Étape 3 : Tester l'assignation - -1. **Redémarrez votre app** : - - - Dans le terminal Flutter, tapez `R` (Hot Restart) - -2. **Ouvrez une tâche** - -3. **Cliquez sur "Gérer les utilisateurs assignés"** - -4. **Assignez un utilisateur** - -5. **Vérifiez dans les logs** : - - ``` - 📌 TaskService.assignUserToTask: taskId=xxx, userIdToAssign=yyy - ✅ TaskService.assignUserToTask: Succès - ``` - -6. **Vérifiez dans Firebase** : - - Allez sur : https://console.firebase.google.com/project/flutter-todo-web-305fb/firestore/data - - Sélectionnez la tâche - - Le champ `assignedTo` doit contenir : `["UID_de_l_utilisateur"]` - -## 🐛 Si ça ne fonctionne toujours pas - -### Vérification 1 : Les règles sont-elles vraiment déployées ? - -- Rafraîchissez la page des règles -- Vérifiez que la ligne 33 est bien : `request.resource.data.userId == resource.data.userId;` -- Pas de mention de `ownerName` - -### Vérification 2 : Y a-t-il des erreurs dans la console ? - -Regardez les logs Flutter. Si vous voyez : - -``` -❌ TaskService.assignUserToTask: Erreur permission-denied - Missing or insufficient permissions -``` - -Cela signifie que les règles ne sont pas déployées ou incorrectes. - -### Vérification 3 : Le champ assignedTo existe-t-il ? - -Dans Firestore Data, si le champ `assignedTo` n'existe pas du tout, créez-le manuellement : - -- Type : `array` -- Valeur : `[]` - -## 📊 Checklist finale - -- [ ] Règles Firestore déployées (version simplifiée) -- [ ] Toutes les tâches ont le champ `assignedTo` (même si vide) -- [ ] App redémarrée avec `R` -- [ ] Assignation testée -- [ ] Champ `assignedTo` vérifié dans Firebase Data -- [ ] Badge "X assigné(s)" visible dans l'UI - -## 🎯 Résultat attendu - -Après ces 3 étapes : - -1. **Dans l'UI** : Le badge "1 assigné" doit apparaître -2. **Dans Firebase** : `assignedTo: ["riXsDCyTOVZi0gyr3pKZUxAkjT02"]` -3. **Chez l'utilisateur assigné** : La tâche apparaît dans sa liste diff --git a/integration_test/app_flow_test.dart b/integration_test/app_flow_test.dart index b5f3b81..909224e 100644 --- a/integration_test/app_flow_test.dart +++ b/integration_test/app_flow_test.dart @@ -5,33 +5,6 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('app launches successfully', (WidgetTester tester) async { - // Test basique : vérifier que l'app démarre - // Note : Les tests d'intégration complets avec Firebase nécessitent - // une configuration spécifique et un environnement de test - expect(true, isTrue); }); - - // TODO: Ajouter des tests d'intégration Firebase une fois configuré - // Les tests d'intégration avec Firebase nécessitent : - // 1. Un projet Firebase de test - // 2. Des credentials de test - // 3. L'émulateur Firestore pour les tests - // - // Exemple de test à ajouter plus tard : - // testWidgets('login then create task', (WidgetTester tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 4)); - // - // // Remplir le formulaire de connexion avec un compte de test - // await tester.enterText( - // find.byType(TextFormField).at(0), 'test@example.com'); - // await tester.enterText( - // find.byType(TextFormField).at(1), 'password123'); - // await tester.tap(find.text('Se connecter')); - // await tester.pumpAndSettle(); - // - // // Vérifier que l'utilisateur est connecté et peut créer une tâche - // expect(find.text('Mes tâches'), findsOneWidget); - // }); } diff --git a/lib/app.dart b/lib/app.dart index d2ed3d0..b454bc7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,7 +8,6 @@ import 'features/auth/data/auth_service.dart'; import 'features/tasks/presentation/providers/task_provider.dart'; import 'features/tasks/data/task_service.dart'; -/// Widget racine de l'application avec support du thème dark/light class TodoApp extends StatelessWidget { const TodoApp({super.key}); @@ -16,16 +15,9 @@ class TodoApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ - // Provider de thème ChangeNotifierProvider(create: (_) => ThemeProvider()), - - // Service d'authentification ChangeNotifierProvider(create: (_) => AuthService()), - - // Service Firestore pour les tâches Provider(create: (_) => TaskService()), - - // Provider des tâches (dépendant du TaskService) ChangeNotifierProxyProvider( create: (_) => TaskProvider(), update: (_, taskService, taskProvider) { @@ -37,11 +29,6 @@ class TodoApp extends StatelessWidget { ], child: Consumer( builder: (context, themeProvider, child) { - print( - '🌙 App: Reconstruction avec themeMode: ${themeProvider.themeMode}', - ); // ✅ Debug - - // ✅ INITIALISATION CORRIGÉE WidgetsBinding.instance.addPostFrameCallback((_) { if (!themeProvider.isDarkMode && themeProvider.themeMode == ThemeMode.system) { @@ -52,16 +39,10 @@ class TodoApp extends StatelessWidget { return MaterialApp.router( title: 'Todo List Pro', debugShowCheckedModeBanner: false, - - // ✅ THÈMES CONFIGURÉS theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, - themeMode: - themeProvider.themeMode, // ✅ Utilise directement le themeMode - // Configuration de la navigation + themeMode: themeProvider.themeMode, routerConfig: AppRouter.router, - - // Configuration pour l'accessibilité builder: (context, child) { return MediaQuery( data: MediaQuery.of( diff --git a/lib/features/tasks/data/firestore_task_service.dart b/lib/features/tasks/data/firestore_task_service.dart index 0447670..2567060 100644 --- a/lib/features/tasks/data/firestore_task_service.dart +++ b/lib/features/tasks/data/firestore_task_service.dart @@ -2,21 +2,14 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../domain/models/task.dart'; -/// Service Firestore pour gérer les tâches -/// Gère la communication avec Firebase Firestore class FirestoreTaskService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; - // Nom de la collection dans Firestore static const String _collectionName = 'tasks'; - /// Référence à la collection des tâches CollectionReference> get _tasksCollection => _firestore.collection(_collectionName); - // ==================== CONVERSION ==================== - - /// Convertir un modèle Task en Map pour Firestore Map _taskToMap(Task task) { return { 'title': task.title, @@ -34,7 +27,6 @@ class FirestoreTaskService { }; } - /// Convertir un document Firestore en modèle Task Task _mapToTask(String id, Map data) { return Task( id: id, @@ -51,27 +43,16 @@ class FirestoreTaskService { ); } - // ==================== OPÉRATIONS CRUD ==================== - - /// Créer une nouvelle tâche dans Firestore Future createTask(Task task) async { try { final taskData = _taskToMap(task); - print('🔥 Firestore createTask - Données envoyées: $taskData'); - print('🔥 userId: ${taskData['userId']}'); - print('🔥 ownerName: ${taskData['ownerName']}'); - print('🔥 tags: ${taskData['tags']}'); - final docRef = await _tasksCollection.add(taskData); - print('✅ Tâche créée avec succès: ${docRef.id}'); return docRef.id; } catch (e) { - print('❌ Erreur Firestore createTask: $e'); throw Exception('Erreur lors de la création de la tâche: $e'); } } - /// Récupérer toutes les tâches Future> getAllTasks() async { try { final snapshot = await _tasksCollection @@ -86,7 +67,6 @@ class FirestoreTaskService { } } - /// Observer toutes les tâches en temps réel Stream> watchAllTasks() { try { return _tasksCollection @@ -102,7 +82,6 @@ class FirestoreTaskService { } } - /// Récupérer une tâche par son ID Future getTaskById(String id) async { try { final doc = await _tasksCollection.doc(id).get(); @@ -115,7 +94,6 @@ class FirestoreTaskService { } } - /// Observer les tâches par statut Stream> watchTasksByStatus(bool isCompleted) { try { return _tasksCollection @@ -132,7 +110,6 @@ class FirestoreTaskService { } } - /// Mettre à jour une tâche Future updateTask(Task task) async { try { await _tasksCollection.doc(task.id).update(_taskToMap(task)); @@ -141,7 +118,6 @@ class FirestoreTaskService { } } - /// Supprimer une tâche Future deleteTask(String id) async { try { await _tasksCollection.doc(id).delete(); @@ -150,7 +126,6 @@ class FirestoreTaskService { } } - /// Basculer l'état de complétion d'une tâche Future toggleTaskCompletion(String id) async { try { final task = await getTaskById(id); @@ -164,7 +139,6 @@ class FirestoreTaskService { } } - /// Supprimer toutes les tâches complétées Future deleteCompletedTasks() async { try { final snapshot = await _tasksCollection @@ -185,7 +159,6 @@ class FirestoreTaskService { } } - /// Récupérer les statistiques des tâches Future> getTaskStats() async { try { final allTasks = await getAllTasks(); diff --git a/lib/features/tasks/data/task_service.dart b/lib/features/tasks/data/task_service.dart index 69e113d..2e33d4b 100644 --- a/lib/features/tasks/data/task_service.dart +++ b/lib/features/tasks/data/task_service.dart @@ -2,47 +2,28 @@ import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; import '../domain/models/task.dart'; -/// Service pour CRUD des tâches dans Cloud Firestore. class TaskService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; - // Using per-user subcollections now; no global _tasksCollection required. - - /// Retourne un stream des tâches visibles pour l'utilisateur courant - /// (tâches créées par lui OU tâches où il est assigné) - /// Attend que l'utilisateur soit connecté avant de commencer à écouter Stream> tasksStream() { - // Écouter les changements d'authentification et basculer sur le stream approprié return _auth.authStateChanges().switchMap((user) { if (user == null) { - // Pas d'utilisateur connecté, retourner un stream vide return Stream.value([]); } final uid = user.uid; final col = _firestore.collection('tasks'); - debugPrint('🔄 TaskService.tasksStream: Démarrage pour utilisateur $uid'); - - // Firestore ne supporte pas les requêtes OR directement. - // On va donc récupérer les deux streams et les combiner: - // 1. Tâches créées par l'utilisateur - // 2. Tâches où l'utilisateur est assigné - final myTasksStream = col .where('userId', isEqualTo: uid) .orderBy('createdAt', descending: true) .snapshots() .map((snap) { - debugPrint( - '🔄 myTasksStream: ${snap.docs.length} tâches créées par moi', - ); return snap.docs .map((d) => Task.fromMap(d.data(), id: d.id)) .toList(); @@ -53,30 +34,16 @@ class TaskService { .orderBy('createdAt', descending: true) .snapshots() .map((snap) { - debugPrint( - '🔄 assignedTasksStream: ${snap.docs.length} tâches assignées à moi', - ); - for (var doc in snap.docs) { - debugPrint( - ' - ${doc.data()['title']}: assignedTo=${doc.data()['assignedTo']}', - ); - } return snap.docs .map((d) => Task.fromMap(d.data(), id: d.id)) .toList(); }); - // Combiner les deux streams et éliminer les doublons return Rx.combineLatest2< List, List, List >(myTasksStream, assignedTasksStream, (myTasks, assignedTasks) { - debugPrint( - '🔄 Combinaison: ${myTasks.length} créées + ${assignedTasks.length} assignées', - ); - - // Créer un Map pour éliminer les doublons (par id) final Map uniqueTasks = {}; for (var task in myTasks) { uniqueTasks[task.id] = task; @@ -85,17 +52,9 @@ class TaskService { uniqueTasks[task.id] = task; } - // Retourner la liste triée par date de création final allTasks = uniqueTasks.values.toList(); allTasks.sort((a, b) => b.createdAt.compareTo(a.createdAt)); - debugPrint('🔄 Total final: ${allTasks.length} tâches'); - for (var task in allTasks) { - debugPrint( - ' - ${task.title}: créateur=${task.ownerId}, assignés=${task.assignedTo}', - ); - } - return allTasks; }); }); @@ -105,7 +64,6 @@ class TaskService { final uid = _auth.currentUser?.uid; if (uid == null) throw Exception('Utilisateur non authentifié'); try { - // Ensure owner fields are included. Try to get displayName from auth or users collection String ownerName = _auth.currentUser?.displayName ?? ''; if (ownerName.isEmpty) { try { @@ -113,14 +71,12 @@ class TaskService { ownerName = userDoc.data()?['name'] ?? userDoc.data()?['displayName'] ?? ''; } catch (_) { - // ignore errors reading user doc; ownerName can stay empty + // ignore } } final data = task.toMap(); - // createdAt will be set server-side for consistency data.remove('createdAt'); - // Override owner fields to be safe data['userId'] = uid; data['ownerName'] = ownerName; @@ -129,7 +85,6 @@ class TaskService { 'createdAt': FieldValue.serverTimestamp(), }); } on FirebaseException catch (e) { - // Provide clearer message for permission issues throw Exception('Firestore addTask failed: ${e.code} ${e.message}'); } } @@ -139,7 +94,6 @@ class TaskService { if (uid == null) throw Exception('Utilisateur non authentifié'); try { final data = task.toMap(); - // Prevent owner fields from being changed by client data.remove('createdAt'); data.remove('userId'); data.remove('ownerName'); @@ -173,53 +127,25 @@ class TaskService { } } - /// Assigner un utilisateur à une tâche Future assignUserToTask(String taskId, String userIdToAssign) async { final uid = _auth.currentUser?.uid; if (uid == null) throw Exception('Utilisateur non authentifié'); try { - debugPrint( - '📌 TaskService.assignUserToTask: taskId=$taskId, userIdToAssign=$userIdToAssign', - ); - debugPrint(' Current user UID: $uid'); - - // Vérifier d'abord que la tâche existe et a le champ assignedTo final taskDoc = await _firestore.collection('tasks').doc(taskId).get(); if (!taskDoc.exists) { - debugPrint('❌ La tâche n\'existe pas: $taskId'); throw Exception('Tâche introuvable'); } - final taskData = taskDoc.data(); - debugPrint( - ' Tâche actuelle: userId=${taskData?['userId']}, assignedTo=${taskData?['assignedTo']}', - ); - - // Mettre à jour avec arrayUnion await _firestore.collection('tasks').doc(taskId).update({ 'assignedTo': FieldValue.arrayUnion([userIdToAssign]), }); - - debugPrint('✅ TaskService.assignUserToTask: Succès'); - - // Vérifier que la mise à jour a bien été appliquée - final updatedDoc = await _firestore.collection('tasks').doc(taskId).get(); - final updatedData = updatedDoc.data(); - debugPrint( - ' Tâche après update: assignedTo=${updatedData?['assignedTo']}', - ); } on FirebaseException catch (e) { - debugPrint( - '❌ TaskService.assignUserToTask: Erreur ${e.code} - ${e.message}', - ); - debugPrint(' Details: ${e.toString()}'); throw Exception( 'Firestore assignUserToTask failed: ${e.code} ${e.message}', ); } } - /// Retirer un utilisateur assigné d'une tâche Future unassignUserFromTask( String taskId, String userIdToRemove, @@ -227,24 +153,16 @@ class TaskService { final uid = _auth.currentUser?.uid; if (uid == null) throw Exception('Utilisateur non authentifié'); try { - debugPrint( - '📌 TaskService.unassignUserFromTask: taskId=$taskId, userIdToRemove=$userIdToRemove', - ); await _firestore.collection('tasks').doc(taskId).update({ 'assignedTo': FieldValue.arrayRemove([userIdToRemove]), }); - debugPrint('✅ TaskService.unassignUserFromTask: Succès'); } on FirebaseException catch (e) { - debugPrint( - '❌ TaskService.unassignUserFromTask: Erreur ${e.code} - ${e.message}', - ); throw Exception( 'Firestore unassignUserFromTask failed: ${e.code} ${e.message}', ); } } - /// Récupérer la liste des utilisateurs (pour l'assignation) Future>> getAllUsers() async { final uid = _auth.currentUser?.uid; if (uid == null) throw Exception('Utilisateur non authentifié'); @@ -258,7 +176,7 @@ class TaskService { 'email': doc.data()['email'] ?? '', }, ) - .where((user) => user['id'] != uid) // Exclure l'utilisateur courant + .where((user) => user['id'] != uid) .toList(); } on FirebaseException catch (e) { throw Exception('Firestore getAllUsers failed: ${e.code} ${e.message}'); diff --git a/lib/features/tasks/presentation/providers/task_provider.dart b/lib/features/tasks/presentation/providers/task_provider.dart index 8c319b6..d9f1b10 100644 --- a/lib/features/tasks/presentation/providers/task_provider.dart +++ b/lib/features/tasks/presentation/providers/task_provider.dart @@ -5,9 +5,7 @@ import 'package:flutter/foundation.dart'; import '../../domain/models/task.dart'; import '../../data/task_service.dart'; -/// Provider pour gérer l'état des tâches class TaskProvider extends ChangeNotifier { - // ===== DONNÉES PRIVÉES ===== final List _tasks = []; late TaskService _taskService; StreamSubscription>? _tasksSub; @@ -16,29 +14,19 @@ class TaskProvider extends ChangeNotifier { bool _isLoading = false; String? _errorMessage; - // ===== GETTERS PUBLICS ===== - - /// Liste de toutes les tâches List get allTasks => List.unmodifiable(_tasks); String? get errorMessage => _errorMessage; - /// Liste des tâches filtrées et triées List get filteredTasks { var filtered = _applyFilter(_tasks); var sorted = _applySort(filtered); return sorted; } - /// Filtre actuel TaskFilter get currentFilter => _currentFilter; - - /// Tri actuel TaskSort get currentSort => _currentSort; - - /// État de chargement bool get isLoading => _isLoading; - /// Statistiques TaskStats get stats { final total = _tasks.length; final completed = _tasks.where((task) => task.isCompleted).length; @@ -57,7 +45,6 @@ class TaskProvider extends ChangeNotifier { ); } - /// Permet d'injecter le service et d'écouter le stream void setTaskService(TaskService service) { _taskService = service; _tasksSub?.cancel(); @@ -74,7 +61,6 @@ class TaskProvider extends ChangeNotifier { notifyListeners(); }, onError: (e, st) { - // Firestore can emit errors (for example when a required index is missing). _errorMessage = e?.toString() ?? 'Erreur inconnue sur Firestore'; _isLoading = false; notifyListeners(); @@ -92,25 +78,18 @@ class TaskProvider extends ChangeNotifier { super.dispose(); } - // ===== ACTIONS CRUD ===== - - /// Ajouter une nouvelle tâche Future addTask(Task task) async { await _taskService.addTask(task); - // la mise à jour arrive via le stream } - /// Modifier une tâche existante Future updateTask(Task updatedTask) async { await _taskService.updateTask(updatedTask); } - /// Supprimer une tâche Future deleteTask(String taskId) async { await _taskService.deleteTask(taskId); } - /// Basculer l'état de completion d'une tâche Future toggleTaskCompletion(String taskId) async { final index = _tasks.indexWhere((task) => task.id == taskId); if (index != -1) { @@ -119,38 +98,28 @@ class TaskProvider extends ChangeNotifier { } } - /// Assigner un utilisateur à une tâche Future assignUserToTask(String taskId, String userId) async { await _taskService.assignUserToTask(taskId, userId); } - /// Retirer un utilisateur assigné d'une tâche Future unassignUserFromTask(String taskId, String userId) async { await _taskService.unassignUserFromTask(taskId, userId); } - /// Récupérer la liste de tous les utilisateurs Future>> getAllUsers() async { return await _taskService.getAllUsers(); } - // ===== FILTRES ET TRI ===== - - /// Changer le filtre void setFilter(TaskFilter filter) { _currentFilter = filter; notifyListeners(); } - /// Changer le tri void setSort(TaskSort sort) { _currentSort = sort; notifyListeners(); } - // ===== MÉTHODES PRIVÉES ===== - - /// Appliquer le filtre actuel List _applyFilter(List tasks) { switch (_currentFilter) { case TaskFilter.all: @@ -168,7 +137,6 @@ class TaskProvider extends ChangeNotifier { } } - /// Appliquer le tri actuel List _applySort(List tasks) { switch (_currentSort) { case TaskSort.createdAt: @@ -182,9 +150,6 @@ class TaskProvider extends ChangeNotifier { } } - // ===== DONNÉES DE TEST ===== - - /// Charger des données de test void loadTestData() { _isLoading = true; notifyListeners(); @@ -239,7 +204,6 @@ class TaskProvider extends ChangeNotifier { } } -/// Filtres disponibles pour les tâches enum TaskFilter { all('Toutes'), pending('À faire'), @@ -250,7 +214,6 @@ enum TaskFilter { final String label; } -/// Options de tri pour les tâches enum TaskSort { createdAt('Date de création'), dueDate('Date d\'échéance'); @@ -259,7 +222,6 @@ enum TaskSort { final String label; } -/// Statistiques des tâches class TaskStats { final int total; final int completed; diff --git a/lib/main.dart b/lib/main.dart index eb4a71e..163a47b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,42 +5,24 @@ import 'firebase_options.dart'; import 'app.dart'; -/// Point d'entrée principal de l'application -/// -/// Cette fonction main() est appelée au démarrage de l'app. -/// Elle configure l'environnement Flutter avant de lancer l'interface void main() async { - // ===== INITIALISATION FLUTTER ===== - // OBLIGATOIRE quand on fait des opérations async avant runApp() WidgetsFlutterBinding.ensureInitialized(); - // ===== CONFIGURATION DE L'INTERFACE SYSTÈME ===== - // Configure la barre de statut et la navigation (Android/iOS) SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( - // Barre de statut transparente avec icônes sombres statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, - - // Barre de navigation système (Android) systemNavigationBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.dark, ), ); - // ===== ORIENTATION DE L'ÉCRAN ===== - // Force l'orientation portrait pour une meilleure UX mobile await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, // Portrait normal - DeviceOrientation.portraitDown, // Portrait inversé + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, ]); - // TODO: Le Lead Auth initialisera Firebase ici - // Initialisation de Firebase avec options multi-plateformes - // Sur web, FirebaseOptions est nécessaire. Nous utilisons le fichier - // `lib/firebase_options.dart` (généré ou rempli manuellement). await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - // ===== LANCEMENT DE L'APPLICATION ===== runApp(const TodoApp()); } From 6c705c3666bbaa3546bc373a7e5067718264baa0 Mon Sep 17 00:00:00 2001 From: Loris Date: Thu, 6 Nov 2025 12:13:50 +0100 Subject: [PATCH 4/4] Suppression des commentaires useless et fichiers innutiles poluant le code --- integration_test/app_flow_test.dart | 10 --- lib/core/router/app_router.dart | 45 +------------ lib/core/theme/app_colors.dart | 41 ++---------- lib/core/theme/app_text_styles.dart | 12 +--- lib/core/theme/app_theme.dart | 34 ---------- lib/core/theme/theme_provider.dart | 30 +-------- lib/features/auth/data/auth_service.dart | 29 --------- .../presentation/screens/login_screen.dart | 46 -------------- .../presentation/screens/register_screen.dart | 48 -------------- lib/features/auth/ui/auth_page.dart | 1 - lib/features/splash/ui/splash_page.dart | 1 - lib/features/tasks/data/migrate_tasks.dart | 28 +-------- lib/features/tasks/data/task_repository.dart | 14 ----- lib/features/tasks/data/task_service.dart | 1 - .../tasks/data/test_firestore_update.dart | 63 ++----------------- lib/features/tasks/domain/models/task.dart | 15 +---- .../tasks/domain/models/task_category.dart | 1 - .../screens/migration_screen.dart | 2 - .../screens/task_list_screen.dart | 24 +------ .../widgets/assign_users_dialog.dart | 19 ------ .../presentation/widgets/empty_state.dart | 8 --- .../presentation/widgets/tag_selector.dart | 1 - .../widgets/task_filter_chips.dart | 16 ++--- .../presentation/widgets/task_modal.dart | 52 +++------------ .../widgets/task_sort_button.dart | 1 - .../presentation/widgets/task_stats_card.dart | 4 -- .../tasks/presentation/widgets/task_tile.dart | 32 ---------- .../tasks/ui/debug_firestore_page.dart | 12 ---- .../tasks/ui/firestore_test_widget.dart | 4 -- lib/shared/widgets/custom_button.dart | 1 - lib/shared/widgets/custom_text_field.dart | 20 ------ lib/shared/widgets/splash_screen.dart | 36 ++--------- lib/shared/widgets/theme_switch.dart | 16 +---- 33 files changed, 35 insertions(+), 632 deletions(-) delete mode 100644 integration_test/app_flow_test.dart diff --git a/integration_test/app_flow_test.dart b/integration_test/app_flow_test.dart deleted file mode 100644 index 909224e..0000000 --- a/integration_test/app_flow_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('app launches successfully', (WidgetTester tester) async { - expect(true, isTrue); - }); -} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 618c157..6d7ee48 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -// Imports des écrans import '../../features/auth/presentation/screens/login_screen.dart'; import '../../features/auth/presentation/screens/register_screen.dart'; import '../../features/tasks/presentation/screens/task_detail_screen.dart'; @@ -9,16 +8,7 @@ import '../../features/tasks/presentation/screens/task_form_screen.dart'; import '../../features/tasks/presentation/screens/task_list_screen.dart'; import '../../shared/widgets/splash_screen.dart'; -/// Configuration centralisée de la navigation avec go_router -/// -/// go_router est le nouveau standard pour la navigation Flutter : -/// - Navigation déclarative (on déclare les routes, pas les actions) -/// - Support natif du web (URLs dans la barre d'adresse) -/// - Navigation typée (pas d'erreurs de routes) -/// - Gestion automatique de la pile de navigation class AppRouter { - // ===== CONSTANTES DE ROUTES ===== - // Toujours utiliser des constantes pour éviter les erreurs de frappe static const String splash = '/'; static const String login = '/login'; static const String register = '/register'; @@ -27,24 +17,15 @@ class AppRouter { static const String taskEdit = '/tasks/:id/edit'; static const String taskDetail = '/tasks/:id'; - /// Configuration du routeur principal static final GoRouter router = GoRouter( - // Route de démarrage de l'app initialLocation: splash, - - // Gestion des erreurs de navigation errorBuilder: (context, state) => const _ErrorScreen(), - - // ===== DÉFINITION DES ROUTES ===== routes: [ - // ===== ROUTE SPLASH ===== GoRoute( path: splash, name: 'splash', builder: (context, state) => const SplashScreen(), ), - - // ===== ROUTES D'AUTHENTIFICATION ===== GoRoute( path: login, name: 'login', @@ -56,34 +37,24 @@ class AppRouter { name: 'register', builder: (context, state) => const RegisterScreen(), ), - - // ===== ROUTES DES TÂCHES ===== - - // Liste des tâches (écran principal) GoRoute( path: tasks, name: 'tasks', builder: (context, state) => const TaskListScreen(), ), - - // Création d'une nouvelle tâche GoRoute( path: taskForm, name: 'task-form', builder: (context, state) => const TaskFormScreen(), ), - - // Édition d'une tâche existante GoRoute( path: taskEdit, name: 'task-edit', builder: (context, state) { final taskId = state.pathParameters['id']!; - return TaskFormScreen(taskId: taskId); // Mode édition + return TaskFormScreen(taskId: taskId); }, ), - - // Détail d'une tâche (lecture seule) GoRoute( path: taskDetail, name: 'task-detail', @@ -96,12 +67,7 @@ class AppRouter { ); } -/// Extension pour simplifier la navigation dans l'app -/// -/// Cette extension ajoute des méthodes pratiques au BuildContext -/// Utilisation : context.goToTasks() au lieu de context.go('/tasks') extension AppRouterExtension on BuildContext { - // ===== NAVIGATION SIMPLE (remplace la page actuelle) ===== void goToSplash() => go(AppRouter.splash); void goToLogin() => go(AppRouter.login); void goToRegister() => go(AppRouter.register); @@ -109,18 +75,11 @@ extension AppRouterExtension on BuildContext { void goToTaskForm() => go(AppRouter.taskForm); void goToTaskEdit(String taskId) => go('/tasks/$taskId/edit'); void goToTaskDetail(String taskId) => go('/tasks/$taskId'); - - // ===== NAVIGATION AVEC EMPILAGE (garde la page précédente) ===== void pushTaskForm() => push(AppRouter.taskForm); void pushTaskDetail(String taskId) => push('/tasks/$taskId'); - - // ===== RETOUR EN ARRIÈRE ===== void goBack() => pop(); } -/// Écran d'erreur personnalisé -/// -/// Affiché quand une route n'existe pas ou qu'il y a une erreur de navigation class _ErrorScreen extends StatelessWidget { const _ErrorScreen(); @@ -131,7 +90,7 @@ class _ErrorScreen extends StatelessWidget { title: const Text('Erreur'), leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => context.goToTasks(), // Retour à l'accueil + onPressed: () => context.goToTasks(), ), ), body: const Center( diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 99a5942..59e27a0 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -1,24 +1,16 @@ import 'package:flutter/material.dart'; -/// Couleurs de l'application avec support dark/light class AppColors { - // ===== COULEURS PRINCIPALES ===== static const Color primary = Color(0xFF6366F1); static const Color secondary = Color(0xFF8B5CF6); static const Color tertiary = Color(0xFF06B6D4); - - // ===== COULEURS SYSTÈME ===== static const Color success = Color(0xFF10B981); static const Color warning = Color(0xFFF59E0B); static const Color error = Color(0xFFEF4444); static const Color info = Color(0xFF3B82F6); - - // ===== COULEURS COMMUNES ===== static const Color onPrimary = Colors.white; static const Color onSecondary = Colors.white; static const Color onError = Colors.white; - - // ===== THÈME CLAIR ===== static const Color lightBackground = Color(0xFFFAFAFA); static const Color lightSurface = Color(0xFFFFFFFF); static const Color lightSurfaceVariant = Color(0xFFF3F4F6); @@ -26,23 +18,13 @@ class AppColors { static const Color lightOnSurfaceVariant = Color(0xFF6B7280); static const Color lightOnBackground = Color(0xFF1F2937); static const Color lightOutline = Color(0xFFE5E7EB); - - // ===== THÈME SOMBRE - COULEURS AMÉLIORÉES ===== static const Color darkBackground = Color(0xFF0F172A); static const Color darkSurface = Color(0xFF1E293B); static const Color darkSurfaceVariant = Color(0xFF334155); - static const Color darkOnSurface = Color( - 0xFFF1F5F9, - ); // ✅ Plus clair pour meilleure lisibilité - static const Color darkOnSurfaceVariant = Color( - 0xFFCBD5E1, - ); // ✅ AMÉLIORÉ : Plus clair et contrasté - static const Color darkOnBackground = Color( - 0xFFF8FAFC, - ); // ✅ AMÉLIORÉ : Encore plus clair + static const Color darkOnSurface = Color(0xFFF1F5F9); + static const Color darkOnSurfaceVariant = Color(0xFFCBD5E1); + static const Color darkOnBackground = Color(0xFFF8FAFC); static const Color darkOutline = Color(0xFF475569); - - // ===== GRADIENTS ===== static const LinearGradient primaryGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -55,30 +37,24 @@ class AppColors { colors: [darkSurface, darkSurfaceVariant], ); - // ===== MÉTHODES DYNAMIQUES ===== - - /// Background selon le thème static Color getBackground(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkBackground : lightBackground; } - /// Surface selon le thème static Color getSurface(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkSurface : lightSurface; } - /// Surface variant selon le thème static Color getSurfaceVariant(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkSurfaceVariant : lightSurfaceVariant; } - /// OnSurface selon le thème static Color getOnSurface(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkOnSurface @@ -91,37 +67,30 @@ class AppColors { : lightOnSurfaceVariant; } - /// OnBackground selon le thème static Color getOnBackground(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkOnBackground : lightOnBackground; } - /// Outline selon le thème static Color getOutline(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? darkOutline : lightOutline; } - // ===== COULEURS SPÉCIALES POUR TEXTES ===== - - /// Couleur pour les titres de section en mode dark static Color getSectionTitle(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? const Color(0xFFE2E8F0) - : const Color(0xFF374151); // Gris foncé en light + : const Color(0xFF374151); } - /// Couleur pour les labels/descriptions en mode dark static Color getLabel(BuildContext context) { return Theme.of(context).brightness == Brightness.dark ? const Color(0xFFCBD5E1) - : const Color(0xFF6B7280); // Gris moyen en light + : const Color(0xFF6B7280); } - // ===== COMPATIBILITÉ (pour l'ancien code) ===== static const Color background = lightBackground; static const Color surface = lightSurface; static const Color surfaceVariant = lightSurfaceVariant; diff --git a/lib/core/theme/app_text_styles.dart b/lib/core/theme/app_text_styles.dart index fbbe9a4..c61546d 100644 --- a/lib/core/theme/app_text_styles.dart +++ b/lib/core/theme/app_text_styles.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'app_colors.dart'; -/// Styles de texte de l'application class AppTextStyles { - // ===== TITRES ===== static TextStyle headlineLarge(BuildContext context) => TextStyle( fontSize: 32, fontWeight: FontWeight.bold, @@ -23,7 +21,6 @@ class AppTextStyles { color: AppColors.getOnSurface(context), ); - // ===== TITRES DE SECTION ===== static TextStyle titleLarge(BuildContext context) => TextStyle( fontSize: 22, fontWeight: FontWeight.w600, @@ -42,14 +39,12 @@ class AppTextStyles { color: AppColors.getOnSurface(context), ); - // ===== ÉTIQUETTES ===== static TextStyle labelLarge(BuildContext context) => TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.getOnSurfaceVariant(context), ); - // ===== CORPS DE TEXTE ===== static TextStyle bodyLarge(BuildContext context) => TextStyle( fontSize: 16, fontWeight: FontWeight.normal, @@ -68,7 +63,6 @@ class AppTextStyles { color: AppColors.getOnSurfaceVariant(context), ); - // ===== STYLES SPÉCIAUX ===== static TextStyle taskTitle(BuildContext context) => TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -85,23 +79,21 @@ class AppTextStyles { static TextStyle taskDescription(BuildContext context) => TextStyle(fontSize: 14, color: AppColors.getOnSurfaceVariant(context)); - // ===== TITRE SECTION SPÉCIAL (pour "Filtrer les tâches") ===== static TextStyle sectionTitle(BuildContext context) => TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.getSectionTitle(context), ); - // ===== STYLES CONSTANTS (pour compatibilité) ===== static const TextStyle constantTitleLarge = TextStyle( fontSize: 22, fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), // Couleur fixe pour les const + color: Color(0xFF1F2937), ); static const TextStyle constantBodyMedium = TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Color(0xFF6B7280), // Couleur fixe pour les const + color: Color(0xFF6B7280), ); } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index f00744e..5baa936 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -3,9 +3,7 @@ import 'package:flutter/services.dart'; import 'app_colors.dart'; -/// Configuration des thèmes de l'application class AppTheme { - // ===== RAYONS DE BORDURE ===== static const BorderRadius radiusSmall = BorderRadius.all(Radius.circular(8)); static const BorderRadius radiusMedium = BorderRadius.all( Radius.circular(12), @@ -15,12 +13,10 @@ class AppTheme { Radius.circular(24), ); - // ===== ESPACEMENT ===== static const EdgeInsets paddingSmall = EdgeInsets.all(8); static const EdgeInsets paddingMedium = EdgeInsets.all(16); static const EdgeInsets paddingLarge = EdgeInsets.all(24); - // ===== OMBRES ===== static List get shadowSmall => [ BoxShadow( color: Colors.black.withOpacity(0.05), @@ -45,13 +41,10 @@ class AppTheme { ), ]; - // ===== THÈME CLAIR ===== static ThemeData get lightTheme { return ThemeData( useMaterial3: true, brightness: Brightness.light, - - // Couleurs principales colorScheme: const ColorScheme.light( primary: AppColors.primary, secondary: AppColors.secondary, @@ -68,8 +61,6 @@ class AppTheme { surfaceVariant: AppColors.lightSurfaceVariant, onSurfaceVariant: AppColors.lightOnSurfaceVariant, ), - - // Configuration de l'AppBar appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, @@ -82,8 +73,6 @@ class AppTheme { fontWeight: FontWeight.w600, ), ), - - // Configuration des cartes cardTheme: CardThemeData( color: AppColors.lightSurface, elevation: 0, @@ -92,8 +81,6 @@ class AppTheme { side: const BorderSide(color: AppColors.lightOutline, width: 1), ), ), - - // Configuration des boutons elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, @@ -103,8 +90,6 @@ class AppTheme { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), ), - - // Configuration des champs de texte inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.lightSurfaceVariant, @@ -129,15 +114,11 @@ class AppTheme { borderSide: const BorderSide(color: AppColors.error, width: 2), ), ), - - // Configuration du FAB floatingActionButtonTheme: const FloatingActionButtonThemeData( backgroundColor: AppColors.primary, foregroundColor: Colors.white, elevation: 4, ), - - // Configuration des bottom sheets bottomSheetTheme: const BottomSheetThemeData( backgroundColor: AppColors.lightSurface, shape: RoundedRectangleBorder( @@ -147,13 +128,10 @@ class AppTheme { ); } - // ===== THÈME SOMBRE ===== static ThemeData get darkTheme { return ThemeData( useMaterial3: true, brightness: Brightness.dark, - - // Couleurs principales colorScheme: const ColorScheme.dark( primary: AppColors.primary, secondary: AppColors.secondary, @@ -170,8 +148,6 @@ class AppTheme { surfaceVariant: AppColors.darkSurfaceVariant, onSurfaceVariant: AppColors.darkOnSurfaceVariant, ), - - // Configuration de l'AppBar appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, @@ -184,8 +160,6 @@ class AppTheme { fontWeight: FontWeight.w600, ), ), - - // Configuration des cartes cardTheme: CardThemeData( color: AppColors.darkSurface, elevation: 0, @@ -194,8 +168,6 @@ class AppTheme { side: const BorderSide(color: AppColors.darkOutline, width: 1), ), ), - - // Configuration des boutons elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, @@ -205,8 +177,6 @@ class AppTheme { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), ), - - // Configuration des champs de texte inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.darkSurfaceVariant, @@ -231,15 +201,11 @@ class AppTheme { borderSide: const BorderSide(color: AppColors.error, width: 2), ), ), - - // Configuration du FAB floatingActionButtonTheme: const FloatingActionButtonThemeData( backgroundColor: AppColors.primary, foregroundColor: Colors.white, elevation: 4, ), - - // Configuration des bottom sheets bottomSheetTheme: const BottomSheetThemeData( backgroundColor: AppColors.darkSurface, shape: RoundedRectangleBorder( diff --git a/lib/core/theme/theme_provider.dart b/lib/core/theme/theme_provider.dart index 17e7b18..738adfd 100644 --- a/lib/core/theme/theme_provider.dart +++ b/lib/core/theme/theme_provider.dart @@ -1,76 +1,48 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// Provider pour gérer le thème de l'application class ThemeProvider extends ChangeNotifier { ThemeMode _themeMode = ThemeMode.system; bool _isDarkMode = false; - // ===== GETTERS ===== ThemeMode get themeMode => _themeMode; bool get isDarkMode => _isDarkMode; - // ===== MÉTHODES ===== - - /// Basculer entre thème clair et sombre void toggleTheme() { - print('🌙 ThemeProvider: Basculement du thème'); // ✅ Debug - if (_themeMode == ThemeMode.system) { - // Si on est en mode système, passer en mode manuel _isDarkMode = !_isDarkMode; _themeMode = _isDarkMode ? ThemeMode.dark : ThemeMode.light; } else { - // Si on est en mode manuel, basculer _isDarkMode = !_isDarkMode; _themeMode = _isDarkMode ? ThemeMode.dark : ThemeMode.light; } - print('🌙 Nouveau mode: $_themeMode, isDark: $_isDarkMode'); // ✅ Debug - _updateSystemChrome(); - notifyListeners(); // ✅ Important pour mettre à jour l'UI + notifyListeners(); } - /// Définir le thème explicitement void setThemeMode(ThemeMode mode) { - print('🌙 ThemeProvider: setThemeMode($mode)'); // ✅ Debug - _themeMode = mode; _isDarkMode = mode == ThemeMode.dark; _updateSystemChrome(); notifyListeners(); } - /// Suivre le thème système void followSystemTheme() { - print('🌙 ThemeProvider: Suivi du thème système'); // ✅ Debug - _themeMode = ThemeMode.system; notifyListeners(); } - /// Initialiser selon le thème système void initializeTheme(BuildContext context) { final brightness = MediaQuery.of(context).platformBrightness; _isDarkMode = brightness == Brightness.dark; - print( - '🌙 ThemeProvider: Initialisation - brightness: $brightness, isDark: $_isDarkMode', - ); // ✅ Debug - - // Si on n'a pas encore défini de mode, utiliser le système if (_themeMode == ThemeMode.system) { _updateSystemChrome(); } } - /// Mettre à jour la barre de statut void _updateSystemChrome() { - print( - '🌙 ThemeProvider: Mise à jour SystemChrome pour mode: $_themeMode', - ); // ✅ Debug - final isDark = _themeMode == ThemeMode.dark || (_themeMode == ThemeMode.system && _isDarkMode); diff --git a/lib/features/auth/data/auth_service.dart b/lib/features/auth/data/auth_service.dart index 4520f7a..062ebda 100644 --- a/lib/features/auth/data/auth_service.dart +++ b/lib/features/auth/data/auth_service.dart @@ -2,17 +2,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; -/// Service d'authentification utilisant Firebase Auth + création d'un document -/// utilisateur dans Cloud Firestore sous la collection `users`. -/// -/// Remarque: les fichiers natifs `google-services.json` (Android) et -/// `GoogleService-Info.plist` (iOS) doivent être ajoutés localement. - -/// Service d'authentification simple (en attendant Firebase) -/// -/// Credentials génériques pour tester l'app : -/// Email: admin@todolist.com -/// Password: 123456 class AuthService extends ChangeNotifier { final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; @@ -21,21 +10,18 @@ class AuthService extends ChangeNotifier { bool _isLoading = false; AuthService() { - // Écoute les changements d'auth et notifie _auth.authStateChanges().listen((u) { _user = u; notifyListeners(); }); } - // ===== GETTERS ===== User? get user => _user; bool get isLoggedIn => _user != null; bool get isLoading => _isLoading; String? get currentUserEmail => _user?.email; String? get userId => _user?.uid; - /// Connexion via Firebase Auth Future login(String email, String password) async { try { _isLoading = true; @@ -47,9 +33,6 @@ class AuthService extends ChangeNotifier { ); _user = credential.user; - debugPrint( - 'AuthService.login -> uid=${_user?.uid} email=${_user?.email}', - ); _isLoading = false; notifyListeners(); return AuthResult.success(); @@ -64,7 +47,6 @@ class AuthService extends ChangeNotifier { } } - /// Inscription avec création d'un document user en Firestore Future register( String email, String password, @@ -81,10 +63,6 @@ class AuthService extends ChangeNotifier { _user = credential.user; - // Créer/mettre à jour le document utilisateur - debugPrint( - 'AuthService.register -> uid=${_user?.uid} email=${_user?.email}', - ); if (_user != null) { try { await _firestore.collection('users').doc(_user!.uid).set({ @@ -93,9 +71,6 @@ class AuthService extends ChangeNotifier { 'createdAt': FieldValue.serverTimestamp(), }); } on FirebaseException catch (e) { - debugPrint( - 'AuthService.register firestore error: ${e.code} ${e.message}', - ); _isLoading = false; notifyListeners(); return AuthResult.error('Erreur Firestore: ${e.message} (${e.code})'); @@ -116,14 +91,12 @@ class AuthService extends ChangeNotifier { } } - /// Déconnexion Future logout() async { await _auth.signOut(); _user = null; notifyListeners(); } - /// Réinitialisation du mot de passe Future resetPassword(String email) async { try { _isLoading = true; @@ -147,14 +120,12 @@ class AuthService extends ChangeNotifier { } } - /// Optionnel: vérification d'état au démarrage (déjà couvert par authStateChanges) Future checkAuthStatus() async { _user = _auth.currentUser; notifyListeners(); } } -/// Résultat d'une opération d'authentification class AuthResult { final bool success; final String? errorMessage; diff --git a/lib/features/auth/presentation/screens/login_screen.dart b/lib/features/auth/presentation/screens/login_screen.dart index efedcdf..fee9151 100644 --- a/lib/features/auth/presentation/screens/login_screen.dart +++ b/lib/features/auth/presentation/screens/login_screen.dart @@ -9,13 +9,6 @@ import '../../../../shared/widgets/custom_button.dart'; import '../../../../shared/widgets/custom_text_field.dart'; import '../../data/auth_service.dart'; -/// Écran de connexion moderne et élégant -/// -/// Fonctionnalités : -/// - Design moderne avec gradient -/// - Formulaire avec validation -/// - Animation et feedback utilisateur -/// - Navigation fluide class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -25,16 +18,13 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State with SingleTickerProviderStateMixin { - // Contrôleurs pour les champs de texte final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final GlobalKey _formKey = GlobalKey(); - // États du formulaire bool _isLoading = false; bool _obscurePassword = true; - // Animation late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; @@ -43,7 +33,6 @@ class _LoginScreenState extends State void initState() { super.initState(); - // Configuration des animations _animationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, @@ -58,7 +47,6 @@ class _LoginScreenState extends State CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); - // Démarrer l'animation _animationController.forward(); } @@ -70,13 +58,11 @@ class _LoginScreenState extends State super.dispose(); } - /// Fonction de connexion (simulée pour l'instant) Future _handleLogin() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); - // Utiliser le service d'authentification final authService = context.read(); final result = await authService.login( _emailController.text.trim(), @@ -88,10 +74,8 @@ class _LoginScreenState extends State setState(() => _isLoading = false); if (result.success) { - // Navigation vers les tâches context.goToTasks(); } else { - // Afficher un message d'erreur ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result.errorMessage ?? 'Erreur lors de la connexion'), @@ -101,7 +85,6 @@ class _LoginScreenState extends State } } - /// Fonction pour gérer le mot de passe oublié Future _handleForgotPassword() async { final emailController = TextEditingController(); final formKey = GlobalKey(); @@ -148,7 +131,6 @@ class _LoginScreenState extends State ); if (emailToReset != null && mounted) { - // Simuler l'envoi de l'email setState(() => _isLoading = true); await Future.delayed(const Duration(seconds: 1)); @@ -198,29 +180,19 @@ class _LoginScreenState extends State child: Column( children: [ const SizedBox(height: 60), - - // ===== HEADER AVEC LOGO ===== _buildHeader(), - const SizedBox(height: 60), - - // ===== FORMULAIRE DE CONNEXION ===== _buildLoginForm(), - const SizedBox(height: 30), - - // ===== LIENS D'ACTIONS ===== _buildActionLinks(), ], ), ); } - /// Header avec logo et titre Widget _buildHeader() { return Column( children: [ - // Logo de l'app Container( width: 100, height: 100, @@ -243,8 +215,6 @@ class _LoginScreenState extends State ), const SizedBox(height: 24), - - // Titre principal const Text( 'Todo List Pro', style: TextStyle( @@ -255,8 +225,6 @@ class _LoginScreenState extends State ), const SizedBox(height: 8), - - // Sous-titre const Text( 'Organisez votre vie, une tâche à la fois', style: TextStyle(fontSize: 16, color: Colors.white70), @@ -266,7 +234,6 @@ class _LoginScreenState extends State ); } - /// Formulaire de connexion Widget _buildLoginForm() { return Container( padding: AppTheme.paddingLarge, @@ -286,7 +253,6 @@ class _LoginScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Titre du formulaire Text( 'Connexion', style: AppTextStyles.titleLarge(context), @@ -302,8 +268,6 @@ class _LoginScreenState extends State ), const SizedBox(height: 32), - - // Champ email CustomTextField( controller: _emailController, label: 'Email', @@ -314,8 +278,6 @@ class _LoginScreenState extends State ), const SizedBox(height: 16), - - // Champ mot de passe CustomTextField( controller: _passwordController, label: 'Mot de passe', @@ -333,8 +295,6 @@ class _LoginScreenState extends State ), const SizedBox(height: 24), - - // Bouton de connexion CustomButton( onPressed: _isLoading ? null : _handleLogin, isLoading: _isLoading, @@ -346,11 +306,9 @@ class _LoginScreenState extends State ); } - /// Liens d'actions (inscription, mot de passe oublié) Widget _buildActionLinks() { return Column( children: [ - // Lien vers inscription TextButton( onPressed: () => context.goToRegister(), child: const Text( @@ -358,8 +316,6 @@ class _LoginScreenState extends State style: TextStyle(color: Colors.white), ), ), - - // Lien mot de passe oublié TextButton( onPressed: _handleForgotPassword, child: const Text( @@ -371,7 +327,6 @@ class _LoginScreenState extends State ); } - /// Validation de l'email String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre email'; @@ -382,7 +337,6 @@ class _LoginScreenState extends State return null; } - /// Validation du mot de passe String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre mot de passe'; diff --git a/lib/features/auth/presentation/screens/register_screen.dart b/lib/features/auth/presentation/screens/register_screen.dart index 1c476b6..cf092bb 100644 --- a/lib/features/auth/presentation/screens/register_screen.dart +++ b/lib/features/auth/presentation/screens/register_screen.dart @@ -10,13 +10,6 @@ import '../../../../shared/widgets/custom_button.dart'; import '../../../../shared/widgets/custom_text_field.dart'; import '../../data/auth_service.dart'; -/// Écran d'inscription moderne et élégant -/// -/// Fonctionnalités : -/// - Design moderne avec gradient -/// - Formulaire avec validation -/// - Animation et feedback utilisateur -/// - Navigation fluide class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); @@ -26,7 +19,6 @@ class RegisterScreen extends StatefulWidget { class _RegisterScreenState extends State with SingleTickerProviderStateMixin { - // Contrôleurs pour les champs de texte final TextEditingController _nameController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @@ -34,12 +26,10 @@ class _RegisterScreenState extends State TextEditingController(); final GlobalKey _formKey = GlobalKey(); - // États du formulaire bool _isLoading = false; bool _obscurePassword = true; bool _obscureConfirmPassword = true; - // Animation late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; @@ -48,7 +38,6 @@ class _RegisterScreenState extends State void initState() { super.initState(); - // Configuration des animations _animationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, @@ -63,7 +52,6 @@ class _RegisterScreenState extends State CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); - // Démarrer l'animation _animationController.forward(); } @@ -77,13 +65,11 @@ class _RegisterScreenState extends State super.dispose(); } - /// Fonction d'inscription Future _handleRegister() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); - // Utiliser le service d'authentification fourni par Provider final authService = context.read(); final result = await authService.register( _emailController.text.trim(), @@ -96,7 +82,6 @@ class _RegisterScreenState extends State setState(() => _isLoading = false); if (result.success) { - // Afficher un message de succès ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Inscription réussie !'), @@ -104,10 +89,8 @@ class _RegisterScreenState extends State ), ); - // Navigation vers les tâches context.goToTasks(); } else { - // Afficher un message d'erreur ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result.errorMessage ?? 'Erreur lors de l\'inscription'), @@ -146,29 +129,19 @@ class _RegisterScreenState extends State child: Column( children: [ const SizedBox(height: 40), - - // ===== HEADER AVEC LOGO ===== _buildHeader(), - const SizedBox(height: 40), - - // ===== FORMULAIRE D'INSCRIPTION ===== _buildRegisterForm(), - const SizedBox(height: 24), - - // ===== LIEN VERS CONNEXION ===== _buildLoginLink(), ], ), ); } - /// Header avec logo et titre Widget _buildHeader() { return Column( children: [ - // Logo de l'app Container( width: 100, height: 100, @@ -191,8 +164,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 24), - - // Titre principal const Text( 'Créer un compte', style: TextStyle( @@ -203,8 +174,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 8), - - // Sous-titre const Text( 'Rejoignez-nous et organisez vos tâches', style: TextStyle(fontSize: 16, color: Colors.white70), @@ -214,7 +183,6 @@ class _RegisterScreenState extends State ); } - /// Formulaire d'inscription Widget _buildRegisterForm() { return Container( padding: AppTheme.paddingLarge, @@ -234,7 +202,6 @@ class _RegisterScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Titre du formulaire Text( 'Inscription', style: AppTextStyles.titleLarge(context), @@ -250,8 +217,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 32), - - // Champ nom CustomTextField( controller: _nameController, label: 'Nom complet', @@ -262,8 +227,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 16), - - // Champ email CustomTextField( controller: _emailController, label: 'Email', @@ -274,8 +237,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 16), - - // Champ mot de passe CustomTextField( controller: _passwordController, label: 'Mot de passe', @@ -293,8 +254,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 16), - - // Champ confirmation mot de passe CustomTextField( controller: _confirmPasswordController, label: 'Confirmer le mot de passe', @@ -315,8 +274,6 @@ class _RegisterScreenState extends State ), const SizedBox(height: 24), - - // Bouton d'inscription CustomButton( onPressed: _isLoading ? null : _handleRegister, isLoading: _isLoading, @@ -328,7 +285,6 @@ class _RegisterScreenState extends State ); } - /// Lien vers la page de connexion Widget _buildLoginLink() { return TextButton( onPressed: () => context.goToLogin(), @@ -339,7 +295,6 @@ class _RegisterScreenState extends State ); } - /// Validation du nom String? _validateName(String? value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre nom'; @@ -350,7 +305,6 @@ class _RegisterScreenState extends State return null; } - /// Validation de l'email String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre email'; @@ -361,7 +315,6 @@ class _RegisterScreenState extends State return null; } - /// Validation du mot de passe String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre mot de passe'; @@ -372,7 +325,6 @@ class _RegisterScreenState extends State return null; } - /// Validation de la confirmation du mot de passe String? _validateConfirmPassword(String? value) { if (value == null || value.isEmpty) { return 'Veuillez confirmer votre mot de passe'; diff --git a/lib/features/auth/ui/auth_page.dart b/lib/features/auth/ui/auth_page.dart index 964a8b2..35782a7 100644 --- a/lib/features/auth/ui/auth_page.dart +++ b/lib/features/auth/ui/auth_page.dart @@ -11,7 +11,6 @@ class AuthPage extends StatelessWidget { body: Center( child: ElevatedButton( onPressed: () { - // TODO: implémenter login; pour l’instant on va sur /tasks context.go('/tasks'); }, child: const Text('Se connecter (mock)'), diff --git a/lib/features/splash/ui/splash_page.dart b/lib/features/splash/ui/splash_page.dart index 39499e4..f6a80b0 100644 --- a/lib/features/splash/ui/splash_page.dart +++ b/lib/features/splash/ui/splash_page.dart @@ -13,7 +13,6 @@ class _SplashPageState extends State { void initState() { super.initState(); Future.delayed(const Duration(milliseconds: 600), () { - // TODO: remplacer par vérif de session Firebase if (mounted) { context.go('/auth'); } diff --git a/lib/features/tasks/data/migrate_tasks.dart b/lib/features/tasks/data/migrate_tasks.dart index e40de44..e42ad54 100644 --- a/lib/features/tasks/data/migrate_tasks.dart +++ b/lib/features/tasks/data/migrate_tasks.dart @@ -1,71 +1,45 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -/// Script de migration pour ajouter le champ assignedTo aux tâches existantes class TaskMigration { final FirebaseFirestore _firestore = FirebaseFirestore.instance; - /// Migrer toutes les tâches pour ajouter le champ assignedTo Future migrateAllTasks() async { - print('🔄 Début de la migration des tâches...'); - try { - // Récupérer toutes les tâches final snapshot = await _firestore.collection('tasks').get(); - print('📊 ${snapshot.docs.length} tâches trouvées'); - int migrated = 0; int skipped = 0; - // Mettre à jour chaque tâche for (var doc in snapshot.docs) { final data = doc.data(); - // Vérifier si le champ assignedTo existe déjà if (!data.containsKey('assignedTo')) { await doc.reference.update({ - 'assignedTo': [], // Tableau vide par défaut + 'assignedTo': [], }); migrated++; - print('✅ Tâche ${doc.id} migrée'); } else { skipped++; - print('⏭️ Tâche ${doc.id} déjà migrée'); } } - - print('✅ Migration terminée !'); - print(' - Tâches migrées: $migrated'); - print(' - Tâches déjà à jour: $skipped'); - print(' - Total: ${snapshot.docs.length}'); } catch (e) { - print('❌ Erreur lors de la migration: $e'); rethrow; } } - /// Vérifier si la migration est nécessaire Future needsMigration() async { try { final snapshot = await _firestore.collection('tasks').limit(1).get(); if (snapshot.docs.isEmpty) { - print('ℹ️ Aucune tâche dans la base de données'); return false; } final firstTask = snapshot.docs.first.data(); final needsMigration = !firstTask.containsKey('assignedTo'); - if (needsMigration) { - print('⚠️ Migration nécessaire - champ assignedTo manquant'); - } else { - print('✅ Pas de migration nécessaire'); - } - return needsMigration; } catch (e) { - print('❌ Erreur lors de la vérification: $e'); return false; } } diff --git a/lib/features/tasks/data/task_repository.dart b/lib/features/tasks/data/task_repository.dart index a04fa1a..4418996 100644 --- a/lib/features/tasks/data/task_repository.dart +++ b/lib/features/tasks/data/task_repository.dart @@ -1,62 +1,48 @@ import '../domain/models/task.dart'; import 'firestore_task_service.dart'; -/// Repository pour gérer les tâches -/// Fournit une abstraction au-dessus du service Firestore class TaskRepository { final FirestoreTaskService _firestoreService; TaskRepository({FirestoreTaskService? firestoreService}) : _firestoreService = firestoreService ?? FirestoreTaskService(); - // ==================== OPÉRATIONS CRUD ==================== - - /// Créer une nouvelle tâche Future createTask(Task task) async { return await _firestoreService.createTask(task); } - /// Récupérer toutes les tâches Future> getAllTasks() async { return await _firestoreService.getAllTasks(); } - /// Observer toutes les tâches en temps réel Stream> watchAllTasks() { return _firestoreService.watchAllTasks(); } - /// Récupérer une tâche par son ID Future getTaskById(String id) async { return await _firestoreService.getTaskById(id); } - /// Observer les tâches par statut Stream> watchTasksByStatus(bool isCompleted) { return _firestoreService.watchTasksByStatus(isCompleted); } - /// Mettre à jour une tâche Future updateTask(Task task) async { await _firestoreService.updateTask(task); } - /// Supprimer une tâche Future deleteTask(String id) async { await _firestoreService.deleteTask(id); } - /// Basculer l'état de complétion d'une tâche Future toggleTaskCompletion(String id) async { await _firestoreService.toggleTaskCompletion(id); } - /// Supprimer toutes les tâches complétées Future deleteCompletedTasks() async { return await _firestoreService.deleteCompletedTasks(); } - /// Récupérer les statistiques Future> getTaskStats() async { return await _firestoreService.getTaskStats(); } diff --git a/lib/features/tasks/data/task_service.dart b/lib/features/tasks/data/task_service.dart index 2e33d4b..b669d65 100644 --- a/lib/features/tasks/data/task_service.dart +++ b/lib/features/tasks/data/task_service.dart @@ -71,7 +71,6 @@ class TaskService { ownerName = userDoc.data()?['name'] ?? userDoc.data()?['displayName'] ?? ''; } catch (_) { - // ignore } } diff --git a/lib/features/tasks/data/test_firestore_update.dart b/lib/features/tasks/data/test_firestore_update.dart index 74dadbd..bbdbb5e 100644 --- a/lib/features/tasks/data/test_firestore_update.dart +++ b/lib/features/tasks/data/test_firestore_update.dart @@ -1,115 +1,60 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -/// Script de test pour vérifier que Firestore accepte les mises à jour d'assignation -/// -/// COMMENT UTILISER : -/// 1. Importez ce fichier dans un écran existant -/// 2. Appelez testFirestoreAssignation() avec un ID de tâche -/// 3. Regardez les logs dans la console class FirestoreAssignationTester { final FirebaseFirestore _firestore = FirebaseFirestore.instance; - final FirebaseAuth _auth = FirebaseAuth.instance; - /// Test direct de mise à jour Firestore Future testDirectUpdate(String taskId, String userIdToAssign) async { - debugPrint('🧪 TEST: Début du test d\'assignation directe'); - debugPrint(' TaskId: $taskId'); - debugPrint(' UserIdToAssign: $userIdToAssign'); - debugPrint(' Current User: ${_auth.currentUser?.uid}'); - try { - // 1. Lire la tâche AVANT final beforeDoc = await _firestore.collection('tasks').doc(taskId).get(); if (!beforeDoc.exists) { - debugPrint('❌ TEST: La tâche n\'existe pas'); return; } - final beforeData = beforeDoc.data()!; - debugPrint(' AVANT: assignedTo = ${beforeData['assignedTo']}'); - debugPrint(' AVANT: userId = ${beforeData['userId']}'); - - // 2. Mettre à jour avec arrayUnion - debugPrint('🧪 TEST: Tentative de mise à jour...'); await _firestore.collection('tasks').doc(taskId).update({ 'assignedTo': FieldValue.arrayUnion([userIdToAssign]), }); - debugPrint('✅ TEST: update() réussi côté client'); - - // 3. Attendre un peu pour que Firestore propage await Future.delayed(const Duration(seconds: 2)); - // 4. Lire la tâche APRÈS final afterDoc = await _firestore.collection('tasks').doc(taskId).get(); final afterData = afterDoc.data()!; - debugPrint(' APRÈS: assignedTo = ${afterData['assignedTo']}'); - // 5. Vérifier si la mise à jour a vraiment fonctionné final assignedTo = afterData['assignedTo'] as List?; if (assignedTo != null && assignedTo.contains(userIdToAssign)) { - debugPrint('✅ TEST RÉUSSI: L\'UID est bien dans le tableau !'); } else { - debugPrint( - '❌ TEST ÉCHOUÉ: Le tableau est vide ou ne contient pas l\'UID', - ); - debugPrint( - ' Cela signifie que Firestore a rejeté la mise à jour côté serveur', - ); - debugPrint(' Vérifiez les règles de sécurité Firestore'); } - } on FirebaseException catch (e) { - debugPrint('❌ TEST: Erreur Firestore ${e.code} - ${e.message}'); - debugPrint(' ${e.toString()}'); - } catch (e) { - debugPrint('❌ TEST: Erreur inattendue: $e'); + } on FirebaseException { + } catch (_) { } } - /// Test avec set() au lieu de update() Future testWithSet(String taskId, String userIdToAssign) async { - debugPrint('🧪 TEST SET: Utilisation de set() avec merge'); - try { - // Lire d'abord la tâche final doc = await _firestore.collection('tasks').doc(taskId).get(); if (!doc.exists) { - debugPrint('❌ TEST SET: Tâche inexistante'); return; } final data = doc.data()!; final currentAssignedTo = (data['assignedTo'] as List?) ?? []; - // Ajouter l'UID s'il n'est pas déjà présent final newAssignedTo = List.from(currentAssignedTo); if (!newAssignedTo.contains(userIdToAssign)) { newAssignedTo.add(userIdToAssign); } - debugPrint(' Nouveau tableau: $newAssignedTo'); - - // Utiliser set() avec merge await _firestore.collection('tasks').doc(taskId).set({ 'assignedTo': newAssignedTo, }, SetOptions(merge: true)); - debugPrint('✅ TEST SET: set() réussi côté client'); - - // Vérifier après 2 secondes await Future.delayed(const Duration(seconds: 2)); - final afterDoc = await _firestore.collection('tasks').doc(taskId).get(); - final afterData = afterDoc.data()!; - debugPrint(' APRÈS SET: assignedTo = ${afterData['assignedTo']}'); - } on FirebaseException catch (e) { - debugPrint('❌ TEST SET: Erreur ${e.code} - ${e.message}'); + await _firestore.collection('tasks').doc(taskId).get(); + } on FirebaseException { } } } -/// Widget de test à ajouter temporairement dans votre app class FirestoreTestButton extends StatelessWidget { final String taskId; final String userIdToAssign; diff --git a/lib/features/tasks/domain/models/task.dart b/lib/features/tasks/domain/models/task.dart index 8565d3c..4367863 100644 --- a/lib/features/tasks/domain/models/task.dart +++ b/lib/features/tasks/domain/models/task.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:cloud_firestore/cloud_firestore.dart' show Timestamp; -/// Modèle d'une tâche @immutable class Task { final String id; @@ -14,9 +13,6 @@ class Task { final DateTime createdAt; final DateTime? dueDate; final List tags; - - /// Liste des UIDs des utilisateurs assignés à cette tâche (en plus du créateur) - /// Le créateur (ownerId) a toujours accès, pas besoin de l'ajouter ici final List assignedTo; const Task({ @@ -33,7 +29,6 @@ class Task { this.assignedTo = const [], }); - /// Créer une copie modifiée de la tâche Task copyWith({ String? id, String? title, @@ -62,7 +57,6 @@ class Task { ); } - /// Basculer l'état de completion ✅ MÉTHODE MANQUANTE Task toggleCompleted() { return copyWith(isCompleted: !isCompleted); } @@ -80,7 +74,6 @@ class Task { return 'Task(id: $id, title: $title, isCompleted: $isCompleted, priority: $priority)'; } - /// Sérialisation pour Firestore Map toMap() { final map = { 'title': title, @@ -88,21 +81,17 @@ class Task { 'isCompleted': isCompleted, 'priority': priority.value, 'tags': tags, - 'assignedTo': assignedTo, // Liste des UIDs assignés + 'assignedTo': assignedTo, }; - // Owner info map['userId'] = ownerId; map['ownerName'] = ownerName; - - // createdAt and dueDate: include if present. Firestore accepts DateTime. map['createdAt'] = createdAt; if (dueDate != null) map['dueDate'] = dueDate; return map; } - /// Désérialisation depuis Firestore / Map factory Task.fromMap(Map map, {required String id}) { DateTime parseDate(dynamic v) { if (v == null) return DateTime.now(); @@ -112,7 +101,6 @@ class Task { if (v is String) return DateTime.parse(v); if (v is Timestamp) return v.toDate(); if (v is Map && v['seconds'] != null) { - // Map representation from some platforms final seconds = v['seconds']; return DateTime.fromMillisecondsSinceEpoch( (seconds is int) @@ -167,7 +155,6 @@ class Task { } } -/// Niveaux de priorité des tâches enum TaskPriority { low('Faible', 1), medium('Moyenne', 2), diff --git a/lib/features/tasks/domain/models/task_category.dart b/lib/features/tasks/domain/models/task_category.dart index 09b5198..2cd2a86 100644 --- a/lib/features/tasks/domain/models/task_category.dart +++ b/lib/features/tasks/domain/models/task_category.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -/// Catégories prédéfinies pour les tâches class TaskCategory { final String id; final String label; diff --git a/lib/features/tasks/presentation/screens/migration_screen.dart b/lib/features/tasks/presentation/screens/migration_screen.dart index 9956687..cae269b 100644 --- a/lib/features/tasks/presentation/screens/migration_screen.dart +++ b/lib/features/tasks/presentation/screens/migration_screen.dart @@ -3,7 +3,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../../core/theme/app_colors.dart'; -/// Page temporaire pour migrer les tâches existantes class MigrationScreen extends StatefulWidget { const MigrationScreen({super.key}); @@ -85,7 +84,6 @@ class _MigrationScreenState extends State { _status = '✅ Migration terminée ! $count tâches migrées.'; }); - // Retourner à l'écran précédent après 2 secondes Future.delayed(const Duration(seconds: 2), () { if (mounted) Navigator.of(context).pop(); }); diff --git a/lib/features/tasks/presentation/screens/task_list_screen.dart b/lib/features/tasks/presentation/screens/task_list_screen.dart index be7e855..2ae0949 100644 --- a/lib/features/tasks/presentation/screens/task_list_screen.dart +++ b/lib/features/tasks/presentation/screens/task_list_screen.dart @@ -17,7 +17,6 @@ import '../widgets/task_stats_card.dart'; import '../widgets/task_tile.dart'; import '../widgets/task_sort_button.dart'; -/// Écran principal des tâches avec interface moderne class TaskListScreen extends StatefulWidget { const TaskListScreen({super.key}); @@ -36,17 +35,12 @@ class _TaskListScreenState extends State void initState() { super.initState(); - // Écouter les changements de recherche _searchController.addListener(() { setState(() { _searchQuery = _searchController.text.toLowerCase(); }); }); - // Charger les données de test - // Les tâches sont maintenant fournies par TaskService -> TaskProvider via Firestore - - // Animation du FAB _fabAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, @@ -59,7 +53,6 @@ class _TaskListScreenState extends State ), ); - // Délai avant l'apparition du FAB Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _fabAnimationController.forward(); }); @@ -115,7 +108,7 @@ class _TaskListScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.getBackground(context), // ✅ CORRIGÉ + backgroundColor: AppColors.getBackground(context), body: Consumer( builder: (context, taskProvider, child) { if (taskProvider.isLoading) { @@ -123,7 +116,6 @@ class _TaskListScreenState extends State } if (taskProvider.errorMessage != null) { - // Afficher un message lisible en cas d'erreur (par ex. index Firestore manquant) return CustomScrollView( slivers: [ SliverFillRemaining( @@ -155,7 +147,6 @@ class _TaskListScreenState extends State const SizedBox(height: 16), TextButton( onPressed: () { - // Ouvrir la console Firebase (non disponible ici) — instructions manuelles ci-dessous }, child: const Text( 'Voir les indexes dans la console Firebase', @@ -218,7 +209,7 @@ class _TaskListScreenState extends State elevation: 0, flexibleSpace: Container( decoration: const BoxDecoration( - gradient: AppColors.primaryGradient, // ✅ CORRIGÉ + gradient: AppColors.primaryGradient, ), child: const FlexibleSpaceBar( title: Text( @@ -230,7 +221,6 @@ class _TaskListScreenState extends State ), ), actions: [ - // Affichage de l'utilisateur connecté Consumer( builder: (context, authService, child) { final email = authService.currentUserEmail; @@ -261,16 +251,8 @@ class _TaskListScreenState extends State }, ), const TaskSortButton(), - // ✅ DEBUG : Voir l'état du thème Consumer( builder: (context, themeProvider, child) { - print( - '🌙 TaskListScreen: Theme brightness: ${Theme.of(context).brightness}', - ); - print( - '🌙 TaskListScreen: ThemeProvider mode: ${themeProvider.themeMode}', - ); - return const Padding( padding: EdgeInsets.only(right: 8), child: ThemeSwitch(showLabel: false), @@ -286,7 +268,6 @@ class _TaskListScreenState extends State ); } - /// Filtre les tâches selon la requête de recherche List _filterTasks(List tasks) { if (_searchQuery.isEmpty) return tasks; @@ -303,7 +284,6 @@ class _TaskListScreenState extends State }).toList(); } - /// Barre de recherche élégante et animée Widget _buildSearchBar() { return SliverToBoxAdapter( child: Padding( diff --git a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart index 5c538d1..71b3723 100644 --- a/lib/features/tasks/presentation/widgets/assign_users_dialog.dart +++ b/lib/features/tasks/presentation/widgets/assign_users_dialog.dart @@ -5,7 +5,6 @@ import '../../../../core/theme/app_colors.dart'; import '../../domain/models/task.dart'; import '../providers/task_provider.dart'; -/// Dialog élégant et moderne pour assigner des utilisateurs à une tâche class AssignUsersDialog extends StatefulWidget { final Task task; @@ -27,19 +26,16 @@ class _AssignUsersDialogState extends State late Animation _fadeAnimation; late Animation _slideAnimation; - // Sauvegarder l'état initial pour permettre l'annulation late List _initialAssignedUsers; @override void initState() { super.initState(); - // Sauvegarder l'état initial des assignations _initialAssignedUsers = List.from(widget.task.assignedTo); _loadUsers(); - // Animations d'entrée _animationController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, @@ -103,7 +99,6 @@ class _AssignUsersDialogState extends State }); } - /// Annule toutes les modifications et restaure l'état initial Future _cancelChanges() async { try { final provider = context.read(); @@ -112,17 +107,14 @@ class _AssignUsersDialogState extends State ); final currentAssignedUsers = currentTask.assignedTo; - // Trouver les utilisateurs à retirer (qui ont été ajoutés) final usersToRemove = currentAssignedUsers .where((userId) => !_initialAssignedUsers.contains(userId)) .toList(); - // Trouver les utilisateurs à rajouter (qui ont été retirés) final usersToAdd = _initialAssignedUsers .where((userId) => !currentAssignedUsers.contains(userId)) .toList(); - // Effectuer les modifications pour restaurer l'état initial for (final userId in usersToRemove) { await provider.unassignUserFromTask(widget.task.id, userId); } @@ -155,7 +147,6 @@ class _AssignUsersDialogState extends State ), ); - // Fermer le dialog après avoir annulé Navigator.of(context).pop(); } } catch (e) { @@ -174,14 +165,11 @@ class _AssignUsersDialogState extends State try { final provider = context.read(); if (isAssigned) { - // L'utilisateur EST déjà assigné → on le RETIRE await provider.unassignUserFromTask(widget.task.id, userId); } else { - // L'utilisateur N'EST PAS assigné → on l'AJOUTE await provider.assignUserToTask(widget.task.id, userId); } - // Animation de succès if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -272,7 +260,6 @@ class _AssignUsersDialogState extends State } Widget _buildHeader() { - // Récupérer la tâche à jour depuis le Provider final currentTask = context.watch().allTasks.firstWhere( (t) => t.id == widget.task.id, orElse: () => widget.task, @@ -491,14 +478,12 @@ class _AssignUsersDialogState extends State final userName = user['name'] as String? ?? 'Sans nom'; final userEmail = user['email'] as String? ?? ''; - // Récupérer la tâche à jour depuis le Provider final currentTask = context.watch().allTasks.firstWhere( (t) => t.id == widget.task.id, orElse: () => widget.task, ); final isAssigned = currentTask.assignedTo.contains(userId); - // Couleur de l'avatar basée sur le nom final avatarColor = _getAvatarColor(userName); return AnimatedContainer( @@ -613,7 +598,6 @@ class _AssignUsersDialogState extends State } Widget _buildFooter() { - // Récupérer la tâche à jour depuis le Provider final currentTask = context.watch().allTasks.firstWhere( (t) => t.id == widget.task.id, orElse: () => widget.task, @@ -638,7 +622,6 @@ class _AssignUsersDialogState extends State ), ), ), - // Bouton Annuler - Restaure l'état initial TextButton( onPressed: _cancelChanges, style: TextButton.styleFrom( @@ -648,7 +631,6 @@ class _AssignUsersDialogState extends State child: const Text('Annuler'), ), const SizedBox(width: 8), - // Bouton Modifier - Ferme le dialog (changements déjà appliqués) ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( @@ -666,7 +648,6 @@ class _AssignUsersDialogState extends State ); } - /// Génère une couleur d'avatar basée sur le nom Color _getAvatarColor(String name) { final colors = [ AppColors.primary, diff --git a/lib/features/tasks/presentation/widgets/empty_state.dart b/lib/features/tasks/presentation/widgets/empty_state.dart index 446c8fc..a3e0fbf 100644 --- a/lib/features/tasks/presentation/widgets/empty_state.dart +++ b/lib/features/tasks/presentation/widgets/empty_state.dart @@ -7,7 +7,6 @@ import '../../../../shared/widgets/custom_button.dart'; import '../providers/task_provider.dart'; import 'task_modal.dart'; -/// État vide élégant avec illustration et actions class EmptyState extends StatefulWidget { const EmptyState({super.key}); @@ -89,15 +88,12 @@ class _EmptyStateState extends State with TickerProviderStateMixin { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - // Illustration animée ScaleTransition( scale: _scaleAnimation, child: _buildIllustration(hasNoTasks, currentFilter), ), const SizedBox(height: 32), - - // Texte principal SlideTransition( position: _slideAnimation, child: _buildContent(hasNoTasks, currentFilter), @@ -174,8 +170,6 @@ class _EmptyStateState extends State with TickerProviderStateMixin { ), const SizedBox(height: 32), - - // Boutons d'action _buildActionButtons(hasNoTasks, currentFilter), ], ); @@ -183,7 +177,6 @@ class _EmptyStateState extends State with TickerProviderStateMixin { Widget _buildActionButtons(bool hasNoTasks, TaskFilter currentFilter) { if (hasNoTasks) { - // Première tâche return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -209,7 +202,6 @@ class _EmptyStateState extends State with TickerProviderStateMixin { ], ); } else { - // Filtres sans résultats return Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/tasks/presentation/widgets/tag_selector.dart b/lib/features/tasks/presentation/widgets/tag_selector.dart index 967cc35..7caf62a 100644 --- a/lib/features/tasks/presentation/widgets/tag_selector.dart +++ b/lib/features/tasks/presentation/widgets/tag_selector.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import '../../domain/models/task_category.dart'; import '../../../../core/theme/app_colors.dart'; -/// Widget pour sélectionner les tags/catégories d'une tâche class TagSelector extends StatelessWidget { final List selectedTags; final Function(String) onTagToggle; diff --git a/lib/features/tasks/presentation/widgets/task_filter_chips.dart b/lib/features/tasks/presentation/widgets/task_filter_chips.dart index e58d107..beb88dc 100644 --- a/lib/features/tasks/presentation/widgets/task_filter_chips.dart +++ b/lib/features/tasks/presentation/widgets/task_filter_chips.dart @@ -5,7 +5,6 @@ import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_theme.dart'; import '../providers/task_provider.dart'; -/// Chips pour filtrer les tâches avec couleurs spécifiques class TaskFilterChips extends StatelessWidget { const TaskFilterChips({super.key}); @@ -16,7 +15,6 @@ class TaskFilterChips extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Titre amélioré pour le mode dark Padding( padding: const EdgeInsets.only(bottom: 12, left: 4), child: Text( @@ -24,14 +22,10 @@ class TaskFilterChips extends StatelessWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.getSectionTitle( - context, - ), // ✅ Visible en mode dark + color: AppColors.getSectionTitle(context), ), ), ), - - // Chips de filtrage avec couleurs spécifiques SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -94,7 +88,6 @@ class TaskFilterChips extends StatelessWidget { ); } - /// Retourne les couleurs spécifiques pour chaque filtre FilterColors _getFilterColors(TaskFilter filter) { switch (filter) { case TaskFilter.all: @@ -107,7 +100,7 @@ class TaskFilterChips extends StatelessWidget { case TaskFilter.pending: return FilterColors( - selectedColor: AppColors.warning, // 🟡 Orange pour "À faire" + selectedColor: AppColors.warning, backgroundColor: AppColors.warning.withOpacity(0.1), borderColor: AppColors.warning.withOpacity(0.3), textColor: AppColors.warning, @@ -115,7 +108,7 @@ class TaskFilterChips extends StatelessWidget { case TaskFilter.completed: return FilterColors( - selectedColor: AppColors.success, // 🟢 Vert pour "Terminées" + selectedColor: AppColors.success, backgroundColor: AppColors.success.withOpacity(0.1), borderColor: AppColors.success.withOpacity(0.3), textColor: AppColors.success, @@ -123,7 +116,7 @@ class TaskFilterChips extends StatelessWidget { case TaskFilter.highPriority: return FilterColors( - selectedColor: AppColors.error, // 🔴 Rouge pour "Priorité haute" + selectedColor: AppColors.error, backgroundColor: AppColors.error.withOpacity(0.1), borderColor: AppColors.error.withOpacity(0.3), textColor: AppColors.error, @@ -132,7 +125,6 @@ class TaskFilterChips extends StatelessWidget { } } -/// Classe pour organiser les couleurs d'un filtre class FilterColors { final Color selectedColor; final Color backgroundColor; diff --git a/lib/features/tasks/presentation/widgets/task_modal.dart b/lib/features/tasks/presentation/widgets/task_modal.dart index 683f336..e8ad5f3 100644 --- a/lib/features/tasks/presentation/widgets/task_modal.dart +++ b/lib/features/tasks/presentation/widgets/task_modal.dart @@ -11,7 +11,6 @@ import '../providers/task_provider.dart'; import 'assign_users_dialog.dart'; import 'tag_selector.dart'; -/// Modal élégant pour créer/éditer une tâche - VERSION STABLE class TaskModal extends StatefulWidget { final Task? task; @@ -36,7 +35,6 @@ class _TaskModalState extends State { void initState() { super.initState(); - // Pré-remplir si on édite if (_isEditing) { _titleController.text = widget.task!.title; _descriptionController.text = widget.task!.description; @@ -83,12 +81,7 @@ class _TaskModalState extends State { final taskProvider = context.read(); final currentUser = FirebaseAuth.instance.currentUser; - print('🔐 TaskModal - Utilisateur connecté: ${currentUser?.uid}'); - print('🔐 TaskModal - Email: ${currentUser?.email}'); - print('🔐 TaskModal - DisplayName: ${currentUser?.displayName}'); - if (currentUser == null) { - // L'utilisateur n'est pas connecté, on ne peut pas créer de tâche ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vous devez être connecté pour créer une tâche'), @@ -99,13 +92,11 @@ class _TaskModalState extends State { } if (_isEditing) { - // Récupérer la tâche actuelle depuis le provider pour avoir les assignations à jour final currentTask = taskProvider.allTasks.firstWhere( (t) => t.id == widget.task!.id, orElse: () => widget.task!, ); - // Modifier la tâche existante en gardant les assignations actuelles final updatedTask = currentTask.copyWith( title: _titleController.text.trim(), description: _descriptionController.text.trim(), @@ -113,11 +104,8 @@ class _TaskModalState extends State { dueDate: _selectedDueDate, tags: _selectedTags, ); - print('📝 TaskModal - Modification tâche: ${updatedTask.id}'); - print(' - assignedTo conservé: ${updatedTask.assignedTo}'); taskProvider.updateTask(updatedTask); } else { - // Créer une nouvelle tâche avec ownerId et ownerName final newTask = Task( id: DateTime.now().millisecondsSinceEpoch.toString(), title: _titleController.text.trim(), @@ -131,24 +119,18 @@ class _TaskModalState extends State { assignedTo: const [], tags: _selectedTags, ); - print('✨ TaskModal - Création nouvelle tâche:'); - print(' - title: ${newTask.title}'); - print(' - ownerId: ${newTask.ownerId}'); - print(' - ownerName: ${newTask.ownerName}'); - print(' - tags: ${newTask.tags}'); taskProvider.addTask(newTask); } - Navigator.of(context).pop(); // ✅ Fermeture explicite + Navigator.of(context).pop(); } @override Widget build(BuildContext context) { return Container( - // ✅ HAUTEUR FIXE pour éviter les problèmes de contraintes height: MediaQuery.of(context).size.height * 0.9, decoration: BoxDecoration( - color: AppColors.surface, // ✅ Couleur dynamique + color: AppColors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(25)), ), child: Column( @@ -178,7 +160,6 @@ class _TaskModalState extends State { ), child: Column( children: [ - // Indicateur de drag Container( width: 40, height: 4, @@ -249,7 +230,6 @@ class _TaskModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Titre de la tâche CustomTextField( controller: _titleController, label: 'Titre de la tâche', @@ -264,8 +244,6 @@ class _TaskModalState extends State { ), const SizedBox(height: 20), - - // Description CustomTextField( controller: _descriptionController, label: 'Description (optionnel)', @@ -275,18 +253,10 @@ class _TaskModalState extends State { ), const SizedBox(height: 30), - - // Sélection de priorité _buildPrioritySelector(), - const SizedBox(height: 30), - - // Sélection de date _buildDateSelector(), - const SizedBox(height: 30), - - // Sélection de tags/catégories TagSelector( selectedTags: _selectedTags, onTagToggle: (tagId) { @@ -299,19 +269,13 @@ class _TaskModalState extends State { }); }, ), - - // Bouton d'assignation (seulement en mode édition) if (_isEditing) ...[ const SizedBox(height: 30), _buildAssignUsersButton(), ], const SizedBox(height: 40), - - // Boutons d'action _buildActionButtons(), - - // Espacement supplémentaire pour le scroll const SizedBox(height: 20), ], ), @@ -388,7 +352,7 @@ class _TaskModalState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.onSurface, // ✅ Couleur dynamique + color: AppColors.onSurface, ), ), @@ -399,7 +363,7 @@ class _TaskModalState extends State { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.surfaceVariant, // ✅ Couleur dynamique + color: AppColors.surfaceVariant, borderRadius: AppTheme.radiusMedium, border: Border.all(color: AppColors.primary.withOpacity(0.2)), ), @@ -409,7 +373,7 @@ class _TaskModalState extends State { Icons.calendar_today, color: _selectedDueDate != null ? AppColors.primary - : AppColors.onSurfaceVariant, // ✅ Couleur dynamique + : AppColors.onSurfaceVariant, ), const SizedBox(width: 12), Expanded( @@ -419,9 +383,8 @@ class _TaskModalState extends State { : 'Sélectionner une date d\'échéance', style: TextStyle( color: _selectedDueDate != null - ? AppColors - .onSurface // ✅ Couleur dynamique - : AppColors.onSurfaceVariant, // ✅ Couleur dynamique + ? AppColors.onSurface + : AppColors.onSurfaceVariant, fontWeight: _selectedDueDate != null ? FontWeight.w500 : FontWeight.normal, @@ -447,7 +410,6 @@ class _TaskModalState extends State { } Widget _buildAssignUsersButton() { - // Récupérer la tâche à jour depuis le Provider si on modifie une tâche existante final currentTask = widget.task != null ? context.watch().allTasks.firstWhere( (t) => t.id == widget.task!.id, diff --git a/lib/features/tasks/presentation/widgets/task_sort_button.dart b/lib/features/tasks/presentation/widgets/task_sort_button.dart index 57fa3d4..23118b2 100644 --- a/lib/features/tasks/presentation/widgets/task_sort_button.dart +++ b/lib/features/tasks/presentation/widgets/task_sort_button.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import '../providers/task_provider.dart'; -/// Bouton d'options pour trier les tâches class TaskSortButton extends StatelessWidget { const TaskSortButton({super.key}); diff --git a/lib/features/tasks/presentation/widgets/task_stats_card.dart b/lib/features/tasks/presentation/widgets/task_stats_card.dart index f8b2ceb..7366b5b 100644 --- a/lib/features/tasks/presentation/widgets/task_stats_card.dart +++ b/lib/features/tasks/presentation/widgets/task_stats_card.dart @@ -4,7 +4,6 @@ import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_theme.dart'; import '../providers/task_provider.dart'; -/// Carte de statistiques avec animations class TaskStatsCard extends StatefulWidget { final TaskStats stats; @@ -28,7 +27,6 @@ class _TaskStatsCardState extends State vsync: this, ); - // Créer des animations décalées pour chaque statistique _progressAnimations = List.generate(4, (index) { return Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( @@ -121,8 +119,6 @@ class _TaskStatsCardState extends State ], ), ), - - // Indicateur circulaire de progression AnimatedBuilder( animation: _progressAnimations[0], builder: (context, child) { diff --git a/lib/features/tasks/presentation/widgets/task_tile.dart b/lib/features/tasks/presentation/widgets/task_tile.dart index eb12ff2..3cfe0a1 100644 --- a/lib/features/tasks/presentation/widgets/task_tile.dart +++ b/lib/features/tasks/presentation/widgets/task_tile.dart @@ -7,7 +7,6 @@ import '../../../../core/theme/app_theme.dart'; import '../../domain/models/task.dart'; import '../../domain/models/task_category.dart'; -/// Tuile élégante pour afficher une tâche class TaskTile extends StatefulWidget { final Task task; final VoidCallback onTap; @@ -130,15 +129,9 @@ class _TaskTileState extends State padding: AppTheme.paddingMedium, child: Row( children: [ - // Checkbox personnalisée _buildCustomCheckbox(), - const SizedBox(width: 16), - - // Contenu principal Expanded(child: _buildMainContent()), - - // Actions _buildActions(), ], ), @@ -175,7 +168,6 @@ class _TaskTileState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Titre avec style selon l'état Text( widget.task.title, style: TextStyle( @@ -206,8 +198,6 @@ class _TaskTileState extends State ], const SizedBox(height: 8), - - // Métadonnées (priorité, date, créateur, assignés) _buildMetadata(), ], ); @@ -217,27 +207,20 @@ class _TaskTileState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Première ligne : Priorité et Date Wrap( spacing: 8, runSpacing: 4, children: [ - // Priorité _buildPriorityChip(), - - // Date d'échéance if (widget.task.dueDate != null) _buildDueDateChip(), ], ), - - // Deuxième ligne : Tags/Catégories if (widget.task.tags.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 4, children: [ - // Afficher tous les tags ...widget.task.tags.map((tagId) { final category = TaskCategory.findById(tagId); if (category == null) return const SizedBox.shrink(); @@ -275,8 +258,6 @@ class _TaskTileState extends State ], ), ], - - // Troisième ligne : Créateur et Utilisateurs assignés if (widget.task.ownerName.isNotEmpty || widget.task.assignedTo.isNotEmpty) ...[ const SizedBox(height: 8), @@ -284,10 +265,7 @@ class _TaskTileState extends State spacing: 8, runSpacing: 4, children: [ - // Badge du créateur if (widget.task.ownerName.isNotEmpty) _buildOwnerBadge(), - - // Badges des utilisateurs assignés if (widget.task.assignedTo.isNotEmpty) _buildAssignedUsersBadge(), ], ), @@ -296,7 +274,6 @@ class _TaskTileState extends State ); } - /// Badge élégant pour afficher le créateur de la tâche Widget _buildOwnerBadge() { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -316,7 +293,6 @@ class _TaskTileState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - // Avatar du créateur CircleAvatar( radius: 8, backgroundColor: AppColors.primary, @@ -347,16 +323,9 @@ class _TaskTileState extends State ); } - /// Badge pour afficher les utilisateurs assignés (avatars empilés) Widget _buildAssignedUsersBadge() { final assignedCount = widget.task.assignedTo.length; - if (kDebugMode) { - print( - '🎨 TaskTile: Affichage badge assignés - task.id=${widget.task.id}, assignedCount=$assignedCount, assignedTo=${widget.task.assignedTo}', - ); - } - return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( @@ -438,7 +407,6 @@ class _TaskTileState extends State return Row( mainAxisSize: MainAxisSize.min, children: [ - // Bouton supprimer IconButton( onPressed: () => _showDeleteDialog(), icon: Icon( diff --git a/lib/features/tasks/ui/debug_firestore_page.dart b/lib/features/tasks/ui/debug_firestore_page.dart index 46b6238..e51529a 100644 --- a/lib/features/tasks/ui/debug_firestore_page.dart +++ b/lib/features/tasks/ui/debug_firestore_page.dart @@ -2,7 +2,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -/// Page de débogage pour tester la connexion Firestore class DebugFirestorePage extends StatefulWidget { const DebugFirestorePage({super.key}); @@ -30,7 +29,6 @@ class _DebugFirestorePageState extends State { Future _checkConnection() async { _addLog('🔍 Vérification de la connexion Firebase...'); - // 1. Vérifier Firebase Auth final user = FirebaseAuth.instance.currentUser; if (user == null) { _addLog('❌ Aucun utilisateur connecté !'); @@ -39,17 +37,13 @@ class _DebugFirestorePageState extends State { _addLog('✅ Utilisateur connecté: ${user.email}'); _addLog(' UID: ${user.uid}'); - // 2. Vérifier Firestore try { final firestore = FirebaseFirestore.instance; _addLog('📊 Instance Firestore créée'); - - // 3. Tester lecture de la collection tasks _addLog('📖 Tentative de lecture de la collection "tasks"...'); final snapshot = await firestore.collection('tasks').get(); _addLog('✅ Lecture réussie ! ${snapshot.docs.length} documents trouvés'); - // 4. Afficher les documents existants if (snapshot.docs.isEmpty) { _addLog('⚠️ Collection vide - aucune tâche trouvée'); } else { @@ -101,7 +95,6 @@ class _DebugFirestorePageState extends State { _addLog('✅ Tâche créée avec succès !'); _addLog(' Document ID: ${docRef.id}'); - // Relire pour vérifier await _checkConnection(); } catch (e) { _addLog('❌ ERREUR lors de l\'écriture: $e'); @@ -139,7 +132,6 @@ service cloud.firestore { ), body: Column( children: [ - // Zone d'actions Container( padding: const EdgeInsets.all(16), color: Colors.orange.shade50, @@ -179,8 +171,6 @@ service cloud.firestore { ], ), ), - - // Zone de logs Expanded( child: Container( color: Colors.grey.shade900, @@ -216,8 +206,6 @@ service cloud.firestore { ), ), ), - - // Indicateur de chargement if (_isLoading) const LinearProgressIndicator( backgroundColor: Colors.orange, diff --git a/lib/features/tasks/ui/firestore_test_widget.dart b/lib/features/tasks/ui/firestore_test_widget.dart index d49a55e..fdb0cc4 100644 --- a/lib/features/tasks/ui/firestore_test_widget.dart +++ b/lib/features/tasks/ui/firestore_test_widget.dart @@ -1,7 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -/// Widget de test pour vérifier la connexion Firestore class FirestoreTestWidget extends StatefulWidget { const FirestoreTestWidget({super.key}); @@ -24,7 +23,6 @@ class _FirestoreTestWidgetState extends State { _addLog('🔍 Test de connexion Firestore...'); try { - // Test 1: Lire la collection tasks _addLog('📖 Lecture de la collection tasks...'); final snapshot = await _firestore.collection('tasks').get(); _addLog('✅ Collection tasks lue avec succès'); @@ -32,8 +30,6 @@ class _FirestoreTestWidgetState extends State { if (snapshot.docs.isEmpty) { _addLog('⚠️ Aucune tâche trouvée'); - - // Test 2: Essayer de créer une tâche de test _addLog('📝 Tentative de création d\'une tâche de test...'); final docRef = await _firestore.collection('tasks').add({ 'title': 'Tâche de test', diff --git a/lib/shared/widgets/custom_button.dart b/lib/shared/widgets/custom_button.dart index 16474dd..4d12232 100644 --- a/lib/shared/widgets/custom_button.dart +++ b/lib/shared/widgets/custom_button.dart @@ -5,7 +5,6 @@ import '../../core/theme/app_theme.dart'; enum ButtonVariant { primary, secondary, outline, text } -/// Bouton personnalisé avec plusieurs variantes class CustomButton extends StatelessWidget { final Widget child; final VoidCallback? onPressed; diff --git a/lib/shared/widgets/custom_text_field.dart b/lib/shared/widgets/custom_text_field.dart index 1152ff2..085e070 100644 --- a/lib/shared/widgets/custom_text_field.dart +++ b/lib/shared/widgets/custom_text_field.dart @@ -3,14 +3,6 @@ import 'package:flutter/material.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; -/// Champ de saisie personnalisé et réutilisable -/// -/// Fonctionnalités : -/// - Design cohérent avec le thème -/// - Validation intégrée -/// - Icônes prefix/suffix -/// - Support de tous les types de clavier -/// - États focus/erreur gérés automatiquement class CustomTextField extends StatelessWidget { final TextEditingController? controller; final String label; @@ -46,7 +38,6 @@ class CustomTextField extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Label du champ Text( label, style: TextStyle( @@ -57,8 +48,6 @@ class CustomTextField extends StatelessWidget { ), const SizedBox(height: 8), - - // Champ de saisie TextFormField( controller: controller, keyboardType: keyboardType, @@ -73,23 +62,16 @@ class CustomTextField extends StatelessWidget { color: AppColors.getOnSurface(context), ), decoration: InputDecoration( - // Texte d'aide hintText: hint, hintStyle: TextStyle( color: AppColors.getOnSurfaceVariant(context).withOpacity(0.7), ), - - // Icônes prefixIcon: prefixIcon != null ? Icon(prefixIcon, color: AppColors.primary) : null, suffixIcon: suffixIcon, - - // Style du conteneur filled: true, fillColor: AppColors.getSurfaceVariant(context), - - // Bordures border: OutlineInputBorder( borderRadius: AppTheme.radiusMedium, borderSide: BorderSide.none, @@ -113,8 +95,6 @@ class CustomTextField extends StatelessWidget { borderRadius: AppTheme.radiusMedium, borderSide: const BorderSide(color: AppColors.error, width: 2), ), - - // Espacement interne contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, diff --git a/lib/shared/widgets/splash_screen.dart b/lib/shared/widgets/splash_screen.dart index 2697431..93844dc 100644 --- a/lib/shared/widgets/splash_screen.dart +++ b/lib/shared/widgets/splash_screen.dart @@ -3,10 +3,6 @@ import 'package:flutter/material.dart'; import '../../core/router/app_router.dart'; import '../../core/theme/app_colors.dart'; -/// Écran de démarrage de l'application -/// -/// Cet écran s'affiche pendant le chargement initial et redirige ensuite -/// vers l'écran approprié (login si pas connecté, tâches si connecté) class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -16,7 +12,6 @@ class SplashScreen extends StatefulWidget { class _SplashScreenState extends State with SingleTickerProviderStateMixin { - // Contrôleur d'animation pour l'effet de fondu late AnimationController _animationController; late Animation _fadeAnimation; @@ -24,52 +19,37 @@ class _SplashScreenState extends State void initState() { super.initState(); - // Configuration de l'animation de fondu _animationController = AnimationController( duration: const Duration(seconds: 2), - vsync: this, // this = _SplashScreenState qui implémente TickerProvider + vsync: this, ); _fadeAnimation = Tween( - begin: 0.0, // Transparent au début - end: 1.0, // Opaque à la fin + begin: 0.0, + end: 1.0, ).animate( CurvedAnimation( parent: _animationController, - curve: Curves.easeIn, // Animation progressive + curve: Curves.easeIn, ), ); - // Démarrer l'animation et la navigation _startSplashSequence(); } - /// Séquence de démarrage : animation + redirection Future _startSplashSequence() async { - // Démarrer l'animation _animationController.forward(); - // Attendre 3 secondes await Future.delayed(const Duration(seconds: 3)); - // Vérifier si le widget est encore monté (bonne pratique) if (!mounted) return; - // TODO: Le Lead Auth ajoutera ici la vérification de session - // if (authProvider.isLoggedIn) { - // context.goToTasks(); - // } else { - // context.goToLogin(); - // } - - // Pour l'instant, toujours aller au login context.goToLogin(); } @override void dispose() { - // IMPORTANT : libérer les ressources pour éviter les fuites mémoire _animationController.dispose(); super.dispose(); } @@ -77,7 +57,6 @@ class _SplashScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - // Gradient de fond pour un effet moderne body: Container( decoration: const BoxDecoration(gradient: AppColors.primaryGradient), child: Center( @@ -89,7 +68,6 @@ class _SplashScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Logo de l'app (icône temporaire) Container( width: 80, height: 80, @@ -112,8 +90,6 @@ class _SplashScreenState extends State ), const SizedBox(height: 24), - - // Nom de l'app const Text( 'Todo List Pro', style: TextStyle( @@ -124,16 +100,12 @@ class _SplashScreenState extends State ), const SizedBox(height: 8), - - // Slogan const Text( 'Organisez votre quotidien', style: TextStyle(fontSize: 16, color: Colors.white70), ), const SizedBox(height: 40), - - // Indicateur de chargement const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), diff --git a/lib/shared/widgets/theme_switch.dart b/lib/shared/widgets/theme_switch.dart index f77b3cf..b924b2f 100644 --- a/lib/shared/widgets/theme_switch.dart +++ b/lib/shared/widgets/theme_switch.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/theme_provider.dart'; -/// Switch pour basculer entre thème clair/sombre - VERSION CORRIGÉE class ThemeSwitch extends StatefulWidget { final bool showLabel; final EdgeInsets? padding; @@ -26,14 +25,12 @@ class _ThemeSwitchState extends State void initState() { super.initState(); - // Animation principale pour le slide _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); - // Animation de pulse pour le feedback _pulseController = AnimationController( duration: const Duration(milliseconds: 150), vsync: this, @@ -52,12 +49,10 @@ class _ThemeSwitchState extends State } void _onThemeToggle() { - // Animation de feedback _pulseController.forward().then((_) { _pulseController.reverse(); }); - // Changer le thème context.read().toggleTheme(); } @@ -65,10 +60,8 @@ class _ThemeSwitchState extends State Widget build(BuildContext context) { return Consumer( builder: (context, themeProvider, child) { - // ✅ SYNCHRONISATION : Utiliser l'état réel du thème final isDark = Theme.of(context).brightness == Brightness.dark; - // Synchroniser l'animation avec l'état réel WidgetsBinding.instance.addPostFrameCallback((_) { if (isDark && !_controller.isCompleted) { _controller.forward(); @@ -92,8 +85,6 @@ class _ThemeSwitchState extends State ), const SizedBox(width: 8), ], - - // ✅ SWITCH AMÉLIORÉ ScaleTransition( scale: _pulseAnimation, child: GestureDetector( @@ -121,11 +112,10 @@ class _ThemeSwitchState extends State ), child: Stack( children: [ - // ✅ INDICATEUR SYNCHRONISÉ AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - left: isDark ? 30 : 2, // ✅ Basé sur le thème réel + left: isDark ? 30 : 2, top: 2, child: Container( width: 28, @@ -148,9 +138,7 @@ class _ThemeSwitchState extends State isDark ? Icons.dark_mode : Icons.light_mode, - key: ValueKey( - isDark, - ), // ✅ Key basée sur l'état réel + key: ValueKey(isDark), size: 16, color: isDark ? AppColors.primary