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:
19
app/lib/core/constants/app_keys.dart
Normal file → Executable file
19
app/lib/core/constants/app_keys.dart
Normal file → Executable file
@@ -50,9 +50,12 @@ class AppKeys {
|
||||
static const Duration sessionDefaultExpiry = Duration(days: 7);
|
||||
|
||||
// Clés API externes
|
||||
static const String mapboxApiKeyDev = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVmNjN5MTM5djJtczdsMW92cjQ0ciJ9.pUCMuvWPB3cuBaPh4ywTAw';
|
||||
static const String mapboxApiKeyRec = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVlZ3FiMGx0NDJpc2k4YnkxaWZ2dSJ9.OqGJtjlWRgB4fIjECCB8WA';
|
||||
static const String mapboxApiKeyProd = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw';
|
||||
static const String mapboxApiKeyDev =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVmNjN5MTM5djJtczdsMW92cjQ0ciJ9.pUCMuvWPB3cuBaPh4ywTAw';
|
||||
static const String mapboxApiKeyRec =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVlZ3FiMGx0NDJpc2k4YnkxaWZ2dSJ9.OqGJtjlWRgB4fIjECCB8WA';
|
||||
static const String mapboxApiKeyProd =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw';
|
||||
|
||||
// Méthode pour obtenir la clé API Mapbox en fonction de l'environnement actuel
|
||||
static String getMapboxApiKey(String environment) {
|
||||
@@ -114,11 +117,6 @@ class AppKeys {
|
||||
|
||||
// Types de règlements (basés sur la maquette Figma)
|
||||
static const Map<int, Map<String, dynamic>> typesReglements = {
|
||||
0: {
|
||||
'titre': 'Pas de règlement',
|
||||
'couleur': 0xFF757575, // Gris foncé
|
||||
'icon_data': Icons.money_off,
|
||||
},
|
||||
1: {
|
||||
'titre': 'Espèce',
|
||||
'couleur': 0xFFDAA520, // Goldenrod
|
||||
@@ -134,6 +132,11 @@ class AppKeys {
|
||||
'couleur': 0xFF0099FF, // Bleu flashy
|
||||
'icon_data': Icons.credit_card,
|
||||
},
|
||||
4: {
|
||||
'titre': 'Non renseigné',
|
||||
'couleur': 0xFF9E9E9E, // Gris moyen
|
||||
'icon_data': Icons.help_outline,
|
||||
},
|
||||
};
|
||||
|
||||
// Types de passages (basés sur la maquette Figma)
|
||||
|
||||
0
app/lib/core/constants/reponse-login.json
Normal file → Executable file
0
app/lib/core/constants/reponse-login.json
Normal file → Executable file
2
app/lib/core/data/models/amicale_model.dart
Normal file → Executable file
2
app/lib/core/data/models/amicale_model.dart
Normal file → Executable file
@@ -222,7 +222,7 @@ class AmicaleModel extends HiveObject {
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return AmicaleModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
adresse1: adresse1 ?? this.adresse1,
|
||||
adresse2: adresse2 ?? this.adresse2,
|
||||
|
||||
0
app/lib/core/data/models/client_model.dart
Normal file → Executable file
0
app/lib/core/data/models/client_model.dart
Normal file → Executable file
16
app/lib/core/data/models/membre_model.dart
Normal file → Executable file
16
app/lib/core/data/models/membre_model.dart
Normal file → Executable file
@@ -4,6 +4,22 @@ import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
|
||||
part 'membre_model.g.dart';
|
||||
|
||||
/// Modèle représentant un membre d'une amicale.
|
||||
///
|
||||
/// IMPORTANT : Ce modèle représente TOUS les membres d'une amicale,
|
||||
/// pas seulement l'utilisateur connecté. Pour l'utilisateur connecté, voir UserModel.
|
||||
///
|
||||
/// La box Hive 'membres' contient plusieurs enregistrements (tous les membres de l'amicale).
|
||||
///
|
||||
/// Relations avec les autres modèles :
|
||||
/// - UserModel : représente uniquement l'utilisateur connecté (current user)
|
||||
/// - UserSectorModel : utilise MembreModel.id pour associer les membres aux secteurs
|
||||
/// ATTENTION : UserSectorModel.id = MembreModel.id (pas UserModel.id)
|
||||
///
|
||||
/// Chaque membre a son propre ID unique qui est utilisé pour :
|
||||
/// - L'attribution aux secteurs (via UserSectorModel)
|
||||
/// - La gestion des passages
|
||||
/// - Les statistiques par membre
|
||||
@HiveType(typeId: 5) // Utilisation d'un typeId unique
|
||||
class MembreModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
|
||||
0
app/lib/core/data/models/operation_model.dart
Normal file → Executable file
0
app/lib/core/data/models/operation_model.dart
Normal file → Executable file
24
app/lib/core/data/models/passage_model.dart
Normal file → Executable file
24
app/lib/core/data/models/passage_model.dart
Normal file → Executable file
@@ -12,7 +12,7 @@ class PassageModel extends HiveObject {
|
||||
final int fkOperation;
|
||||
|
||||
@HiveField(2)
|
||||
final int fkSector;
|
||||
final int? fkSector;
|
||||
|
||||
@HiveField(3)
|
||||
final int fkUser;
|
||||
@@ -24,7 +24,7 @@ class PassageModel extends HiveObject {
|
||||
final String fkAdresse;
|
||||
|
||||
@HiveField(6)
|
||||
final DateTime passedAt;
|
||||
final DateTime? passedAt;
|
||||
|
||||
@HiveField(7)
|
||||
final String numero;
|
||||
@@ -95,11 +95,11 @@ class PassageModel extends HiveObject {
|
||||
PassageModel({
|
||||
required this.id,
|
||||
required this.fkOperation,
|
||||
required this.fkSector,
|
||||
this.fkSector,
|
||||
required this.fkUser,
|
||||
required this.fkType,
|
||||
required this.fkAdresse,
|
||||
required this.passedAt,
|
||||
this.passedAt,
|
||||
required this.numero,
|
||||
required this.rue,
|
||||
this.rueBis = '',
|
||||
@@ -136,7 +136,11 @@ class PassageModel extends HiveObject {
|
||||
final int fkOperation = rawFkOperation is String ? int.parse(rawFkOperation) : rawFkOperation as int;
|
||||
|
||||
final dynamic rawFkSector = json['fk_sector'];
|
||||
final int fkSector = rawFkSector is String ? int.parse(rawFkSector) : rawFkSector as int;
|
||||
final int? fkSector = rawFkSector == null
|
||||
? null
|
||||
: rawFkSector is String
|
||||
? int.parse(rawFkSector)
|
||||
: rawFkSector as int;
|
||||
|
||||
final dynamic rawFkUser = json['fk_user'];
|
||||
final int fkUser = rawFkUser is String ? int.parse(rawFkUser) : rawFkUser as int;
|
||||
@@ -153,8 +157,10 @@ class PassageModel extends HiveObject {
|
||||
final dynamic rawNbPassages = json['nb_passages'];
|
||||
final int nbPassages = rawNbPassages is String ? int.parse(rawNbPassages) : rawNbPassages as int;
|
||||
|
||||
// Convertir la date
|
||||
final DateTime passedAt = DateTime.parse(json['passed_at']);
|
||||
// Convertir la date (nullable)
|
||||
final DateTime? passedAt = json['passed_at'] != null
|
||||
? DateTime.parse(json['passed_at'])
|
||||
: null;
|
||||
|
||||
return PassageModel(
|
||||
id: id,
|
||||
@@ -203,7 +209,7 @@ class PassageModel extends HiveObject {
|
||||
'fk_user': fkUser,
|
||||
'fk_type': fkType,
|
||||
'fk_adresse': fkAdresse,
|
||||
'passed_at': passedAt.toIso8601String(),
|
||||
'passed_at': passedAt?.toIso8601String(),
|
||||
'numero': numero,
|
||||
'rue': rue,
|
||||
'rue_bis': rueBis,
|
||||
@@ -293,6 +299,6 @@ class PassageModel extends HiveObject {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PassageModel(id: $id, fkOperation: $fkOperation, fkSector: $fkSector, fkUser: $fkUser, fkType: $fkType, adresse: $fkAdresse, ville: $ville, montant: $montant)';
|
||||
return 'PassageModel(id: $id, fkOperation: $fkOperation, fkSector: $fkSector, fkUser: $fkUser, fkType: $fkType, adresse: $fkAdresse, ville: $ville, montant: $montant, passedAt: $passedAt)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ class PassageModelAdapter extends TypeAdapter<PassageModel> {
|
||||
return PassageModel(
|
||||
id: fields[0] as int,
|
||||
fkOperation: fields[1] as int,
|
||||
fkSector: fields[2] as int,
|
||||
fkSector: fields[2] as int?,
|
||||
fkUser: fields[3] as int,
|
||||
fkType: fields[4] as int,
|
||||
fkAdresse: fields[5] as String,
|
||||
passedAt: fields[6] as DateTime,
|
||||
passedAt: fields[6] as DateTime?,
|
||||
numero: fields[7] as String,
|
||||
rue: fields[8] as String,
|
||||
rueBis: fields[9] as String,
|
||||
|
||||
0
app/lib/core/data/models/region_model.dart
Normal file → Executable file
0
app/lib/core/data/models/region_model.dart
Normal file → Executable file
0
app/lib/core/data/models/sector_model.dart
Normal file → Executable file
0
app/lib/core/data/models/sector_model.dart
Normal file → Executable file
14
app/lib/core/data/models/user_model.dart
Normal file → Executable file
14
app/lib/core/data/models/user_model.dart
Normal file → Executable file
@@ -2,6 +2,16 @@ import 'package:hive/hive.dart';
|
||||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
/// Modèle représentant l'utilisateur actuellement connecté (current user).
|
||||
///
|
||||
/// IMPORTANT : Ce modèle est utilisé UNIQUEMENT pour l'utilisateur connecté,
|
||||
/// pas pour les membres de l'amicale. Pour les membres, utilisez MembreModel.
|
||||
///
|
||||
/// La box Hive 'users' ne contient qu'un seul enregistrement : l'utilisateur actuel.
|
||||
///
|
||||
/// Relations avec les autres modèles :
|
||||
/// - MembreModel : représente TOUS les membres d'une amicale (y compris l'utilisateur actuel s'il est membre)
|
||||
/// - UserSectorModel : associe les membres (pas les users) aux secteurs
|
||||
@HiveType(typeId: 0)
|
||||
class UserModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
@@ -201,13 +211,13 @@ class UserModel extends HiveObject {
|
||||
DateTime? dateEmbauche,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
username: username ?? this.username,
|
||||
firstName: firstName ?? this.firstName,
|
||||
role: role ?? this.role,
|
||||
createdAt: this.createdAt,
|
||||
createdAt: createdAt,
|
||||
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
|
||||
0
app/lib/core/data/models/user_sector_model.dart
Normal file → Executable file
0
app/lib/core/data/models/user_sector_model.dart
Normal file → Executable file
0
app/lib/core/models/loading_state.dart
Normal file → Executable file
0
app/lib/core/models/loading_state.dart
Normal file → Executable file
0
app/lib/core/repositories/amicale_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/amicale_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/client_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/client_repository.dart
Normal file → Executable file
55
app/lib/core/repositories/membre_repository.dart
Normal file → Executable file
55
app/lib/core/repositories/membre_repository.dart
Normal file → Executable 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
122
app/lib/core/repositories/operation_repository.dart
Normal file → Executable 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
59
app/lib/core/repositories/passage_repository.dart
Normal file → Executable 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
0
app/lib/core/repositories/region_repository.dart
Normal file → Executable file
369
app/lib/core/repositories/sector_repository.dart
Normal file → Executable file
369
app/lib/core/repositories/sector_repository.dart
Normal file → Executable 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
81
app/lib/core/repositories/user_repository.dart
Normal file → Executable 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,
|
||||
|
||||
0
app/lib/core/services/api_service.dart
Normal file → Executable file
0
app/lib/core/services/api_service.dart
Normal file → Executable file
0
app/lib/core/services/app_info_service.dart
Normal file → Executable file
0
app/lib/core/services/app_info_service.dart
Normal file → Executable file
0
app/lib/core/services/connectivity_service.dart
Normal file → Executable file
0
app/lib/core/services/connectivity_service.dart
Normal file → Executable file
24
app/lib/core/services/current_amicale_service.dart
Normal file → Executable file
24
app/lib/core/services/current_amicale_service.dart
Normal file → Executable file
@@ -6,7 +6,8 @@ import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
|
||||
class CurrentAmicaleService extends ChangeNotifier {
|
||||
static CurrentAmicaleService? _instance;
|
||||
static CurrentAmicaleService get instance => _instance ??= CurrentAmicaleService._internal();
|
||||
static CurrentAmicaleService get instance =>
|
||||
_instance ??= CurrentAmicaleService._internal();
|
||||
CurrentAmicaleService._internal();
|
||||
|
||||
AmicaleModel? _currentAmicale;
|
||||
@@ -23,7 +24,8 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
? '${_currentAmicale!.adresse1} ${_currentAmicale!.adresse2}'.trim()
|
||||
: null;
|
||||
String? get amicaleFullAddress => _currentAmicale != null
|
||||
? '${amicaleAddress ?? ''} ${_currentAmicale!.codePostal} ${_currentAmicale!.ville}'.trim()
|
||||
? '${amicaleAddress ?? ''} ${_currentAmicale!.codePostal} ${_currentAmicale!.ville}'
|
||||
.trim()
|
||||
: null;
|
||||
bool get amicaleIsActive => _currentAmicale?.chkActive ?? false;
|
||||
bool get isClient => _currentAmicale?.fkType == 1;
|
||||
@@ -33,13 +35,11 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
_currentAmicale?.gpsLat.isNotEmpty == true &&
|
||||
_currentAmicale?.gpsLng.isNotEmpty == true;
|
||||
|
||||
double? get latitude => hasGpsCoordinates
|
||||
? double.tryParse(_currentAmicale!.gpsLat)
|
||||
: null;
|
||||
double? get latitude =>
|
||||
hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLat) : null;
|
||||
|
||||
double? get longitude => hasGpsCoordinates
|
||||
? double.tryParse(_currentAmicale!.gpsLng)
|
||||
: null;
|
||||
double? get longitude =>
|
||||
hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLng) : null;
|
||||
|
||||
// === SETTERS ===
|
||||
Future<void> setAmicale(AmicaleModel? amicale) async {
|
||||
@@ -85,7 +85,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
} else {
|
||||
// Si l'amicale n'est pas la bonne, la chercher ou l'effacer
|
||||
_currentAmicale = null;
|
||||
debugPrint('⚠️ Amicale ${amicaleId} non trouvée dans Hive');
|
||||
debugPrint('⚠️ Amicale $amicaleId non trouvée dans Hive');
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -119,7 +119,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> loadFromHive() async {
|
||||
try {
|
||||
try {
|
||||
final box = Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
_currentAmicale = box.get('current_amicale');
|
||||
|
||||
@@ -133,7 +133,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur chargement amicale depuis Hive: $e');
|
||||
_currentAmicale = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === RESET POUR TESTS ===
|
||||
@@ -141,4 +141,4 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
_instance?._currentAmicale = null;
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/core/services/current_user_service.dart
Normal file → Executable file
0
app/lib/core/services/current_user_service.dart
Normal file → Executable file
159
app/lib/core/services/data_loading_service.dart
Normal file → Executable file
159
app/lib/core/services/data_loading_service.dart
Normal file → Executable file
@@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/hive_web_fix.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
@@ -12,7 +11,6 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/repositories/client_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/amicale_repository.dart';
|
||||
import 'package:geosector_app/chat/models/conversation_model.dart';
|
||||
import 'package:geosector_app/chat/models/message_model.dart';
|
||||
import 'package:geosector_app/core/models/loading_state.dart';
|
||||
@@ -20,7 +18,8 @@ import 'package:geosector_app/core/models/loading_state.dart';
|
||||
/// Service singleton pour gérer le chargement et la gestion des données au login
|
||||
class DataLoadingService extends ChangeNotifier {
|
||||
static DataLoadingService? _instance;
|
||||
static DataLoadingService get instance => _instance ??= DataLoadingService._internal();
|
||||
static DataLoadingService get instance =>
|
||||
_instance ??= DataLoadingService._internal();
|
||||
DataLoadingService._internal();
|
||||
|
||||
// État du chargement
|
||||
@@ -43,14 +42,22 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// === GETTERS POUR LES BOXES ===
|
||||
Box<OperationModel> get _operationBox => Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
Box<SectorModel> get _sectorBox => Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
Box<PassageModel> get _passageBox => Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
Box<MembreModel> get _membreBox => Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
Box<UserSectorModel> get _userSectorBox => Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
Box<AmicaleModel> get _amicaleBox => Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
Box<ConversationModel> get _chatConversationBox => Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
|
||||
Box<MessageModel> get _chatMessageBox => Hive.box<MessageModel>(AppKeys.chatMessagesBoxName);
|
||||
Box<OperationModel> get _operationBox =>
|
||||
Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
Box<SectorModel> get _sectorBox =>
|
||||
Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
Box<PassageModel> get _passageBox =>
|
||||
Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
Box<MembreModel> get _membreBox =>
|
||||
Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
Box<UserSectorModel> get _userSectorBox =>
|
||||
Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
Box<AmicaleModel> get _amicaleBox =>
|
||||
Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
Box<ConversationModel> get _chatConversationBox =>
|
||||
Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
|
||||
Box<MessageModel> get _chatMessageBox =>
|
||||
Hive.box<MessageModel>(AppKeys.chatMessagesBoxName);
|
||||
Box get _settingsBox => Hive.box(AppKeys.settingsBoxName);
|
||||
|
||||
/// Traite toutes les données reçues de l'API lors du login
|
||||
@@ -71,8 +78,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _processOperations(apiResult['operations']);
|
||||
}
|
||||
|
||||
if (apiResult['secteurs'] != null) {
|
||||
await _processSectors(apiResult['secteurs']);
|
||||
if (apiResult['sectors'] != null) {
|
||||
await _processSectors(apiResult['sectors']);
|
||||
}
|
||||
|
||||
if (apiResult['passages'] != null) {
|
||||
@@ -86,8 +93,16 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _processMembres(apiResult['membres']);
|
||||
}
|
||||
|
||||
if (apiResult['userSecteurs'] != null) {
|
||||
await _processUserSectors(apiResult['userSecteurs']);
|
||||
// ATTENTION : L'API envoie 'users_sectors', pas 'userSecteurs'
|
||||
if (apiResult['users_sectors'] != null) {
|
||||
debugPrint('📋 Traitement des associations users_sectors depuis le login');
|
||||
await _processUserSectors(apiResult['users_sectors'], clearAll: true);
|
||||
} else if (apiResult['userSecteurs'] != null) {
|
||||
// Fallback pour compatibilité si l'API change
|
||||
debugPrint('📋 Traitement des associations userSecteurs depuis le login (fallback)');
|
||||
await _processUserSectors(apiResult['userSecteurs'], clearAll: true);
|
||||
} else {
|
||||
debugPrint('⚠️ Aucune donnée users_sectors/userSecteurs trouvée dans la réponse du login');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du chargement: $e');
|
||||
@@ -107,7 +122,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
|
||||
for (final boxName in requiredBoxes) {
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
throw Exception('La boîte $boxName n\'est pas ouverte. Redémarrez l\'application.');
|
||||
throw Exception(
|
||||
'La boîte $boxName n\'est pas ouverte. Redémarrez l\'application.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +155,10 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les associations user-sectors depuis l'extérieur
|
||||
Future<void> processUserSectorsFromApi(dynamic userSectorsData) async {
|
||||
await _processUserSectors(userSectorsData);
|
||||
/// [clearAll] : si true, vide toute la box avant d'ajouter (pour le login)
|
||||
/// si false, ne supprime que les secteurs concernés (pour les mises à jour)
|
||||
Future<void> processUserSectorsFromApi(dynamic userSectorsData, {bool clearAll = false}) async {
|
||||
await _processUserSectors(userSectorsData, clearAll: clearAll);
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les opérations depuis l'extérieur
|
||||
@@ -225,27 +243,41 @@ class DataLoadingService extends ChangeNotifier {
|
||||
List<dynamic> sectorsList;
|
||||
if (sectorsData is List) {
|
||||
sectorsList = sectorsData;
|
||||
debugPrint('📋 ${sectorsList.length} secteurs à traiter (format: List directe)');
|
||||
} else if (sectorsData is Map && sectorsData.containsKey('data')) {
|
||||
sectorsList = sectorsData['data'] as List<dynamic>;
|
||||
debugPrint('📋 ${sectorsList.length} secteurs à traiter (format: Map avec data)');
|
||||
} else {
|
||||
debugPrint('⚠️ Format de données de secteurs non reconnu');
|
||||
debugPrint('⚠️ Format de données de secteurs non reconnu: ${sectorsData.runtimeType}');
|
||||
debugPrint('⚠️ Contenu: ${sectorsData.toString().substring(0, 200)}...');
|
||||
return;
|
||||
}
|
||||
|
||||
await _sectorBox.clear();
|
||||
|
||||
int count = 0;
|
||||
int errorCount = 0;
|
||||
for (final sectorData in sectorsList) {
|
||||
try {
|
||||
debugPrint('🔄 Traitement secteur ID: ${sectorData['id']}, libelle: "${sectorData['libelle']}"');
|
||||
final sector = SectorModel.fromJson(sectorData);
|
||||
await _sectorBox.put(sector.id, sector);
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement secteur: $e');
|
||||
errorCount++;
|
||||
debugPrint('⚠️ Erreur traitement secteur ${sectorData['id']}: $e');
|
||||
debugPrint('⚠️ Données problématiques: $sectorData');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count secteurs stockés');
|
||||
debugPrint('✅ $count secteurs stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
|
||||
// Vérification finale
|
||||
final storedSectors = _sectorBox.values.toList();
|
||||
debugPrint('🔍 Vérification: ${storedSectors.length} secteurs dans la box');
|
||||
for (final sector in storedSectors) {
|
||||
debugPrint(' - Secteur ${sector.id}: "${sector.libelle}" (${sector.color})');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement secteurs: $e');
|
||||
}
|
||||
@@ -286,7 +318,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count passages stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
debugPrint(
|
||||
'✅ $count passages stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement passages: $e');
|
||||
}
|
||||
@@ -305,7 +338,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _amicaleBox.clear();
|
||||
try {
|
||||
// Les données d'amicale sont un objet unique
|
||||
final Map<String, dynamic> amicaleMap = Map<String, dynamic>.from(amicaleData as Map);
|
||||
final Map<String, dynamic> amicaleMap =
|
||||
Map<String, dynamic>.from(amicaleData as Map);
|
||||
final amicale = AmicaleModel.fromJson(amicaleMap);
|
||||
await _amicaleBox.put(amicale.id, amicale);
|
||||
debugPrint('✅ Amicale stockée: ${amicale.name} (ID: ${amicale.id})');
|
||||
@@ -353,15 +387,17 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count membres stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
debugPrint(
|
||||
'✅ $count membres stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement membres: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processUserSectors(dynamic userSectorsData) async {
|
||||
Future<void> _processUserSectors(dynamic userSectorsData, {bool clearAll = false}) async {
|
||||
try {
|
||||
debugPrint('🔗 Traitement des associations utilisateurs-secteurs...');
|
||||
debugPrint('Type de données reçues: ${userSectorsData.runtimeType}');
|
||||
|
||||
if (userSectorsData == null) {
|
||||
debugPrint('ℹ️ Aucune association utilisateur-secteur à traiter');
|
||||
@@ -371,27 +407,91 @@ class DataLoadingService extends ChangeNotifier {
|
||||
List<dynamic> userSectorsList;
|
||||
if (userSectorsData is List) {
|
||||
userSectorsList = userSectorsData;
|
||||
} else if (userSectorsData is Map && userSectorsData.containsKey('data')) {
|
||||
debugPrint('✅ Données au format List avec ${userSectorsList.length} éléments');
|
||||
} else if (userSectorsData is Map &&
|
||||
userSectorsData.containsKey('data')) {
|
||||
userSectorsList = userSectorsData['data'] as List<dynamic>;
|
||||
debugPrint('✅ Données au format Map[data] avec ${userSectorsList.length} éléments');
|
||||
} else {
|
||||
debugPrint('⚠️ Format de données d\'associations non reconnu');
|
||||
return;
|
||||
}
|
||||
|
||||
await _userSectorBox.clear();
|
||||
// Vérifier si la box est ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
||||
debugPrint('❌ La box UserSector n\'est pas ouverte!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
// Mode login : on vide toute la box
|
||||
await _userSectorBox.clear();
|
||||
debugPrint('📦 Box UserSector vidée complètement (mode login)');
|
||||
} else {
|
||||
// Mode mise à jour : on ne supprime que les associations des secteurs concernés
|
||||
// car l'API ne retourne que les associations pour le(s) secteur(s) modifié(s)
|
||||
|
||||
// Identifier les secteurs concernés dans les nouvelles données
|
||||
final Set<int> sectorsToUpdate = {};
|
||||
for (final data in userSectorsList) {
|
||||
if (data['fk_sector'] != null) {
|
||||
final fkSector = data['fk_sector'] is String
|
||||
? int.parse(data['fk_sector'])
|
||||
: data['fk_sector'] as int;
|
||||
sectorsToUpdate.add(fkSector);
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer uniquement les associations des secteurs concernés
|
||||
if (sectorsToUpdate.isNotEmpty) {
|
||||
debugPrint('🗑️ Suppression des associations pour les secteurs: $sectorsToUpdate');
|
||||
final keysToDelete = <dynamic>[];
|
||||
|
||||
for (var i = 0; i < _userSectorBox.length; i++) {
|
||||
final key = _userSectorBox.keyAt(i);
|
||||
final userSector = _userSectorBox.getAt(i);
|
||||
if (userSector != null && sectorsToUpdate.contains(userSector.fkSector)) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
await _userSectorBox.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('📦 ${keysToDelete.length} associations supprimées');
|
||||
}
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (final userSectorData in userSectorsList) {
|
||||
try {
|
||||
final userSector = UserSectorModel.fromJson(userSectorData);
|
||||
await _userSectorBox.put('${userSector.id}_${userSector.fkSector}', userSector);
|
||||
final key = '${userSector.id}_${userSector.fkSector}';
|
||||
await _userSectorBox.put(key, userSector);
|
||||
debugPrint('✅ Association sauvegardée: ${userSector.firstName} ${userSector.name} (ID: ${userSector.id}) -> Secteur ${userSector.fkSector}');
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement association: $e');
|
||||
debugPrint('⚠️ Données problématiques: $userSectorData');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count associations stockées');
|
||||
|
||||
// Vérifier le contenu de la box après sauvegarde
|
||||
debugPrint('📦 Contenu de la box UserSector après sauvegarde:');
|
||||
debugPrint(' - Nombre d\'entrées: ${_userSectorBox.length}');
|
||||
for (var i = 0; i < _userSectorBox.length && i < 5; i++) {
|
||||
final key = _userSectorBox.keyAt(i);
|
||||
final value = _userSectorBox.getAt(i);
|
||||
if (value != null) {
|
||||
debugPrint(' - [$key]: ${value.firstName} ${value.name} (ID: ${value.id}) -> Secteur ${value.fkSector}');
|
||||
}
|
||||
}
|
||||
if (_userSectorBox.length > 5) {
|
||||
debugPrint(' ... et ${_userSectorBox.length - 5} autres associations');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement associations: $e');
|
||||
}
|
||||
@@ -449,7 +549,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Nettoyage Android terminé. $filesDeleted fichiers supprimés');
|
||||
debugPrint(
|
||||
'✅ Nettoyage Android terminé. $filesDeleted fichiers supprimés');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur nettoyage Android: $e');
|
||||
}
|
||||
|
||||
0
app/lib/core/services/hive_adapters.dart
Normal file → Executable file
0
app/lib/core/services/hive_adapters.dart
Normal file → Executable file
32
app/lib/core/services/hive_reset_service.dart
Normal file → Executable file
32
app/lib/core/services/hive_reset_service.dart
Normal file → Executable file
@@ -4,7 +4,6 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
// Importations conditionnelles pour le web vs non-web
|
||||
import 'js_interface.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/hive_web_fix.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/data/models/client_model.dart';
|
||||
@@ -22,7 +21,8 @@ class HiveResetService {
|
||||
/// Réinitialise complètement Hive et recrée les boîtes nécessaires
|
||||
static Future<bool> resetAndRecreateHiveBoxes() async {
|
||||
try {
|
||||
debugPrint('HiveResetService: Début de la réinitialisation complète de Hive');
|
||||
debugPrint(
|
||||
'HiveResetService: Début de la réinitialisation complète de Hive');
|
||||
|
||||
// Approche plus radicale pour le web : supprimer directement IndexedDB
|
||||
if (kIsWeb) {
|
||||
@@ -67,7 +67,8 @@ class HiveResetService {
|
||||
// Rouvrir les boîtes essentielles
|
||||
await _reopenEssentialBoxes();
|
||||
|
||||
debugPrint('HiveResetService: Réinitialisation complète terminée avec succès');
|
||||
debugPrint(
|
||||
'HiveResetService: Réinitialisation complète terminée avec succès');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('HiveResetService: Erreur lors de la réinitialisation: $e');
|
||||
@@ -75,31 +76,6 @@ class HiveResetService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ferme toutes les boîtes Hive ouvertes
|
||||
static Future<void> _closeAllBoxes() async {
|
||||
final boxNames = [
|
||||
AppKeys.userBoxName,
|
||||
AppKeys.amicaleBoxName,
|
||||
AppKeys.clientsBoxName,
|
||||
AppKeys.operationsBoxName,
|
||||
AppKeys.sectorsBoxName,
|
||||
AppKeys.passagesBoxName,
|
||||
AppKeys.settingsBoxName,
|
||||
AppKeys.membresBoxName,
|
||||
AppKeys.userSectorBoxName,
|
||||
AppKeys.chatConversationsBoxName,
|
||||
AppKeys.chatMessagesBoxName,
|
||||
AppKeys.regionsBoxName,
|
||||
];
|
||||
|
||||
for (final boxName in boxNames) {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
debugPrint('HiveResetService: Fermeture de la boîte $boxName');
|
||||
await Hive.box(boxName).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistre tous les adaptateurs Hive
|
||||
static void _registerAdapters() {
|
||||
debugPrint('HiveResetService: Enregistrement des adaptateurs Hive');
|
||||
|
||||
0
app/lib/core/services/hive_reset_state_service.dart
Normal file → Executable file
0
app/lib/core/services/hive_reset_state_service.dart
Normal file → Executable file
1
app/lib/core/services/hive_service.dart
Normal file → Executable file
1
app/lib/core/services/hive_service.dart
Normal file → Executable file
@@ -426,6 +426,7 @@ class HiveService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
/// Vider une Box individuelle
|
||||
|
||||
3
app/lib/core/services/hive_web_fix.dart
Normal file → Executable file
3
app/lib/core/services/hive_web_fix.dart
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:js' as js;
|
||||
import 'package:geosector_app/core/services/js_stub.dart'
|
||||
if (dart.library.js) 'dart:js' as js;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
|
||||
0
app/lib/core/services/js_interface.dart
Normal file → Executable file
0
app/lib/core/services/js_interface.dart
Normal file → Executable file
5
app/lib/core/services/js_stub.dart
Normal file → Executable file
5
app/lib/core/services/js_stub.dart
Normal file → Executable file
@@ -5,6 +5,11 @@ class JsContext {
|
||||
// Ne fait rien sur les plateformes non-web
|
||||
return null;
|
||||
}
|
||||
|
||||
bool hasProperty(String property) {
|
||||
// Retourne false sur les plateformes non-web
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Contexte JavaScript stub
|
||||
|
||||
0
app/lib/core/services/location_service.dart
Normal file → Executable file
0
app/lib/core/services/location_service.dart
Normal file → Executable file
19
app/lib/core/services/passage_data_service.dart
Normal file → Executable file
19
app/lib/core/services/passage_data_service.dart
Normal file → Executable file
@@ -48,9 +48,15 @@ class PassageDataService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trouver la date du passage le plus récent
|
||||
passagesToUse.sort((a, b) => b.passedAt.compareTo(a.passedAt));
|
||||
final DateTime referenceDate = passagesToUse.first.passedAt;
|
||||
// Trouver la date du passage le plus récent (exclure ceux sans date)
|
||||
final passagesWithDate = passagesToUse.where((p) => p.passedAt != null).toList();
|
||||
if (passagesWithDate.isEmpty) {
|
||||
debugPrint('Aucun passage avec date trouvé');
|
||||
return [];
|
||||
}
|
||||
|
||||
passagesWithDate.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
|
||||
final DateTime referenceDate = passagesWithDate.first.passedAt!;
|
||||
debugPrint(
|
||||
'Date de référence pour le graphique: ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
|
||||
|
||||
@@ -84,12 +90,13 @@ class PassageDataService {
|
||||
|
||||
// Parcourir les passages et les regrouper par date et type
|
||||
for (final passage in passagesToUse) {
|
||||
if (passage.passedAt
|
||||
if (passage.passedAt != null &&
|
||||
passage.passedAt!
|
||||
.isAfter(startDate.subtract(const Duration(days: 1))) &&
|
||||
passage.passedAt
|
||||
passage.passedAt!
|
||||
.isBefore(referenceDate.add(const Duration(days: 1)))) {
|
||||
final dateStr =
|
||||
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}-${passage.passedAt.day.toString().padLeft(2, '0')}';
|
||||
'${passage.passedAt!.year}-${passage.passedAt!.month.toString().padLeft(2, '0')}-${passage.passedAt!.day.toString().padLeft(2, '0')}';
|
||||
final typeId = passage.fkType;
|
||||
|
||||
// Vérifier que le type n'est pas exclu
|
||||
|
||||
0
app/lib/core/services/sync_service.dart
Normal file → Executable file
0
app/lib/core/services/sync_service.dart
Normal file → Executable file
161
app/lib/core/services/theme_service.dart
Executable file
161
app/lib/core/services/theme_service.dart
Executable file
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service pour gérer les préférences de thème de l'application
|
||||
/// Supporte la détection automatique du mode sombre/clair du système
|
||||
class ThemeService extends ChangeNotifier {
|
||||
static ThemeService? _instance;
|
||||
static ThemeService get instance => _instance ??= ThemeService._();
|
||||
|
||||
ThemeService._() {
|
||||
_init();
|
||||
}
|
||||
|
||||
// Préférences stockées
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
// Mode de thème actuel
|
||||
ThemeMode _themeMode = ThemeMode.system;
|
||||
|
||||
// Clé pour stocker les préférences
|
||||
static const String _themeModeKey = 'theme_mode';
|
||||
|
||||
/// Mode de thème actuel
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
|
||||
/// Détecte si le système est en mode sombre
|
||||
bool get isSystemDark {
|
||||
final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
return brightness == Brightness.dark;
|
||||
}
|
||||
|
||||
/// Détermine si l'app doit utiliser le mode sombre
|
||||
bool get isDarkMode {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return false;
|
||||
case ThemeMode.dark:
|
||||
return true;
|
||||
case ThemeMode.system:
|
||||
return isSystemDark;
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise le service
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _loadThemeMode();
|
||||
|
||||
// Observer les changements du système
|
||||
SchedulerBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () {
|
||||
_onSystemBrightnessChanged();
|
||||
};
|
||||
|
||||
debugPrint('🎨 ThemeService initialisé - Mode: $_themeMode, Système sombre: $isSystemDark');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur initialisation ThemeService: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge le mode de thème depuis les préférences
|
||||
Future<void> _loadThemeMode() async {
|
||||
try {
|
||||
final savedMode = _prefs?.getString(_themeModeKey);
|
||||
if (savedMode != null) {
|
||||
_themeMode = ThemeMode.values.firstWhere(
|
||||
(mode) => mode.name == savedMode,
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
}
|
||||
debugPrint('🎨 Mode de thème chargé: $_themeMode');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur chargement thème: $e');
|
||||
_themeMode = ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde le mode de thème
|
||||
Future<void> _saveThemeMode() async {
|
||||
try {
|
||||
await _prefs?.setString(_themeModeKey, _themeMode.name);
|
||||
debugPrint('💾 Mode de thème sauvegardé: $_themeMode');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur sauvegarde thème: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Appelée quand la luminosité du système change
|
||||
void _onSystemBrightnessChanged() {
|
||||
if (_themeMode == ThemeMode.system) {
|
||||
debugPrint('🌗 Changement luminosité système détecté - Sombre: $isSystemDark');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Change le mode de thème
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
if (_themeMode != mode) {
|
||||
_themeMode = mode;
|
||||
await _saveThemeMode();
|
||||
notifyListeners();
|
||||
debugPrint('🎨 Mode de thème changé: $mode');
|
||||
}
|
||||
}
|
||||
|
||||
/// Basculer entre clair et sombre
|
||||
Future<void> toggleTheme() async {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
await setThemeMode(ThemeMode.dark);
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
await setThemeMode(ThemeMode.light);
|
||||
break;
|
||||
case ThemeMode.system:
|
||||
// Si système, basculer vers l'opposé du mode actuel du système
|
||||
await setThemeMode(isSystemDark ? ThemeMode.light : ThemeMode.dark);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourner au mode système
|
||||
Future<void> useSystemTheme() async {
|
||||
await setThemeMode(ThemeMode.system);
|
||||
}
|
||||
|
||||
/// Forcer le mode clair
|
||||
Future<void> useLightTheme() async {
|
||||
await setThemeMode(ThemeMode.light);
|
||||
}
|
||||
|
||||
/// Forcer le mode sombre
|
||||
Future<void> useDarkTheme() async {
|
||||
await setThemeMode(ThemeMode.dark);
|
||||
}
|
||||
|
||||
/// Obtenir une description textuelle du mode actuel
|
||||
String get themeModeDescription {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return 'Clair';
|
||||
case ThemeMode.dark:
|
||||
return 'Sombre';
|
||||
case ThemeMode.system:
|
||||
return 'Automatique (${isSystemDark ? 'sombre' : 'clair'})';
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtenir l'icône appropriée pour le mode actuel
|
||||
IconData get themeModeIcon {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return Icons.light_mode;
|
||||
case ThemeMode.dark:
|
||||
return Icons.dark_mode;
|
||||
case ThemeMode.system:
|
||||
return Icons.brightness_auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
app/lib/core/theme/app_theme.dart
Normal file → Executable file
62
app/lib/core/theme/app_theme.dart
Normal file → Executable file
@@ -7,19 +7,20 @@ class AppTheme {
|
||||
static const Color accentColor = Color(0xFF00E09D); // Vert
|
||||
static const Color errorColor = Color(0xFFE41B13); // Rouge
|
||||
static const Color warningColor = Color(0xFFF7A278); // Orange
|
||||
static const Color backgroundLightColor = Color(0xFFF4F5F6); // Gris très clair
|
||||
static const Color backgroundLightColor =
|
||||
Color(0xFFF4F5F6); // Gris très clair
|
||||
static const Color backgroundDarkColor = Color(0xFF111827);
|
||||
static const Color textLightColor = Color(0xFF000000); // Noir
|
||||
static const Color textDarkColor = Color(0xFFF9FAFB);
|
||||
|
||||
|
||||
// Couleurs de texte supplémentaires
|
||||
static const Color textSecondaryColor = Color(0xFF7F8C8D);
|
||||
static const Color textLightSecondaryColor = Color(0xFFBDC3C7);
|
||||
|
||||
|
||||
// Couleurs des boutons
|
||||
static const Color buttonSuccessColor = Color(0xFF2ECC71);
|
||||
static const Color buttonDangerColor = Color(0xFFE74C3C);
|
||||
|
||||
|
||||
// Couleurs des charts
|
||||
static const List<Color> chartColors = [
|
||||
primaryColor,
|
||||
@@ -30,7 +31,7 @@ class AppTheme {
|
||||
Color(0xFF9B59B6),
|
||||
Color(0xFF1ABC9C),
|
||||
];
|
||||
|
||||
|
||||
// Ombres
|
||||
static List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
@@ -40,7 +41,7 @@ class AppTheme {
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
static List<BoxShadow> buttonShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
@@ -49,14 +50,14 @@ class AppTheme {
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
// Rayons des bordures
|
||||
static const double borderRadiusSmall = 4.0;
|
||||
static const double borderRadiusMedium = 8.0;
|
||||
static const double borderRadiusLarge = 12.0;
|
||||
static const double borderRadiusXL = 16.0;
|
||||
static const double borderRadiusRounded = 50.0;
|
||||
|
||||
|
||||
// Espacement
|
||||
static const double spacingXS = 4.0;
|
||||
static const double spacingS = 8.0;
|
||||
@@ -71,15 +72,13 @@ class AppTheme {
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
fontFamily: 'Figtree',
|
||||
colorScheme: ColorScheme.light(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
tertiary: accentColor,
|
||||
background: backgroundLightColor,
|
||||
surface: Colors.white,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onBackground: textLightColor,
|
||||
onSurface: textLightColor,
|
||||
error: errorColor,
|
||||
),
|
||||
@@ -94,7 +93,8 @@ class AppTheme {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusRounded),
|
||||
@@ -110,7 +110,8 @@ class AppTheme {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
side: const BorderSide(color: primaryColor),
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
),
|
||||
@@ -119,7 +120,8 @@ class AppTheme {
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingS),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingS),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
@@ -143,7 +145,8 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingM),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
@@ -166,15 +169,13 @@ class AppTheme {
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
fontFamily: 'Figtree',
|
||||
colorScheme: ColorScheme.dark(
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
tertiary: accentColor,
|
||||
background: backgroundDarkColor,
|
||||
surface: const Color(0xFF1F2937),
|
||||
surface: Color(0xFF1F2937),
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onBackground: textDarkColor,
|
||||
onSurface: textDarkColor,
|
||||
error: errorColor,
|
||||
),
|
||||
@@ -189,7 +190,8 @@ class AppTheme {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusRounded),
|
||||
@@ -205,7 +207,8 @@ class AppTheme {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
side: const BorderSide(color: primaryColor),
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
),
|
||||
@@ -214,7 +217,8 @@ class AppTheme {
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingS),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingS),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
@@ -238,7 +242,8 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingM),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 4,
|
||||
@@ -269,10 +274,13 @@ class AppTheme {
|
||||
titleSmall: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodySmall: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
bodySmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
labelMedium: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelMedium:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelSmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/core/utils/api_exception.dart
Normal file → Executable file
0
app/lib/core/utils/api_exception.dart
Normal file → Executable file
Reference in New Issue
Block a user