feat: Gestion des secteurs et migration v3.0.4+304

- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 3bbc599ab4
commit 1018b86537
620 changed files with 120502 additions and 91396 deletions

231
app/lib/presentation/admin/admin_amicale_page.dart Normal file → Executable file
View File

@@ -11,7 +11,6 @@ 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';
@@ -52,7 +51,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
void _loadCurrentUser() {
final currentUser = widget.userRepository.getCurrentUser();
debugPrint('🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
debugPrint(
'🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
debugPrint('🔍 _loadCurrentUser - fkEntite: ${currentUser?.fkEntite}');
if (currentUser == null) {
@@ -70,8 +70,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}
// Vérifier immédiatement si l'amicale existe
final amicale = widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
debugPrint('🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
final amicale =
widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
debugPrint(
'🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
setState(() {
_currentUser = currentUser;
@@ -85,8 +87,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
_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)}');
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');
}
@@ -117,16 +121,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
onSubmit: (updatedUser) async {
try {
// Convertir le UserModel mis à jour vers MembreModel
final updatedMembre = MembreModel.fromUserModel(updatedUser, membre);
final updatedMembre =
MembreModel.fromUserModel(updatedUser, membre);
// Utiliser directement updateMembre qui passe par l'API /users
final success = await widget.membreRepository.updateMembre(updatedMembre);
final success =
await widget.membreRepository.updateMembre(updatedMembre);
if (success && mounted) {
Navigator.of(context).pop();
ApiException.showSuccess(context, 'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
ApiException.showSuccess(context,
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
} else if (!success && mounted) {
ApiException.showError(context, Exception('Erreur lors de la mise à jour'));
ApiException.showError(
context, Exception('Erreur lors de la mise à jour'));
}
} catch (e) {
debugPrint('❌ Erreur mise à jour membre: $e');
@@ -139,42 +147,119 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
);
}
void _handleResetPassword(MembreModel membre) async {
// Afficher un dialog de confirmation
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.lock_reset, color: Colors.blue),
SizedBox(width: 8),
Text('Réinitialiser le mot de passe'),
],
),
content: Text(
'Voulez-vous réinitialiser le mot de passe de ${membre.firstName} ${membre.name} ?\n\n'
'Un email sera envoyé à l\'utilisateur avec les instructions de réinitialisation.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
child: const Text('Réinitialiser'),
),
],
),
);
if (confirm != true) return;
try {
debugPrint('🔐 Réinitialisation du mot de passe pour: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
final success = await widget.membreRepository.resetMemberPassword(membre.id);
if (success && mounted) {
ApiException.showSuccess(
context,
'Mot de passe réinitialisé avec succès. Un email a été envoyé à ${membre.email}',
);
} else if (mounted) {
ApiException.showError(
context,
Exception('Erreur lors de la réinitialisation du mot de passe'),
);
}
} catch (e) {
debugPrint('❌ Erreur réinitialisation mot de passe: $e');
if (mounted) {
ApiException.showError(context, e);
}
}
}
void _handleDeleteMembre(MembreModel membre) async {
try {
debugPrint('🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
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.'));
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);
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 passagesRealises = allUserPassages
.where((passage) =>
passage.fkOperation == _currentOperationId && passage.fkType != 2)
.toList();
final passagesAFinaliser = 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');
debugPrint(
'🔍 Passages 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();
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);
_showDeleteMemberWithPassagesDialog(
membre, totalPassages, autresmembres);
} else {
debugPrint('➡️ Affichage dialog simple (pas de passages)');
_showSimpleDeleteConfirmation(membre);
@@ -192,7 +277,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
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: [
@@ -222,7 +308,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
int totalPassages,
List<MembreModel> autresmembres,
) {
int? selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
int?
selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
showDialog(
context: context,
@@ -272,13 +359,19 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
Text(
'Sélectionnez un membre pour récupérer tous les passages ($totalPassages) :',
),
const SizedBox(height: 4),
const Text(
'* Cela peut concerner aussi les anciennes opérations s\'il avait des passages affectés',
style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: selectedMemberForTransfer,
decoration: const InputDecoration(
labelText: 'Membre destinataire',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
contentPadding: EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
items: autresmembres
.map((m) => DropdownMenuItem(
@@ -290,7 +383,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
setDialogState(() {
selectedMemberForTransfer = value;
});
debugPrint('✅ Membre destinataire sélectionné: $value');
debugPrint(
'✅ Membre destinataire sélectionné: $value');
},
),
@@ -305,7 +399,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
),
child: Row(
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 16),
const Icon(Icons.check_circle,
color: Colors.green, size: 16),
const SizedBox(width: 8),
Text(
'Membre sélectionné',
@@ -329,7 +424,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.withOpacity(0.3)),
border:
Border.all(color: Colors.green.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -371,23 +467,31 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
ElevatedButton(
onPressed: selectedMemberForTransfer != null
? () async {
debugPrint('🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
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);
await _deleteMemberAPI(
membre.id, selectedMemberForTransfer!,
hasPassages: true);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: selectedMemberForTransfer != null ? Colors.red : null,
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),
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',
selectedMemberForTransfer != null
? 'Supprimer et transférer'
: 'Sélectionner un membre',
),
],
),
@@ -400,13 +504,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}
// Méthode unifiée pour appeler l'API de suppression
Future<void> _deleteMemberAPI(int membreId, int transferToUserId, {bool hasPassages = false}) async {
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');
debugPrint(
'🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
success = await widget.membreRepository.deleteMembre(
membreId,
transferToUserId,
@@ -422,14 +528,18 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
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}';
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'));
ApiException.showError(
context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('❌ Erreur suppression membre: $e');
@@ -445,9 +555,11 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
final success = await widget.membreRepository.updateMembre(updatedMember);
if (success && mounted) {
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
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'));
ApiException.showError(
context, Exception('Erreur lors de la désactivation'));
}
} catch (e) {
if (mounted) {
@@ -519,17 +631,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
);
// 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) {
// Fermer le dialog
Navigator.of(context).pop();
// 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})');
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'));
ApiException.showError(
context, Exception('Erreur lors de la création du membre'));
}
} catch (e) {
debugPrint('❌ Erreur création membre: $e');
@@ -593,20 +708,27 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
if (_currentUser != null && _currentUser!.fkEntite != null)
Expanded(
child: ValueListenableBuilder<Box<AmicaleModel>>(
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
valueListenable:
widget.amicaleRepository.getAmicalesBox().listenable(),
builder: (context, amicalesBox, child) {
debugPrint('🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
debugPrint('🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
debugPrint('🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
debugPrint(
'🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
debugPrint(
'🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
debugPrint(
'🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
debugPrint('🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
debugPrint(
'🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
if (amicale == null) {
// Ajouter plus d'informations de debug
debugPrint('❌ PROBLÈME: Amicale non trouvée');
debugPrint('❌ fkEntite recherché: ${_currentUser!.fkEntite}');
debugPrint('❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
debugPrint(
'❌ fkEntite recherché: ${_currentUser!.fkEntite}');
debugPrint(
'❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
return Center(
child: Column(
@@ -634,11 +756,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}
return ValueListenableBuilder<Box<MembreModel>>(
valueListenable: widget.membreRepository.getMembresBox().listenable(),
valueListenable:
widget.membreRepository.getMembresBox().listenable(),
builder: (context, membresBox, child) {
// Filtrer les membres par amicale
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
final membres = membresBox.values
.where((membre) =>
membre.fkEntite == _currentUser!.fkEntite)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -721,6 +847,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
membres: membres,
onEdit: _handleEditMembre,
onDelete: _handleDeleteMembre,
onResetPassword: _handleResetPassword,
membreRepository: widget.membreRepository,
),
),