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 6a609fb467
commit 599b9fcda0
662 changed files with 213221 additions and 174243 deletions

0
app/lib/core/repositories/amicale_repository.dart Normal file → Executable file
View File

0
app/lib/core/repositories/client_repository.dart Normal file → Executable file
View File

55
app/lib/core/repositories/membre_repository.dart Normal file → Executable file
View File

@@ -9,15 +9,28 @@ class MembreRepository extends ChangeNotifier {
// Constructeur sans paramètres - utilise ApiService.instance
MembreRepository();
// Cache de la box pour éviter les vérifications répétées
Box<MembreModel>? _cachedMembreBox;
// 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<MembreModel> get _membreBox {
_ensureBoxIsOpen();
return Hive.box<MembreModel>(AppKeys.membresBoxName);
if (_cachedMembreBox == null) {
if (!Hive.isBoxOpen(AppKeys.membresBoxName)) {
throw Exception('La boîte ${AppKeys.membresBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
}
_cachedMembreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
debugPrint('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
}
return _cachedMembreBox!;
}
bool _isLoading = false;
// Méthode pour réinitialiser le cache après modification de la box
void _resetCache() {
_cachedMembreBox = null;
}
// Getters
bool get isLoading => _isLoading;
List<MembreModel> get membres => getAllMembres();
@@ -35,14 +48,6 @@ class MembreRepository extends ChangeNotifier {
}
}
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
const boxName = AppKeys.membresBoxName;
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName dans MembreRepository...');
await Hive.openBox<MembreModel>(boxName);
}
}
// === MÉTHODES SPÉCIFIQUES AUX MEMBRES ===
@@ -101,12 +106,14 @@ class MembreRepository extends ChangeNotifier {
// Sauvegarder un membre
Future<void> saveMembreBox(MembreModel membre) async {
await _membreBox.put(membre.id, membre);
_resetCache(); // Réinitialiser le cache après modification
notifyListeners();
}
// Supprimer un membre
Future<void> deleteMembreBox(int id) async {
await _membreBox.delete(id);
_resetCache(); // Réinitialiser le cache après modification
notifyListeners();
}
@@ -154,7 +161,7 @@ class MembreRepository extends ChangeNotifier {
isActive: membre.isActive,
);
// Sauvegarder localement dans Hive
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
await saveMembreBox(createdMember);
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
@@ -200,6 +207,28 @@ class MembreRepository extends ChangeNotifier {
}
}
// Réinitialiser le mot de passe d'un membre via l'API
Future<bool> resetMemberPassword(int membreId) async {
_isLoading = true;
notifyListeners();
try {
final response = await ApiService.instance.post('/users/$membreId/reset-password');
if (response.statusCode == 200) {
return true;
}
return false;
} catch (e) {
debugPrint('Erreur lors de la réinitialisation du mot de passe: $e');
rethrow;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Supprimer un membre via l'API avec transfert optionnel
Future<bool> deleteMembre(int membreId, [int? transferToUserId, int? operationId]) async {
_isLoading = true;
@@ -283,6 +312,7 @@ class MembreRepository extends ChangeNotifier {
}
debugPrint('$count membres traités et stockés');
_resetCache(); // Réinitialiser le cache après traitement des données API
notifyListeners();
} catch (e) {
debugPrint('Erreur lors du traitement des membres: $e');
@@ -337,6 +367,7 @@ class MembreRepository extends ChangeNotifier {
// Vider la boîte des membres
Future<void> clearMembres() async {
await _membreBox.clear();
_resetCache(); // Réinitialiser le cache après suppression de toutes les données
notifyListeners();
}
}

122
app/lib/core/repositories/operation_repository.dart Normal file → Executable file
View File

@@ -51,7 +51,9 @@ class OperationRepository extends ChangeNotifier {
OperationModel? getCurrentOperation() {
try {
// Récupérer toutes les opérations actives
final activeOperations = _operationBox.values.where((operation) => operation.isActive == true).toList();
final activeOperations = _operationBox.values
.where((operation) => operation.isActive == true)
.toList();
if (activeOperations.isEmpty) {
debugPrint('⚠️ Aucune opération active trouvée');
@@ -62,10 +64,12 @@ class OperationRepository extends ChangeNotifier {
activeOperations.sort((a, b) => b.id.compareTo(a.id));
final currentOperation = activeOperations.first;
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
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');
debugPrint(
'❌ Erreur lors de la récupération de l\'opération courante: $e');
return null;
}
}
@@ -79,7 +83,10 @@ class OperationRepository extends ChangeNotifier {
// 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
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 [];
@@ -104,13 +111,17 @@ class OperationRepository extends ChangeNotifier {
notifyListeners();
try {
debugPrint('🔄 Traitement de ${operationsData.length} opérations depuis l\'API');
debugPrint(
'🔄 Traitement de ${operationsData.length} opérations depuis l\'API');
for (var operationData in operationsData) {
final operationJson = operationData as Map<String, dynamic>;
final operationId = operationJson['id'] is String ? int.parse(operationJson['id']) : operationJson['id'] as int;
final operationId = operationJson['id'] is String
? int.parse(operationJson['id'])
: operationJson['id'] as int;
debugPrint('📝 Traitement opération ID: $operationId, libelle: ${operationJson['libelle']}');
debugPrint(
'📝 Traitement opération ID: $operationId, libelle: ${operationJson['libelle']}');
// Vérifier si l'opération existe déjà
OperationModel? existingOperation = getOperationById(operationId);
@@ -123,11 +134,14 @@ class OperationRepository extends ChangeNotifier {
} else {
// Mettre à jour l'opération existante
final updatedOperation = existingOperation.copyWith(
name: operationJson['libelle'], // ← Correction: utiliser 'libelle' au lieu de 'name'
name: operationJson[
'libelle'], // ← Correction: utiliser 'libelle' au lieu de 'name'
fkEntite: operationJson['fk_entite'],
dateDebut: DateTime.parse(operationJson['date_deb']),
dateFin: DateTime.parse(operationJson['date_fin']),
isActive: operationJson['chk_active'] == true || operationJson['chk_active'] == 1 || operationJson['chk_active'] == "1",
isActive: operationJson['chk_active'] == true ||
operationJson['chk_active'] == 1 ||
operationJson['chk_active'] == "1",
lastSyncedAt: DateTime.now(),
isSynced: true,
);
@@ -136,7 +150,8 @@ class OperationRepository extends ChangeNotifier {
}
}
debugPrint('🎉 Traitement terminé - ${_operationBox.length} opérations dans la box');
debugPrint(
'🎉 Traitement terminé - ${_operationBox.length} opérations dans la box');
} catch (e) {
debugPrint('❌ Erreur lors du traitement des opérations: $e');
debugPrint('❌ Stack trace: ${StackTrace.current}');
@@ -147,7 +162,8 @@ class OperationRepository extends ChangeNotifier {
}
// Créer une opération
Future<bool> createOperation(String name, DateTime dateDebut, DateTime dateFin) async {
Future<bool> createOperation(
String name, DateTime dateDebut, DateTime dateFin) async {
_isLoading = true;
notifyListeners();
@@ -155,14 +171,17 @@ class OperationRepository extends ChangeNotifier {
// Préparer les données pour l'API
final Map<String, dynamic> data = {
'name': name,
'date_deb': dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'date_fin': dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'date_deb':
dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'date_fin':
dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
};
debugPrint('🚀 Création d\'une nouvelle opération: $data');
// Appeler l'API pour créer l'opération
final response = await ApiService.instance.post('/operations', data: data);
final response =
await ApiService.instance.post('/operations', data: data);
if (response.statusCode == 201 || response.statusCode == 200) {
debugPrint('✅ Opération créée avec succès');
@@ -184,7 +203,8 @@ class OperationRepository extends ChangeNotifier {
}
// Traiter la réponse complète après création d'opération
Future<void> _processCreationResponse(Map<String, dynamic> responseData) async {
Future<void> _processCreationResponse(
Map<String, dynamic> responseData) async {
try {
debugPrint('🔄 Traitement de la réponse de création d\'opération');
@@ -196,19 +216,22 @@ class OperationRepository extends ChangeNotifier {
// Traiter les secteurs (groupe secteurs) via DataLoadingService
if (responseData['secteurs'] != null) {
await DataLoadingService.instance.processSectorsFromApi(responseData['secteurs']);
await DataLoadingService.instance
.processSectorsFromApi(responseData['secteurs']);
debugPrint('✅ Secteurs traités');
}
// Traiter les passages (groupe passages) via DataLoadingService
if (responseData['passages'] != null) {
await DataLoadingService.instance.processPassagesFromApi(responseData['passages']);
await DataLoadingService.instance
.processPassagesFromApi(responseData['passages']);
debugPrint('✅ Passages traités');
}
// Traiter les users_sectors (groupe users_sectors) via DataLoadingService
if (responseData['users_sectors'] != null) {
await DataLoadingService.instance.processUserSectorsFromApi(responseData['users_sectors']);
await DataLoadingService.instance
.processUserSectorsFromApi(responseData['users_sectors']);
debugPrint('✅ Users_sectors traités');
}
@@ -268,7 +291,12 @@ class OperationRepository extends ChangeNotifier {
}
// Mettre à jour une opération
Future<bool> updateOperation(int id, {String? name, DateTime? dateDebut, DateTime? dateFin, bool? isActive, int? fkEntite}) async {
Future<bool> updateOperation(int id,
{String? name,
DateTime? dateDebut,
DateTime? dateFin,
bool? isActive,
int? fkEntite}) async {
_isLoading = true;
notifyListeners();
@@ -284,15 +312,22 @@ class OperationRepository extends ChangeNotifier {
final Map<String, dynamic> data = {
'id': id,
'name': name ?? existingOperation.name,
'date_deb': (dateDebut ?? existingOperation.dateDebut).toIso8601String().split('T')[0],
'date_fin': (dateFin ?? existingOperation.dateFin).toIso8601String().split('T')[0],
'chk_active': isActive ?? existingOperation.isActive, // Utiliser chk_active comme dans l'API
'fk_entite': fkEntite ?? existingOperation.fkEntite, // ← Inclure fkEntite
'date_deb': (dateDebut ?? existingOperation.dateDebut)
.toIso8601String()
.split('T')[0],
'date_fin': (dateFin ?? existingOperation.dateFin)
.toIso8601String()
.split('T')[0],
'chk_active': isActive ??
existingOperation.isActive, // Utiliser chk_active comme dans l'API
'fk_entite':
fkEntite ?? existingOperation.fkEntite, // ← Inclure fkEntite
};
debugPrint('🔄 Mise à jour de l\'opération $id avec les données: $data');
// Appeler l'API pour mettre à jour l'opération
final response = await ApiService.instance.put('/operations/$id', data: data);
final response =
await ApiService.instance.put('/operations/$id', data: data);
if (response.statusCode == 200) {
debugPrint('✅ Opération $id mise à jour avec succès');
@@ -375,7 +410,8 @@ class OperationRepository extends ChangeNotifier {
final response = await ApiService.instance.delete('/operations/$id');
if (response.statusCode == 200 || response.statusCode == 204) {
debugPrint('✅ Suppression opération active réussie - Traitement complet');
debugPrint(
'✅ Suppression opération active réussie - Traitement complet');
// Traiter la réponse complète qui contient tous les groupes de données
if (response.data != null) {
@@ -389,7 +425,8 @@ class OperationRepository extends ChangeNotifier {
return true;
}
debugPrint('❌ Échec suppression opération active - Code: ${response.statusCode}');
debugPrint(
'❌ Échec suppression opération active - Code: ${response.statusCode}');
return false;
} catch (e) {
debugPrint('❌ Erreur lors de la suppression de l\'opération active: $e');
@@ -401,9 +438,11 @@ class OperationRepository extends ChangeNotifier {
}
// Traiter la réponse complète après suppression d'opération active
Future<void> _processActiveDeleteResponse(Map<String, dynamic> responseData) async {
Future<void> _processActiveDeleteResponse(
Map<String, dynamic> responseData) async {
try {
debugPrint('🔄 Traitement de la réponse de suppression d\'opération active');
debugPrint(
'🔄 Traitement de la réponse de suppression d\'opération active');
// Vider toutes les Box concernées
await _clearAllRelatedBoxes();
@@ -416,25 +455,30 @@ class OperationRepository extends ChangeNotifier {
// Traiter les secteurs (groupe secteurs) via DataLoadingService
if (responseData['secteurs'] != null) {
await DataLoadingService.instance.processSectorsFromApi(responseData['secteurs']);
await DataLoadingService.instance
.processSectorsFromApi(responseData['secteurs']);
debugPrint('✅ Secteurs traités');
}
// Traiter les passages (groupe passages) via DataLoadingService
if (responseData['passages'] != null) {
await DataLoadingService.instance.processPassagesFromApi(responseData['passages']);
await DataLoadingService.instance
.processPassagesFromApi(responseData['passages']);
debugPrint('✅ Passages traités');
}
// Traiter les users_sectors (groupe users_sectors) via DataLoadingService
if (responseData['users_sectors'] != null) {
await DataLoadingService.instance.processUserSectorsFromApi(responseData['users_sectors']);
await DataLoadingService.instance
.processUserSectorsFromApi(responseData['users_sectors']);
debugPrint('✅ Users_sectors traités');
}
debugPrint('🎉 Tous les groupes de données ont été traités après suppression opération active');
debugPrint(
'🎉 Tous les groupes de données ont été traités après suppression opération active');
} catch (e) {
debugPrint('❌ Erreur lors du traitement de la réponse de suppression: $e');
debugPrint(
'❌ Erreur lors du traitement de la réponse de suppression: $e');
// Ne pas faire échouer la suppression si le traitement des données supplémentaires échoue
}
}
@@ -456,7 +500,8 @@ class OperationRepository extends ChangeNotifier {
}
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
final userSectorsBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
final userSectorsBox =
Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
await userSectorsBox.clear();
}
@@ -467,14 +512,17 @@ class OperationRepository extends ChangeNotifier {
}
// Export Excel d'une opération
Future<void> exportOperationToExcel(int operationId, String operationName) async {
Future<void> exportOperationToExcel(
int operationId, String operationName) async {
try {
debugPrint('📊 Export Excel opération $operationId: $operationName');
// Générer le nom de fichier avec la date actuelle
final now = DateTime.now();
final dateStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final fileName = 'operation_${operationName.replaceAll(' ', '_')}_$dateStr.xlsx';
final dateStr =
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final fileName =
'operation_${operationName.replaceAll(' ', '_')}_$dateStr.xlsx';
// Appeler l'API pour télécharger le fichier Excel
await ApiService.instance.downloadOperationExcel(operationId, fileName);

59
app/lib/core/repositories/passage_repository.dart Normal file → Executable file
View File

@@ -12,11 +12,24 @@ class PassageRepository extends ChangeNotifier {
// Cache pour les statistiques
Map<String, dynamic>? _cachedStats;
// Cache de la box pour éviter les vérifications répétées
Box<PassageModel>? _cachedPassageBox;
// 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 {
_ensureBoxIsOpen();
return Hive.box<PassageModel>(AppKeys.passagesBoxName);
if (_cachedPassageBox == null) {
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
throw Exception('La boîte ${AppKeys.passagesBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
}
_cachedPassageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
debugPrint('PassageRepository: Box ${AppKeys.passagesBoxName} mise en cache');
}
return _cachedPassageBox!;
}
// Méthode pour réinitialiser le cache après modification de la box
void _resetCache() {
_cachedPassageBox = null;
}
// Méthode pour exposer la Box Hive (nécessaire pour ValueListenableBuilder)
@@ -55,14 +68,6 @@ class PassageRepository extends ChangeNotifier {
bool get isLoading => _isLoading;
List<PassageModel> get passages => getAllPassages();
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
const boxName = AppKeys.passagesBoxName;
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName dans PassageRepository...');
await Hive.openBox<PassageModel>(boxName);
}
}
// Récupérer tous les passages
List<PassageModel> getAllPassages() {
@@ -78,6 +83,11 @@ class PassageRepository extends ChangeNotifier {
List<PassageModel> getPassagesBySectorId(int sectorId) {
return _passageBox.values.where((passage) => passage.fkSector == sectorId).toList();
}
// Récupérer les passages orphelins (sans secteur)
List<PassageModel> getOrphanPassages() {
return _passageBox.values.where((passage) => passage.fkSector == null).toList();
}
// Récupérer les passages par type
List<PassageModel> getPassagesByType(int type) {
@@ -102,10 +112,13 @@ class PassageRepository extends ChangeNotifier {
// Récupérer les passages par date
List<PassageModel> getPassagesByDate(DateTime date) {
return _passageBox.values.where((passage) {
// Ignorer les passages sans date
if (passage.passedAt == null) return false;
final passageDate = DateTime(
passage.passedAt.year,
passage.passedAt.month,
passage.passedAt.day,
passage.passedAt!.year,
passage.passedAt!.month,
passage.passedAt!.day,
);
final searchDate = DateTime(date.year, date.month, date.day);
return passageDate.isAtSameMomentAs(searchDate);
@@ -115,15 +128,24 @@ class PassageRepository extends ChangeNotifier {
// Sauvegarder un passage
Future<void> savePassage(PassageModel passage) async {
await _passageBox.put(passage.id, passage);
_resetCache(); // Réinitialiser le cache après modification
notifyListeners();
_notifyPassageStream();
}
// Sauvegarder plusieurs passages
Future<void> savePassages(List<PassageModel> passages) async {
for (final passage in passages) {
await _passageBox.put(passage.id, passage);
}
if (passages.isEmpty) return;
// Créer une map avec l'ID comme clé pour putAll
final Map<dynamic, PassageModel> passagesMap = {
for (final passage in passages) passage.id: passage
};
// Sauvegarder tous les passages en une seule opération
await _passageBox.putAll(passagesMap);
_resetCache(); // Réinitialiser le cache après modification massive
notifyListeners();
_notifyPassageStream();
}
@@ -131,6 +153,7 @@ class PassageRepository extends ChangeNotifier {
// Supprimer un passage
Future<void> deletePassage(int id) async {
await _passageBox.delete(id);
_resetCache(); // Réinitialiser le cache après suppression
notifyListeners();
_notifyPassageStream();
}
@@ -269,6 +292,7 @@ class PassageRepository extends ChangeNotifier {
}
debugPrint('$count passages traités et stockés');
_resetCache(); // Réinitialiser le cache après traitement des données API
notifyListeners();
_notifyPassageStream();
} catch (e) {
@@ -361,6 +385,7 @@ class PassageRepository extends ChangeNotifier {
// Vider tous les passages
Future<void> clearAllPassages() async {
await _passageBox.clear();
_resetCache(); // Réinitialiser le cache après suppression de toutes les données
notifyListeners();
_notifyPassageStream();
}

0
app/lib/core/repositories/region_repository.dart Normal file → Executable file
View File

369
app/lib/core/repositories/sector_repository.dart Normal file → Executable file
View File

@@ -1,30 +1,37 @@
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:dio/dio.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/services/data_loading_service.dart';
class SectorRepository extends ChangeNotifier {
// Constructeur sans paramètres - utilise ApiService.instance
SectorRepository();
// Cache de la box pour éviter les vérifications répétées
Box<SectorModel>? _cachedSectorBox;
// 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<SectorModel> get _sectorBox {
_ensureBoxIsOpen();
return Hive.box<SectorModel>(AppKeys.sectorsBoxName);
if (_cachedSectorBox == null) {
if (!Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
throw Exception('La boîte ${AppKeys.sectorsBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
}
_cachedSectorBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
debugPrint('SectorRepository: Box ${AppKeys.sectorsBoxName} mise en cache');
}
return _cachedSectorBox!;
}
// Constante pour l'ID par défaut
static const int defaultSectorId = 1;
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
debugPrint('SectorRepository: Vérification de l\'ouverture de la boîte ${AppKeys.sectorsBoxName}...');
const boxName = AppKeys.sectorsBoxName;
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName dans SectorRepository...');
await Hive.openBox<SectorModel>(boxName);
}
// Méthode pour réinitialiser le cache après modification de la box
void _resetCache() {
_cachedSectorBox = null;
}
// Récupérer tous les secteurs
@@ -40,12 +47,14 @@ class SectorRepository extends ChangeNotifier {
// Sauvegarder un secteur
Future<void> saveSector(SectorModel sector) async {
await _sectorBox.put(sector.id, sector);
_resetCache(); // Réinitialiser le cache après modification
notifyListeners();
}
// Supprimer un secteur
Future<void> deleteSector(int id) async {
await _sectorBox.delete(id);
_resetCache(); // Réinitialiser le cache après modification
notifyListeners();
}
@@ -58,6 +67,7 @@ class SectorRepository extends ChangeNotifier {
for (final sector in sectors) {
await _sectorBox.put(sector.id, sector);
}
_resetCache(); // Réinitialiser le cache après modification massive
notifyListeners();
}
@@ -98,6 +108,7 @@ class SectorRepository extends ChangeNotifier {
}
debugPrint('$count secteurs traités et stockés');
_resetCache(); // Réinitialiser le cache après traitement des données API
notifyListeners();
} catch (e) {
debugPrint('Erreur lors du traitement des secteurs: $e');
@@ -127,63 +138,355 @@ class SectorRepository extends ChangeNotifier {
}
// Créer un nouveau secteur via l'API
Future<SectorModel?> createSector(SectorModel sector) async {
Future<Map<String, dynamic>> createSector(SectorModel sector, {required List<int> users, required int fkEntite, required int operationId}) async {
try {
// Préparer les données à envoyer
final Map<String, dynamic> requestData = {
...sector.toJson(),
'users': users,
'fk_entite': fkEntite,
'operation_id': operationId,
};
final response = await ApiService.instance.post(
AppKeys.sectorsEndpoint,
data: sector.toJson(),
data: requestData,
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
// Gérer la réponse correctement
final dynamic responseRaw = response is Response ? response.data : response;
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
if (responseData['status'] == 'success' && responseData['sector'] != null) {
final SectorModel newSector = SectorModel.fromJson(responseData['sector']);
if (responseData['status'] == 'success') {
// L'API peut retourner soit 'sector' (objet complet) soit 'sector_id' (ID seulement)
SectorModel newSector;
if (responseData['sector'] != null) {
// Cas où l'API retourne l'objet secteur complet
newSector = SectorModel.fromJson(responseData['sector']);
} else if (responseData['sector_id'] != null) {
// Cas où l'API retourne seulement l'ID du secteur créé
final sectorId = responseData['sector_id'] is String
? int.parse(responseData['sector_id'])
: responseData['sector_id'] as int;
// Créer le secteur avec les données envoyées et l'ID reçu
newSector = sector.copyWith(id: sectorId);
} else {
debugPrint('Erreur: Aucune donnée de secteur dans la réponse');
return {
'status': 'error',
'message': 'Aucune donnée de secteur dans la réponse'
};
}
// Sauvegarder le secteur
await saveSector(newSector);
return newSector;
// Traiter les passages retournés s'ils existent
if (responseData['passages_sector'] != null) {
try {
final passagesData = responseData['passages_sector'] as List<dynamic>;
debugPrint('Traitement de ${passagesData.length} passages retournés');
// Utiliser PassageRepository pour traiter les passages
final passageRepository = PassageRepository();
// Convertir chaque passage au format complet attendu
final List<PassageModel> passagesToSave = [];
for (final passageData in passagesData) {
try {
// Caster passageData en Map<String, dynamic>
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
// L'API retourne déjà des passages complets, on les utilise directement
final passage = PassageModel.fromJson(passageDataMap);
passagesToSave.add(passage);
} catch (e) {
debugPrint('Erreur lors du traitement d\'un passage: $e');
}
}
// Sauvegarder tous les passages
if (passagesToSave.isNotEmpty) {
await passageRepository.savePassages(passagesToSave);
debugPrint('${passagesToSave.length} passages sauvegardés');
}
} catch (e) {
debugPrint('Erreur lors du traitement des passages: $e');
// Ne pas faire échouer la création du secteur si le traitement des passages échoue
}
}
// Traiter les users_sectors retournés s'ils existent
if (responseData['users_sectors'] != null) {
try {
final usersSectorsData = responseData['users_sectors'] as List<dynamic>;
debugPrint('Traitement de ${usersSectorsData.length} associations utilisateur-secteur');
// Sauvegarder les associations dans la box UserSector via DataLoadingService
await DataLoadingService.instance.processUserSectorsFromApi(usersSectorsData);
for (final userData in usersSectorsData) {
debugPrint('Utilisateur ${userData['first_name']} ${userData['name']} (ID: ${userData['id']}) assigné au secteur ${userData['fk_sector']}');
}
} catch (e) {
debugPrint('Erreur lors du traitement des users_sectors: $e');
}
}
// Afficher les statistiques si disponibles
if (responseData['passages_created'] != null || responseData['passages_integrated'] != null) {
final created = responseData['passages_created'] ?? 0;
final integrated = responseData['passages_integrated'] ?? 0;
debugPrint('Statistiques: $created passages créés, $integrated passages intégrés');
}
// Retourner le secteur et toutes les informations
return {
'status': 'success',
'sector': newSector,
'passages_created': responseData['passages_created'] ?? 0,
'passages_integrated': responseData['passages_integrated'] ?? 0,
'passages_total': (responseData['passages_created'] ?? 0) + (responseData['passages_integrated'] ?? 0),
'warning': responseData['warning'],
'intersecting_departments': responseData['intersecting_departments'],
};
}
return null;
return {
'status': 'error',
'message': responseData['message'] ?? 'Erreur lors de la création'
};
} catch (e) {
return null;
debugPrint('Erreur lors de la création du secteur: $e');
rethrow;
}
}
// Mettre à jour un secteur via l'API
Future<SectorModel?> updateSector(SectorModel sector) async {
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users}) async {
try {
// Préparer les données à envoyer
final Map<String, dynamic> requestData = {
...sector.toJson(),
};
// Ajouter les utilisateurs si fournis
if (users != null) {
requestData['users'] = users;
}
final response = await ApiService.instance.put(
'${AppKeys.sectorsEndpoint}/${sector.id}',
data: sector.toJson(),
data: requestData,
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
// Gérer la réponse correctement
final dynamic responseRaw = response is Response ? response.data : response;
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
if (responseData['status'] == 'success' && responseData['sector'] != null) {
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
await saveSector(updatedSector);
return updatedSector;
if (responseData['status'] == 'success') {
// Sauvegarder le secteur mis à jour
if (responseData['sector'] != null) {
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
await saveSector(updatedSector);
}
// Traiter les passages retournés s'ils existent
if (responseData['passages_sector'] != null) {
try {
final passagesData = responseData['passages_sector'] as List<dynamic>;
debugPrint('Traitement de ${passagesData.length} passages après UPDATE');
// Utiliser PassageRepository pour traiter les passages
final passageRepository = PassageRepository();
// Vider d'abord tous les passages du secteur
await _deleteAllPassagesOfSector(sector.id);
// Puis sauvegarder tous les passages retournés
final List<PassageModel> passagesToSave = [];
for (final passageData in passagesData) {
try {
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
final passage = PassageModel.fromJson(passageDataMap);
passagesToSave.add(passage);
} catch (e) {
debugPrint('Erreur lors du traitement d\'un passage: $e');
}
}
if (passagesToSave.isNotEmpty) {
await passageRepository.savePassages(passagesToSave);
debugPrint('${passagesToSave.length} passages sauvegardés après UPDATE');
}
} catch (e) {
debugPrint('Erreur lors du traitement des passages: $e');
}
}
// Traiter les users_sectors retournés s'ils existent
if (responseData['users_sectors'] != null) {
try {
final usersSectorsData = responseData['users_sectors'] as List<dynamic>;
debugPrint('Traitement de ${usersSectorsData.length} associations utilisateur-secteur');
// Sauvegarder les associations dans la box UserSector via DataLoadingService
await DataLoadingService.instance.processUserSectorsFromApi(usersSectorsData);
for (final userData in usersSectorsData) {
debugPrint('Utilisateur ${userData['first_name']} ${userData['name']} (ID: ${userData['id']}) assigné au secteur ${userData['fk_sector']}');
}
} catch (e) {
debugPrint('Erreur lors du traitement des users_sectors: $e');
}
}
// Afficher les statistiques
final orphaned = responseData['passages_orphaned'] ?? 0;
final updated = responseData['passages_updated'] ?? 0;
final created = responseData['passages_created'] ?? 0;
final total = responseData['passages_total'] ?? 0;
debugPrint('Statistiques UPDATE: $orphaned orphelins, $updated mis à jour, $created créés, $total total');
// Retourner toutes les informations
return {
'status': 'success',
'sector': responseData['sector'] != null ? SectorModel.fromJson(responseData['sector']) : null,
'passages_orphaned': orphaned,
'passages_updated': updated,
'passages_created': created,
'passages_total': total,
'warning': responseData['warning'],
'intersecting_departments': responseData['intersecting_departments'],
};
}
return null;
return {
'status': 'error',
'message': responseData['message'] ?? 'Erreur lors de la mise à jour'
};
} catch (e) {
return null;
debugPrint('Erreur lors de la mise à jour du secteur: $e');
return {
'status': 'error',
'message': 'Erreur de connexion au serveur'
};
}
}
// Supprimer un secteur via l'API
Future<bool> deleteSectorFromApi(int id) async {
Future<Map<String, dynamic>> deleteSectorFromApi(int id) async {
try {
final response = await ApiService.instance.delete(
'${AppKeys.sectorsEndpoint}/$id',
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
final Map<String, dynamic> responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'success') {
// 1. Supprimer tous les passages de ce secteur dans Hive
await _deleteAllPassagesOfSector(id);
// 2. Supprimer le secteur de Hive
await deleteSector(id);
return true;
// 3. Importer les passages orphelins retournés par l'API
if (responseData['passages_sector'] != null) {
await _importOrphanPassages(responseData['passages_sector'] as List<dynamic>);
}
// Vérifier que le secteur a bien été supprimé
final deletedSector = getSectorById(id);
if (deletedSector != null) {
debugPrint('ATTENTION: Le secteur $id existe encore après suppression!');
} else {
debugPrint('Secteur $id supprimé avec succès de Hive');
}
return {
'status': 'success',
'passages_deleted': responseData['passages_deleted'] ?? 0,
'passages_reassigned': responseData['passages_reassigned'] ?? 0,
};
}
return false;
return {
'status': 'error',
'message': responseData['message'] ?? 'Erreur lors de la suppression',
};
} catch (e) {
return false;
debugPrint('Erreur lors de la suppression du secteur: $e');
return {
'status': 'error',
'message': 'Erreur de connexion au serveur',
};
}
}
// Supprimer tous les passages d'un secteur
Future<void> _deleteAllPassagesOfSector(int sectorId) async {
try {
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
debugPrint('La boîte des passages n\'est pas ouverte');
return;
}
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
final List<dynamic> keysToDelete = [];
// Identifier toutes les clés des passages du secteur
for (final entry in passagesBox.toMap().entries) {
final passage = entry.value;
if (passage.fkSector == sectorId) {
keysToDelete.add(entry.key);
}
}
if (keysToDelete.isEmpty) {
debugPrint('Aucun passage à supprimer pour le secteur $sectorId');
return;
}
// Supprimer tous les passages en une seule opération
await passagesBox.deleteAll(keysToDelete);
debugPrint('${keysToDelete.length} passages supprimés du secteur $sectorId en une seule opération');
} catch (e) {
debugPrint('Erreur lors de la suppression des passages: $e');
}
}
// Importer les passages orphelins après suppression d'un secteur
Future<void> _importOrphanPassages(List<dynamic> passagesData) async {
try {
if (passagesData.isEmpty) {
debugPrint('Aucun passage orphelin à importer');
return;
}
final passageRepository = PassageRepository();
final List<PassageModel> passagesToSave = [];
for (final passageData in passagesData) {
try {
// Les passages orphelins ont fk_sector = null
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
final passage = PassageModel.fromJson(passageDataMap);
passagesToSave.add(passage);
} catch (e) {
debugPrint('Erreur lors du traitement d\'un passage orphelin: $e');
}
}
if (passagesToSave.isNotEmpty) {
await passageRepository.savePassages(passagesToSave);
debugPrint('${passagesToSave.length} passages orphelins importés avec fk_sector = null');
}
} catch (e) {
debugPrint('Erreur lors de l\'importation des passages orphelins: $e');
}
}
}

81
app/lib/core/repositories/user_repository.dart Normal file → Executable file
View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
@@ -59,7 +58,9 @@ class UserRepository extends ChangeNotifier {
List<OperationModel> get operations {
try {
if (Hive.isBoxOpen(AppKeys.operationsBoxName)) {
return Hive.box<OperationModel>(AppKeys.operationsBoxName).values.toList();
return Hive.box<OperationModel>(AppKeys.operationsBoxName)
.values
.toList();
}
return [];
} catch (e) {
@@ -142,7 +143,8 @@ class UserRepository extends ChangeNotifier {
// === AUTHENTIFICATION ===
/// Login API PHP
Future<Map<String, dynamic>> loginAPI(String username, String password, {required String type}) async {
Future<Map<String, dynamic>> loginAPI(String username, String password,
{required String type}) async {
try {
return await ApiService.instance.login(username, password, type: type);
} catch (e) {
@@ -152,11 +154,19 @@ class UserRepository extends ChangeNotifier {
}
/// Register API PHP - Uniquement pour les administrateurs
Future<Map<String, dynamic>> registerAPI(String email, String name, String amicaleName, String postalCode, String cityName) async {
Future<Map<String, dynamic>> registerAPI(String email, String name,
String amicaleName, String postalCode, String cityName) async {
try {
final Map<String, dynamic> data = {'email': email, 'name': name, 'amicale_name': amicaleName, 'postal_code': postalCode, 'city_name': cityName};
final Map<String, dynamic> data = {
'email': email,
'name': name,
'amicale_name': amicaleName,
'postal_code': postalCode,
'city_name': cityName
};
final response = await ApiService.instance.post(AppKeys.registerEndpoint, data: data);
final response =
await ApiService.instance.post(AppKeys.registerEndpoint, data: data);
return response.data;
} catch (e) {
debugPrint('❌ Erreur register API: $e');
@@ -175,7 +185,8 @@ class UserRepository extends ChangeNotifier {
}
/// Méthode d'inscription (uniquement pour les administrateurs)
Future<bool> register(String email, String password, String name, String amicaleName, String postalCode, String cityName) async {
Future<bool> register(String email, String password, String name,
String amicaleName, String postalCode, String cityName) async {
_isLoading = true;
notifyListeners();
@@ -183,10 +194,13 @@ class UserRepository extends ChangeNotifier {
debugPrint('📝 Tentative d\'inscription: $email');
// Enregistrer l'administrateur via l'API
final apiResult = await registerAPI(email, name, amicaleName, postalCode, cityName);
final apiResult =
await registerAPI(email, name, amicaleName, postalCode, cityName);
// Créer l'administrateur local
final int userId = apiResult['user_id'] is String ? int.parse(apiResult['user_id']) : apiResult['user_id'];
final int userId = apiResult['user_id'] is String
? int.parse(apiResult['user_id'])
: apiResult['user_id'];
final now = DateTime.now();
final newAdmin = UserModel(
id: userId,
@@ -220,7 +234,8 @@ class UserRepository extends ChangeNotifier {
}
/// Connexion simplifiée avec DataLoadingService
Future<bool> login(String username, String password, {required String type}) async {
Future<bool> login(String username, String password,
{required String type}) async {
_isLoading = true;
notifyListeners();
@@ -242,8 +257,10 @@ class UserRepository extends ChangeNotifier {
// Étape 3: Traitement des données utilisateur (35%)
debugPrint('👤 Traitement des données utilisateur...');
if (apiResult['user'] != null && apiResult['user'] is Map<String, dynamic>) {
final user = _processUserData(apiResult['user'] as Map<String, dynamic>, apiResult['session_id'], apiResult['session_expiry']);
if (apiResult['user'] != null &&
apiResult['user'] is Map<String, dynamic>) {
final user = _processUserData(apiResult['user'] as Map<String, dynamic>,
apiResult['session_id'], apiResult['session_expiry']);
// Sauvegarder via le service
await CurrentUserService.instance.setUser(user);
@@ -308,17 +325,10 @@ class UserRepository extends ChangeNotifier {
// Réinitialiser l'état de HiveResetStateService
hiveResetStateService.reset();
if (context.mounted) {
context.go('/');
}
debugPrint('✅ Déconnexion réussie');
return true;
} catch (e) {
debugPrint('❌ Erreur déconnexion: $e');
if (context.mounted) {
context.go('/'); // Forcer la redirection même en cas d'erreur
}
return false;
} finally {
_isLoading = false;
@@ -327,7 +337,9 @@ class UserRepository extends ChangeNotifier {
}
/// Connexion avec interface utilisateur et progression
Future<bool> loginWithUI(BuildContext context, String username, String password, {required String type}) async {
Future<bool> loginWithUI(
BuildContext context, String username, String password,
{required String type}) async {
try {
// Créer et afficher l'overlay de progression
_progressOverlay = LoadingProgressOverlayUtils.show(
@@ -389,7 +401,8 @@ class UserRepository extends ChangeNotifier {
// === ACCESSEURS DÉLÉGUÉS AUX SERVICES ===
/// Simplifier les getters d'amicale
AmicaleModel? getCurrentUserAmicale() => CurrentAmicaleService.instance.currentAmicale;
AmicaleModel? getCurrentUserAmicale() =>
CurrentAmicaleService.instance.currentAmicale;
/// Obtenir tous les utilisateurs locaux
List<UserModel> getAllUsers() {
@@ -543,7 +556,8 @@ class UserRepository extends ChangeNotifier {
/// Créer ou mettre à jour une amicale localement
Future<AmicaleModel> saveAmicale(AmicaleModel amicale) async {
if (Hive.isBoxOpen(AppKeys.amicaleBoxName)) {
await Hive.box<AmicaleModel>(AppKeys.amicaleBoxName).put(amicale.id, amicale);
await Hive.box<AmicaleModel>(AppKeys.amicaleBoxName)
.put(amicale.id, amicale);
notifyListeners();
}
return amicale;
@@ -589,7 +603,8 @@ class UserRepository extends ChangeNotifier {
return;
}
final unsyncedUsers = _userBox.values.where((user) => !user.isSynced).toList();
final unsyncedUsers =
_userBox.values.where((user) => !user.isSynced).toList();
if (unsyncedUsers.isEmpty) {
return;
@@ -640,7 +655,8 @@ class UserRepository extends ChangeNotifier {
// === TRAITEMENT DES DONNÉES UTILISATEUR ===
/// Méthode pour traiter les données utilisateur reçues de l'API
UserModel _processUserData(Map<String, dynamic> userData, String? sessionId, String? sessionExpiry) {
UserModel _processUserData(
Map<String, dynamic> userData, String? sessionId, String? sessionExpiry) {
debugPrint('👤 Traitement des données utilisateur: ${userData.toString()}');
// Convertir l'ID en int
@@ -660,15 +676,20 @@ class UserRepository extends ChangeNotifier {
// Convertir fk_entite en int si présent
final dynamic rawFkEntite = userData['fk_entite'];
final int? fkEntite = rawFkEntite != null ? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int) : null;
final int? fkEntite = rawFkEntite != null
? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int)
: null;
// Convertir fk_titre en int si présent
final dynamic rawFkTitre = userData['fk_titre'];
final int? fkTitre = rawFkTitre != null ? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int) : null;
final int? fkTitre = rawFkTitre != null
? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int)
: null;
// Traiter les dates si présentes
DateTime? dateNaissance;
if (userData['date_naissance'] != null && userData['date_naissance'] != '') {
if (userData['date_naissance'] != null &&
userData['date_naissance'] != '') {
try {
dateNaissance = DateTime.parse(userData['date_naissance']);
} catch (e) {
@@ -685,7 +706,8 @@ class UserRepository extends ChangeNotifier {
}
}
debugPrint('✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
debugPrint(
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
// Créer un utilisateur avec toutes les données disponibles
return UserModel(
@@ -700,7 +722,8 @@ class UserRepository extends ChangeNotifier {
isActive: true,
isSynced: true,
sessionId: sessionId,
sessionExpiry: sessionExpiry != null ? DateTime.parse(sessionExpiry) : null,
sessionExpiry:
sessionExpiry != null ? DateTime.parse(sessionExpiry) : null,
sectName: userData['sect_name'],
fkEntite: fkEntite,
fkTitre: fkTitre,