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.fk_role as role,
|
||||
u.fk_entite,
|
||||
u.infos,
|
||||
u.chk_alert_email,
|
||||
u.chk_suivi,
|
||||
u.date_naissance,
|
||||
u.date_embauche,
|
||||
u.matricule,
|
||||
u.chk_active,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
@@ -226,11 +224,11 @@ class UserController {
|
||||
$email = trim(strtolower($data['email']));
|
||||
$name = trim($data['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;
|
||||
|
||||
// Vérification des longueurs d'entrée
|
||||
if (strlen($email) > 255 || strlen($name) > 255) {
|
||||
if (strlen($email) > 75 || strlen($name) > 50) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Email ou nom trop long'
|
||||
@@ -270,26 +268,24 @@ class UserController {
|
||||
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
|
||||
$mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null;
|
||||
$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;
|
||||
$suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0;
|
||||
$dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null;
|
||||
$dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null;
|
||||
$matricule = isset($data['matricule']) ? trim($data['matricule']) : '';
|
||||
|
||||
// Insertion en base de données
|
||||
$stmt = $this->db->prepare('
|
||||
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,
|
||||
fk_entite, infos, chk_alert_email, chk_suivi,
|
||||
date_naissance, date_embauche, matricule,
|
||||
fk_entite, chk_alert_email, chk_suivi,
|
||||
date_naissance, date_embauche,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?,
|
||||
?, ?,
|
||||
NOW(), ?, 1
|
||||
)
|
||||
');
|
||||
@@ -303,12 +299,10 @@ class UserController {
|
||||
$mobile,
|
||||
$role,
|
||||
$entiteId,
|
||||
$infos,
|
||||
$alertEmail,
|
||||
$suivi,
|
||||
$dateNaissance,
|
||||
$dateEmbauche,
|
||||
$matricule,
|
||||
$currentUserId
|
||||
]);
|
||||
$userId = $this->db->lastInsertId();
|
||||
@@ -418,12 +412,10 @@ class UserController {
|
||||
'sect_name',
|
||||
'fk_role',
|
||||
'fk_entite',
|
||||
'infos',
|
||||
'chk_alert_email',
|
||||
'chk_suivi',
|
||||
'date_naissance',
|
||||
'date_embauche',
|
||||
'matricule',
|
||||
'chk_active'
|
||||
];
|
||||
|
||||
@@ -443,7 +435,7 @@ class UserController {
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
$updateFields[] = "user_pswd = :password";
|
||||
$updateFields[] = "user_pass_hash = :password";
|
||||
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
@@ -498,24 +490,23 @@ class UserController {
|
||||
public function deleteUser(string $id): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur)
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
// Récupérer les infos de l'utilisateur courant
|
||||
$stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
|
||||
$stmt->execute([$currentUserId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
$currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($userRole != '1' && $userRole != '2') {
|
||||
if (!$currentUser) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
'message' => 'Utilisateur courant non trouvé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = Session::getUserId();
|
||||
$userRole = (int)$currentUser['fk_role'];
|
||||
$userEntite = $currentUser['fk_entite'];
|
||||
|
||||
// Empêcher la suppression de son propre compte
|
||||
if ($currentUserId == $id) {
|
||||
@@ -526,37 +517,104 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Désactivation de l'utilisateur plutôt que suppression
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE users
|
||||
SET chk_active = 0,
|
||||
updated_at = NOW(),
|
||||
fk_user_modif = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$currentUserId, $id]);
|
||||
// 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 ($stmt->rowCount() === 0) {
|
||||
if (!$userToDelete) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé'
|
||||
'message' => 'Utilisateur cible non trouvé'
|
||||
], 404);
|
||||
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',
|
||||
'deactivatedBy' => $currentUserId,
|
||||
'userId' => $id
|
||||
'deletedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'passage_transfer' => $transferTo && $operationId ? "Vers utilisateur $transferTo pour operation $operationId" : 'Aucun'
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Utilisateur désactivé avec succès'
|
||||
'message' => 'Utilisateur supprimé avec succès'
|
||||
]);
|
||||
} 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',
|
||||
'error' => $e->getMessage(),
|
||||
'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": [
|
||||
{
|
||||
"name": "geosector_app",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"dependencies": [
|
||||
"connectivity_plus",
|
||||
"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
|
||||
|
||||
#### Pour les **Distributeurs** (Rôle 1)
|
||||
#### Pour les **Membres** (Rôle 1)
|
||||
|
||||
- ✅ Visualisation des secteurs assignés sur carte interactive
|
||||
- ✅ Suivi GPS en temps réel des tournées
|
||||
@@ -368,7 +368,7 @@ Timeout : "Délai d'attente dépassé"
|
||||
|
||||
### 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
|
||||
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
|
||||
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
|
||||
|
||||
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({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "4143491003"
|
||||
serviceWorkerVersion: "1907117848"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,13 +3,13 @@ const MANIFEST = 'flutter-app-manifest';
|
||||
const TEMP = 'flutter-temp-cache';
|
||||
const CACHE_NAME = 'flutter-app-cache';
|
||||
|
||||
const RESOURCES = {"flutter_bootstrap.js": "d64cc0cfde96e267f6c2306a601e64e1",
|
||||
"version.json": "b01f276d289a7da276afae267e1f955a",
|
||||
const RESOURCES = {"flutter_bootstrap.js": "8a2b42aa8136fe0225b2b62c68d0dbcd",
|
||||
"version.json": "e8d1bf0cf13b62f11b193e5c745aa8d1",
|
||||
"index.html": "2aab03d10fea3b608e3eddc0fc0077e5",
|
||||
"/": "2aab03d10fea3b608e3eddc0fc0077e5",
|
||||
"favicon-64.png": "259540a3217e969237530444ca0eaed3",
|
||||
"favicon-16.png": "106142fb24eba190e475dbe6513cc9ff",
|
||||
"main.dart.js": "201747c331c2e399470d7f971f0387a8",
|
||||
"main.dart.js": "b45a67a2883e7ac3efa7375758392344",
|
||||
"flutter.js": "83d881c1dbb6d6bcd6b42e274605b69c",
|
||||
"favicon.png": "21510778ead066ac826ad69302400773",
|
||||
"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/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
|
||||
"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/logo-geosector-1024.png": "adb1be034f0b983acf6246369a794de5",
|
||||
"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
|
||||
final response = await ApiService.instance.post('/users', data: data);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
// Créer le membre avec les données retournées par l'API
|
||||
final createdMember = MembreModel.fromJson(response.data);
|
||||
if (response.statusCode == 201) {
|
||||
// Extraire l'ID de la réponse API
|
||||
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);
|
||||
|
||||
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;
|
||||
} 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
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
@@ -174,25 +200,45 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer un membre via l'API
|
||||
Future<bool> deleteMembre(int id) async {
|
||||
// Supprimer un membre via l'API avec transfert optionnel
|
||||
Future<bool> deleteMembre(int membreId, [int? transferToUserId, int? operationId]) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Appeler l'API users au lieu de membres (correction ici)
|
||||
final response = await ApiService.instance.delete('/users/$id');
|
||||
String endpoint = '/users/$membreId';
|
||||
|
||||
// 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) {
|
||||
// Supprimer le membre localement
|
||||
await deleteMembreBox(id);
|
||||
await deleteMembreBox(membreId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la suppression du membre: $e');
|
||||
return false;
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
@@ -41,6 +41,44 @@ class OperationRepository extends ChangeNotifier {
|
||||
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
|
||||
Future<void> saveOperation(OperationModel operation) async {
|
||||
await _operationBox.put(operation.id, operation);
|
||||
|
||||
@@ -9,6 +9,9 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Constructeur sans paramètres - utilise ApiService.instance
|
||||
PassageRepository();
|
||||
|
||||
// Cache pour les statistiques
|
||||
Map<String, dynamic>? _cachedStats;
|
||||
|
||||
// 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
|
||||
Box<PassageModel> get _passageBox {
|
||||
@@ -16,6 +19,19 @@ class PassageRepository extends ChangeNotifier {
|
||||
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
|
||||
StreamController<List<PassageModel>>? _passageStreamController;
|
||||
|
||||
@@ -73,6 +89,16 @@ class PassageRepository extends ChangeNotifier {
|
||||
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
|
||||
List<PassageModel> getPassagesByDate(DateTime date) {
|
||||
return _passageBox.values.where((passage) {
|
||||
@@ -263,11 +289,14 @@ class PassageRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Statistiques
|
||||
Map<String, int> getPassageStatistics() {
|
||||
// Recalculer les statistiques (appelée par le ValueListenableBuilder)
|
||||
Map<String, dynamic> calculatePassageStatistics() {
|
||||
final allPassages = getAllPassages();
|
||||
|
||||
return {
|
||||
debugPrint('📊 Calcul des statistiques: ${allPassages.length} passages');
|
||||
|
||||
// Statistiques globales
|
||||
final globalStats = {
|
||||
'total': allPassages.length,
|
||||
'effectues': allPassages.where((p) => p.fkType == 1).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,
|
||||
'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
|
||||
|
||||
@@ -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/membre_table_widget.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 DotsPainter extends CustomPainter {
|
||||
@@ -42,12 +45,16 @@ class AdminAmicalePage extends StatefulWidget {
|
||||
final UserRepository userRepository;
|
||||
final AmicaleRepository amicaleRepository;
|
||||
final MembreRepository membreRepository;
|
||||
final PassageRepository passageRepository;
|
||||
final OperationRepository operationRepository;
|
||||
|
||||
const AdminAmicalePage({
|
||||
super.key,
|
||||
required this.userRepository,
|
||||
required this.amicaleRepository,
|
||||
required this.membreRepository,
|
||||
required this.passageRepository,
|
||||
required this.operationRepository,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -57,11 +64,13 @@ class AdminAmicalePage extends StatefulWidget {
|
||||
class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
UserModel? _currentUser;
|
||||
String? _errorMessage;
|
||||
int? _currentOperationId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCurrentUser();
|
||||
_loadCurrentOperation();
|
||||
}
|
||||
|
||||
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) {
|
||||
showDialog(
|
||||
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(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
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: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -155,20 +227,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
// Utiliser la méthode qui passe par l'API
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Suppression simple : pas de passages, donc pas de paramètres
|
||||
await _deleteMemberAPI(membre.id, 0, hasPassages: false);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
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() {
|
||||
if (_currentUser?.fkEntite == null) return;
|
||||
|
||||
@@ -243,17 +542,23 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Créer le membre via l'API (retourne maintenant le membre créé)
|
||||
final createdMembre = await widget.membreRepository.createMembre(newMembre);
|
||||
|
||||
if (createdMembre != null && mounted) {
|
||||
// Fermer le dialog
|
||||
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) {
|
||||
// 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'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création membre: $e');
|
||||
if (mounted) {
|
||||
// En cas d'exception, ne pas fermer le dialog
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
userRepository: userRepository,
|
||||
amicaleRepository: amicaleRepository,
|
||||
membreRepository: membreRepository,
|
||||
passageRepository: passageRepository,
|
||||
operationRepository: operationRepository,
|
||||
);
|
||||
case _PageType.operations:
|
||||
return const Scaffold(body: Center(child: Text('Page Opérations')));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: geosector_app
|
||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||
publish_to: 'none'
|
||||
version: 0.3.4
|
||||
version: 0.3.5
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
Reference in New Issue
Block a user