Mise en place suppression membre

This commit is contained in:
d6soft
2025-06-12 16:39:44 +02:00
parent 4244b961fd
commit b9672a6228
27 changed files with 125586 additions and 123640 deletions

22
api/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#fa1b49",
"activityBar.background": "#fa1b49",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#155e02",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#fa1b49",
"statusBar.background": "#dd0531",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#fa1b49",
"statusBarItem.remoteBackground": "#dd0531",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#dd0531",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#dd053199",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#dd0531"
}

View File

@@ -127,12 +127,10 @@ class UserController {
u.encrypted_mobile, u.encrypted_mobile,
u.fk_role as role, u.fk_role as role,
u.fk_entite, u.fk_entite,
u.infos,
u.chk_alert_email, u.chk_alert_email,
u.chk_suivi, u.chk_suivi,
u.date_naissance, u.date_naissance,
u.date_embauche, u.date_embauche,
u.matricule,
u.chk_active, u.chk_active,
u.created_at, u.created_at,
u.updated_at, u.updated_at,
@@ -226,11 +224,11 @@ class UserController {
$email = trim(strtolower($data['email'])); $email = trim(strtolower($data['email']));
$name = trim($data['name']); $name = trim($data['name']);
$firstName = isset($data['first_name']) ? trim($data['first_name']) : ''; $firstName = isset($data['first_name']) ? trim($data['first_name']) : '';
$role = isset($data['role']) ? trim($data['role']) : '1'; $role = isset($data['role']) ? (int)$data['role'] : 1;
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1; $entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
// Vérification des longueurs d'entrée // Vérification des longueurs d'entrée
if (strlen($email) > 255 || strlen($name) > 255) { if (strlen($email) > 75 || strlen($name) > 50) {
Response::json([ Response::json([
'status' => 'error', 'status' => 'error',
'message' => 'Email ou nom trop long' 'message' => 'Email ou nom trop long'
@@ -270,26 +268,24 @@ class UserController {
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null; $phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
$mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null; $mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null;
$sectName = isset($data['sect_name']) ? trim($data['sect_name']) : ''; $sectName = isset($data['sect_name']) ? trim($data['sect_name']) : '';
$infos = isset($data['infos']) ? trim($data['infos']) : '';
$alertEmail = isset($data['chk_alert_email']) ? (int)$data['chk_alert_email'] : 1; $alertEmail = isset($data['chk_alert_email']) ? (int)$data['chk_alert_email'] : 1;
$suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0; $suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0;
$dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null; $dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null;
$dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null; $dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null;
$matricule = isset($data['matricule']) ? trim($data['matricule']) : '';
// Insertion en base de données // Insertion en base de données
$stmt = $this->db->prepare(' $stmt = $this->db->prepare('
INSERT INTO users ( INSERT INTO users (
encrypted_email, user_pswd, encrypted_name, first_name, encrypted_email, user_pass_hash, encrypted_name, first_name,
sect_name, encrypted_phone, encrypted_mobile, fk_role, sect_name, encrypted_phone, encrypted_mobile, fk_role,
fk_entite, infos, chk_alert_email, chk_suivi, fk_entite, chk_alert_email, chk_suivi,
date_naissance, date_embauche, matricule, date_naissance, date_embauche,
created_at, fk_user_creat, chk_active created_at, fk_user_creat, chk_active
) VALUES ( ) VALUES (
?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?,
NOW(), ?, 1 NOW(), ?, 1
) )
'); ');
@@ -303,12 +299,10 @@ class UserController {
$mobile, $mobile,
$role, $role,
$entiteId, $entiteId,
$infos,
$alertEmail, $alertEmail,
$suivi, $suivi,
$dateNaissance, $dateNaissance,
$dateEmbauche, $dateEmbauche,
$matricule,
$currentUserId $currentUserId
]); ]);
$userId = $this->db->lastInsertId(); $userId = $this->db->lastInsertId();
@@ -418,12 +412,10 @@ class UserController {
'sect_name', 'sect_name',
'fk_role', 'fk_role',
'fk_entite', 'fk_entite',
'infos',
'chk_alert_email', 'chk_alert_email',
'chk_suivi', 'chk_suivi',
'date_naissance', 'date_naissance',
'date_embauche', 'date_embauche',
'matricule',
'chk_active' 'chk_active'
]; ];
@@ -443,7 +435,7 @@ class UserController {
], 400); ], 400);
return; return;
} }
$updateFields[] = "user_pswd = :password"; $updateFields[] = "user_pass_hash = :password";
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT); $params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
} }
@@ -498,24 +490,23 @@ class UserController {
public function deleteUser(string $id): void { public function deleteUser(string $id): void {
Session::requireAuth(); Session::requireAuth();
// Vérification des droits d'accès (rôle administrateur)
$currentUserId = Session::getUserId(); $currentUserId = Session::getUserId();
// Récupérer le rôle de l'utilisateur depuis la base de données // Récupérer les infos de l'utilisateur courant
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?'); $stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
$stmt->execute([$currentUserId]); $stmt->execute([$currentUserId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
$userRole = $result ? $result['fk_role'] : null;
if ($userRole != '1' && $userRole != '2') { if (!$currentUser) {
Response::json([ Response::json([
'status' => 'error', 'status' => 'error',
'message' => 'Accès non autorisé' 'message' => 'Utilisateur courant non trouvé'
], 403); ], 403);
return; return;
} }
$currentUserId = Session::getUserId(); $userRole = (int)$currentUser['fk_role'];
$userEntite = $currentUser['fk_entite'];
// Empêcher la suppression de son propre compte // Empêcher la suppression de son propre compte
if ($currentUserId == $id) { if ($currentUserId == $id) {
@@ -526,37 +517,104 @@ class UserController {
return; return;
} }
try { // Récupérer l'utilisateur cible
// Désactivation de l'utilisateur plutôt que suppression $stmt2 = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
$stmt = $this->db->prepare(' $stmt2->execute([$id]);
UPDATE users $userToDelete = $stmt2->fetch(PDO::FETCH_ASSOC);
SET chk_active = 0,
updated_at = NOW(),
fk_user_modif = ?
WHERE id = ?
');
$stmt->execute([$currentUserId, $id]);
if ($stmt->rowCount() === 0) { if (!$userToDelete) {
Response::json([ Response::json([
'status' => 'error', 'status' => 'error',
'message' => 'Utilisateur non trouvé' 'message' => 'Utilisateur cible non trouvé'
], 404); ], 404);
return; return;
} }
LogService::log('Utilisateur GeoSector désactivé', [ // Contrôle des droits
if ($userRole === 1) {
Response::json([
'status' => 'error',
'message' => "Vous n'avez pas le droit de supprimer un utilisateur"
], 403);
return;
} elseif ($userRole === 2) {
if ($userEntite != $userToDelete['fk_entite']) {
Response::json([
'status' => 'error',
'message' => "Vous n'avez pas le droit de supprimer un utilisateur d'une autre amicale"
], 403);
return;
}
}
// fk_role > 2 : tout est permis (hors auto-suppression)
// ——— Gestion du transfert éventuel ———
$transferTo = isset($_GET['transfer_to']) ? trim($_GET['transfer_to']) : null;
$operationId = isset($_GET['operation_id']) ? trim($_GET['operation_id']) : null;
if (($transferTo && !$operationId) || (!$transferTo && $operationId)) {
Response::json([
'status' => 'error',
'message' => "Il faut fournir transfer_to ET operation_id ou aucun des deux"
], 400);
return;
}
if ($transferTo && $operationId) {
try {
$stmt3 = $this->db->prepare('
UPDATE passages
SET fk_user = :new_user_id
WHERE fk_user = :delete_user_id
AND fk_operation = :operation_id
');
$stmt3->execute([
'new_user_id' => $transferTo,
'delete_user_id' => $id,
'operation_id' => $operationId
]);
} catch (PDOException $e) {
Response::json([
'status' => 'error',
'message' => 'Erreur lors du transfert des passages',
'error' => $e->getMessage()
], 500);
return;
}
}
// —— Suppression réelle de l'utilisateur ——
try {
// Supprimer les enregistrements dépendants dans ope_users
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
$stmtOpeUsers->execute([$id]);
// Ici éventuellement : d'autres suppressions en cascade si besoin
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur non trouvé ou déjà supprimé'
], 404);
return;
}
LogService::log('Utilisateur GeoSector supprimé', [
'level' => 'info', 'level' => 'info',
'deactivatedBy' => $currentUserId, 'deletedBy' => $currentUserId,
'userId' => $id 'userId' => $id,
'passage_transfer' => $transferTo && $operationId ? "Vers utilisateur $transferTo pour operation $operationId" : 'Aucun'
]); ]);
Response::json([ Response::json([
'status' => 'success', 'status' => 'success',
'message' => 'Utilisateur désactivé avec succès' 'message' => 'Utilisateur supprimé avec succès'
]); ]);
} catch (PDOException $e) { } catch (PDOException $e) {
LogService::log('Erreur lors de la désactivation d\'un utilisateur GeoSector', [ LogService::log('Erreur lors de la suppression d\'un utilisateur GeoSector', [
'level' => 'error', 'level' => 'error',
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'userId' => $id 'userId' => $id

File diff suppressed because one or more lines are too long

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"packages": [ "packages": [
{ {
"name": "geosector_app", "name": "geosector_app",
"version": "0.3.4", "version": "0.3.5",
"dependencies": [ "dependencies": [
"connectivity_plus", "connectivity_plus",
"cupertino_icons", "cupertino_icons",

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@ GEOSECTOR est une solution complète développée en Flutter qui révolutionne l
### 🎯 Fonctionnalités métier ### 🎯 Fonctionnalités métier
#### Pour les **Distributeurs** (Rôle 1) #### Pour les **Membres** (Rôle 1)
- ✅ Visualisation des secteurs assignés sur carte interactive - ✅ Visualisation des secteurs assignés sur carte interactive
- ✅ Suivi GPS en temps réel des tournées - ✅ Suivi GPS en temps réel des tournées
@@ -368,7 +368,7 @@ Timeout : "Délai d'attente dépassé"
### Hiérarchie des permissions ### Hiérarchie des permissions
Distributeur (Rôle 1) : Consultation et distribution dans ses secteurs Membre (Rôle 1) : Consultation et distribution dans ses secteurs
Admin Amicale (Rôle 2) : Gestion complète de son amicale et ses membres Admin Amicale (Rôle 2) : Gestion complète de son amicale et ses membres
Super Admin (Rôle 3+) : Administration globale multi-amicales Super Admin (Rôle 3+) : Administration globale multi-amicales
@@ -387,6 +387,237 @@ Checkbox statut actif : Contrôle d'accès aux comptes
Édition contextuelle : Champs modifiables selon les permissions Édition contextuelle : Champs modifiables selon les permissions
Actions conditionnelles : Boutons disponibles selon le niveau d'autorisation Actions conditionnelles : Boutons disponibles selon le niveau d'autorisation
## 👥 Gestion des membres (Admin Amicale)
### 🎯 Vue d'ensemble
La gestion des membres est une fonctionnalité clé pour les **Admins Amicale** (Rôle 2) qui permet une administration complète des équipes au sein de leur amicale. Cette interface centralise toutes les opérations liées aux membres avec une approche sécurisée et intuitive.
### 📱 Interface AdminAmicalePage
L'interface principale `admin_amicale_page.dart` offre une vue d'ensemble complète :
- **Informations de l'amicale** : Affichage des détails de l'amicale courante
- **Liste des membres** : Tableau interactif avec actions contextuelles
- **Ajout de membres** : Bouton d'action pour créer de nouveaux comptes
- **Opération courante** : Indication de l'opération active en cours
### ✨ Fonctionnalités principales
#### 🆕 Création de nouveaux membres
```dart
// Workflow de création
1. Clic sur "Ajouter un membre"
2. Ouverture du UserFormDialog
3. Formulaire vierge avec valeurs par défaut
4. Sélection du rôle (Membre/Administrateur)
5. Configuration du statut actif
6. Validation et création via API
7. Attribution automatique à l'amicale courante
```
**Champs obligatoires :**
- Email (unique dans le système)
- Au moins un nom (nom de famille OU nom de tournée)
- Rôle dans l'amicale
**Champs optionnels :**
- Nom d'utilisateur (éditable pour les admins)
- Prénom, téléphones, dates
- Nom de tournée (pour identification terrain)
#### ✏️ Modification des membres existants
```dart
// Actions disponibles
- Clic sur une ligne Ouverture du formulaire d'édition
- Modification de tous les champs personnels
- Changement de rôle (Membre Administrateur)
- Activation/Désactivation du compte
- Gestion du nom de tournée
```
**Workflow de modification :**
1. Sélection du membre dans le tableau
2. Ouverture automatique du `UserFormDialog`
3. Formulaire pré-rempli avec données existantes
4. Modification des champs souhaités
5. Validation et mise à jour via API
6. Synchronisation automatique avec Hive
#### 🗑️ Suppression intelligente des membres
La suppression des membres intègre une **logique métier avancée** pour préserver l'intégrité des données :
##### 🔍 Vérification des passages
```dart
// Algorithme de vérification
1. Récupération de l'opération courante
2. Analyse des passages du membre pour cette opération
3. Classification : passages réalisés vs passages à finaliser
4. Comptage total des passages actifs
```
##### 📊 Scénarios de suppression
**Cas 1 : Aucun passage (suppression simple)**
```http
DELETE /users/{id}
```
- Confirmation simple
- Suppression directe
- Aucun transfert nécessaire
**Cas 2 : Passages existants (suppression avec transfert)**
```http
DELETE /users/{id}?transfer_to={destinataire}&operation_id={operation}
```
- Dialog d'avertissement avec détails
- Sélection obligatoire d'un membre destinataire
- Transfert automatique de tous les passages
- Préservation de l'historique
**Cas 3 : Alternative recommandée (désactivation)**
```dart
// Mise à jour du statut
membre.copyWith(isActive: false)
```
- Préservation complète des données
- Blocage de la connexion
- Maintien de l'historique
- Réactivation possible ultérieurement
### 🔐 Sécurité et permissions
#### Contrôles d'accès
- **Isolation par amicale** : Admins limités à leur amicale
- **Vérification des rôles** : Validation côté client et serveur
- **Opération courante** : Filtrage par contexte d'opération
- **Validation API** : Contrôles d'unicité et cohérence
#### Gestion des erreurs
```mermaid
graph TD
A[Action utilisateur] --> B[Validation locale]
B --> C[Appel API]
C --> D{Succès ?}
D -->|Oui| E[Mise à jour Hive]
D -->|Non| F[Affichage erreur]
E --> G[Notification succès]
F --> H[Dialog reste ouvert]
```
### 🎨 Interface utilisateur
#### Tableaux interactifs
**MembreTableWidget** - Composant principal :
- Colonnes : ID, Prénom, Nom, Email, Rôle, Statut
- Actions : Modification, Suppression
- Alternance de couleurs pour lisibilité
- Tri et navigation intuitifs
**MembreRowWidget** - Ligne individuelle :
- Clic pour édition rapide
- Boutons d'action contextuels
- Indicateurs visuels de statut
- Tooltip informatifs
#### Formulaires adaptatifs
**UserFormDialog** - Modale réutilisable :
- Layout responsive (>900px vs mobile)
- Validation en temps réel
- Gestion des erreurs inline
- Sauvegarde avec feedback
### 📡 Synchronisation et réactivité
#### Architecture ValueListenableBuilder
```dart
// Écoute automatique des changements
ValueListenableBuilder<Box<MembreModel>>(
valueListenable: membreRepository.getMembresBox().listenable(),
builder: (context, membresBox, child) {
// Interface mise à jour automatiquement
final membres = membresBox.values
.where((m) => m.fkEntite == currentUser.fkEntite)
.toList();
return MembreTableWidget(membres: membres);
},
)
```
#### Principe "API First"
1. **Validation API** : Tentative sur serveur en priorité
2. **Succès** → Sauvegarde locale + mise à jour interface
3. **Erreur** → Affichage message + maintien état local
4. **Cohérence** : Données locales toujours synchronisées
### 🔄 Workflow complet
```mermaid
sequenceDiagram
participant A as Admin
participant UI as Interface
participant R as Repository
participant API as Serveur
participant H as Hive
A->>UI: Action membre
UI->>R: Appel repository
R->>API: Requête HTTP
API-->>R: Réponse
alt Succès
R->>H: Sauvegarde locale
H-->>UI: Notification changement
UI-->>A: Interface mise à jour
else Erreur
R-->>UI: Exception
UI-->>A: Message d'erreur
end
```
### 🎯 Bonnes pratiques
#### Pour les administrateurs
1. **Vérification avant suppression** : Toujours examiner les passages
2. **Préférer la désactivation** : Éviter la perte de données
3. **Noms de tournée** : Utiliser des identifiants terrain clairs
4. **Rôles appropriés** : Limiter les administrateurs aux besoins
#### Gestion des erreurs courantes
| Erreur | Cause | Solution |
| ----------------------- | ------------- | ------------------------ |
| Email déjà utilisé | Duplication | Choisir un autre email |
| Membre avec passages | Données liées | Transférer ou désactiver |
| Aucune opération active | Configuration | Vérifier les opérations |
| Accès refusé | Permissions | Vérifier le rôle admin |
Cette architecture garantit une gestion des membres robuste, sécurisée et intuitive, optimisant l'efficacité administrative tout en préservant l'intégrité des données opérationnelles. 👥✨
## 🗺️ Cartes et géolocalisation ## 🗺️ Cartes et géolocalisation
Flutter Map : Rendu cartographique haute performance Flutter Map : Rendu cartographique haute performance

View File

@@ -1 +1 @@
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/pierre/dev/geosector/app/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=0987d131841f12618fa22c05d7871702_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]} {"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/pierre/dev/geosector/app/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=9c247933552af22255bf791d596f2dce_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}

View File

@@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"1425e5e9ec5eeb4f225c401d8db69b860e0fde
_flutter.loader.load({ _flutter.loader.load({
serviceWorkerSettings: { serviceWorkerSettings: {
serviceWorkerVersion: "4143491003" serviceWorkerVersion: "1907117848"
} }
}); });

View File

@@ -3,13 +3,13 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {"flutter_bootstrap.js": "d64cc0cfde96e267f6c2306a601e64e1", const RESOURCES = {"flutter_bootstrap.js": "8a2b42aa8136fe0225b2b62c68d0dbcd",
"version.json": "b01f276d289a7da276afae267e1f955a", "version.json": "e8d1bf0cf13b62f11b193e5c745aa8d1",
"index.html": "2aab03d10fea3b608e3eddc0fc0077e5", "index.html": "2aab03d10fea3b608e3eddc0fc0077e5",
"/": "2aab03d10fea3b608e3eddc0fc0077e5", "/": "2aab03d10fea3b608e3eddc0fc0077e5",
"favicon-64.png": "259540a3217e969237530444ca0eaed3", "favicon-64.png": "259540a3217e969237530444ca0eaed3",
"favicon-16.png": "106142fb24eba190e475dbe6513cc9ff", "favicon-16.png": "106142fb24eba190e475dbe6513cc9ff",
"main.dart.js": "201747c331c2e399470d7f971f0387a8", "main.dart.js": "b45a67a2883e7ac3efa7375758392344",
"flutter.js": "83d881c1dbb6d6bcd6b42e274605b69c", "flutter.js": "83d881c1dbb6d6bcd6b42e274605b69c",
"favicon.png": "21510778ead066ac826ad69302400773", "favicon.png": "21510778ead066ac826ad69302400773",
"icons/Icon-192.png": "f36879dd176101fac324b68793e4683c", "icons/Icon-192.png": "f36879dd176101fac324b68793e4683c",
@@ -29,7 +29,7 @@ const RESOURCES = {"flutter_bootstrap.js": "d64cc0cfde96e267f6c2306a601e64e1",
"assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362", "assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362",
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce", "assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
"assets/AssetManifest.bin": "bb9240a2148a79f4e1593ed3a51f47d0", "assets/AssetManifest.bin": "bb9240a2148a79f4e1593ed3a51f47d0",
"assets/fonts/MaterialIcons-Regular.otf": "6adfc0c15b0e12095dad895cfa1299cb", "assets/fonts/MaterialIcons-Regular.otf": "ac09b81b3261e74c47ed73d08f520ce8",
"assets/assets/images/geosector-logo.png": "b78408af5aa357b1107e1cb7be9e7c1e", "assets/assets/images/geosector-logo.png": "b78408af5aa357b1107e1cb7be9e7c1e",
"assets/assets/images/logo-geosector-1024.png": "adb1be034f0b983acf6246369a794de5", "assets/assets/images/logo-geosector-1024.png": "adb1be034f0b983acf6246369a794de5",
"assets/assets/images/icon-geosector.svg": "c9dd0fb514a53ee434b57895cf6cd5fd", "assets/assets/images/icon-geosector.svg": "c9dd0fb514a53ee434b57895cf6cd5fd",

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"app_name":"geosector_app","version":"0.3.4","package_name":"geosector_app"} {"app_name":"geosector_app","version":"0.3.5","package_name":"geosector_app"}

View File

@@ -127,18 +127,44 @@ class MembreRepository extends ChangeNotifier {
// Appeler l'API users // Appeler l'API users
final response = await ApiService.instance.post('/users', data: data); final response = await ApiService.instance.post('/users', data: data);
if (response.statusCode == 201 || response.statusCode == 200) { if (response.statusCode == 201) {
// Créer le membre avec les données retournées par l'API // Extraire l'ID de la réponse API
final createdMember = MembreModel.fromJson(response.data); final responseData = response.data;
debugPrint('🎉 Réponse API création utilisateur: $responseData');
// Sauvegarder localement // L'API retourne {"status":"success","message":"Utilisateur créé avec succès","id":"10027748"}
final userId = responseData['id'] is String ? int.parse(responseData['id']) : responseData['id'] as int;
// Créer le nouveau membre avec l'ID retourné par l'API
final createdMember = MembreModel(
id: userId,
fkEntite: membre.fkEntite,
role: membre.role,
fkTitre: membre.fkTitre,
name: membre.name,
firstName: membre.firstName,
username: membre.username,
sectName: membre.sectName,
email: membre.email,
phone: membre.phone,
mobile: membre.mobile,
dateNaissance: membre.dateNaissance,
dateEmbauche: membre.dateEmbauche,
createdAt: DateTime.now(),
isActive: membre.isActive,
);
// Sauvegarder localement dans Hive
await saveMembreBox(createdMember); await saveMembreBox(createdMember);
return createdMember; // Retourner le membre créé debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
return createdMember;
} }
debugPrint('❌ Échec création membre - Code: ${response.statusCode}');
return null; return null;
} catch (e) { } catch (e) {
debugPrint('Erreur lors de la création du membre: $e'); debugPrint('Erreur lors de la création du membre: $e');
rethrow; // Propager l'exception pour la gestion d'erreurs rethrow; // Propager l'exception pour la gestion d'erreurs
} finally { } finally {
_isLoading = false; _isLoading = false;
@@ -174,25 +200,45 @@ class MembreRepository extends ChangeNotifier {
} }
} }
// Supprimer un membre via l'API // Supprimer un membre via l'API avec transfert optionnel
Future<bool> deleteMembre(int id) async { Future<bool> deleteMembre(int membreId, [int? transferToUserId, int? operationId]) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
try { try {
// Appeler l'API users au lieu de membres (correction ici) String endpoint = '/users/$membreId';
final response = await ApiService.instance.delete('/users/$id');
// Construire les paramètres query SEULEMENT si on a des passages à transférer
List<String> queryParams = [];
if (transferToUserId != null && transferToUserId > 0) {
queryParams.add('transfer_to=$transferToUserId');
// Ajouter operation_id SEULEMENT s'il y a un transfert
if (operationId != null && operationId > 0) {
queryParams.add('operation_id=$operationId');
}
}
// Ajouter les paramètres à l'endpoint
if (queryParams.isNotEmpty) {
endpoint += '?${queryParams.join('&')}';
}
debugPrint('🔗 DELETE endpoint: $endpoint');
final response = await ApiService.instance.delete(endpoint);
if (response.statusCode == 200 || response.statusCode == 204) { if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer le membre localement // Supprimer le membre localement
await deleteMembreBox(id); await deleteMembreBox(membreId);
return true; return true;
} }
return false; return false;
} catch (e) { } catch (e) {
debugPrint('Erreur lors de la suppression du membre: $e'); debugPrint('Erreur lors de la suppression du membre: $e');
return false; rethrow;
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();

View File

@@ -41,6 +41,44 @@ class OperationRepository extends ChangeNotifier {
return _operationBox.get(id); return _operationBox.get(id);
} }
OperationModel? getCurrentOperation() {
try {
// Récupérer toutes les opérations actives
final activeOperations = _operationBox.values.where((operation) => operation.isActive == true).toList();
if (activeOperations.isEmpty) {
debugPrint('⚠️ Aucune opération active trouvée');
return null;
}
// Trier par ID décroissant et prendre la première (ID le plus élevé)
activeOperations.sort((a, b) => b.id.compareTo(a.id));
final currentOperation = activeOperations.first;
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
return currentOperation;
} catch (e) {
debugPrint('❌ Erreur lors de la récupération de l\'opération courante: $e');
return null;
}
}
// Méthode helper pour récupérer seulement l'ID de l'opération courante
int? getCurrentOperationId() {
final currentOperation = getCurrentOperation();
return currentOperation?.id;
}
// Méthode pour récupérer toutes les opérations actives (utile pour debug/admin)
List<OperationModel> getActiveOperations() {
try {
return _operationBox.values.where((operation) => operation.isActive == true).toList()..sort((a, b) => b.id.compareTo(a.id)); // Tri par ID décroissant
} catch (e) {
debugPrint('❌ Erreur lors de la récupération des opérations actives: $e');
return [];
}
}
// Sauvegarder une opération // Sauvegarder une opération
Future<void> saveOperation(OperationModel operation) async { Future<void> saveOperation(OperationModel operation) async {
await _operationBox.put(operation.id, operation); await _operationBox.put(operation.id, operation);

View File

@@ -9,6 +9,9 @@ class PassageRepository extends ChangeNotifier {
// Constructeur sans paramètres - utilise ApiService.instance // Constructeur sans paramètres - utilise ApiService.instance
PassageRepository(); PassageRepository();
// Cache pour les statistiques
Map<String, dynamic>? _cachedStats;
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire // Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
// et vérifier qu'elle est ouverte avant accès // et vérifier qu'elle est ouverte avant accès
Box<PassageModel> get _passageBox { Box<PassageModel> get _passageBox {
@@ -16,6 +19,19 @@ class PassageRepository extends ChangeNotifier {
return Hive.box<PassageModel>(AppKeys.passagesBoxName); return Hive.box<PassageModel>(AppKeys.passagesBoxName);
} }
// Méthode pour exposer la Box Hive (nécessaire pour ValueListenableBuilder)
Box<PassageModel> getPassagesBox() {
try {
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
throw Exception('La boîte passages n\'est pas ouverte');
}
return Hive.box<PassageModel>(AppKeys.passagesBoxName);
} catch (e) {
debugPrint('Erreur lors de l\'accès à la boîte passages: $e');
rethrow;
}
}
// Stream pour notifier des changements de passages // Stream pour notifier des changements de passages
StreamController<List<PassageModel>>? _passageStreamController; StreamController<List<PassageModel>>? _passageStreamController;
@@ -73,6 +89,16 @@ class PassageRepository extends ChangeNotifier {
return _passageBox.values.where((passage) => passage.fkOperation == operationId).toList(); return _passageBox.values.where((passage) => passage.fkOperation == operationId).toList();
} }
// Récupérer les passages par utilisateur
List<PassageModel> getPassagesByUser(int userId) {
try {
return _passageBox.values.where((passage) => passage.fkUser == userId).toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des passages par utilisateur: $e');
return [];
}
}
// Récupérer les passages par date // Récupérer les passages par date
List<PassageModel> getPassagesByDate(DateTime date) { List<PassageModel> getPassagesByDate(DateTime date) {
return _passageBox.values.where((passage) { return _passageBox.values.where((passage) {
@@ -263,11 +289,14 @@ class PassageRepository extends ChangeNotifier {
} }
} }
// Statistiques // Recalculer les statistiques (appelée par le ValueListenableBuilder)
Map<String, int> getPassageStatistics() { Map<String, dynamic> calculatePassageStatistics() {
final allPassages = getAllPassages(); final allPassages = getAllPassages();
return { debugPrint('📊 Calcul des statistiques: ${allPassages.length} passages');
// Statistiques globales
final globalStats = {
'total': allPassages.length, 'total': allPassages.length,
'effectues': allPassages.where((p) => p.fkType == 1).length, 'effectues': allPassages.where((p) => p.fkType == 1).length,
'a_finaliser': allPassages.where((p) => p.fkType == 2).length, 'a_finaliser': allPassages.where((p) => p.fkType == 2).length,
@@ -276,6 +305,57 @@ class PassageRepository extends ChangeNotifier {
'lots': allPassages.where((p) => p.fkType == 5).length, 'lots': allPassages.where((p) => p.fkType == 5).length,
'maisons_vides': allPassages.where((p) => p.fkType == 6).length, 'maisons_vides': allPassages.where((p) => p.fkType == 6).length,
}; };
// Statistiques par utilisateur
final Map<int, Map<String, int>> statsByUser = {};
// Grouper les passages par utilisateur
final passagesByUser = <int, List<PassageModel>>{};
for (final passage in allPassages) {
passagesByUser.putIfAbsent(passage.fkUser, () => []).add(passage);
}
// Calculer les statistiques pour chaque utilisateur
for (final entry in passagesByUser.entries) {
final userId = entry.key;
final userPassages = entry.value;
statsByUser[userId] = {
'total': userPassages.length,
'effectues': userPassages.where((p) => p.fkType == 1).length,
'a_finaliser': userPassages.where((p) => p.fkType == 2).length,
'refuses': userPassages.where((p) => p.fkType == 3).length,
'dons': userPassages.where((p) => p.fkType == 4).length,
'lots': userPassages.where((p) => p.fkType == 5).length,
'maisons_vides': userPassages.where((p) => p.fkType == 6).length,
};
}
// Statistiques par type
final statsByType = {
1: allPassages.where((p) => p.fkType == 1).length,
2: allPassages.where((p) => p.fkType == 2).length,
3: allPassages.where((p) => p.fkType == 3).length,
4: allPassages.where((p) => p.fkType == 4).length,
5: allPassages.where((p) => p.fkType == 5).length,
6: allPassages.where((p) => p.fkType == 6).length,
};
// Mettre en cache et retourner
_cachedStats = {
'global': globalStats,
'by_user': statsByUser,
'by_type': statsByType,
'user_count': statsByUser.length,
'calculated_at': DateTime.now().toIso8601String(),
};
return _cachedStats!;
}
// Getter simple pour les statistiques (optionnel, pour usage sans ValueListenableBuilder)
Map<String, dynamic> getPassageStatistics() {
return _cachedStats ?? calculatePassageStatistics();
} }
// Vider tous les passages // Vider tous les passages

View File

@@ -12,6 +12,9 @@ import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart'; import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart'; import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
import 'package:geosector_app/core/utils/api_exception.dart'; import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
/// Class pour dessiner les petits points blancs sur le fond /// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter { class DotsPainter extends CustomPainter {
@@ -42,12 +45,16 @@ class AdminAmicalePage extends StatefulWidget {
final UserRepository userRepository; final UserRepository userRepository;
final AmicaleRepository amicaleRepository; final AmicaleRepository amicaleRepository;
final MembreRepository membreRepository; final MembreRepository membreRepository;
final PassageRepository passageRepository;
final OperationRepository operationRepository;
const AdminAmicalePage({ const AdminAmicalePage({
super.key, super.key,
required this.userRepository, required this.userRepository,
required this.amicaleRepository, required this.amicaleRepository,
required this.membreRepository, required this.membreRepository,
required this.passageRepository,
required this.operationRepository,
}); });
@override @override
@@ -57,11 +64,13 @@ class AdminAmicalePage extends StatefulWidget {
class _AdminAmicalePageState extends State<AdminAmicalePage> { class _AdminAmicalePageState extends State<AdminAmicalePage> {
UserModel? _currentUser; UserModel? _currentUser;
String? _errorMessage; String? _errorMessage;
int? _currentOperationId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadCurrentUser(); _loadCurrentUser();
_loadCurrentOperation();
} }
void _loadCurrentUser() { void _loadCurrentUser() {
@@ -94,6 +103,19 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}); });
} }
// Méthode pour charger l'opération courante
void _loadCurrentOperation() {
final currentOperation = widget.operationRepository.getCurrentOperation();
_currentOperationId = currentOperation?.id;
if (currentOperation != null) {
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
debugPrint('📅 Période: ${currentOperation.dateDebut.toString().substring(0, 10)}${currentOperation.dateFin.toString().substring(0, 10)}');
} else {
debugPrint('⚠️ Aucune opération courante trouvée');
}
}
void _handleEditMembre(MembreModel membre) { void _handleEditMembre(MembreModel membre) {
showDialog( showDialog(
context: context, context: context,
@@ -141,12 +163,62 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
); );
} }
void _handleDeleteMembre(MembreModel membre) { void _handleDeleteMembre(MembreModel membre) async {
try {
debugPrint('🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
// Vérifier qu'on a une opération courante
if (_currentOperationId == null) {
debugPrint('❌ Aucune opération courante');
ApiException.showError(context, Exception('Aucune opération active trouvée. Impossible de supprimer le membre.'));
return;
}
debugPrint('🎯 Opération courante: $_currentOperationId');
// Filtrer les passages par opération courante ET par utilisateur
final allUserPassages = widget.passageRepository.getPassagesByUser(membre.id);
debugPrint('📊 Total passages du membre: ${allUserPassages.length}');
final passagesRealises = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType != 2).toList();
final passagesAFinaliser = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType == 2).toList();
final totalPassages = passagesRealises.length + passagesAFinaliser.length;
debugPrint('🔍 Passages réalisés (opération $_currentOperationId): ${passagesRealises.length}');
debugPrint('🔍 Passages à finaliser (opération $_currentOperationId): ${passagesAFinaliser.length}');
debugPrint('🔍 Total passages pour l\'opération $_currentOperationId: $totalPassages');
// Récupérer les autres membres de l'amicale (pour le transfert)
final autresmembres = widget.membreRepository.getMembresByAmicale(_currentUser!.fkEntite!).where((m) => m.id != membre.id && m.isActive == true).toList();
debugPrint('👥 Autres membres disponibles: ${autresmembres.length}');
// Afficher le dialog de confirmation approprié
if (totalPassages > 0) {
debugPrint('➡️ Affichage dialog avec passages');
_showDeleteMemberWithPassagesDialog(membre, totalPassages, autresmembres);
} else {
debugPrint('➡️ Affichage dialog simple (pas de passages)');
_showSimpleDeleteConfirmation(membre);
}
} catch (e) {
debugPrint('❌ Erreur lors de la vérification des passages: $e');
if (mounted) {
ApiException.showError(context, e);
}
}
}
void _showSimpleDeleteConfirmation(MembreModel membre) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'), title: const Text('Confirmer la suppression'),
content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\nCette action est irréversible.'), content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
'Ce membre n\'a aucun passage enregistré pour l\'opération courante.\n'
'Cette action est irréversible.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
@@ -155,20 +227,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
try { // Suppression simple : pas de passages, donc pas de paramètres
// Utiliser la méthode qui passe par l'API await _deleteMemberAPI(membre.id, 0, hasPassages: false);
final success = await widget.membreRepository.deleteMembre(membre.id);
if (success && mounted) {
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} supprimé avec succès');
} else if (!success && mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Colors.red,
@@ -181,6 +241,245 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
); );
} }
void _showDeleteMemberWithPassagesDialog(
MembreModel membre,
int totalPassages,
List<MembreModel> autresmembres,
) {
int? selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Expanded(child: Text('Attention - Passages détectés')),
],
),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Le membre ${membre.firstName} ${membre.name} a $totalPassages passage(s) enregistré(s).',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Section transfert
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📋 Transférer les passages',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
Text(
'Sélectionnez un membre pour récupérer tous les passages ($totalPassages) :',
),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: selectedMemberForTransfer,
decoration: const InputDecoration(
labelText: 'Membre destinataire',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: autresmembres
.map((m) => DropdownMenuItem(
value: m.id,
child: Text('${m.firstName} ${m.name}'),
))
.toList(),
onChanged: (value) {
setDialogState(() {
selectedMemberForTransfer = value;
});
debugPrint('✅ Membre destinataire sélectionné: $value');
},
),
// Indicateur visuel de sélection
if (selectedMemberForTransfer != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 16),
const SizedBox(width: 8),
Text(
'Membre sélectionné',
style: TextStyle(
color: Colors.green.shade700,
fontSize: 12,
),
),
],
),
),
],
],
),
),
const SizedBox(height: 16),
// Option de désactivation
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'💡 Alternative recommandée',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
const SizedBox(height: 8),
const Text(
'Vous pouvez désactiver ce membre au lieu de le supprimer. '
'Cela préservera l\'historique des passages tout en empêchant la connexion.',
),
],
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await _deactivateMember(membre);
},
style: TextButton.styleFrom(
foregroundColor: Colors.green,
),
child: const Text('Désactiver seulement'),
),
ElevatedButton(
onPressed: selectedMemberForTransfer != null
? () async {
debugPrint('🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
Navigator.of(context).pop();
// Suppression avec passages : inclure les paramètres
await _deleteMemberAPI(membre.id, selectedMemberForTransfer!, hasPassages: true);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: selectedMemberForTransfer != null ? Colors.red : null,
foregroundColor: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (selectedMemberForTransfer != null) const Icon(Icons.delete_forever, size: 16),
if (selectedMemberForTransfer != null) const SizedBox(width: 4),
Text(
selectedMemberForTransfer != null ? 'Supprimer et transférer' : 'Sélectionner un membre',
),
],
),
),
],
);
},
),
);
}
// Méthode unifiée pour appeler l'API de suppression
Future<void> _deleteMemberAPI(int membreId, int transferToUserId, {bool hasPassages = false}) async {
try {
bool success;
if (hasPassages && transferToUserId > 0 && _currentOperationId != null) {
// Suppression avec transfert de passages (inclure operation_id)
debugPrint('🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
success = await widget.membreRepository.deleteMembre(
membreId,
transferToUserId,
_currentOperationId,
);
} else {
// Suppression simple (pas de passages, donc pas de paramètres)
debugPrint('🗑️ Suppression simple - Aucun passage à transférer');
success = await widget.membreRepository.deleteMembre(membreId);
}
if (success && mounted) {
String message = 'Membre supprimé avec succès';
if (hasPassages && transferToUserId > 0) {
final transferMember = widget.membreRepository.getMembreById(transferToUserId);
final currentOperation = widget.operationRepository.getCurrentOperation();
message += '\nPassages de l\'opération "${currentOperation?.name}" transférés à ${transferMember?.firstName} ${transferMember?.name}';
}
ApiException.showSuccess(context, message);
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('❌ Erreur suppression membre: $e');
if (mounted) {
ApiException.showError(context, e);
}
}
}
Future<void> _deactivateMember(MembreModel membre) async {
try {
final updatedMember = membre.copyWith(isActive: false);
final success = await widget.membreRepository.updateMembre(updatedMember);
if (success && mounted) {
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la désactivation'));
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
}
}
void _handleAddMembre() { void _handleAddMembre() {
if (_currentUser?.fkEntite == null) return; if (_currentUser?.fkEntite == null) return;
@@ -243,17 +542,23 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
createdAt: DateTime.now(), createdAt: DateTime.now(),
); );
// Créer le membre via l'API (retourne maintenant le membre créé)
final createdMembre = await widget.membreRepository.createMembre(newMembre); final createdMembre = await widget.membreRepository.createMembre(newMembre);
if (createdMembre != null && mounted) { if (createdMembre != null && mounted) {
// Fermer le dialog
Navigator.of(context).pop(); Navigator.of(context).pop();
ApiException.showSuccess(context, 'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès');
// Afficher le message de succès avec les informations du membre créé
ApiException.showSuccess(context, 'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
} else if (mounted) { } else if (mounted) {
// En cas d'échec, ne pas fermer le dialog pour permettre la correction
ApiException.showError(context, Exception('Erreur lors de la création du membre')); ApiException.showError(context, Exception('Erreur lors de la création du membre'));
} }
} catch (e) { } catch (e) {
debugPrint('❌ Erreur création membre: $e'); debugPrint('❌ Erreur création membre: $e');
if (mounted) { if (mounted) {
// En cas d'exception, ne pas fermer le dialog
ApiException.showError(context, e); ApiException.showError(context, e);
} }
} }

View File

@@ -122,6 +122,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
userRepository: userRepository, userRepository: userRepository,
amicaleRepository: amicaleRepository, amicaleRepository: amicaleRepository,
membreRepository: membreRepository, membreRepository: membreRepository,
passageRepository: passageRepository,
operationRepository: operationRepository,
); );
case _PageType.operations: case _PageType.operations:
return const Scaffold(body: Center(child: Text('Page Opérations'))); return const Scaffold(body: Center(child: Text('Page Opérations')));

View File

@@ -1,7 +1,7 @@
name: geosector_app name: geosector_app
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
publish_to: 'none' publish_to: 'none'
version: 0.3.4 version: 0.3.5
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'