Mise en place suppression membre
This commit is contained in:
22
api/.vscode/settings.json
vendored
Normal file
22
api/.vscode/settings.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur cible
|
||||||
|
$stmt2 = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
|
||||||
|
$stmt2->execute([$id]);
|
||||||
|
$userToDelete = $stmt2->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$userToDelete) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Utilisateur cible non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
try {
|
||||||
// Désactivation de l'utilisateur plutôt que suppression
|
// Supprimer les enregistrements dépendants dans ope_users
|
||||||
$stmt = $this->db->prepare('
|
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
|
||||||
UPDATE users
|
$stmtOpeUsers->execute([$id]);
|
||||||
SET chk_active = 0,
|
|
||||||
updated_at = NOW(),
|
// Ici éventuellement : d'autres suppressions en cascade si besoin
|
||||||
fk_user_modif = ?
|
|
||||||
WHERE id = ?
|
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
|
||||||
');
|
$stmt->execute([$id]);
|
||||||
$stmt->execute([$currentUserId, $id]);
|
|
||||||
|
|
||||||
if ($stmt->rowCount() === 0) {
|
if ($stmt->rowCount() === 0) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Utilisateur non trouvé'
|
'message' => 'Utilisateur non trouvé ou déjà supprimé'
|
||||||
], 404);
|
], 404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService::log('Utilisateur GeoSector désactivé', [
|
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
@@ -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.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
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
@@ -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
@@ -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
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"]}
|
||||||
Binary file not shown.
@@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"1425e5e9ec5eeb4f225c401d8db69b860e0fde
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "4143491003"
|
serviceWorkerVersion: "1907117848"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
124118
app/build/web/main.dart.js
124118
app/build/web/main.dart.js
File diff suppressed because one or more lines are too long
@@ -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"}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')));
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user