Livraison d ela gestion des opérations v0.4.0
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
|
||||
@@ -60,58 +61,64 @@ class MembreModel extends HiveObject {
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
factory MembreModel.fromJson(Map<String, dynamic> json) {
|
||||
// Convertir l'ID en int, qu'il soit déjà int ou string
|
||||
final dynamic rawId = json['id'];
|
||||
final int id = rawId is String ? int.parse(rawId) : rawId as int;
|
||||
try {
|
||||
// Convertir l'ID en int, qu'il soit déjà int ou string
|
||||
final dynamic rawId = json['id'];
|
||||
final int id = rawId is String ? int.parse(rawId) : rawId as int;
|
||||
|
||||
// Convertir le rôle en int (ATTENTION: le champ JSON est 'fk_role' pas 'role')
|
||||
final dynamic rawRole = json['fk_role']; // Correction ici !
|
||||
final int role = rawRole is String ? int.parse(rawRole) : rawRole as int;
|
||||
// Convertir le rôle en int (ATTENTION: le champ JSON est 'fk_role' pas 'role')
|
||||
final dynamic rawRole = json['fk_role']; // Correction ici !
|
||||
final int role = rawRole is String ? int.parse(rawRole) : rawRole as int;
|
||||
|
||||
// Convertir fkEntite en int si présent
|
||||
int? fkEntite;
|
||||
if (json['fk_entite'] != null) {
|
||||
final dynamic rawEntite = json['fk_entite'];
|
||||
fkEntite = rawEntite is String ? int.parse(rawEntite) : rawEntite as int;
|
||||
}
|
||||
|
||||
// Convertir fkTitre en int si présent
|
||||
int? fkTitre;
|
||||
if (json['fk_titre'] != null) {
|
||||
final dynamic rawTitre = json['fk_titre'];
|
||||
fkTitre = rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
|
||||
}
|
||||
|
||||
// Gérer les dates nulles ou avec des valeurs invalides comme "0000-00-00"
|
||||
DateTime? parseDate(String? dateStr) {
|
||||
if (dateStr == null || dateStr.isEmpty || dateStr == "0000-00-00") {
|
||||
return null;
|
||||
// Convertir fkEntite en int si présent
|
||||
int? fkEntite;
|
||||
if (json['fk_entite'] != null) {
|
||||
final dynamic rawEntite = json['fk_entite'];
|
||||
fkEntite = rawEntite is String ? int.parse(rawEntite) : rawEntite as int;
|
||||
}
|
||||
try {
|
||||
return DateTime.parse(dateStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return MembreModel(
|
||||
id: id,
|
||||
fkEntite: fkEntite,
|
||||
role: role,
|
||||
fkTitre: fkTitre,
|
||||
name: json['name'],
|
||||
firstName: json['first_name'],
|
||||
username: json['username'],
|
||||
sectName: json['sect_name'],
|
||||
email: json['email'] ?? '',
|
||||
phone: json['phone'],
|
||||
mobile: json['mobile'],
|
||||
dateNaissance: parseDate(json['date_naissance']),
|
||||
dateEmbauche: parseDate(json['date_embauche']),
|
||||
createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : DateTime.now(),
|
||||
// Le champ JSON est 'chk_active' pas 'is_active'
|
||||
isActive: json['chk_active'] == 1 || json['chk_active'] == true,
|
||||
);
|
||||
// Convertir fkTitre en int si présent
|
||||
int? fkTitre;
|
||||
if (json['fk_titre'] != null) {
|
||||
final dynamic rawTitre = json['fk_titre'];
|
||||
fkTitre = rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
|
||||
}
|
||||
|
||||
// Gérer les dates nulles ou avec des valeurs invalides comme "0000-00-00"
|
||||
DateTime? parseDate(String? dateStr) {
|
||||
if (dateStr == null || dateStr.isEmpty || dateStr == "0000-00-00") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return DateTime.parse(dateStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return MembreModel(
|
||||
id: id,
|
||||
fkEntite: fkEntite,
|
||||
role: role,
|
||||
fkTitre: fkTitre,
|
||||
name: json['name'] ?? '', // ← Fallback pour champs manquants
|
||||
firstName: json['first_name'] ?? '', // ← Fallback pour champs manquants
|
||||
username: json['username'] ?? '', // ← Fallback pour champs manquants
|
||||
sectName: json['sect_name'] ?? '',
|
||||
email: json['email'] ?? '', // ← Déjà OK mais renforcé
|
||||
phone: json['phone'],
|
||||
mobile: json['mobile'],
|
||||
dateNaissance: parseDate(json['date_naissance']),
|
||||
dateEmbauche: parseDate(json['date_embauche']),
|
||||
createdAt: DateTime.now(), // ← Simplifié car created_at n'existe pas dans l'API
|
||||
// Le champ JSON est 'chk_active' pas 'is_active'
|
||||
isActive: json['chk_active'] == 1 || json['chk_active'] == true,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur parsing MembreModel: $e');
|
||||
debugPrint('❌ Données JSON: $json');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir en JSON pour l'API
|
||||
|
||||
@@ -25,12 +25,16 @@ class OperationModel extends HiveObject {
|
||||
@HiveField(6)
|
||||
bool isSynced;
|
||||
|
||||
@HiveField(7)
|
||||
final int fkEntite;
|
||||
|
||||
OperationModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.dateDebut,
|
||||
required this.dateFin,
|
||||
required this.lastSyncedAt,
|
||||
required this.fkEntite,
|
||||
this.isActive = true,
|
||||
this.isSynced = false,
|
||||
});
|
||||
@@ -40,15 +44,18 @@ class OperationModel extends HiveObject {
|
||||
// Convertir l'ID en int, qu'il soit déjà int ou string
|
||||
final dynamic rawId = json['id'];
|
||||
final int id = rawId is String ? int.parse(rawId) : rawId as int;
|
||||
|
||||
final dynamic rawEntite = json['fk_entite'];
|
||||
final int fkEntite = rawEntite is String ? int.parse(rawEntite) : rawEntite as int;
|
||||
|
||||
return OperationModel(
|
||||
id: id,
|
||||
name: json['name'],
|
||||
name: json['libelle'], // ← Correction: utiliser 'libelle' au lieu de 'name'
|
||||
dateDebut: DateTime.parse(json['date_deb']),
|
||||
dateFin: DateTime.parse(json['date_fin']),
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isActive: true,
|
||||
isActive: json['chk_active'] == true || json['chk_active'] == 1 || json['chk_active'] == "1",
|
||||
isSynced: true,
|
||||
fkEntite: fkEntite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,6 +67,7 @@ class OperationModel extends HiveObject {
|
||||
'date_deb': dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
'date_fin': dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
'is_active': isActive,
|
||||
'fk_entite': fkEntite,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,15 +79,17 @@ class OperationModel extends HiveObject {
|
||||
bool? isActive,
|
||||
bool? isSynced,
|
||||
DateTime? lastSyncedAt,
|
||||
int? fkEntite,
|
||||
}) {
|
||||
return OperationModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
dateDebut: dateDebut ?? this.dateDebut,
|
||||
dateFin: dateFin ?? this.dateFin,
|
||||
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
fkEntite: fkEntite ?? this.fkEntite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class OperationModelAdapter extends TypeAdapter<OperationModel> {
|
||||
dateDebut: fields[2] as DateTime,
|
||||
dateFin: fields[3] as DateTime,
|
||||
lastSyncedAt: fields[4] as DateTime,
|
||||
fkEntite: fields[7] as int,
|
||||
isActive: fields[5] as bool,
|
||||
isSynced: fields[6] as bool,
|
||||
);
|
||||
@@ -30,7 +31,7 @@ class OperationModelAdapter extends TypeAdapter<OperationModel> {
|
||||
@override
|
||||
void write(BinaryWriter writer, OperationModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -44,7 +45,9 @@ class OperationModelAdapter extends TypeAdapter<OperationModel> {
|
||||
..writeByte(5)
|
||||
..write(obj.isActive)
|
||||
..writeByte(6)
|
||||
..write(obj.isSynced);
|
||||
..write(obj.isSynced)
|
||||
..writeByte(7)
|
||||
..write(obj.fkEntite);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'passage_model.g.dart';
|
||||
@@ -125,66 +126,72 @@ class PassageModel extends HiveObject {
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
factory PassageModel.fromJson(Map<String, dynamic> json) {
|
||||
// Convertir l'ID en int, qu'il soit déjà int ou string
|
||||
final dynamic rawId = json['id'];
|
||||
final int id = rawId is String ? int.parse(rawId) : rawId as int;
|
||||
|
||||
// Convertir les autres champs numériques
|
||||
final dynamic rawFkOperation = json['fk_operation'];
|
||||
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 dynamic rawFkUser = json['fk_user'];
|
||||
final int fkUser = rawFkUser is String ? int.parse(rawFkUser) : rawFkUser as int;
|
||||
|
||||
final dynamic rawFkType = json['fk_type'];
|
||||
final int fkType = rawFkType is String ? int.parse(rawFkType) : rawFkType as int;
|
||||
|
||||
final dynamic rawFkHabitat = json['fk_habitat'];
|
||||
final int fkHabitat = rawFkHabitat is String ? int.parse(rawFkHabitat) : rawFkHabitat as int;
|
||||
|
||||
final dynamic rawFkTypeReglement = json['fk_type_reglement'];
|
||||
final int fkTypeReglement = rawFkTypeReglement is String ? int.parse(rawFkTypeReglement) : rawFkTypeReglement as int;
|
||||
|
||||
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']);
|
||||
|
||||
return PassageModel(
|
||||
id: id,
|
||||
fkOperation: fkOperation,
|
||||
fkSector: fkSector,
|
||||
fkUser: fkUser,
|
||||
fkType: fkType,
|
||||
fkAdresse: json['fk_adresse'] as String,
|
||||
passedAt: passedAt,
|
||||
numero: json['numero'] as String,
|
||||
rue: json['rue'] as String,
|
||||
rueBis: json['rue_bis'] as String? ?? '',
|
||||
ville: json['ville'] as String,
|
||||
residence: json['residence'] as String? ?? '',
|
||||
fkHabitat: fkHabitat,
|
||||
appt: json['appt'] as String? ?? '',
|
||||
niveau: json['niveau'] as String? ?? '',
|
||||
gpsLat: json['gps_lat'] as String,
|
||||
gpsLng: json['gps_lng'] as String,
|
||||
nomRecu: json['nom_recu'] as String? ?? '',
|
||||
remarque: json['remarque'] as String? ?? '',
|
||||
montant: json['montant'] as String,
|
||||
fkTypeReglement: fkTypeReglement,
|
||||
emailErreur: json['email_erreur'] as String? ?? '',
|
||||
nbPassages: nbPassages,
|
||||
name: json['name'] as String,
|
||||
email: json['email'] as String? ?? '',
|
||||
phone: json['phone'] as String? ?? '',
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isActive: true,
|
||||
isSynced: true,
|
||||
);
|
||||
try {
|
||||
// Convertir l'ID en int, qu'il soit déjà int ou string
|
||||
final dynamic rawId = json['id'];
|
||||
final int id = rawId is String ? int.parse(rawId) : rawId as int;
|
||||
|
||||
// Convertir les autres champs numériques
|
||||
final dynamic rawFkOperation = json['fk_operation'];
|
||||
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 dynamic rawFkUser = json['fk_user'];
|
||||
final int fkUser = rawFkUser is String ? int.parse(rawFkUser) : rawFkUser as int;
|
||||
|
||||
final dynamic rawFkType = json['fk_type'];
|
||||
final int fkType = rawFkType is String ? int.parse(rawFkType) : rawFkType as int;
|
||||
|
||||
final dynamic rawFkHabitat = json['fk_habitat'];
|
||||
final int fkHabitat = rawFkHabitat is String ? int.parse(rawFkHabitat) : rawFkHabitat as int;
|
||||
|
||||
final dynamic rawFkTypeReglement = json['fk_type_reglement'];
|
||||
final int fkTypeReglement = rawFkTypeReglement is String ? int.parse(rawFkTypeReglement) : rawFkTypeReglement as int;
|
||||
|
||||
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']);
|
||||
|
||||
return PassageModel(
|
||||
id: id,
|
||||
fkOperation: fkOperation,
|
||||
fkSector: fkSector,
|
||||
fkUser: fkUser,
|
||||
fkType: fkType,
|
||||
fkAdresse: json['fk_adresse']?.toString() ?? '', // ← Gestion null
|
||||
passedAt: passedAt,
|
||||
numero: json['numero']?.toString() ?? '', // ← Gestion null
|
||||
rue: json['rue']?.toString() ?? '', // ← Gestion null
|
||||
rueBis: json['rue_bis']?.toString() ?? '',
|
||||
ville: json['ville']?.toString() ?? '', // ← Gestion null
|
||||
residence: json['residence']?.toString() ?? '',
|
||||
fkHabitat: fkHabitat,
|
||||
appt: json['appt']?.toString() ?? '',
|
||||
niveau: json['niveau']?.toString() ?? '',
|
||||
gpsLat: json['gps_lat']?.toString() ?? '', // ← Gestion null
|
||||
gpsLng: json['gps_lng']?.toString() ?? '', // ← Gestion null
|
||||
nomRecu: json['nom_recu']?.toString() ?? '', // ← Gestion null explicite
|
||||
remarque: json['remarque']?.toString() ?? '',
|
||||
montant: json['montant']?.toString() ?? '0.00', // ← Gestion null avec fallback
|
||||
fkTypeReglement: fkTypeReglement,
|
||||
emailErreur: json['email_erreur']?.toString() ?? '',
|
||||
nbPassages: nbPassages,
|
||||
name: json['name']?.toString() ?? '', // ← Gestion null
|
||||
email: json['email']?.toString() ?? '',
|
||||
phone: json['phone']?.toString() ?? '',
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isActive: true,
|
||||
isSynced: true,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur parsing PassageModel: $e');
|
||||
debugPrint('❌ Données JSON: $json');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir en JSON pour l'API
|
||||
|
||||
@@ -2,7 +2,11 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/data_loading_service.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
|
||||
class OperationRepository extends ChangeNotifier {
|
||||
@@ -13,6 +17,9 @@ class OperationRepository extends ChangeNotifier {
|
||||
return Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
}
|
||||
|
||||
// Getter exposant publiquement la Hive Box
|
||||
Box<OperationModel> get operationBox => _operationBox;
|
||||
|
||||
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
const boxName = AppKeys.operationsBoxName;
|
||||
@@ -97,10 +104,14 @@ class OperationRepository extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
debugPrint('📝 Traitement opération ID: $operationId, libelle: ${operationJson['libelle']}');
|
||||
|
||||
// Vérifier si l'opération existe déjà
|
||||
OperationModel? existingOperation = getOperationById(operationId);
|
||||
|
||||
@@ -108,20 +119,27 @@ class OperationRepository extends ChangeNotifier {
|
||||
// Créer une nouvelle opération
|
||||
final newOperation = OperationModel.fromJson(operationJson);
|
||||
await saveOperation(newOperation);
|
||||
debugPrint('✅ Nouvelle opération créée: ${newOperation.name}');
|
||||
} else {
|
||||
// Mettre à jour l'opération existante
|
||||
final updatedOperation = existingOperation.copyWith(
|
||||
name: operationJson['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",
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isSynced: true,
|
||||
);
|
||||
await saveOperation(updatedOperation);
|
||||
debugPrint('✅ Opération mise à jour: ${updatedOperation.name}');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('🎉 Traitement terminé - ${_operationBox.length} opérations dans la box');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des opérations: $e');
|
||||
debugPrint('❌ Erreur lors du traitement des opérations: $e');
|
||||
debugPrint('❌ Stack trace: ${StackTrace.current}');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
@@ -141,40 +159,116 @@ class OperationRepository extends ChangeNotifier {
|
||||
'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);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
// Récupérer l'ID de la nouvelle opération
|
||||
final operationId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
|
||||
debugPrint('✅ Opération créée avec succès');
|
||||
|
||||
// Créer l'opération localement
|
||||
final newOperation = OperationModel(
|
||||
id: operationId,
|
||||
name: name,
|
||||
dateDebut: dateDebut,
|
||||
dateFin: dateFin,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isActive: true,
|
||||
isSynced: true,
|
||||
);
|
||||
|
||||
await saveOperation(newOperation);
|
||||
// Traiter la réponse complète de l'API
|
||||
await _processCreationResponse(response.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
debugPrint('❌ Échec de la création - Code: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la création de l\'opération: $e');
|
||||
return false;
|
||||
debugPrint('❌ Erreur lors de la création de l\'opération: $e');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter la réponse complète après création d'opération
|
||||
Future<void> _processCreationResponse(Map<String, dynamic> responseData) async {
|
||||
try {
|
||||
debugPrint('🔄 Traitement de la réponse de création d\'opération');
|
||||
|
||||
// Traiter les opérations (groupe operations)
|
||||
if (responseData['operations'] != null) {
|
||||
await processOperationsFromApi(responseData['operations']);
|
||||
debugPrint('✅ Opérations traitées');
|
||||
}
|
||||
|
||||
// Traiter les secteurs (groupe secteurs) via DataLoadingService
|
||||
if (responseData['secteurs'] != null) {
|
||||
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']);
|
||||
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']);
|
||||
debugPrint('✅ Users_sectors traités');
|
||||
}
|
||||
|
||||
debugPrint('🎉 Tous les groupes de données ont été traités avec succès');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du traitement de la réponse: $e');
|
||||
// Ne pas faire échouer la création si le traitement des données supplémentaires échoue
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode universelle pour sauvegarder une opération (création ou mise à jour)
|
||||
Future<bool> saveOperationFromModel(OperationModel operation) async {
|
||||
debugPrint('=== saveOperationFromModel APPELÉ ===');
|
||||
debugPrint('operation.id: ${operation.id}');
|
||||
debugPrint('operation.name: ${operation.name}');
|
||||
|
||||
try {
|
||||
if (operation.id == 0) {
|
||||
// Nouvelle opération - créer
|
||||
debugPrint('=== CRÉATION (POST) ===');
|
||||
return await createOperation(
|
||||
operation.name,
|
||||
operation.dateDebut,
|
||||
operation.dateFin,
|
||||
);
|
||||
} else {
|
||||
// Opération existante - mettre à jour
|
||||
debugPrint('=== MISE À JOUR (PUT) ===');
|
||||
final result = await updateOperation(
|
||||
operation.id,
|
||||
name: operation.name,
|
||||
dateDebut: operation.dateDebut,
|
||||
dateFin: operation.dateFin,
|
||||
isActive: operation.isActive,
|
||||
fkEntite: operation.fkEntite, // ← Inclure fkEntite
|
||||
);
|
||||
debugPrint('=== RÉSULTAT UPDATE: $result ===');
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('=== ERREUR dans saveOperationFromModel: $e ===');
|
||||
// Propager l'exception pour que la page parente puisse la gérer
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour mettre à jour une opération avec un objet OperationModel
|
||||
Future<bool> updateOperationFromModel(OperationModel operation) async {
|
||||
return await updateOperation(
|
||||
operation.id,
|
||||
name: operation.name,
|
||||
dateDebut: operation.dateDebut,
|
||||
dateFin: operation.dateFin,
|
||||
isActive: operation.isActive,
|
||||
fkEntite: operation.fkEntite, // ← Inclure fkEntite
|
||||
);
|
||||
}
|
||||
|
||||
// Mettre à jour une opération
|
||||
Future<bool> updateOperation(int id, {String? name, DateTime? dateDebut, DateTime? dateFin, bool? isActive}) async {
|
||||
Future<bool> updateOperation(int id, {String? name, DateTime? dateDebut, DateTime? dateFin, bool? isActive, int? fkEntite}) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -182,7 +276,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
// Récupérer l'opération existante
|
||||
final existingOperation = getOperationById(id);
|
||||
if (existingOperation == null) {
|
||||
return false;
|
||||
debugPrint('❌ Opération avec l\'ID $id non trouvée');
|
||||
throw Exception('Opération non trouvée');
|
||||
}
|
||||
|
||||
// Préparer les données pour l'API
|
||||
@@ -191,59 +286,203 @@ class OperationRepository extends ChangeNotifier {
|
||||
'name': name ?? existingOperation.name,
|
||||
'date_deb': (dateDebut ?? existingOperation.dateDebut).toIso8601String().split('T')[0],
|
||||
'date_fin': (dateFin ?? existingOperation.dateFin).toIso8601String().split('T')[0],
|
||||
'is_active': isActive ?? existingOperation.isActive,
|
||||
'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);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('✅ Opération $id mise à jour avec succès');
|
||||
// Mettre à jour l'opération localement
|
||||
final updatedOperation = existingOperation.copyWith(
|
||||
name: name,
|
||||
dateDebut: dateDebut,
|
||||
dateFin: dateFin,
|
||||
isActive: isActive,
|
||||
fkEntite: fkEntite,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isSynced: true,
|
||||
);
|
||||
|
||||
await saveOperation(updatedOperation);
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('❌ Échec de la mise à jour - Code: ${response.statusCode}');
|
||||
throw Exception('Échec de la mise à jour de l\'opération');
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la mise à jour de l\'opération: $e');
|
||||
return false;
|
||||
debugPrint('❌ Erreur lors de la mise à jour de l\'opération: $e');
|
||||
// Propager l'exception pour qu'elle soit gérée par l'interface
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer une opération via l'API
|
||||
// Supprimer une opération inactive via l'API
|
||||
Future<bool> deleteOperationViaApi(int id) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Appeler l'API pour supprimer l'opération
|
||||
debugPrint('🗑️ Suppression opération inactive $id');
|
||||
|
||||
// Appeler l'API pour supprimer l'opération inactive
|
||||
final response = await ApiService.instance.delete('/operations/$id');
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
// Supprimer l'opération localement
|
||||
await deleteOperation(id);
|
||||
debugPrint('✅ Suppression réussie - Traitement de la réponse');
|
||||
|
||||
// Traiter la réponse qui contient les 3 dernières opérations
|
||||
if (response.data != null && response.data['operations'] != null) {
|
||||
// Vider la box des opérations
|
||||
await _operationBox.clear();
|
||||
|
||||
// Recharger les opérations depuis la réponse API
|
||||
await processOperationsFromApi(response.data['operations']);
|
||||
debugPrint('✅ Opérations rechargées après suppression');
|
||||
} else {
|
||||
// Fallback : supprimer localement seulement
|
||||
await deleteOperation(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
debugPrint('❌ Échec suppression - Code: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la suppression de l\'opération: $e');
|
||||
return false;
|
||||
debugPrint('❌ Erreur lors de la suppression de l\'opération: $e');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer une opération active via l'API (avec réactivation automatique)
|
||||
Future<bool> deleteActiveOperationViaApi(int id) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
debugPrint('🗑️ Suppression opération active $id');
|
||||
|
||||
// Appeler l'API pour supprimer l'opération active
|
||||
final response = await ApiService.instance.delete('/operations/$id');
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
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) {
|
||||
await _processActiveDeleteResponse(response.data);
|
||||
debugPrint('✅ Données rechargées après suppression opération active');
|
||||
} else {
|
||||
// Fallback : supprimer localement seulement
|
||||
await deleteOperation(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter la réponse complète après suppression d'opération active
|
||||
Future<void> _processActiveDeleteResponse(Map<String, dynamic> responseData) async {
|
||||
try {
|
||||
debugPrint('🔄 Traitement de la réponse de suppression d\'opération active');
|
||||
|
||||
// Vider toutes les Box concernées
|
||||
await _clearAllRelatedBoxes();
|
||||
|
||||
// Traiter les opérations (groupe operations)
|
||||
if (responseData['operations'] != null) {
|
||||
await processOperationsFromApi(responseData['operations']);
|
||||
debugPrint('✅ Opérations traitées');
|
||||
}
|
||||
|
||||
// Traiter les secteurs (groupe secteurs) via DataLoadingService
|
||||
if (responseData['secteurs'] != null) {
|
||||
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']);
|
||||
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']);
|
||||
debugPrint('✅ Users_sectors traités');
|
||||
}
|
||||
|
||||
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');
|
||||
// Ne pas faire échouer la suppression si le traitement des données supplémentaires échoue
|
||||
}
|
||||
}
|
||||
|
||||
// Vider toutes les Box liées lors de suppression d'opération active
|
||||
Future<void> _clearAllRelatedBoxes() async {
|
||||
try {
|
||||
// Vider les Box respectives avant rechargement complet
|
||||
await _operationBox.clear();
|
||||
|
||||
if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
|
||||
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
await sectorsBox.clear();
|
||||
}
|
||||
|
||||
if (Hive.isBoxOpen(AppKeys.passagesBoxName)) {
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
await passagesBox.clear();
|
||||
}
|
||||
|
||||
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
||||
final userSectorsBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
await userSectorsBox.clear();
|
||||
}
|
||||
|
||||
debugPrint('✅ Toutes les Box ont été vidées');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du vidage des Box: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Export Excel d'une opération
|
||||
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';
|
||||
|
||||
// Appeler l'API pour télécharger le fichier Excel
|
||||
await ApiService.instance.downloadOperationExcel(operationId, fileName);
|
||||
|
||||
debugPrint('✅ Export Excel terminé pour opération $operationId');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'export Excel: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
import 'package:geosector_app/core/services/data_loading_service.dart';
|
||||
import 'package:geosector_app/core/services/hive_service.dart';
|
||||
import 'package:geosector_app/core/services/hive_reset_state_service.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
@@ -280,7 +281,7 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnexion simplifiée avec DataLoadingService
|
||||
/// Déconnexion simplifiée avec HiveService
|
||||
Future<bool> logout(BuildContext context) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
@@ -301,8 +302,8 @@ class UserRepository extends ChangeNotifier {
|
||||
await CurrentUserService.instance.clearUser();
|
||||
await CurrentAmicaleService.instance.clearAmicale();
|
||||
|
||||
// Nettoyage complet via DataLoadingService
|
||||
await DataLoadingService.instance.cleanDataAfterLogout();
|
||||
// Nettoyage des données via HiveService (préserve les utilisateurs)
|
||||
await HiveService.instance.cleanDataOnLogout();
|
||||
|
||||
// Réinitialiser l'état de HiveResetStateService
|
||||
hiveResetStateService.reset();
|
||||
|
||||
@@ -307,6 +307,69 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Export Excel d'une opération
|
||||
Future<void> downloadOperationExcel(int operationId, String fileName) async {
|
||||
try {
|
||||
debugPrint('📊 Téléchargement Excel pour opération $operationId');
|
||||
|
||||
final response = await _dio.get(
|
||||
'/operations/$operationId/export/excel',
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes, // Important pour les fichiers binaires
|
||||
headers: {
|
||||
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('✅ Fichier Excel reçu (${response.data.length} bytes)');
|
||||
|
||||
if (kIsWeb) {
|
||||
// Pour le web : déclencher le téléchargement via le navigateur
|
||||
_downloadFileWeb(response.data, fileName);
|
||||
} else {
|
||||
// Pour mobile : sauvegarder dans le dossier de téléchargements
|
||||
await _downloadFileMobile(response.data, fileName);
|
||||
}
|
||||
|
||||
debugPrint('✅ Export Excel terminé: $fileName');
|
||||
} else {
|
||||
throw ApiException('Erreur lors du téléchargement: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de l\'export Excel', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
// Téléchargement pour le web
|
||||
void _downloadFileWeb(List<int> bytes, String fileName) {
|
||||
final blob = html.Blob([bytes]);
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
|
||||
final anchor = html.AnchorElement(href: url)
|
||||
..setAttribute('download', fileName)
|
||||
..click();
|
||||
|
||||
html.Url.revokeObjectUrl(url);
|
||||
debugPrint('🌐 Téléchargement web déclenché: $fileName');
|
||||
}
|
||||
|
||||
// Téléchargement pour mobile
|
||||
Future<void> _downloadFileMobile(List<int> bytes, String fileName) async {
|
||||
try {
|
||||
// Pour mobile, on pourrait utiliser path_provider pour obtenir le dossier de téléchargements
|
||||
// et file_picker ou similar pour sauvegarder le fichier
|
||||
// Pour l'instant, on lance juste une exception informative
|
||||
throw const ApiException('Téléchargement mobile non implémenté. Utilisez la version web.');
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode de nettoyage pour les tests
|
||||
static void reset() {
|
||||
_instance = null;
|
||||
|
||||
@@ -126,6 +126,28 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES PUBLIQUES POUR TRAITEMENT EXTERNE ===
|
||||
|
||||
/// Méthode publique pour traiter les secteurs depuis l'extérieur
|
||||
Future<void> processSectorsFromApi(dynamic sectorsData) async {
|
||||
await _processSectors(sectorsData);
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les passages depuis l'extérieur
|
||||
Future<void> processPassagesFromApi(dynamic passagesData) async {
|
||||
await _processPassages(passagesData);
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les associations user-sectors depuis l'extérieur
|
||||
Future<void> processUserSectorsFromApi(dynamic userSectorsData) async {
|
||||
await _processUserSectors(userSectorsData);
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les opérations depuis l'extérieur
|
||||
Future<void> processOperationsFromApi(dynamic operationsData) async {
|
||||
await _processOperations(operationsData);
|
||||
}
|
||||
|
||||
// === MÉTHODES DE TRAITEMENT DES DONNÉES ===
|
||||
|
||||
Future<void> _processClients(dynamic clientsData) async {
|
||||
@@ -251,17 +273,20 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _passageBox.clear();
|
||||
|
||||
int count = 0;
|
||||
int errorCount = 0;
|
||||
for (final passageData in passagesList) {
|
||||
try {
|
||||
final passage = PassageModel.fromJson(passageData);
|
||||
await _passageBox.put(passage.id, passage);
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement passage: $e');
|
||||
errorCount++;
|
||||
debugPrint('⚠️ Erreur traitement passage ${passageData['id']}: $e');
|
||||
// Continue avec le passage suivant au lieu de s'arrêter
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count passages stockés');
|
||||
debugPrint('✅ $count passages stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement passages: $e');
|
||||
}
|
||||
@@ -278,7 +303,6 @@ 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);
|
||||
@@ -316,17 +340,20 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _membreBox.clear();
|
||||
|
||||
int count = 0;
|
||||
int errorCount = 0;
|
||||
for (final membreData in membresList) {
|
||||
try {
|
||||
final membre = MembreModel.fromJson(membreData);
|
||||
await _membreBox.put(membre.id, membre);
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement membre: $e');
|
||||
errorCount++;
|
||||
debugPrint('⚠️ Erreur traitement membre ${membreData['id']}: $e');
|
||||
// Continue avec le membre suivant au lieu de s'arrêter
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count membres stockés');
|
||||
debugPrint('✅ $count membres stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement membres: $e');
|
||||
}
|
||||
|
||||
499
app/lib/core/services/hive_service.dart
Normal file
499
app/lib/core/services/hive_service.dart
Normal file
@@ -0,0 +1,499 @@
|
||||
import 'dart:io';
|
||||
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/hive_web_fix.dart';
|
||||
import 'package:geosector_app/core/services/hive_adapters.dart';
|
||||
|
||||
// Import de tous les modèles typés
|
||||
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';
|
||||
import 'package:geosector_app/core/data/models/operation_model.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/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/chat/models/conversation_model.dart';
|
||||
import 'package:geosector_app/chat/models/message_model.dart';
|
||||
|
||||
/// Service singleton centralisé pour la gestion complète des Box Hive
|
||||
/// Utilisé par main.dart pour l'initialisation et par logout pour le nettoyage
|
||||
class HiveService {
|
||||
static HiveService? _instance;
|
||||
static HiveService get instance => _instance ??= HiveService._internal();
|
||||
HiveService._internal();
|
||||
|
||||
/// Configuration des Box typées de l'application
|
||||
static const List<HiveBoxConfig> _boxConfigs = [
|
||||
HiveBoxConfig<UserModel>(AppKeys.userBoxName, 'UserModel'),
|
||||
HiveBoxConfig<AmicaleModel>(AppKeys.amicaleBoxName, 'AmicaleModel'),
|
||||
HiveBoxConfig<ClientModel>(AppKeys.clientsBoxName, 'ClientModel'),
|
||||
HiveBoxConfig<OperationModel>(AppKeys.operationsBoxName, 'OperationModel'),
|
||||
HiveBoxConfig<SectorModel>(AppKeys.sectorsBoxName, 'SectorModel'),
|
||||
HiveBoxConfig<PassageModel>(AppKeys.passagesBoxName, 'PassageModel'),
|
||||
HiveBoxConfig<MembreModel>(AppKeys.membresBoxName, 'MembreModel'),
|
||||
HiveBoxConfig<UserSectorModel>(AppKeys.userSectorBoxName, 'UserSectorModel'),
|
||||
HiveBoxConfig<ConversationModel>(AppKeys.chatConversationsBoxName, 'ConversationModel'),
|
||||
HiveBoxConfig<MessageModel>(AppKeys.chatMessagesBoxName, 'MessageModel'),
|
||||
HiveBoxConfig<dynamic>(AppKeys.settingsBoxName, 'Settings'),
|
||||
HiveBoxConfig<dynamic>(AppKeys.regionsBoxName, 'Regions'),
|
||||
];
|
||||
|
||||
bool _isInitialized = false;
|
||||
bool get isInitialized => _isInitialized;
|
||||
// === INITIALISATION COMPLÈTE (appelée par main.dart) ===
|
||||
|
||||
/// Initialisation complète de Hive avec réinitialisation totale
|
||||
Future<void> initializeAndResetHive() async {
|
||||
if (_isInitialized) {
|
||||
debugPrint('ℹ️ HiveService déjà initialisé');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('🔧 Initialisation complète de Hive avec reset...');
|
||||
|
||||
// 1. Initialisation de base de Hive
|
||||
await Hive.initFlutter();
|
||||
debugPrint('✅ Hive.initFlutter() terminé');
|
||||
|
||||
// 2. Enregistrement des adaptateurs
|
||||
_registerAdapters();
|
||||
|
||||
// 3. Destruction complète des anciennes données
|
||||
await _destroyAllData();
|
||||
|
||||
// 4. Création des Box vides et propres
|
||||
await _createAllBoxes();
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ HiveService initialisé avec succès');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'initialisation complète: $e');
|
||||
_isInitialized = false;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// === INITIALISATION SIMPLE (appelée par splash_page si besoin) ===
|
||||
|
||||
/// Initialisation simple sans reset (utilisée par splash_page si déjà initialisé)
|
||||
Future<void> ensureBoxesAreOpen() async {
|
||||
try {
|
||||
debugPrint('🔍 Vérification et ouverture des Box...');
|
||||
|
||||
// Vérifier si toutes les Box sont ouvertes
|
||||
bool allOpen = true;
|
||||
for (final config in _boxConfigs) {
|
||||
if (!Hive.isBoxOpen(config.name)) {
|
||||
allOpen = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allOpen) {
|
||||
debugPrint('✅ Toutes les Box sont déjà ouvertes');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ouvrir les Box manquantes
|
||||
await _createAllBoxes();
|
||||
debugPrint('✅ Box manquantes ouvertes');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la vérification des Box: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// === NETTOYAGE LOGOUT (appelé lors du logout) ===
|
||||
|
||||
/// Nettoyage sélectif lors du logout (préserve les utilisateurs)
|
||||
Future<void> cleanDataOnLogout() async {
|
||||
try {
|
||||
debugPrint('🧹 Nettoyage des données au logout...');
|
||||
|
||||
// Nettoyer toutes les Box sauf les utilisateurs
|
||||
for (final config in _boxConfigs) {
|
||||
if (config.name != AppKeys.userBoxName) {
|
||||
await _clearSingleBox(config.name);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Nettoyage logout terminé (utilisateurs préservés)');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du nettoyage logout: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES PRIVÉES D'INITIALISATION ===
|
||||
|
||||
/// Enregistrement de tous les adaptateurs Hive
|
||||
void _registerAdapters() {
|
||||
try {
|
||||
if (!Hive.isAdapterRegistered(0)) {
|
||||
// Utiliser HiveAdapters existant pour enregistrer tous les adaptateurs
|
||||
HiveAdapters.registerAll();
|
||||
debugPrint('🔌 Adaptateurs Hive enregistrés via HiveAdapters');
|
||||
} else {
|
||||
debugPrint('ℹ️ Adaptateurs déjà enregistrés');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur enregistrement adaptateurs: $e');
|
||||
// Ne pas faire échouer l'initialisation
|
||||
}
|
||||
}
|
||||
|
||||
/// Destruction complète de toutes les données selon la plateforme
|
||||
Future<void> _destroyAllData() async {
|
||||
try {
|
||||
debugPrint('💥 Destruction complète des données Hive...');
|
||||
|
||||
// 1. Fermer toutes les Box ouvertes
|
||||
await _closeAllOpenBoxes();
|
||||
|
||||
// 2. Suppression selon la plateforme
|
||||
if (kIsWeb) {
|
||||
await _destroyDataWeb();
|
||||
} else if (Platform.isIOS) {
|
||||
await _destroyDataIOS();
|
||||
} else if (Platform.isAndroid) {
|
||||
await _destroyDataAndroid();
|
||||
} else {
|
||||
await _destroyDataDesktop();
|
||||
}
|
||||
|
||||
// 3. Attendre pour s'assurer que tout est détruit
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
debugPrint('✅ Destruction complète terminée');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur destruction: $e');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
}
|
||||
|
||||
/// Fermeture de toutes les Box ouvertes
|
||||
Future<void> _closeAllOpenBoxes() async {
|
||||
try {
|
||||
debugPrint('🔒 Fermeture de toutes les Box ouvertes...');
|
||||
|
||||
// Fermer les Box configurées
|
||||
for (final config in _boxConfigs) {
|
||||
try {
|
||||
if (Hive.isBoxOpen(config.name)) {
|
||||
await Hive.box(config.name).close();
|
||||
debugPrint('🔒 Box ${config.name} fermée');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur fermeture ${config.name}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer aussi les Box potentiellement orphelines
|
||||
final orphanBoxes = ['auth', 'temp', 'cache', 'locations', 'messages'];
|
||||
for (final boxName in orphanBoxes) {
|
||||
try {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
await Hive.box(boxName).close();
|
||||
debugPrint('🔒 Box orpheline $boxName fermée');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur fermeture orpheline $boxName: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur fermeture générale: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Destruction des données sur Web
|
||||
Future<void> _destroyDataWeb() async {
|
||||
try {
|
||||
debugPrint('🌐 Destruction Web...');
|
||||
|
||||
// Sur Web, utiliser le HiveWebFix si disponible
|
||||
try {
|
||||
await HiveWebFix.resetHiveCompletely();
|
||||
debugPrint('✅ Destruction Web via HiveWebFix');
|
||||
return;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ HiveWebFix échoué, fallback...');
|
||||
}
|
||||
|
||||
// Fallback : supprimer Box par Box
|
||||
for (final config in _boxConfigs) {
|
||||
try {
|
||||
await Hive.deleteBoxFromDisk(config.name);
|
||||
debugPrint('🗑️ Box Web ${config.name} supprimée');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur suppression Web ${config.name}: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur destruction Web: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Destruction des données sur iOS
|
||||
Future<void> _destroyDataIOS() async {
|
||||
try {
|
||||
debugPrint('🍎 Destruction iOS...');
|
||||
|
||||
// Méthode 1: Destruction totale
|
||||
try {
|
||||
await Hive.deleteFromDisk();
|
||||
debugPrint('✅ Destruction iOS complète');
|
||||
return;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Destruction iOS totale échouée, méthode alternative...');
|
||||
}
|
||||
|
||||
// Méthode 2: Suppression des fichiers manuellement
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final hiveDir = Directory('${appDir.path}/hive');
|
||||
|
||||
if (await hiveDir.exists()) {
|
||||
await hiveDir.delete(recursive: true);
|
||||
debugPrint('✅ Dossier Hive iOS supprimé');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Suppression dossier iOS échouée: $e');
|
||||
}
|
||||
|
||||
// Méthode 3: Fallback Box par Box
|
||||
await _fallbackDeleteBoxes();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur destruction iOS: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Destruction des données sur Android
|
||||
Future<void> _destroyDataAndroid() async {
|
||||
try {
|
||||
debugPrint('🤖 Destruction Android...');
|
||||
|
||||
// Méthode 1: Destruction totale
|
||||
try {
|
||||
await Hive.deleteFromDisk();
|
||||
debugPrint('✅ Destruction Android complète');
|
||||
return;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Destruction Android totale échouée, méthode alternative...');
|
||||
}
|
||||
|
||||
// Méthode 2: Suppression des fichiers .hive et .lock
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final files = await appDir.list().toList();
|
||||
|
||||
int deletedCount = 0;
|
||||
for (final file in files) {
|
||||
final fileName = file.path.split('/').last;
|
||||
if (fileName.endsWith('.hive') || fileName.endsWith('.lock')) {
|
||||
try {
|
||||
await file.delete();
|
||||
deletedCount++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur suppression fichier $fileName: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $deletedCount fichiers Android supprimés');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Suppression fichiers Android échouée: $e');
|
||||
}
|
||||
|
||||
// Méthode 3: Fallback Box par Box
|
||||
await _fallbackDeleteBoxes();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur destruction Android: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Destruction des données sur Desktop
|
||||
Future<void> _destroyDataDesktop() async {
|
||||
try {
|
||||
debugPrint('🖥️ Destruction Desktop...');
|
||||
|
||||
// Destruction totale
|
||||
await Hive.deleteFromDisk();
|
||||
debugPrint('✅ Destruction Desktop complète');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Destruction Desktop échouée, fallback...');
|
||||
await _fallbackDeleteBoxes();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback : suppression Box par Box
|
||||
Future<void> _fallbackDeleteBoxes() async {
|
||||
debugPrint('🔄 Fallback: suppression Box par Box...');
|
||||
|
||||
for (final config in _boxConfigs) {
|
||||
try {
|
||||
await Hive.deleteBoxFromDisk(config.name);
|
||||
debugPrint('🗑️ Box fallback ${config.name} supprimée');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur suppression fallback ${config.name}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Création de toutes les Box vides et propres
|
||||
Future<void> _createAllBoxes() async {
|
||||
try {
|
||||
debugPrint('🆕 Création de toutes les Box...');
|
||||
|
||||
for (int i = 0; i < _boxConfigs.length; i++) {
|
||||
final config = _boxConfigs[i];
|
||||
|
||||
try {
|
||||
await _createSingleBox(config);
|
||||
debugPrint('✅ Box ${config.name} créée (${i + 1}/${_boxConfigs.length})');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création ${config.name}: $e');
|
||||
// Continuer même en cas d'erreur
|
||||
}
|
||||
|
||||
// Petite pause entre les créations
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
debugPrint('✅ Toutes les Box ont été créées');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création des Box: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Création d'une Box individuelle avec le bon type
|
||||
Future<void> _createSingleBox(HiveBoxConfig config) async {
|
||||
try {
|
||||
if (Hive.isBoxOpen(config.name)) {
|
||||
debugPrint('ℹ️ Box ${config.name} déjà ouverte');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case 'UserModel':
|
||||
await Hive.openBox<UserModel>(config.name);
|
||||
break;
|
||||
case 'AmicaleModel':
|
||||
await Hive.openBox<AmicaleModel>(config.name);
|
||||
break;
|
||||
case 'ClientModel':
|
||||
await Hive.openBox<ClientModel>(config.name);
|
||||
break;
|
||||
case 'OperationModel':
|
||||
await Hive.openBox<OperationModel>(config.name);
|
||||
break;
|
||||
case 'SectorModel':
|
||||
await Hive.openBox<SectorModel>(config.name);
|
||||
break;
|
||||
case 'PassageModel':
|
||||
await Hive.openBox<PassageModel>(config.name);
|
||||
break;
|
||||
case 'MembreModel':
|
||||
await Hive.openBox<MembreModel>(config.name);
|
||||
break;
|
||||
case 'UserSectorModel':
|
||||
await Hive.openBox<UserSectorModel>(config.name);
|
||||
break;
|
||||
case 'ConversationModel':
|
||||
await Hive.openBox<ConversationModel>(config.name);
|
||||
break;
|
||||
case 'MessageModel':
|
||||
await Hive.openBox<MessageModel>(config.name);
|
||||
break;
|
||||
default:
|
||||
// Pour Settings, Regions, etc.
|
||||
await Hive.openBox(config.name);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création spécifique ${config.name}: $e');
|
||||
// Fallback : essayer sans type
|
||||
try {
|
||||
await Hive.openBox(config.name);
|
||||
debugPrint('⚠️ Box ${config.name} créée sans type');
|
||||
} catch (e2) {
|
||||
debugPrint('❌ Échec total ${config.name}: $e2');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
/// Vider une Box individuelle
|
||||
Future<void> _clearSingleBox(String boxName) async {
|
||||
try {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
await Hive.box(boxName).clear();
|
||||
debugPrint('🧹 Box $boxName vidée');
|
||||
} else {
|
||||
debugPrint('ℹ️ Box $boxName n\'est pas ouverte, impossible de la vider');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur vidage $boxName: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérification que toutes les Box sont ouvertes
|
||||
bool areAllBoxesOpen() {
|
||||
for (final config in _boxConfigs) {
|
||||
if (!Hive.isBoxOpen(config.name)) {
|
||||
debugPrint('❌ Box ${config.name} n\'est pas ouverte');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
debugPrint('✅ Toutes les Box sont ouvertes');
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Récupération sécurisée d'une Box typée
|
||||
Box<T> getTypedBox<T>(String boxName) {
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
throw Exception('La Box $boxName n\'est pas ouverte');
|
||||
}
|
||||
return Hive.box<T>(boxName);
|
||||
}
|
||||
|
||||
/// Récupération sécurisée d'une Box non-typée
|
||||
Box getBox(String boxName) {
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
throw Exception('La Box $boxName n\'est pas ouverte');
|
||||
}
|
||||
return Hive.box(boxName);
|
||||
}
|
||||
|
||||
/// Liste des noms de toutes les Box configurées
|
||||
List<String> getAllBoxNames() {
|
||||
return _boxConfigs.map((config) => config.name).toList();
|
||||
}
|
||||
|
||||
/// Diagnostic complet de l'état des Box
|
||||
Map<String, bool> getDiagnostic() {
|
||||
final diagnostic = <String, bool>{};
|
||||
for (final config in _boxConfigs) {
|
||||
diagnostic[config.name] = Hive.isBoxOpen(config.name);
|
||||
}
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
/// Reset complet du service (pour tests)
|
||||
static void reset() {
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration d'une Box Hive avec type
|
||||
class HiveBoxConfig<T> {
|
||||
final String name;
|
||||
final String type;
|
||||
|
||||
const HiveBoxConfig(this.name, this.type);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/services/hive_adapters.dart';
|
||||
|
||||
void main() async {
|
||||
// IMPORTANT: Configurer l'URL strategy pour éviter les # dans les URLs
|
||||
@@ -17,7 +16,7 @@ void main() async {
|
||||
// Initialiser les services essentiels
|
||||
await _initializeServices();
|
||||
|
||||
// Initialiser Hive avec gestion des erreurs
|
||||
// Initialiser Hive de façon minimale (le traitement lourd se fait dans splash)
|
||||
await _initializeHive();
|
||||
|
||||
// Configurer l'orientation de l'application (mobile uniquement)
|
||||
@@ -52,14 +51,15 @@ Future<void> _initializeServices() async {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise Hive de façon minimale (le traitement lourd se fait dans splash_page)
|
||||
Future<void> _initializeHive() async {
|
||||
try {
|
||||
debugPrint('🔧 Initialisation minimale de Hive...');
|
||||
|
||||
// SEULEMENT l'initialisation de base - pas d'adaptateurs, pas de Box
|
||||
await Hive.initFlutter();
|
||||
|
||||
// Enregistrer tous les adapters
|
||||
HiveAdapters.registerAll();
|
||||
|
||||
debugPrint('✅ Hive et TypeAdapters initialisés');
|
||||
debugPrint('✅ Hive initialisé (traitement lourd dans splash_page)');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur Hive: $e');
|
||||
rethrow;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
@@ -16,29 +15,6 @@ import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
||||
|
||||
/// Class pour dessiner les petits points blancs sur le fond
|
||||
class DotsPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.white.withOpacity(0.5)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final random = math.Random(42); // Seed fixe pour consistance
|
||||
final numberOfDots = (size.width * size.height) ~/ 1500;
|
||||
|
||||
for (int i = 0; i < numberOfDots; i++) {
|
||||
final x = random.nextDouble() * size.width;
|
||||
final y = random.nextDouble() * size.height;
|
||||
final radius = 1.0 + random.nextDouble() * 2.0;
|
||||
canvas.drawCircle(Offset(x, y), radius, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// Page d'administration de l'amicale et des membres
|
||||
/// Cette page est intégrée dans le tableau de bord administrateur
|
||||
class AdminAmicalePage extends StatefulWidget {
|
||||
@@ -571,128 +547,165 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Colors.red.shade300],
|
||||
return SafeArea(
|
||||
child:
|
||||
// Contenu principal
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Mon amicale et ses membres',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
// Contenu principal
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Mon amicale et ses membres',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Message d'erreur si présent
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Message d'erreur si présent
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
// Contenu principal avec ValueListenableBuilder
|
||||
if (_currentUser != null && _currentUser!.fkEntite != null)
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<AmicaleModel>>(
|
||||
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
builder: (context, amicalesBox, child) {
|
||||
debugPrint('🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint('🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint('🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
|
||||
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
|
||||
debugPrint('🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
|
||||
if (amicale == null) {
|
||||
// Ajouter plus d'informations de debug
|
||||
debugPrint('❌ PROBLÈME: Amicale non trouvée');
|
||||
debugPrint('❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint('❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary.withOpacity(0.7),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Amicale non trouvée',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'L\'amicale associée à votre compte n\'existe plus.\nfkEntite: ${_currentUser!.fkEntite}',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Contenu principal avec ValueListenableBuilder
|
||||
if (_currentUser != null && _currentUser!.fkEntite != null)
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<AmicaleModel>>(
|
||||
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
builder: (context, amicalesBox, child) {
|
||||
debugPrint('🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint('🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint('🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
return ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: widget.membreRepository.getMembresBox().listenable(),
|
||||
builder: (context, membresBox, child) {
|
||||
// Filtrer les membres par amicale
|
||||
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
|
||||
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
|
||||
|
||||
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
|
||||
debugPrint('🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
|
||||
if (amicale == null) {
|
||||
// Ajouter plus d'informations de debug
|
||||
debugPrint('❌ PROBLÈME: Amicale non trouvée');
|
||||
debugPrint('❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint('❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary.withOpacity(0.7),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section Amicale
|
||||
Text(
|
||||
'Informations de l\'amicale',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Amicale non trouvée',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'L\'amicale associée à votre compte n\'existe plus.\nfkEntite: ${_currentUser!.fkEntite}',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
return ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: widget.membreRepository.getMembresBox().listenable(),
|
||||
builder: (context, membresBox, child) {
|
||||
// Filtrer les membres par amicale
|
||||
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
|
||||
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
|
||||
// Tableau Amicale
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: AmicaleTableWidget(
|
||||
amicales: [amicale],
|
||||
onEdit: null,
|
||||
onDelete: null,
|
||||
amicaleRepository: widget.amicaleRepository,
|
||||
userRepository: widget.userRepository,
|
||||
apiService: ApiService.instance,
|
||||
showActionsColumn: false,
|
||||
),
|
||||
),
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section Amicale
|
||||
Text(
|
||||
'Informations de l\'amicale',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section Membres
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Membres de l\'amicale (${membres.length})',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _handleAddMembre,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter un membre'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tableau Amicale
|
||||
Container(
|
||||
// Tableau Membres
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -704,84 +717,32 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: AmicaleTableWidget(
|
||||
amicales: [amicale],
|
||||
onEdit: null,
|
||||
onDelete: null,
|
||||
amicaleRepository: widget.amicaleRepository,
|
||||
userRepository: widget.userRepository,
|
||||
apiService: ApiService.instance,
|
||||
showActionsColumn: false,
|
||||
child: MembreTableWidget(
|
||||
membres: membres,
|
||||
onEdit: _handleEditMembre,
|
||||
onDelete: _handleDeleteMembre,
|
||||
membreRepository: widget.membreRepository,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section Membres
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Membres de l\'amicale (${membres.length})',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _handleAddMembre,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter un membre'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tableau Membres
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: MembreTableWidget(
|
||||
membres: membres,
|
||||
onEdit: _handleEditMembre,
|
||||
onDelete: _handleDeleteMembre,
|
||||
membreRepository: widget.membreRepository,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Message si pas d'utilisateur connecté
|
||||
if (_currentUser == null)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
// Message si pas d'utilisateur connecté
|
||||
if (_currentUser == null)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,8 @@ import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'dart:math' as math;
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart';
|
||||
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
|
||||
@@ -35,7 +31,7 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminDashboardHomePage extends StatefulWidget {
|
||||
const AdminDashboardHomePage({Key? key}) : super(key: key);
|
||||
const AdminDashboardHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminDashboardHomePage> createState() => _AdminDashboardHomePageState();
|
||||
@@ -54,127 +50,10 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
List<PaymentData> paymentData = [];
|
||||
Map<int, int> passagesByType = {};
|
||||
|
||||
// Future pour initialiser les boîtes Hive
|
||||
late Future<void> _initFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialiser les boîtes Hive avant de charger les données
|
||||
_initFuture = _initHiveBoxes().then((_) {
|
||||
// Charger les données une fois les boîtes initialisées
|
||||
_loadDashboardData();
|
||||
|
||||
// Après l'affichage des logs "VERIFICATION FINALE DES DONNEES",
|
||||
// attendre un court délai puis rafraîchir automatiquement les données
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = true; // Afficher le spinner pendant le rafraîchissement
|
||||
});
|
||||
_loadDashboardData(); // Rafraîchir les données
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Méthode pour initialiser les boîtes Hive nécessaires
|
||||
Future<void> _initHiveBoxes() async {
|
||||
try {
|
||||
debugPrint('AdminDashboardHomePage: Initialisation des boîtes Hive...');
|
||||
|
||||
// Liste des boîtes à ouvrir
|
||||
final boxesToOpen = [
|
||||
{
|
||||
'name': AppKeys.operationsBoxName,
|
||||
'type': 'OperationModel',
|
||||
'opened': false
|
||||
},
|
||||
{
|
||||
'name': AppKeys.passagesBoxName,
|
||||
'type': 'PassageModel',
|
||||
'opened': false
|
||||
},
|
||||
{
|
||||
'name': AppKeys.sectorsBoxName,
|
||||
'type': 'SectorModel',
|
||||
'opened': false
|
||||
},
|
||||
];
|
||||
|
||||
// Ouvrir chaque boîte
|
||||
for (final boxInfo in boxesToOpen) {
|
||||
final boxName = boxInfo['name'] as String;
|
||||
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Ouverture de la boîte $boxName...');
|
||||
try {
|
||||
switch (boxInfo['type']) {
|
||||
case 'OperationModel':
|
||||
await Hive.openBox<OperationModel>(boxName);
|
||||
break;
|
||||
case 'PassageModel':
|
||||
await Hive.openBox<PassageModel>(boxName);
|
||||
break;
|
||||
case 'SectorModel':
|
||||
await Hive.openBox<SectorModel>(boxName);
|
||||
break;
|
||||
}
|
||||
boxInfo['opened'] = true;
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Boîte $boxName ouverte avec succès');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte $boxName: $boxError');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
} else {
|
||||
boxInfo['opened'] = true;
|
||||
debugPrint('AdminDashboardHomePage: Boîte $boxName déjà ouverte');
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si toutes les boîtes ont été ouvertes
|
||||
final allBoxesOpened = boxesToOpen.every((box) => box['opened'] == true);
|
||||
|
||||
if (allBoxesOpened) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Toutes les boîtes Hive ont été ouvertes avec succès');
|
||||
} else {
|
||||
// Identifier les boîtes qui n'ont pas pu être ouvertes
|
||||
final failedBoxes = boxesToOpen
|
||||
.where((box) => box['opened'] == false)
|
||||
.map((box) => box['name'])
|
||||
.join(', ');
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Certaines boîtes n\'ont pas pu être ouvertes: $failedBoxes');
|
||||
}
|
||||
|
||||
// Afficher le nombre d'éléments dans chaque boîte
|
||||
debugPrint('VERIFICATION FINALE DES DONNEES');
|
||||
if (Hive.isBoxOpen(AppKeys.operationsBoxName)) {
|
||||
final operationsBox =
|
||||
Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
debugPrint('Nombre d\'opérations: ${operationsBox.length}');
|
||||
}
|
||||
if (Hive.isBoxOpen(AppKeys.passagesBoxName)) {
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
debugPrint('Nombre de passages: ${passagesBox.length}');
|
||||
}
|
||||
if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
|
||||
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
debugPrint('Nombre de secteurs: ${sectorsBox.length}');
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Initialisation des boîtes Hive terminée');
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Erreur lors de l\'initialisation des boîtes Hive: $e');
|
||||
// Ne pas propager l'erreur, mais retourner normalement
|
||||
// pour éviter que le FutureBuilder ne reste bloqué en état d'erreur
|
||||
}
|
||||
_loadDashboardData();
|
||||
}
|
||||
|
||||
/// Prépare les données pour le graphique de paiement
|
||||
@@ -192,9 +71,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
|
||||
// Calculer les montants par type de règlement
|
||||
for (final passage in passages) {
|
||||
if (passage.fkTypeReglement != null &&
|
||||
passage.montant != null &&
|
||||
passage.montant.isNotEmpty) {
|
||||
if (passage.fkTypeReglement != null && passage.montant != null && passage.montant.isNotEmpty) {
|
||||
final typeId = passage.fkTypeReglement;
|
||||
final amount = double.tryParse(passage.montant) ?? 0.0;
|
||||
paymentAmounts[typeId] = (paymentAmounts[typeId] ?? 0.0) + amount;
|
||||
@@ -224,61 +101,25 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Chargement des données du tableau de bord...');
|
||||
debugPrint('AdminDashboardHomePage: Chargement des données du tableau de bord...');
|
||||
// Utiliser les instances globales définies dans app.dart
|
||||
// Pas besoin de Provider.of car les instances sont déjà disponibles
|
||||
|
||||
// S'assurer que la boîte des opérations est ouverte avant d'y accéder
|
||||
OperationModel? currentOperation;
|
||||
try {
|
||||
// Vérifier si la boîte Hive est ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Ouverture de la boîte operations dans _loadDashboardData...');
|
||||
try {
|
||||
await Hive.openBox<OperationModel>(AppKeys.operationsBoxName);
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Boîte operations ouverte avec succès dans _loadDashboardData');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte operations dans _loadDashboardData: $boxError');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer l'opération en cours
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Récupération de l\'opération en cours...');
|
||||
currentOperation = userRepository.getCurrentOperation();
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Erreur lors de la récupération de l\'opération: $boxError');
|
||||
// Afficher un message d'erreur ou gérer l'erreur de manière appropriée
|
||||
}
|
||||
// Récupérer l'opération en cours (les boxes sont déjà ouvertes par SplashPage)
|
||||
final currentOperation = userRepository.getCurrentOperation();
|
||||
debugPrint('AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}');
|
||||
|
||||
if (currentOperation != null) {
|
||||
// Charger les passages pour l'opération en cours
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Chargement des passages pour l\'opération ${currentOperation.id}...');
|
||||
final passages =
|
||||
passageRepository.getPassagesByOperation(currentOperation.id);
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: ${passages.length} passages récupérés');
|
||||
debugPrint('AdminDashboardHomePage: Chargement des passages pour l\'opération ${currentOperation.id}...');
|
||||
final passages = passageRepository.getPassagesByOperation(currentOperation.id);
|
||||
debugPrint('AdminDashboardHomePage: ${passages.length} passages récupérés');
|
||||
|
||||
// Calculer le nombre total de passages
|
||||
totalPassages = passages.length;
|
||||
|
||||
// Calculer le montant total collecté
|
||||
totalAmounts = passages.fold(
|
||||
0.0,
|
||||
(sum, passage) =>
|
||||
sum +
|
||||
(passage.montant.isNotEmpty
|
||||
? double.tryParse(passage.montant) ?? 0.0
|
||||
: 0.0));
|
||||
totalAmounts = passages.fold(0.0, (sum, passage) => sum + (passage.montant.isNotEmpty ? double.tryParse(passage.montant) ?? 0.0 : 0.0));
|
||||
|
||||
// Préparer les données pour le graphique de paiement
|
||||
_preparePaymentData(passages);
|
||||
@@ -295,8 +136,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
passagesByType.forEach((typeId, count) {
|
||||
final typeInfo = AppKeys.typesPassages[typeId];
|
||||
final typeName = typeInfo != null ? typeInfo['titre'] : 'Inconnu';
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Type $typeId ($typeName): $count passages');
|
||||
debugPrint('AdminDashboardHomePage: Type $typeId ($typeName): $count passages');
|
||||
});
|
||||
|
||||
// Charger les statistiques par membre
|
||||
@@ -305,8 +145,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
|
||||
// Compter les passages par membre
|
||||
for (final passage in passages) {
|
||||
memberCounts[passage.fkUser] =
|
||||
(memberCounts[passage.fkUser] ?? 0) + 1;
|
||||
memberCounts[passage.fkUser] = (memberCounts[passage.fkUser] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Récupérer les informations des membres
|
||||
@@ -321,11 +160,9 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
}
|
||||
|
||||
// Trier les membres par nombre de passages (décroissant)
|
||||
memberStats
|
||||
.sort((a, b) => (b['count'] as int).compareTo(a['count'] as int));
|
||||
memberStats.sort((a, b) => (b['count'] as int).compareTo(a['count'] as int));
|
||||
} else {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Aucune opération en cours, impossible de charger les passages');
|
||||
debugPrint('AdminDashboardHomePage: Aucune opération en cours, impossible de charger les passages');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
@@ -340,8 +177,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Données chargées: isDataLoaded=$isDataLoaded, totalPassages=$totalPassages, passagesByType=${passagesByType.length} types');
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'AdminDashboardHomePage: Erreur lors du chargement des données: $e');
|
||||
debugPrint('AdminDashboardHomePage: Erreur lors du chargement des données: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
@@ -353,289 +189,230 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('Building AdminDashboardHomePage');
|
||||
return FutureBuilder<void>(
|
||||
future: _initFuture,
|
||||
builder: (context, snapshot) {
|
||||
// Afficher un indicateur de chargement pendant l'initialisation des boîtes Hive
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
debugPrint('FutureBuilder: ConnectionState.waiting');
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
// Même si nous avons une erreur, nous continuons à afficher le contenu
|
||||
// car nous avons modifié _initHiveBoxes pour ne pas propager les erreurs
|
||||
if (snapshot.hasError) {
|
||||
debugPrint('FutureBuilder: hasError - ${snapshot.error}');
|
||||
// Nous affichons un message d'erreur mais continuons à afficher le contenu
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Erreur lors de l\'initialisation: ${snapshot.error}'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 5),
|
||||
action: SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_initFuture = _initHiveBoxes().then((_) {
|
||||
_loadDashboardData();
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
debugPrint('FutureBuilder: Initialisation réussie');
|
||||
}
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
// L'initialisation a réussi, afficher le contenu
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
// Utiliser l'instance globale définie dans app.dart
|
||||
// Récupérer l'opération en cours (les boîtes sont déjà ouvertes par SplashPage)
|
||||
final currentOperation = userRepository.getCurrentOperation();
|
||||
|
||||
// Récupérer l'opération en cours (les boîtes sont déjà ouvertes)
|
||||
final currentOperation = userRepository.getCurrentOperation();
|
||||
// Titre dynamique avec l'ID et le nom de l'opération
|
||||
final String title = currentOperation != null ? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}' : 'Synthèse de l\'opération';
|
||||
|
||||
// Titre dynamique avec l'ID et le nom de l'opération
|
||||
final String title = currentOperation != null
|
||||
? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}'
|
||||
: 'Synthèse de l\'opération';
|
||||
|
||||
return Stack(children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Colors.red.shade300],
|
||||
),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
return Stack(children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Colors.red.shade300],
|
||||
),
|
||||
// Contenu de la page
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingL),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
// Contenu de la page
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingL),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre avec bouton de rafraîchissement sur la même ligne
|
||||
Row(
|
||||
children: [
|
||||
// Titre avec bouton de rafraîchissement sur la même ligne
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style:
|
||||
Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bouton de rafraîchissement
|
||||
if (!isLoading)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Rafraîchir les données',
|
||||
onPressed: _loadDashboardData,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
// Afficher un indicateur de chargement si les données ne sont pas encore chargées
|
||||
if (isLoading && !isDataLoaded)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
// Bouton de rafraîchissement
|
||||
if (!isLoading)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Rafraîchir les données',
|
||||
onPressed: _loadDashboardData,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
// Afficher un indicateur de chargement si les données ne sont pas encore chargées
|
||||
if (isLoading && !isDataLoaded)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
|
||||
// Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour
|
||||
if (isDataLoaded || isLoading) ...[
|
||||
// Cartes de synthèse
|
||||
isDesktop
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildSummaryCard(
|
||||
context,
|
||||
'Passages totaux',
|
||||
totalPassages.toString(),
|
||||
Icons.map_outlined,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildSummaryCard(
|
||||
context,
|
||||
'Montant collecté',
|
||||
'${totalAmounts.toStringAsFixed(2)} €',
|
||||
Icons.euro_outlined,
|
||||
AppTheme.buttonSuccessColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: SectorDistributionCard(
|
||||
key: ValueKey(
|
||||
'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
'Passages totaux',
|
||||
totalPassages.toString(),
|
||||
Icons.map_outlined,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
'Montant collecté',
|
||||
'${totalAmounts.toStringAsFixed(2)} €',
|
||||
Icons.euro_outlined,
|
||||
AppTheme.buttonSuccessColor,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
SectorDistributionCard(
|
||||
key: ValueKey(
|
||||
'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 200,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Graphique d'activité
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: ActivityChart(
|
||||
key: ValueKey(
|
||||
'activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 350,
|
||||
showAllPassages:
|
||||
true, // Tous les passages, pas seulement ceux de l'utilisateur courant
|
||||
title: 'Passages réalisés par jour (15 derniers jours)',
|
||||
daysToShow: 15,
|
||||
),
|
||||
// Si vous avez besoin de passer l'ID de l'opération en cours, décommentez les lignes suivantes
|
||||
// child: ActivityChart(
|
||||
// height: 350,
|
||||
// loadFromHive: true,
|
||||
// showAllPassages: true,
|
||||
// title: 'Passages réalisés par jour (15 derniers jours)',
|
||||
// daysToShow: 15,
|
||||
// operationId: userRepository.getCurrentOperation()?.id,
|
||||
// ),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Graphiques de répartition
|
||||
isDesktop
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPassageTypeCard(context),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
child: _buildPaymentTypeCard(context),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
_buildPassageTypeCard(context),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
_buildPaymentTypeCard(context),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Actions rapides - uniquement visible sur le web
|
||||
if (kIsWeb) ...[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
padding: const EdgeInsets.all(AppTheme.spacingM),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Actions sur cette opération',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
// Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour
|
||||
if (isDataLoaded || isLoading) ...[
|
||||
// Cartes de synthèse
|
||||
isDesktop
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildSummaryCard(
|
||||
context,
|
||||
'Passages totaux',
|
||||
totalPassages.toString(),
|
||||
Icons.map_outlined,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
Wrap(
|
||||
spacing: AppTheme.spacingM,
|
||||
runSpacing: AppTheme.spacingM,
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context,
|
||||
'Exporter les données',
|
||||
Icons.file_download_outlined,
|
||||
AppTheme.primaryColor,
|
||||
() {},
|
||||
),
|
||||
_buildActionButton(
|
||||
context,
|
||||
'Gérer les secteurs',
|
||||
Icons.map_outlined,
|
||||
AppTheme.accentColor,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildSummaryCard(
|
||||
context,
|
||||
'Montant collecté',
|
||||
'${totalAmounts.toStringAsFixed(2)} €',
|
||||
Icons.euro_outlined,
|
||||
AppTheme.buttonSuccessColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: SectorDistributionCard(
|
||||
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
'Passages totaux',
|
||||
totalPassages.toString(),
|
||||
Icons.map_outlined,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
'Montant collecté',
|
||||
'${totalAmounts.toStringAsFixed(2)} €',
|
||||
Icons.euro_outlined,
|
||||
AppTheme.buttonSuccessColor,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
SectorDistributionCard(
|
||||
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 200,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Graphique d'activité
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: ActivityChart(
|
||||
key: ValueKey('activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||
height: 350,
|
||||
showAllPassages: true, // Tous les passages, pas seulement ceux de l'utilisateur courant
|
||||
title: 'Passages réalisés par jour (15 derniers jours)',
|
||||
daysToShow: 15,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Graphiques de répartition
|
||||
isDesktop
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPassageTypeCard(context),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
child: _buildPaymentTypeCard(context),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
_buildPassageTypeCard(context),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
_buildPaymentTypeCard(context),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Actions rapides - uniquement visible sur le web
|
||||
if (kIsWeb) ...[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
padding: const EdgeInsets.all(AppTheme.spacingM),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Actions sur cette opération',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
Wrap(
|
||||
spacing: AppTheme.spacingM,
|
||||
runSpacing: AppTheme.spacingM,
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context,
|
||||
'Exporter les données',
|
||||
Icons.file_download_outlined,
|
||||
AppTheme.primaryColor,
|
||||
() {},
|
||||
),
|
||||
_buildActionButton(
|
||||
context,
|
||||
'Gérer les secteurs',
|
||||
Icons.map_outlined,
|
||||
AppTheme.accentColor,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
]);
|
||||
},
|
||||
);
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(
|
||||
@@ -661,8 +438,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'admin_history_page.dart';
|
||||
import 'admin_communication_page.dart';
|
||||
import 'admin_map_page.dart';
|
||||
import 'admin_amicale_page.dart';
|
||||
import 'admin_operations_page.dart';
|
||||
|
||||
/// Class pour dessiner les petits points blancs sur le fond
|
||||
class DotsPainter extends CustomPainter {
|
||||
@@ -126,7 +127,10 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
operationRepository: operationRepository,
|
||||
);
|
||||
case _PageType.operations:
|
||||
return const Scaffold(body: Center(child: Text('Page Opérations')));
|
||||
return AdminOperationsPage(
|
||||
operationRepository: operationRepository,
|
||||
userRepository: userRepository,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
817
app/lib/presentation/admin/admin_operations_page.dart
Normal file
817
app/lib/presentation/admin/admin_operations_page.dart
Normal file
@@ -0,0 +1,817 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/presentation/widgets/operation_form_dialog.dart';
|
||||
|
||||
/// Page d'administration des opérations annuelles
|
||||
/// Cette page est intégrée dans le tableau de bord administrateur
|
||||
/// FOND TRANSPARENT - le fond dégradé est géré par AdminDashboardPage
|
||||
class AdminOperationsPage extends StatefulWidget {
|
||||
final OperationRepository operationRepository;
|
||||
final UserRepository userRepository;
|
||||
|
||||
const AdminOperationsPage({
|
||||
super.key,
|
||||
required this.operationRepository,
|
||||
required this.userRepository,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdminOperationsPage> createState() => _AdminOperationsPageState();
|
||||
}
|
||||
|
||||
class _AdminOperationsPageState extends State<AdminOperationsPage> {
|
||||
late int? _userAmicaleId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_userAmicaleId = widget.userRepository.getCurrentUser()?.fkEntite;
|
||||
debugPrint('🔧 AdminOperationsPage initialisée - UserAmicaleId: $_userAmicaleId');
|
||||
}
|
||||
|
||||
void _showCreateOperationDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => OperationFormDialog(
|
||||
title: 'Créer une nouvelle opération',
|
||||
operationRepository: widget.operationRepository,
|
||||
userRepository: widget.userRepository,
|
||||
onSuccess: () {
|
||||
// Simple callback pour rafraîchir l'interface
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditOperationDialog(OperationModel op) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => OperationFormDialog(
|
||||
title: op.isActive ? 'Modifier l\'opération active : ${op.name}' : 'Modifier l\'opération : ${op.name}',
|
||||
operation: op,
|
||||
operationRepository: widget.operationRepository,
|
||||
userRepository: widget.userRepository,
|
||||
onSuccess: () {
|
||||
// Simple callback pour rafraîchir l'interface
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Récupère les passages réalisés (fkType != 2) pour une opération
|
||||
int _getCompletedPassagesCount(int operationId) {
|
||||
try {
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
final completedPassages = passagesBox.values.where((passage) => passage.fkOperation == operationId && passage.fkType != 2).length;
|
||||
debugPrint('🔍 Passages réalisés pour opération $operationId: $completedPassages');
|
||||
return completedPassages;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du comptage des passages: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDelete(OperationModel op, List<OperationModel> operations) async {
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
if (currentUser == null) {
|
||||
ApiException.showError(context, Exception("Utilisateur non connecté"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'il reste au moins une opération
|
||||
if (operations.length <= 1) {
|
||||
ApiException.showError(context, Exception("Impossible de supprimer la dernière opération"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Cas 1: Opération inactive - Suppression simple pour role > 1
|
||||
if (!op.isActive && currentUser.role > 1) {
|
||||
final confirmed = await _showSimpleDeleteDialog(op);
|
||||
if (confirmed == true) {
|
||||
await _performSimpleDelete(op);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Cas 2: Opération active avec role = 2 - Vérification des passages
|
||||
if (op.isActive && currentUser.role == 2) {
|
||||
final completedPassagesCount = _getCompletedPassagesCount(op.id);
|
||||
|
||||
if (completedPassagesCount > 0) {
|
||||
// Il y a des passages réalisés - Dialog d'avertissement avec confirmation par nom
|
||||
final confirmed = await _showActiveDeleteWithPassagesDialog(op, completedPassagesCount);
|
||||
if (confirmed == true) {
|
||||
await _performActiveDelete(op);
|
||||
}
|
||||
} else {
|
||||
// Pas de passages réalisés - Suppression simple
|
||||
final confirmed = await _showActiveDeleteDialog(op);
|
||||
if (confirmed == true) {
|
||||
await _performActiveDelete(op);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Cas 3: Role > 2 - Suppression autorisée sans restrictions
|
||||
if (currentUser.role > 2) {
|
||||
final confirmed = await _showSimpleDeleteDialog(op);
|
||||
if (confirmed == true) {
|
||||
if (op.isActive) {
|
||||
await _performActiveDelete(op);
|
||||
} else {
|
||||
await _performSimpleDelete(op);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Cas par défaut - Pas d'autorisation
|
||||
ApiException.showError(context, Exception("Vous n'avez pas les droits pour supprimer cette opération"));
|
||||
}
|
||||
|
||||
/// Dialog simple pour suppression d'opération inactive
|
||||
Future<bool?> _showSimpleDeleteDialog(OperationModel op) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text("Confirmer la suppression"),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Voulez-vous supprimer l'opération \"${op.name}\" ?"),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"Cette action est définitive.",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text("Annuler"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Supprimer"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dialog pour suppression d'opération active sans passages
|
||||
Future<bool?> _showActiveDeleteDialog(OperationModel op) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text("Supprimer l'opération active"),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Voulez-vous supprimer l'opération active \"${op.name}\" ?"),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info, color: Colors.blue, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Votre dernière opération inactive sera automatiquement réactivée.",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text("Annuler"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Supprimer"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dialog pour suppression d'opération active avec passages réalisés
|
||||
Future<bool?> _showActiveDeleteWithPassagesDialog(OperationModel op, int passagesCount) {
|
||||
final TextEditingController nameController = TextEditingController();
|
||||
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text("ATTENTION - Passages réalisés"),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"$passagesCount passage(s) réalisé(s) trouvé(s)",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"La suppression de cette opération active supprimera définitivement tous les passages réalisés !",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info, color: Colors.blue, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Votre dernière opération inactive sera automatiquement réactivée.",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Pour confirmer, saisissez le nom exact de l'opération :",
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: op.name,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text("Annuler"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: nameController.text.trim() == op.name.trim() ? () => Navigator.of(dialogContext).pop(true) : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Supprimer définitivement"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Suppression simple d'opération inactive
|
||||
Future<void> _performSimpleDelete(OperationModel op) async {
|
||||
try {
|
||||
final success = await widget.operationRepository.deleteOperationViaApi(op.id);
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(context, "Opération supprimée avec succès");
|
||||
setState(() {});
|
||||
} else {
|
||||
throw Exception("Erreur lors de la suppression");
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Suppression d'opération active (avec réactivation automatique)
|
||||
Future<void> _performActiveDelete(OperationModel op) async {
|
||||
try {
|
||||
final success = await widget.operationRepository.deleteActiveOperationViaApi(op.id);
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(context, "Opération active supprimée avec succès. L'opération précédente a été réactivée.");
|
||||
setState(() {});
|
||||
} else {
|
||||
throw Exception("Erreur lors de la suppression");
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleExport(OperationModel operation) async {
|
||||
try {
|
||||
// Afficher un indicateur de chargement
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text("Export Excel de l'opération \"${operation.name}\" en cours..."),
|
||||
],
|
||||
),
|
||||
duration: const Duration(seconds: 10), // Plus long pour le téléchargement
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
|
||||
// Appeler l'export via le repository
|
||||
await widget.operationRepository.exportOperationToExcel(operation.id, operation.name);
|
||||
|
||||
// Masquer le SnackBar de chargement et afficher le succès
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ApiException.showSuccess(context, "Export Excel de l'opération \"${operation.name}\" terminé avec succès !");
|
||||
}
|
||||
} catch (e) {
|
||||
// Masquer le SnackBar de chargement et afficher l'erreur
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||
}
|
||||
|
||||
Widget _buildOperationsTable(List<OperationModel> operations) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// En-tête du tableau
|
||||
_buildTableHeader(theme),
|
||||
|
||||
// Corps du tableau
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(8),
|
||||
bottomRight: Radius.circular(8),
|
||||
),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: operations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final operation = operations[index];
|
||||
return _buildOperationRow(operation, index % 2 == 1, theme, operations);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableHeader(ThemeData theme) {
|
||||
final textStyle = theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Colonne ID
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('ID', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
// Colonne Nom
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Nom de l\'opération', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
// Colonne Date début
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Date début', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
// Colonne Date fin
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Date fin', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
// Colonne Statut
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Statut', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
// Colonne Actions
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Actions', style: textStyle, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOperationRow(OperationModel operation, bool isAlternate, ThemeData theme, List<OperationModel> allOperations) {
|
||||
final textStyle = theme.textTheme.bodyMedium;
|
||||
final backgroundColor = isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface;
|
||||
final canDelete = allOperations.length > 1; // Peut supprimer seulement s'il y a plus d'une opération
|
||||
|
||||
return InkWell(
|
||||
onTap: operation.isActive ? () => _showEditOperationDialog(operation) : null,
|
||||
hoverColor: operation.isActive ? theme.colorScheme.primary.withOpacity(0.05) : null,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Colonne ID
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
operation.id.toString(),
|
||||
style: textStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Colonne Nom
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (operation.isActive) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary.withOpacity(0.6),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
operation.name,
|
||||
style: textStyle?.copyWith(
|
||||
color: operation.isActive ? theme.colorScheme.primary : textStyle.color,
|
||||
fontWeight: operation.isActive ? FontWeight.w600 : textStyle.fontWeight,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Colonne Date début
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
_formatDate(operation.dateDebut),
|
||||
style: textStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Colonne Date fin
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
_formatDate(operation.dateFin),
|
||||
style: textStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Colonne Statut
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: operation.isActive ? Colors.green : Colors.red,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Text(
|
||||
operation.isActive ? 'Active' : 'Inactive',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Colonne Actions
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
// Bouton Delete - Affiché seulement s'il y a plus d'une opération
|
||||
if (canDelete)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_forever,
|
||||
color: theme.colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: () => _handleDelete(operation, allOperations),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
// Bouton Export
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.download,
|
||||
color: theme.colorScheme.secondary,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Exporter',
|
||||
onPressed: () => _handleExport(operation),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
debugPrint('🎨 AdminOperationsPage.build() appelée');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Gestion des opérations annuelles',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Contenu principal avec ValueListenableBuilder
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<OperationModel>>(
|
||||
valueListenable: widget.operationRepository.operationBox.listenable(),
|
||||
builder: (context, operationBox, child) {
|
||||
debugPrint('🔄 ValueListenableBuilder - Nombre d\'opérations: ${operationBox.length}');
|
||||
|
||||
// Filtrer et trier les opérations
|
||||
final allOperations = operationBox.values.toList();
|
||||
allOperations.sort((a, b) => b.id.compareTo(a.id));
|
||||
final operations = allOperations.take(10).toList(); // Limiter à 10 opérations récentes
|
||||
|
||||
debugPrint('📊 Opérations affichées: ${operations.length}');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec bouton d'ajout
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Opérations récentes (${operations.length})',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showCreateOperationDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nouvelle opération'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tableau des opérations
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: operations.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Aucune opération créée",
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Cliquez sur 'Nouvelle opération' pour commencer",
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _buildOperationsTable(operations),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.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';
|
||||
import 'package:geosector_app/core/data/models/operation_model.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/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/chat/models/conversation_model.dart';
|
||||
import 'package:geosector_app/chat/models/message_model.dart';
|
||||
import 'package:geosector_app/core/services/hive_service.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
@@ -108,337 +97,66 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
void _startInitialization() async {
|
||||
// Table rase complète et recréation propre
|
||||
await _completeReset();
|
||||
|
||||
// Finalisation
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Application prête !";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// RESET COMPLET : Destruction totale et recréation propre
|
||||
Future<void> _completeReset() async {
|
||||
try {
|
||||
debugPrint('🧹 RESET COMPLET : Destruction totale des données Hive...');
|
||||
|
||||
// Étape 1: Sauvegarder les utilisateurs existants (optionnel)
|
||||
Map<dynamic, UserModel>? existingUsers;
|
||||
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
|
||||
|
||||
// Étape 1: Initialisation complète de Hive avec HiveService
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Sauvegarde des utilisateurs...";
|
||||
_progress = 0.05;
|
||||
_statusMessage = "Initialisation de la base de données...";
|
||||
_progress = 0.1;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (Hive.isBoxOpen(AppKeys.userBoxName)) {
|
||||
final userBox = Hive.box<UserModel>(AppKeys.userBoxName);
|
||||
existingUsers = Map.from(userBox.toMap());
|
||||
debugPrint('📦 ${existingUsers.length} utilisateurs sauvegardés');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur sauvegarde utilisateurs: $e');
|
||||
existingUsers = null;
|
||||
}
|
||||
|
||||
// Étape 2: DESTRUCTION RADICALE - Fermer tout ce qui peut être ouvert
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Fermeture de toutes les bases de données...";
|
||||
_progress = 0.15;
|
||||
});
|
||||
}
|
||||
|
||||
await _closeAllKnownBoxes();
|
||||
|
||||
// Étape 3: DESTRUCTION RADICALE - Supprimer tout Hive du disque
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Suppression complète des anciennes données...";
|
||||
_progress = 0.25;
|
||||
});
|
||||
}
|
||||
|
||||
await _nukeHiveCompletely();
|
||||
|
||||
// Étape 4: RECRÉATION PROPRE
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Création des nouvelles bases de données...";
|
||||
_progress = 0.40;
|
||||
});
|
||||
}
|
||||
|
||||
await _createAllBoxesFresh();
|
||||
|
||||
// Étape 5: Restaurer les utilisateurs (optionnel)
|
||||
if (existingUsers != null && existingUsers.isNotEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Restauration des utilisateurs...";
|
||||
_progress = 0.80;
|
||||
});
|
||||
}
|
||||
|
||||
await _restoreUsers(existingUsers);
|
||||
}
|
||||
|
||||
// Étape 6: Vérification finale
|
||||
// HiveService fait TOUT le travail lourd (adaptateurs, destruction, recréation)
|
||||
await HiveService.instance.initializeAndResetHive();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Vérification des bases de données...";
|
||||
_progress = 0.90;
|
||||
_progress = 0.7;
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('✅ RESET COMPLET terminé avec succès');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du reset complet: $e');
|
||||
// Étape 2: S'assurer que toutes les Box sont ouvertes
|
||||
await HiveService.instance.ensureBoxesAreOpen();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Erreur critique - Redémarrage recommandé";
|
||||
_statusMessage = "Finalisation...";
|
||||
_progress = 0.9;
|
||||
});
|
||||
}
|
||||
|
||||
// Étape 3: Vérification finale
|
||||
final allBoxesOpen = HiveService.instance.areAllBoxesOpen();
|
||||
if (!allBoxesOpen) {
|
||||
final diagnostic = HiveService.instance.getDiagnostic();
|
||||
debugPrint('❌ Diagnostic des Box: $diagnostic');
|
||||
throw Exception('Certaines bases de données ne sont pas accessibles');
|
||||
}
|
||||
|
||||
// Finalisation
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Application prête !";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ferme toutes les boîtes connues
|
||||
Future<void> _closeAllKnownBoxes() async {
|
||||
try {
|
||||
final allKnownBoxes = [
|
||||
AppKeys.userBoxName,
|
||||
AppKeys.amicaleBoxName,
|
||||
AppKeys.clientsBoxName,
|
||||
AppKeys.regionsBoxName,
|
||||
AppKeys.operationsBoxName,
|
||||
AppKeys.sectorsBoxName,
|
||||
AppKeys.passagesBoxName,
|
||||
AppKeys.membresBoxName,
|
||||
AppKeys.userSectorBoxName,
|
||||
AppKeys.settingsBoxName,
|
||||
AppKeys.chatConversationsBoxName,
|
||||
AppKeys.chatMessagesBoxName,
|
||||
// Boîtes potentiellement problématiques
|
||||
'auth', 'locations', 'messages', 'temp'
|
||||
];
|
||||
|
||||
debugPrint('🔒 Fermeture de ${allKnownBoxes.length} boîtes connues...');
|
||||
|
||||
for (final boxName in allKnownBoxes) {
|
||||
try {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
await Hive.box(boxName).close();
|
||||
debugPrint('✅ Boîte $boxName fermée');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur fermeture $boxName: $e');
|
||||
// Continuer même en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
debugPrint('✅ Initialisation complète de l\'application terminée avec succès');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur fermeture des boîtes: $e');
|
||||
}
|
||||
}
|
||||
debugPrint('❌ Erreur lors de l\'initialisation: $e');
|
||||
|
||||
/// Suppression RADICALE de tout Hive
|
||||
Future<void> _nukeHiveCompletely() async {
|
||||
try {
|
||||
debugPrint('💥 DESTRUCTION NUCLÉAIRE de Hive...');
|
||||
|
||||
if (kIsWeb) {
|
||||
// En version web, supprimer toutes les boîtes possibles une par une
|
||||
final allPossibleBoxes = [
|
||||
AppKeys.userBoxName,
|
||||
AppKeys.amicaleBoxName,
|
||||
AppKeys.clientsBoxName,
|
||||
AppKeys.regionsBoxName,
|
||||
AppKeys.operationsBoxName,
|
||||
AppKeys.sectorsBoxName,
|
||||
AppKeys.passagesBoxName,
|
||||
AppKeys.membresBoxName,
|
||||
AppKeys.userSectorBoxName,
|
||||
AppKeys.settingsBoxName,
|
||||
AppKeys.chatConversationsBoxName,
|
||||
AppKeys.chatMessagesBoxName,
|
||||
// Toutes les boîtes potentiellement corrompues
|
||||
'auth', 'locations', 'messages', 'temp', 'cache', 'data'
|
||||
];
|
||||
|
||||
for (final boxName in allPossibleBoxes) {
|
||||
try {
|
||||
await Hive.deleteBoxFromDisk(boxName);
|
||||
debugPrint('✅ Boîte $boxName DÉTRUITE');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur destruction $boxName: $e');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Sur mobile/desktop, destruction totale
|
||||
try {
|
||||
await Hive.deleteFromDisk();
|
||||
debugPrint('✅ Hive COMPLÈTEMENT DÉTRUIT');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur destruction totale: $e');
|
||||
// Fallback : supprimer boîte par boîte
|
||||
await _deleteBoxesOneByOne();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Erreur d'initialisation - Redémarrage recommandé";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Attendre pour s'assurer que tout est détruit
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur destruction Hive: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback : supprimer les boîtes une par une
|
||||
Future<void> _deleteBoxesOneByOne() async {
|
||||
final allBoxes = [
|
||||
AppKeys.userBoxName,
|
||||
AppKeys.amicaleBoxName,
|
||||
AppKeys.clientsBoxName,
|
||||
AppKeys.regionsBoxName,
|
||||
AppKeys.operationsBoxName,
|
||||
AppKeys.sectorsBoxName,
|
||||
AppKeys.passagesBoxName,
|
||||
AppKeys.membresBoxName,
|
||||
AppKeys.userSectorBoxName,
|
||||
AppKeys.settingsBoxName,
|
||||
AppKeys.chatConversationsBoxName,
|
||||
AppKeys.chatMessagesBoxName,
|
||||
];
|
||||
|
||||
for (final boxName in allBoxes) {
|
||||
try {
|
||||
await Hive.deleteBoxFromDisk(boxName);
|
||||
debugPrint('✅ Boîte $boxName supprimée (fallback)');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur suppression fallback $boxName: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recrée toutes les boîtes VIDES et PROPRES
|
||||
Future<void> _createAllBoxesFresh() async {
|
||||
try {
|
||||
debugPrint('🆕 Création de toutes les boîtes vides...');
|
||||
|
||||
final boxesToCreate = [
|
||||
{'name': AppKeys.userBoxName, 'type': 'UserModel'},
|
||||
{'name': AppKeys.amicaleBoxName, 'type': 'AmicaleModel'},
|
||||
{'name': AppKeys.clientsBoxName, 'type': 'ClientModel'},
|
||||
{'name': AppKeys.regionsBoxName, 'type': 'dynamic'},
|
||||
{'name': AppKeys.operationsBoxName, 'type': 'OperationModel'},
|
||||
{'name': AppKeys.sectorsBoxName, 'type': 'SectorModel'},
|
||||
{'name': AppKeys.passagesBoxName, 'type': 'PassageModel'},
|
||||
{'name': AppKeys.membresBoxName, 'type': 'MembreModel'},
|
||||
{'name': AppKeys.userSectorBoxName, 'type': 'UserSectorModel'},
|
||||
{'name': AppKeys.settingsBoxName, 'type': 'dynamic'},
|
||||
{'name': AppKeys.chatConversationsBoxName, 'type': 'ConversationModel'},
|
||||
{'name': AppKeys.chatMessagesBoxName, 'type': 'MessageModel'},
|
||||
];
|
||||
|
||||
final progressIncrement = 0.35 / boxesToCreate.length; // De 0.40 à 0.75
|
||||
|
||||
for (int i = 0; i < boxesToCreate.length; i++) {
|
||||
final boxInfo = boxesToCreate[i];
|
||||
final boxName = boxInfo['name'] as String;
|
||||
final boxType = boxInfo['type'] as String;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Création de $boxName...";
|
||||
_progress = 0.40 + (progressIncrement * i);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Créer la boîte avec le bon type
|
||||
switch (boxType) {
|
||||
case 'UserModel':
|
||||
await Hive.openBox<UserModel>(boxName);
|
||||
break;
|
||||
case 'AmicaleModel':
|
||||
await Hive.openBox<AmicaleModel>(boxName);
|
||||
break;
|
||||
case 'ClientModel':
|
||||
await Hive.openBox<ClientModel>(boxName);
|
||||
break;
|
||||
case 'OperationModel':
|
||||
await Hive.openBox<OperationModel>(boxName);
|
||||
break;
|
||||
case 'SectorModel':
|
||||
await Hive.openBox<SectorModel>(boxName);
|
||||
break;
|
||||
case 'PassageModel':
|
||||
await Hive.openBox<PassageModel>(boxName);
|
||||
break;
|
||||
case 'MembreModel':
|
||||
await Hive.openBox<MembreModel>(boxName);
|
||||
break;
|
||||
case 'UserSectorModel':
|
||||
await Hive.openBox<UserSectorModel>(boxName);
|
||||
break;
|
||||
case 'ConversationModel':
|
||||
await Hive.openBox<ConversationModel>(boxName);
|
||||
break;
|
||||
case 'MessageModel':
|
||||
await Hive.openBox<MessageModel>(boxName);
|
||||
break;
|
||||
default:
|
||||
await Hive.openBox(boxName);
|
||||
}
|
||||
|
||||
debugPrint('✅ Boîte $boxName créée (type: $boxType)');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création $boxName: $e');
|
||||
// En cas d'erreur, essayer sans type
|
||||
try {
|
||||
await Hive.openBox(boxName);
|
||||
debugPrint('⚠️ Boîte $boxName créée sans type');
|
||||
} catch (e2) {
|
||||
debugPrint('❌ Échec total création $boxName: $e2');
|
||||
}
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création des boîtes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Restaure les utilisateurs sauvegardés
|
||||
Future<void> _restoreUsers(Map<dynamic, UserModel> users) async {
|
||||
try {
|
||||
if (Hive.isBoxOpen(AppKeys.userBoxName)) {
|
||||
final userBox = Hive.box<UserModel>(AppKeys.userBoxName);
|
||||
|
||||
for (final entry in users.entries) {
|
||||
try {
|
||||
await userBox.put(entry.key, entry.value);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur restauration utilisateur ${entry.key}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ ${users.length} utilisateurs restaurés');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur restauration utilisateurs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,51 +2,47 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CustomTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final TextEditingController? controller;
|
||||
final String label;
|
||||
final String? hintText;
|
||||
final String? helperText;
|
||||
final IconData? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final bool obscureText;
|
||||
final TextInputType keyboardType;
|
||||
final String? Function(String?)? validator;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final int? maxLines;
|
||||
final int? minLines;
|
||||
final bool readOnly;
|
||||
final VoidCallback? onTap;
|
||||
final Function(String)? onChanged;
|
||||
final bool isRequired;
|
||||
final bool autofocus;
|
||||
final FocusNode? focusNode;
|
||||
final String? errorText;
|
||||
final Color? fillColor;
|
||||
final String? helperText;
|
||||
final String? Function(String?)? validator;
|
||||
final VoidCallback? onTap;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final int? maxLines;
|
||||
final int? maxLength;
|
||||
final bool obscureText;
|
||||
final Function(String)? onChanged;
|
||||
final Function(String)? onFieldSubmitted;
|
||||
final bool isRequired;
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.controller,
|
||||
required this.label,
|
||||
this.hintText,
|
||||
this.helperText,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.validator,
|
||||
this.inputFormatters,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.readOnly = false,
|
||||
this.onTap,
|
||||
this.onChanged,
|
||||
this.isRequired = false,
|
||||
this.autofocus = false,
|
||||
this.focusNode,
|
||||
this.errorText,
|
||||
this.fillColor,
|
||||
this.helperText,
|
||||
this.validator,
|
||||
this.onTap,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.maxLines = 1,
|
||||
this.maxLength,
|
||||
this.obscureText = false,
|
||||
this.onChanged,
|
||||
this.onFieldSubmitted,
|
||||
this.isRequired = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -56,124 +52,105 @@ class CustomTextField extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label avec indicateur de champ requis
|
||||
if (label.isNotEmpty) ...[
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onBackground,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isRequired) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
// Ajouter un Container avec une ombre pour créer un effet d'élévation
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
readOnly: readOnly,
|
||||
onTap: onTap,
|
||||
onChanged: onChanged,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onBackground.withOpacity(0.5),
|
||||
),
|
||||
errorText: errorText,
|
||||
helperText: helperText,
|
||||
helperStyle: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onBackground.withOpacity(0.6),
|
||||
),
|
||||
prefixIcon: prefixIcon != null
|
||||
? Icon(prefixIcon, color: theme.colorScheme.primary)
|
||||
: null,
|
||||
suffixIcon: suffixIcon,
|
||||
// Couleur de fond différente selon l'état (lecture seule ou éditable)
|
||||
fillColor: fillColor ??
|
||||
(readOnly
|
||||
? const Color(
|
||||
0xFFF8F9FA) // Gris plus clair pour readOnly
|
||||
: const Color(
|
||||
0xFFECEFF1)), // Gris plus foncé pour éditable
|
||||
filled: true,
|
||||
// Ajouter une élévation avec une petite ombre
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
// Ajouter une ombre pour créer un effet d'élévation
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
gapPadding: 0,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
|
||||
// Champ de texte
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
readOnly: readOnly,
|
||||
autofocus: autofocus,
|
||||
onTap: onTap,
|
||||
validator: validator,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLines: maxLines,
|
||||
maxLength: maxLength,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
// Point rouge en haut à droite pour indiquer que le champ est obligatoire
|
||||
if (isRequired)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
margin: const EdgeInsets.only(top: 8, right: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: readOnly ? theme.colorScheme.surfaceContainerHighest.withOpacity(0.3) : theme.colorScheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
buildCounter: maxLength != null
|
||||
? (context, {required currentLength, required isFocused, maxLength}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'$currentLength/${maxLength ?? 0}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: currentLength > (maxLength ?? 0) * 0.8 ? theme.colorScheme.error : theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
491
app/lib/presentation/widgets/operation_form_dialog.dart
Normal file
491
app/lib/presentation/widgets/operation_form_dialog.dart
Normal file
@@ -0,0 +1,491 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
||||
|
||||
class OperationFormDialog extends StatefulWidget {
|
||||
final OperationModel? operation;
|
||||
final String title;
|
||||
final bool readOnly;
|
||||
final OperationRepository operationRepository;
|
||||
final UserRepository userRepository;
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const OperationFormDialog({
|
||||
super.key,
|
||||
this.operation,
|
||||
required this.title,
|
||||
this.readOnly = false,
|
||||
required this.operationRepository,
|
||||
required this.userRepository,
|
||||
this.onSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OperationFormDialog> createState() => _OperationFormDialogState();
|
||||
}
|
||||
|
||||
class _OperationFormDialogState extends State<OperationFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isSubmitting = false;
|
||||
// Controllers
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _dateDebutController;
|
||||
late final TextEditingController _dateFinController;
|
||||
|
||||
// Form values
|
||||
DateTime? _dateDebut;
|
||||
DateTime? _dateFin;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize controllers with operation data if available
|
||||
final operation = widget.operation;
|
||||
_nameController = TextEditingController(text: operation?.name ?? '');
|
||||
|
||||
_dateDebut = operation?.dateDebut;
|
||||
_dateFin = operation?.dateFin;
|
||||
|
||||
_dateDebutController = TextEditingController(
|
||||
text: _dateDebut != null ? DateFormat('dd/MM/yyyy').format(_dateDebut!) : '',
|
||||
);
|
||||
|
||||
_dateFinController = TextEditingController(
|
||||
text: _dateFin != null ? DateFormat('dd/MM/yyyy').format(_dateFin!) : '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_dateDebutController.dispose();
|
||||
_dateFinController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Méthode pour sélectionner une date
|
||||
void _selectDate(BuildContext context, bool isDateDebut) {
|
||||
try {
|
||||
final DateTime initialDate;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
if (isDateDebut) {
|
||||
// Pour la date de début
|
||||
initialDate = _dateDebut ?? DateTime.now();
|
||||
firstDate = DateTime(DateTime.now().year - 2);
|
||||
lastDate = _dateFin ?? DateTime(DateTime.now().year + 5);
|
||||
} else {
|
||||
// Pour la date de fin
|
||||
initialDate = _dateFin ?? (_dateDebut ?? DateTime.now());
|
||||
firstDate = _dateDebut ?? DateTime(DateTime.now().year - 2);
|
||||
lastDate = DateTime(DateTime.now().year + 5);
|
||||
}
|
||||
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: initialDate,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
).then((DateTime? picked) {
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
if (isDateDebut) {
|
||||
_dateDebut = picked;
|
||||
_dateDebutController.text = DateFormat('dd/MM/yyyy').format(picked);
|
||||
|
||||
// Si la date de fin est antérieure à la nouvelle date de début, la réinitialiser
|
||||
if (_dateFin != null && _dateFin!.isBefore(picked)) {
|
||||
_dateFin = null;
|
||||
_dateFinController.clear();
|
||||
}
|
||||
} else {
|
||||
_dateFin = picked;
|
||||
_dateFinController.text = DateFormat('dd/MM/yyyy').format(picked);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Exception lors de l\'affichage du sélecteur de date: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Impossible d\'afficher le sélecteur de date'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
debugPrint('=== _handleSubmit APPELÉ ===');
|
||||
if (_isSubmitting) {
|
||||
debugPrint('=== ARRÊT: En cours de soumission ===');
|
||||
return;
|
||||
}
|
||||
|
||||
// Valider le formulaire uniquement au submit
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
debugPrint('=== ARRÊT: Formulaire invalide ===');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('=== DÉBUT SOUMISSION ===');
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Récupérer l'utilisateur actuel pour le fkEntite
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
final userFkEntite = currentUser?.fkEntite ?? 0;
|
||||
|
||||
final operationData = widget.operation?.copyWith(
|
||||
name: _nameController.text.trim(),
|
||||
dateDebut: _dateDebut!,
|
||||
dateFin: _dateFin!,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
) ??
|
||||
OperationModel(
|
||||
id: 0,
|
||||
name: _nameController.text.trim(),
|
||||
dateDebut: _dateDebut!,
|
||||
dateFin: _dateFin!,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
fkEntite: userFkEntite, // ← Utiliser le fkEntite de l'utilisateur
|
||||
isActive: false,
|
||||
isSynced: false,
|
||||
);
|
||||
|
||||
debugPrint('=== OPERATION DATA ===');
|
||||
debugPrint('operation.id: ${operationData.id}');
|
||||
debugPrint('operation.fkEntite: ${operationData.fkEntite}');
|
||||
debugPrint('user.fkEntite: $userFkEntite');
|
||||
|
||||
debugPrint('=== APPEL REPOSITORY ===');
|
||||
|
||||
// Appel direct du repository - la dialog gère tout
|
||||
final success = await widget.operationRepository.saveOperationFromModel(operationData);
|
||||
|
||||
if (success && mounted) {
|
||||
debugPrint('=== SUCCÈS - AUTO-FERMETURE ===');
|
||||
debugPrint('=== context.mounted: ${context.mounted} ===');
|
||||
|
||||
// Délai pour laisser le temps à Hive de se synchroniser
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (mounted) {
|
||||
debugPrint('=== FERMETURE DIFFÉRÉE ===');
|
||||
|
||||
// Auto-fermeture de la dialog
|
||||
try {
|
||||
debugPrint('=== AVANT Navigator.pop() ===');
|
||||
Navigator.of(context).pop();
|
||||
debugPrint('=== APRÈS Navigator.pop() ===');
|
||||
} catch (e) {
|
||||
debugPrint('=== ERREUR Navigator.pop(): $e ===');
|
||||
}
|
||||
|
||||
// Notifier la page parente pour setState()
|
||||
debugPrint('=== AVANT onSuccess?.call() ===');
|
||||
widget.onSuccess?.call();
|
||||
debugPrint('=== APRÈS onSuccess?.call() ===');
|
||||
|
||||
// Message de succès
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
debugPrint('=== AFFICHAGE MESSAGE SUCCÈS ===');
|
||||
ApiException.showSuccess(context, widget.operation == null ? "Nouvelle opération créée avec succès" : "Opération modifiée avec succès");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (mounted) {
|
||||
debugPrint('=== ÉCHEC - AFFICHAGE ERREUR ===');
|
||||
ApiException.showError(context, Exception(widget.operation == null ? "Échec de la création de l'opération" : "Échec de la mise à jour de l'opération"));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('=== ERREUR dans _handleSubmit: $e ===');
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
} finally {
|
||||
// Réinitialiser l'état de soumission seulement si le widget est encore monté
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.4,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
maxHeight: 700,
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.operation == null ? Icons.add_circle : Icons.edit,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Contenu du formulaire
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom de l'opération
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
label: "Nom de l'opération",
|
||||
readOnly: widget.readOnly,
|
||||
prefixIcon: Icons.event,
|
||||
isRequired: true,
|
||||
maxLength: 100,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return "Veuillez entrer le nom de l'opération";
|
||||
}
|
||||
if (value.trim().length < 5) {
|
||||
return "Le nom doit contenir au moins 5 caractères";
|
||||
}
|
||||
if (value.trim().length > 100) {
|
||||
return "Le nom ne peut pas dépasser 100 caractères";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hintText: "Ex: Calendriers 2024, Opération Noël...",
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Section des dates
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: theme.colorScheme.surface.withOpacity(0.3),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.date_range,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Période de l'opération",
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date de début
|
||||
CustomTextField(
|
||||
controller: _dateDebutController,
|
||||
label: "Date de début",
|
||||
readOnly: true,
|
||||
isRequired: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, true),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
validator: (value) {
|
||||
if (_dateDebut == null) {
|
||||
return "Veuillez sélectionner la date de début";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hintText: "Cliquez pour sélectionner la date",
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date de fin
|
||||
CustomTextField(
|
||||
controller: _dateFinController,
|
||||
label: "Date de fin",
|
||||
readOnly: true,
|
||||
isRequired: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, false),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
validator: (value) {
|
||||
if (_dateFin == null) {
|
||||
return "Veuillez sélectionner la date de fin";
|
||||
}
|
||||
if (_dateDebut != null && _dateFin!.isBefore(_dateDebut!)) {
|
||||
return "La date de fin doit être postérieure à la date de début";
|
||||
}
|
||||
if (_dateDebut != null && _dateFin!.isAtSameMomentAs(_dateDebut!)) {
|
||||
return "La date de fin doit être différente de la date de début";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hintText: "Cliquez pour sélectionner la date",
|
||||
),
|
||||
|
||||
// Indicateur de durée
|
||||
if (_dateDebut != null && _dateFin != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Durée: ${_dateFin!.difference(_dateDebut!).inDays + 1} jour(s)",
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Informations supplémentaires pour les nouvelles opérations
|
||||
if (widget.operation == null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
color: Colors.black87,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"La nouvelle opération sera activée automatiquement et remplacera l'opération active actuelle.",
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Footer avec boutons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (!widget.readOnly)
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||
icon: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(widget.operation == null ? Icons.add : Icons.save),
|
||||
label: Text(_isSubmitting ? 'Enregistrement...' : (widget.operation == null ? 'Créer' : 'Enregistrer')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user