# TODO - Isolation complète des opérations ## 🎯 Objectif Mettre en place une **isolation complète par opération** où chaque opération est totalement autonome et peut être supprimée indépendamment sans impacter les autres opérations ou la table centrale `users`. ## 📊 Architecture cible ``` operations (id: 850) ├── ope_users (id: 2500, fk_operation: 850, fk_user: 100) │ ├── ope_users_sectors (fk_user: 2500 ← ope_users.id, fk_sector: 5400) │ └── ope_pass (fk_user: 2500 ← ope_users.id, fk_sector: 5400) └── ope_sectors (id: 5400, fk_operation: 850) users (id: 100) ← table centrale (conservée même si opération supprimée) ``` --- ## ✅ Tâche 1 : Modification du schéma SQL ### 📁 Fichier : `scripts/orga/fix_fk_constraints.sql` ### Actions - [ ] **1.1** Tester le script SQL sur **dva_geo** (DEV) ```bash incus exec dva-geo -- mysql rca_geo < /var/www/geosector/api/scripts/orga/fix_fk_constraints.sql ``` - [ ] **1.2** Vérifier les contraintes après exécution : ```sql SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'rca_geo' AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass') AND COLUMN_NAME = 'fk_user'; ``` Résultat attendu : - `ope_users_sectors.fk_user → ope_users.id` - `ope_pass.fk_user → ope_users.id` - [ ] **1.3** Appliquer sur **rca_geo** (RECETTE) après validation sur dva_geo - [ ] **1.4** Appliquer sur **pra_geo** (PRODUCTION) après validation sur rca_geo ### ⚠️ Important - Les données existantes doivent être **nettoyées avant** d'appliquer le script - Ou bien : recréer toutes les données avec la nouvelle migration - Les FK `ON DELETE CASCADE` supprimeront automatiquement `ope_users_sectors` et `ope_pass` quand `ope_users` est supprimé --- ## ✅ Tâche 2 : Correction du script de migration2 ### 📁 Fichiers concernés 1. `scripts/migration2/php/lib/SectorMigrator.php` 2. `scripts/migration2/php/lib/PassageMigrator.php` ### Actions #### 2.1 SectorMigrator.php - Migration de ope_users_sectors - [ ] **Ligne 253** : Changer de `users.id` vers `ope_users.id` ```php // ❌ AVANT ':fk_user' => $us['fk_user'], // ID de users (table centrale) // ✅ APRÈS ':fk_user' => $userMapping[$us['fk_user']], // ID de ope_users (mapping) ``` #### 2.2 PassageMigrator.php - Migration de ope_pass - [ ] **Ligne 64-67** : Vérifier le mapping existe - [ ] **Ligne 77** : Passer `ope_users.id` au lieu de `users.id` ```php // ❌ AVANT (ligne 77) $newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $passage['fk_user']); // ✅ APRÈS $newOpeUserId = $userMapping[$passage['fk_user']]; $newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $newOpeUserId); ``` - [ ] **Ligne 164** : Utiliser le paramètre `$userId` qui sera maintenant `ope_users.id` ```php // ❌ AVANT ':fk_user' => $userId, // ID de users (table centrale) // ✅ APRÈS (le paramètre $userId contiendra déjà ope_users.id) ':fk_user' => $userId, // ID de ope_users ``` - [ ] **Ligne 71** : Corriger `verifyUserSectorAssociation` pour vérifier avec `ope_users.id` ```php // ❌ AVANT if (!$this->verifyUserSectorAssociation($newOperationId, $passage['fk_user'], $newOpeSectorId)) { // ✅ APRÈS if (!$this->verifyUserSectorAssociation($newOperationId, $newOpeUserId, $newOpeSectorId)) { ``` #### 2.3 Tester la migration complète - [ ] **Sur dva_geo** : Vider les données d'une entité et relancer la migration ```bash php php/migrate_from_backup.php --mode=entity --entity-id=5 ``` - [ ] **Vérifier** dans la base que : - `ope_users_sectors.fk_user` contient des IDs de `ope_users.id` - `ope_pass.fk_user` contient des IDs de `ope_users.id` - Les valeurs correspondent bien au mapping - [ ] **Vérifier** qu'on peut supprimer une opération et que tout part avec (CASCADE) ```sql DELETE FROM operations WHERE id = 850; -- Doit supprimer automatiquement : -- - ope_users (ON DELETE CASCADE depuis operations) -- - ope_users_sectors (ON DELETE CASCADE depuis ope_users) -- - ope_pass (ON DELETE CASCADE depuis ope_users) -- - ope_sectors (ON DELETE CASCADE depuis operations) ``` --- ## ✅ Tâche 3 : Vérifications API ### Impact sur les endpoints API #### 3.1 Vérifier les requêtes utilisant `ope_pass.fk_user` - [ ] **Rechercher** tous les endpoints qui lisent `ope_pass.fk_user` ```bash grep -r "ope_pass.*fk_user" src/Controllers/ grep -r "fk_user.*ope_pass" src/Controllers/ ``` - [ ] **Vérifier** que ces endpoints : - Font-ils des JOIN avec `users` via `ope_pass.fk_user` ? - Si OUI : Ajouter un JOIN via `ope_users` : ```sql -- ❌ AVANT SELECT op.*, u.encrypted_name FROM ope_pass op JOIN users u ON op.fk_user = u.id -- ✅ APRÈS SELECT op.*, u.encrypted_name FROM ope_pass op JOIN ope_users ou ON op.fk_user = ou.id JOIN users u ON ou.fk_user = u.id ``` #### 3.2 Vérifier les requêtes utilisant `ope_users_sectors.fk_user` - [ ] **Rechercher** tous les endpoints qui lisent `ope_users_sectors.fk_user` ```bash grep -r "ope_users_sectors.*fk_user" src/Controllers/ ``` - [ ] **Vérifier** la même chose : si JOIN avec `users`, ajouter passage par `ope_users` #### 3.3 Endpoints probablement concernés À vérifier : - [ ] `OperationController` - Liste des utilisateurs d'une opération - [ ] `PassageController` - Liste/détails des passages - [ ] `SectorController` - Liste des secteurs avec utilisateurs affectés - [ ] Tout endpoint retournant des statistiques par utilisateur --- ## ✅ Tâche 4 : Corrections API - Response JSON Login ### Impact sur la réponse JSON du login #### 4.1 Groupe `users_sectors` - Ajouter `ope_user_id` **Problème identifié** : Flutter reçoit `users_sectors` avec `id` (users.id) mais les `passages` ont `fk_user` (ope_users.id). Le mapping est impossible. **Solution** : Modifier la requête dans `LoginController.php` (lignes 426 et 1181) pour retourner les deux IDs : ```sql -- ✅ APRÈS SELECT DISTINCT u.id as user_id, -- users.id (table centrale, pour gestion membres) ou.id as ope_user_id, -- ope_users.id (pour lier avec passages/sectors) ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector FROM users u JOIN ope_users ou ON u.id = ou.fk_user JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation WHERE us.fk_sector IN ($sectorIdsString) AND us.fk_operation = ? AND us.chk_active = 1 AND u.chk_active = 1 AND u.id != ? ``` **Résultat JSON attendu** : ```json { "user_id": 123, // users.id (pour gestion des membres dans l'interface) "ope_user_id": 50, // ope_users.id (pour lier avec passages.fk_user et sectors) "first_name": "Jane", "name": "Jane Smith", "sect_name": "Smith", "fk_sector": 456 } ``` **Usage Flutter** : ```dart // Trouver les passages d'un utilisateur passages.where((p) => p.fkUser == usersSectors[i].opeUserId) // ✅ OK ``` - [ ] **Modifier** `LoginController.php` ligne 426 (méthode `login()`) - [ ] **Modifier** `LoginController.php` ligne 1181 (méthode `checkSession()`) - [ ] **Tester** la réponse JSON du login en mode admin --- ## ✅ Tâche 5 : Vérifications Flutter - Gestion des IDs ### Impact sur l'application mobile #### 5.1 Modèles de données - [x] **Vérifier** le modèle `UserSector` (ou équivalent) - Ajouter le champ `opeUserId` (int) pour stocker `ope_users.id` - Conserver `userId` (int) pour stocker `users.id` - ✅ **Fait** : `UserSectorModel` modifié avec les champs `userId` et `opeUserId` - ✅ **Fait** : Adaptateurs Hive régénérés avec `build_runner` - [x] **Vérifier** le modèle `Passage` (ou équivalent) - Le champ `fkUser` pointe maintenant vers `ope_users.id` - ✅ **Fait** : `PassageModel.fkUser` pointe déjà vers `ope_users.id` - [x] **Vérifier** le modèle `User` - Ajouter le champ `opeUserId` (int?) pour stocker l'ID de l'utilisateur dans `ope_users` - ✅ **Fait** : `UserModel` modifié avec `opeUserId` (@HiveField(20)) - ✅ **Fait** : `CurrentUserService` expose `opeUserId` via getter #### 5.2 Gestion des secteurs (Mode Admin) - [x] **Création de secteur** - L'API crée dans `ope_sectors` - Attribution des users : utiliser `ope_user_id` (pas `user_id`) - Endpoint : `POST /api/sectors` - Body : `{ ..., users: [50, 51, 52] }` ← IDs de `ope_users` - ✅ **Fait** : `SectorDialog` utilise `userSector.opeUserId` pour l'attribution - ✅ **Fait** : Liste dédupliquée des membres depuis `UserSectorModel` - [x] **Modification de secteur** - Attribution des users : utiliser `ope_user_id` - Endpoint : `PUT /api/sectors/:id` - Body : `{ ..., users: [50, 51, 52] }` ← IDs de `ope_users` - ✅ **Fait** : `SectorDialog` utilise `userSector.opeUserId` pour l'attribution - [ ] **Suppression de secteur** - L'API supprime dans `ope_pass`, `ope_users_sectors` et `ope_sectors` - CASCADE gère automatiquement les dépendances - Endpoint : `DELETE /api/sectors/:id` #### 5.3 Gestion des membres (Mode Admin) - [ ] **Création de membre** - L'API crée dans `users` (table centrale) - L'API crée aussi dans `ope_users` pour l'opération active - **Réponse attendue** : ```json { "status": "success", "user": { "id": 123, // users.id "ope_user_id": 50, // ope_users.id (nouveau) "first_name": "John", "name": "John Doe", ... } } ``` - Endpoint : `POST /api/users` - Flutter stocke les 2 IDs : `userId` et `opeUserId` - [ ] **Modification de membre** - L'API met à jour `users` (table centrale) - L'API met à jour aussi `ope_users` pour l'opération active - Endpoint : `PUT /api/users/:id` - [ ] **Suppression de membre** - L'API supprime de `ope_users` (opération active) - L'API supprime de `users` (table centrale) - CASCADE supprime automatiquement `ope_users_sectors` et `ope_pass` - Endpoint : `DELETE /api/users/:id?transfer_to=XX` #### 5.4 Gestion des passages (Mode Admin & User) - [x] **Création de passage** - Attribution automatique du `ope_sectors.id` le plus proche - Attribution du `ope_users.id` (utilisateur connecté ou sélectionné) - Endpoint : `POST /api/passages` - Body : `{ ..., fk_user: 50, fk_sector: 456 }` ← IDs de `ope_users` et `ope_sectors` - ✅ **Fait** : `PassageRepository.createPassage()` utilise `CurrentUserService.instance.opeUserId` - [x] **Modification de passage** - Attribution du `ope_users.id` si changement d'utilisateur - Endpoint : `PUT /api/passages/:id` - Body : `{ ..., fk_user: 50 }` ← ID de `ope_users` - ✅ **Fait** : `PassageRepository.updatePassage()` utilise `CurrentUserService.instance.opeUserId` - ✅ **Fait** : Mode offline et online correctement implémentés - [ ] **Suppression de passage** - L'API supprime dans `ope_pass` - Endpoint : `DELETE /api/passages/:id` #### 5.5 Interface Flutter - Mapping des IDs **Scénarios à gérer** : 1. **Affichage des secteurs avec utilisateurs affectés** : ```dart // Utiliser usersSectors[i].opeUserId pour lier avec passages final userPassages = passages.where((p) => p.fkUser == usersSectors[i].opeUserId && p.fkSector == sector.id ).toList(); ``` - ✅ **Fait** : `ActivityChart` filtre par secteurs assignés (pas par userId) - ✅ **Fait** : `MapPage` utilise `userSector.opeUserId` pour filtrer les secteurs 2. **Attribution d'un passage à un utilisateur** : ```dart // Envoyer ope_user_id dans la requête API await apiService.createPassage({ ...passageData, 'fk_user': userSector.opeUserId, // ope_users.id 'fk_sector': sector.id }); ``` - ✅ **Fait** : `PassageRepository` utilise `CurrentUserService.instance.opeUserId` 3. **Affichage du nom d'un utilisateur depuis un passage** : ```dart // Chercher dans usersSectors avec ope_user_id final userSector = usersSectors.firstWhere( (us) => us.opeUserId == passage.fkUser, orElse: () => null ); final userName = userSector?.name ?? 'Inconnu'; ``` - ✅ **Fait** : `PaymentSummaryCard` utilise `opeUserId` pour filtrer les règlements - ✅ **Fait** : `HistoryPage` utilise `opeUserId` pour filtrer et vérifier les permissions - ✅ **Fait** : `UserFieldModePage` compare `passage.fkUser` avec `opeUserId` 4. **Gestion des membres** : ```dart // Conserver les 2 IDs lors de la création final newMember = await apiService.createUser(userData); membres.add(Member( userId: newMember['id'], // users.id opeUserId: newMember['ope_user_id'], // ope_users.id ... )); ``` - ⚠️ **À tester** : Vérifier la réception de `ope_user_id` dans les réponses API 5. **Statistiques par utilisateur** : - ✅ **Fait** : `PassageRepository.getStatisticsByUser()` utilise `passage.fkUser` (ope_users.id) - ✅ **Fait** : Variable renommée `opeUserId` pour clarté 6. **Services et chargement de données** : - ✅ **Fait** : `DataLoadingService` utilise `opeUserId` pour les clés Hive des secteurs - ✅ **Fait** : `CurrentUserService.opeUserId` accessible globalement #### 5.6 Tests d'affichage - [ ] Tester l'affichage des passages avec noms d'utilisateurs - [ ] Tester l'affichage des secteurs avec utilisateurs affectés - [ ] Tester la création d'un membre (vérifier que les 2 IDs sont reçus) - [ ] Tester la suppression d'un membre (vérifier le transfert de passages) - [ ] Tester la création d'un secteur avec attribution d'utilisateurs - [ ] Tester la création d'un passage avec attribution d'utilisateur - [ ] Tester la suppression d'une opération (doit tout nettoyer) --- ### 📝 Modifications Flutter effectuées (2025-01-23) #### Fichiers modifiés 1. **`lib/core/data/models/user_sector_model.dart`** (lignes 11-50) - Ajout du champ `opeUserId` (@HiveField(5)) - Renommage du champ `id` → `userId` (@HiveField(0)) - Mise à jour de `fromJson()` pour parser les deux IDs - Mise à jour de `toJson()` et `copyWith()` 2. **`lib/core/data/models/user_model.dart`** (ligne 122) - Ajout du champ `opeUserId` (@HiveField(20)) - Mise à jour de `fromJson()`, `toJson()` et `copyWith()` 3. **`lib/core/services/current_user_service.dart`** (ligne 24) - Ajout du getter `opeUserId` pour accès global 4. **`lib/core/services/data_loading_service.dart`** (ligne 451) - Utilisation de `userSector.opeUserId` pour les clés Hive 5. **`lib/presentation/dialogs/sector_dialog.dart`** (lignes 86, 538-542, 999-1007) - Changement de source : `MembreModel` → `UserSectorModel` - Utilisation de `opeUserId` pour la sélection des membres - Déduplication des membres (un user peut être sur plusieurs secteurs) 6. **`lib/presentation/pages/history_page.dart`** (lignes 62-63, 126, 997-1009, 1023, 1608, 1629) - Ajout de `currentOpeUserId` (ligne 62) - Utilisation de `UserSectorModel` avec déduplication - Comparaison `passage.fkUser == currentOpeUserId` pour filtrage et permissions 7. **`lib/presentation/user/user_field_mode_page.dart`** (ligne 999-1002) - Correction : `passage.fkUser == userRepository.getCurrentUser()?.opeUserId` 8. **`lib/presentation/widgets/charts/payment_summary_card.dart`** (ligne 425, 428-430) - Utilisation de `currentUser?.opeUserId` pour filtrer les règlements utilisateur 9. **`lib/core/repositories/passage_repository.dart`** (lignes 105, 384-388, 422-426, 577) - Renommage du paramètre : `getPassagesByUser(int opeUserId)` - Utilisation de `CurrentUserService.instance.opeUserId` pour création/modification - Renommage de variable : `userId` → `opeUserId` dans les statistiques 10. **`lib/presentation/pages/map_page.dart`** (lignes 792-795) - **ERREUR CRITIQUE CORRIGÉE** : `us.id` → `us.opeUserId` - Utilisation de `CurrentUserService.instance.opeUserId` 11. **`lib/presentation/pages/home_page.dart`** (lignes 22-32) - Suppression de variable inutilisée : `currentUserId` #### Adaptateurs générés - **`lib/core/data/models/user_sector_model.g.dart`** - Régénéré avec `build_runner` - **`lib/core/data/models/user_model.g.dart`** - Régénéré avec `build_runner` #### Widgets vérifiés (pas de modification nécessaire) - `PassageSummaryCard` - Affiche tous les passages déjà filtrés par l'API - `ActivityChart` - Filtre par secteurs assignés, pas par userId - `SectorDistributionCard` - Affiche tous les secteurs sans filtrage utilisateur - `MembersBoardPassages` - `membre.id` représente déjà `ope_users.id` #### Analyse statique ```bash flutter analyze ``` - ✅ **0 erreur de compilation** - ℹ️ 33 avertissements de style (info uniquement) --- ## 📋 Ordre d'exécution recommandé 1. ✅ **Corriger le code de migration2** (PHP) 2. ✅ **Tester sur dva_geo** avec schéma modifié 3. ✅ **Vérifier l'API** sur dva_geo 4. ✅ **Vérifier Flutter** avec dva_geo 5. 🚀 **Déployer le schéma SQL** sur rca_geo 6. 🚀 **Déployer le code** sur rca_geo 7. ✅ **Tester en recette** 8. 🚀 **Déployer en production** (pra_geo) --- ## 🔍 Requêtes SQL utiles pour vérification ### Vérifier les contraintes FK actuelles ```sql SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND (TABLE_NAME = 'ope_pass' OR TABLE_NAME = 'ope_users_sectors') AND COLUMN_NAME = 'fk_user'; ``` ### Vérifier l'intégrité des données après migration ```sql -- Vérifier que tous les fk_user de ope_pass existent dans ope_users SELECT COUNT(*) as orphans FROM ope_pass op LEFT JOIN ope_users ou ON op.fk_user = ou.id WHERE ou.id IS NULL; -- Résultat attendu : 0 -- Vérifier que tous les fk_user de ope_users_sectors existent dans ope_users SELECT COUNT(*) as orphans FROM ope_users_sectors ous LEFT JOIN ope_users ou ON ous.fk_user = ou.id WHERE ou.id IS NULL; -- Résultat attendu : 0 ``` ### Tester la suppression en cascade ```sql -- Compter avant suppression SELECT (SELECT COUNT(*) FROM ope_users WHERE fk_operation = 850) as ope_users_count, (SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = 850) as ope_users_sectors_count, (SELECT COUNT(*) FROM ope_pass WHERE fk_operation = 850) as ope_pass_count, (SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = 850) as ope_sectors_count; -- Supprimer l'opération DELETE FROM operations WHERE id = 850; -- Vérifier que tout a été supprimé (doit retourner 0 partout) SELECT (SELECT COUNT(*) FROM ope_users WHERE fk_operation = 850) as ope_users_count, (SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = 850) as ope_users_sectors_count, (SELECT COUNT(*) FROM ope_pass WHERE fk_operation = 850) as ope_pass_count, (SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = 850) as ope_sectors_count; ``` --- ## 📝 Notes importantes ### Avantages de cette architecture ✅ **Isolation complète** : Supprimer une opération supprime tout (ope_users, secteurs, passages) ✅ **Performance** : Pas de jointures complexes avec la table centrale `users` ✅ **Historique** : Les données d'une opération sont figées dans le temps ✅ **Simplicité** : Requêtes plus simples, moins de risques d'incohérences ### Implications ⚠️ **Duplication** : Un utilisateur travaillant sur 3 opérations aura 3 entrées dans `ope_users` ⚠️ **Taille** : La table `ope_users` sera plus volumineuse ⚠️ **Jointures** : Pour remonter aux infos de la table `users`, il faut passer par `ope_users.fk_user` ### Rétrocompatibilité ❌ Ce changement **CASSE** la compatibilité avec les données existantes ✅ Nécessite une **re-migration complète** de toutes les entités après modification du schéma ✅ Ou bien : script de transformation des données existantes (plus complexe) --- ## 🎯 Statut - [ ] Schéma SQL modifié sur dva_geo - [ ] Code migration2 corrigé - [ ] API vérifiée et corrigée - [x] **Flutter vérifié et corrigé** ✅ (2025-01-23) - Modèles de données mis à jour (UserSectorModel, UserModel) - Services mis à jour (CurrentUserService, DataLoadingService) - Repositories mis à jour (PassageRepository) - Pages et widgets mis à jour (11 fichiers) - Adaptateurs Hive régénérés - Analyse statique : 0 erreur - [ ] Tests complets sur dva_geo (en attente API) - [ ] Déploiement rca_geo - [ ] Déploiement pra_geo