feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
231
app/lib/presentation/admin/admin_amicale_page.dart
Normal file → Executable file
231
app/lib/presentation/admin/admin_amicale_page.dart
Normal file → Executable file
@@ -11,7 +11,6 @@ import 'package:geosector_app/core/repositories/membre_repository.dart';
|
||||
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
||||
|
||||
@@ -52,7 +51,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
void _loadCurrentUser() {
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
|
||||
debugPrint('🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
|
||||
debugPrint(
|
||||
'🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
|
||||
debugPrint('🔍 _loadCurrentUser - fkEntite: ${currentUser?.fkEntite}');
|
||||
|
||||
if (currentUser == null) {
|
||||
@@ -70,8 +70,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
// Vérifier immédiatement si l'amicale existe
|
||||
final amicale = widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
|
||||
debugPrint('🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
|
||||
final amicale =
|
||||
widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
|
||||
debugPrint(
|
||||
'🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
|
||||
|
||||
setState(() {
|
||||
_currentUser = currentUser;
|
||||
@@ -85,8 +87,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
_currentOperationId = currentOperation?.id;
|
||||
|
||||
if (currentOperation != null) {
|
||||
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
debugPrint('📅 Période: ${currentOperation.dateDebut.toString().substring(0, 10)} → ${currentOperation.dateFin.toString().substring(0, 10)}');
|
||||
debugPrint(
|
||||
'🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
debugPrint(
|
||||
'📅 Période: ${currentOperation.dateDebut.toString().substring(0, 10)} → ${currentOperation.dateFin.toString().substring(0, 10)}');
|
||||
} else {
|
||||
debugPrint('⚠️ Aucune opération courante trouvée');
|
||||
}
|
||||
@@ -117,16 +121,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
onSubmit: (updatedUser) async {
|
||||
try {
|
||||
// Convertir le UserModel mis à jour vers MembreModel
|
||||
final updatedMembre = MembreModel.fromUserModel(updatedUser, membre);
|
||||
final updatedMembre =
|
||||
MembreModel.fromUserModel(updatedUser, membre);
|
||||
|
||||
// Utiliser directement updateMembre qui passe par l'API /users
|
||||
final success = await widget.membreRepository.updateMembre(updatedMembre);
|
||||
final success =
|
||||
await widget.membreRepository.updateMembre(updatedMembre);
|
||||
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ApiException.showSuccess(context, 'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
|
||||
} else if (!success && mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la mise à jour'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la mise à jour'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mise à jour membre: $e');
|
||||
@@ -139,42 +147,119 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleResetPassword(MembreModel membre) async {
|
||||
// Afficher un dialog de confirmation
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.lock_reset, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text('Réinitialiser le mot de passe'),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
'Voulez-vous réinitialiser le mot de passe de ${membre.firstName} ${membre.name} ?\n\n'
|
||||
'Un email sera envoyé à l\'utilisateur avec les instructions de réinitialisation.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try {
|
||||
debugPrint('🔐 Réinitialisation du mot de passe pour: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
|
||||
final success = await widget.membreRepository.resetMemberPassword(membre.id);
|
||||
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(
|
||||
context,
|
||||
'Mot de passe réinitialisé avec succès. Un email a été envoyé à ${membre.email}',
|
||||
);
|
||||
} else if (mounted) {
|
||||
ApiException.showError(
|
||||
context,
|
||||
Exception('Erreur lors de la réinitialisation du mot de passe'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur réinitialisation mot de passe: $e');
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDeleteMembre(MembreModel membre) async {
|
||||
try {
|
||||
debugPrint('🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
debugPrint(
|
||||
'🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
|
||||
// Vérifier qu'on a une opération courante
|
||||
if (_currentOperationId == null) {
|
||||
debugPrint('❌ Aucune opération courante');
|
||||
ApiException.showError(context, Exception('Aucune opération active trouvée. Impossible de supprimer le membre.'));
|
||||
ApiException.showError(
|
||||
context,
|
||||
Exception(
|
||||
'Aucune opération active trouvée. Impossible de supprimer le membre.'));
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🎯 Opération courante: $_currentOperationId');
|
||||
|
||||
// Filtrer les passages par opération courante ET par utilisateur
|
||||
final allUserPassages = widget.passageRepository.getPassagesByUser(membre.id);
|
||||
final allUserPassages =
|
||||
widget.passageRepository.getPassagesByUser(membre.id);
|
||||
debugPrint('📊 Total passages du membre: ${allUserPassages.length}');
|
||||
|
||||
final passagesRealises = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType != 2).toList();
|
||||
final passagesRealises = allUserPassages
|
||||
.where((passage) =>
|
||||
passage.fkOperation == _currentOperationId && passage.fkType != 2)
|
||||
.toList();
|
||||
|
||||
final passagesAFinaliser = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType == 2).toList();
|
||||
final passagesAFinaliser = allUserPassages
|
||||
.where((passage) =>
|
||||
passage.fkOperation == _currentOperationId && passage.fkType == 2)
|
||||
.toList();
|
||||
|
||||
final totalPassages = passagesRealises.length + passagesAFinaliser.length;
|
||||
|
||||
debugPrint('🔍 Passages réalisés (opération $_currentOperationId): ${passagesRealises.length}');
|
||||
debugPrint('🔍 Passages à finaliser (opération $_currentOperationId): ${passagesAFinaliser.length}');
|
||||
debugPrint('🔍 Total passages pour l\'opération $_currentOperationId: $totalPassages');
|
||||
debugPrint(
|
||||
'🔍 Passages réalisés (opération $_currentOperationId): ${passagesRealises.length}');
|
||||
debugPrint(
|
||||
'🔍 Passages à finaliser (opération $_currentOperationId): ${passagesAFinaliser.length}');
|
||||
debugPrint(
|
||||
'🔍 Total passages pour l\'opération $_currentOperationId: $totalPassages');
|
||||
|
||||
// Récupérer les autres membres de l'amicale (pour le transfert)
|
||||
final autresmembres = widget.membreRepository.getMembresByAmicale(_currentUser!.fkEntite!).where((m) => m.id != membre.id && m.isActive == true).toList();
|
||||
final autresmembres = widget.membreRepository
|
||||
.getMembresByAmicale(_currentUser!.fkEntite!)
|
||||
.where((m) => m.id != membre.id && m.isActive == true)
|
||||
.toList();
|
||||
|
||||
debugPrint('👥 Autres membres disponibles: ${autresmembres.length}');
|
||||
|
||||
// Afficher le dialog de confirmation approprié
|
||||
if (totalPassages > 0) {
|
||||
debugPrint('➡️ Affichage dialog avec passages');
|
||||
_showDeleteMemberWithPassagesDialog(membre, totalPassages, autresmembres);
|
||||
_showDeleteMemberWithPassagesDialog(
|
||||
membre, totalPassages, autresmembres);
|
||||
} else {
|
||||
debugPrint('➡️ Affichage dialog simple (pas de passages)');
|
||||
_showSimpleDeleteConfirmation(membre);
|
||||
@@ -192,7 +277,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
|
||||
content: Text(
|
||||
'Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
|
||||
'Ce membre n\'a aucun passage enregistré pour l\'opération courante.\n'
|
||||
'Cette action est irréversible.'),
|
||||
actions: [
|
||||
@@ -222,7 +308,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
int totalPassages,
|
||||
List<MembreModel> autresmembres,
|
||||
) {
|
||||
int? selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
|
||||
int?
|
||||
selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -272,13 +359,19 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
Text(
|
||||
'Sélectionnez un membre pour récupérer tous les passages ($totalPassages) :',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'* Cela peut concerner aussi les anciennes opérations s\'il avait des passages affectés',
|
||||
style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedMemberForTransfer,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre destinataire',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
items: autresmembres
|
||||
.map((m) => DropdownMenuItem(
|
||||
@@ -290,7 +383,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
setDialogState(() {
|
||||
selectedMemberForTransfer = value;
|
||||
});
|
||||
debugPrint('✅ Membre destinataire sélectionné: $value');
|
||||
debugPrint(
|
||||
'✅ Membre destinataire sélectionné: $value');
|
||||
},
|
||||
),
|
||||
|
||||
@@ -305,7 +399,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 16),
|
||||
const Icon(Icons.check_circle,
|
||||
color: Colors.green, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Membre sélectionné',
|
||||
@@ -329,7 +424,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.withOpacity(0.3)),
|
||||
border:
|
||||
Border.all(color: Colors.green.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -371,23 +467,31 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
ElevatedButton(
|
||||
onPressed: selectedMemberForTransfer != null
|
||||
? () async {
|
||||
debugPrint('🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
|
||||
debugPrint(
|
||||
'🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
|
||||
Navigator.of(context).pop();
|
||||
// Suppression avec passages : inclure les paramètres
|
||||
await _deleteMemberAPI(membre.id, selectedMemberForTransfer!, hasPassages: true);
|
||||
await _deleteMemberAPI(
|
||||
membre.id, selectedMemberForTransfer!,
|
||||
hasPassages: true);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selectedMemberForTransfer != null ? Colors.red : null,
|
||||
backgroundColor:
|
||||
selectedMemberForTransfer != null ? Colors.red : null,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (selectedMemberForTransfer != null) const Icon(Icons.delete_forever, size: 16),
|
||||
if (selectedMemberForTransfer != null) const SizedBox(width: 4),
|
||||
if (selectedMemberForTransfer != null)
|
||||
const Icon(Icons.delete_forever, size: 16),
|
||||
if (selectedMemberForTransfer != null)
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
selectedMemberForTransfer != null ? 'Supprimer et transférer' : 'Sélectionner un membre',
|
||||
selectedMemberForTransfer != null
|
||||
? 'Supprimer et transférer'
|
||||
: 'Sélectionner un membre',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -400,13 +504,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
// Méthode unifiée pour appeler l'API de suppression
|
||||
Future<void> _deleteMemberAPI(int membreId, int transferToUserId, {bool hasPassages = false}) async {
|
||||
Future<void> _deleteMemberAPI(int membreId, int transferToUserId,
|
||||
{bool hasPassages = false}) async {
|
||||
try {
|
||||
bool success;
|
||||
|
||||
if (hasPassages && transferToUserId > 0 && _currentOperationId != null) {
|
||||
// Suppression avec transfert de passages (inclure operation_id)
|
||||
debugPrint('🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
|
||||
debugPrint(
|
||||
'🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
|
||||
success = await widget.membreRepository.deleteMembre(
|
||||
membreId,
|
||||
transferToUserId,
|
||||
@@ -422,14 +528,18 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
String message = 'Membre supprimé avec succès';
|
||||
|
||||
if (hasPassages && transferToUserId > 0) {
|
||||
final transferMember = widget.membreRepository.getMembreById(transferToUserId);
|
||||
final currentOperation = widget.operationRepository.getCurrentOperation();
|
||||
message += '\nPassages de l\'opération "${currentOperation?.name}" transférés à ${transferMember?.firstName} ${transferMember?.name}';
|
||||
final transferMember =
|
||||
widget.membreRepository.getMembreById(transferToUserId);
|
||||
final currentOperation =
|
||||
widget.operationRepository.getCurrentOperation();
|
||||
message +=
|
||||
'\nPassages de l\'opération "${currentOperation?.name}" transférés à ${transferMember?.firstName} ${transferMember?.name}';
|
||||
}
|
||||
|
||||
ApiException.showSuccess(context, message);
|
||||
} else if (mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la suppression'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la suppression'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur suppression membre: $e');
|
||||
@@ -445,9 +555,11 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
final success = await widget.membreRepository.updateMembre(updatedMember);
|
||||
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
|
||||
} else if (mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la désactivation'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la désactivation'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -519,17 +631,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
);
|
||||
|
||||
// Créer le membre via l'API (retourne maintenant le membre créé)
|
||||
final createdMembre = await widget.membreRepository.createMembre(newMembre);
|
||||
final createdMembre =
|
||||
await widget.membreRepository.createMembre(newMembre);
|
||||
|
||||
if (createdMembre != null && mounted) {
|
||||
// Fermer le dialog
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher le message de succès avec les informations du membre créé
|
||||
ApiException.showSuccess(context, 'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
|
||||
} else if (mounted) {
|
||||
// En cas d'échec, ne pas fermer le dialog pour permettre la correction
|
||||
ApiException.showError(context, Exception('Erreur lors de la création du membre'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la création du membre'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création membre: $e');
|
||||
@@ -593,20 +708,27 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
if (_currentUser != null && _currentUser!.fkEntite != null)
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<AmicaleModel>>(
|
||||
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
valueListenable:
|
||||
widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
builder: (context, amicalesBox, child) {
|
||||
debugPrint('🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint('🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint('🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
debugPrint(
|
||||
'🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint(
|
||||
'🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint(
|
||||
'🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
|
||||
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
|
||||
debugPrint('🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
debugPrint(
|
||||
'🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
|
||||
if (amicale == null) {
|
||||
// Ajouter plus d'informations de debug
|
||||
debugPrint('❌ PROBLÈME: Amicale non trouvée');
|
||||
debugPrint('❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint('❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
debugPrint(
|
||||
'❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint(
|
||||
'❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -634,11 +756,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: widget.membreRepository.getMembresBox().listenable(),
|
||||
valueListenable:
|
||||
widget.membreRepository.getMembresBox().listenable(),
|
||||
builder: (context, membresBox, child) {
|
||||
// Filtrer les membres par amicale
|
||||
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
|
||||
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
|
||||
final membres = membresBox.values
|
||||
.where((membre) =>
|
||||
membre.fkEntite == _currentUser!.fkEntite)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -721,6 +847,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
membres: membres,
|
||||
onEdit: _handleEditMembre,
|
||||
onDelete: _handleDeleteMembre,
|
||||
onResetPassword: _handleResetPassword,
|
||||
membreRepository: widget.membreRepository,
|
||||
),
|
||||
),
|
||||
|
||||
2
app/lib/presentation/admin/admin_communication_page.dart
Normal file → Executable file
2
app/lib/presentation/admin/admin_communication_page.dart
Normal file → Executable file
@@ -5,7 +5,7 @@ import 'package:geosector_app/presentation/widgets/chat/chat_messages.dart';
|
||||
import 'package:geosector_app/presentation/widgets/chat/chat_input.dart';
|
||||
|
||||
class AdminCommunicationPage extends StatefulWidget {
|
||||
const AdminCommunicationPage({Key? key}) : super(key: key);
|
||||
const AdminCommunicationPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminCommunicationPage> createState() => _AdminCommunicationPageState();
|
||||
|
||||
0
app/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file → Executable file
0
app/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file → Executable file
57
app/lib/presentation/admin/admin_dashboard_page.dart
Normal file → Executable file
57
app/lib/presentation/admin/admin_dashboard_page.dart
Normal file → Executable file
@@ -47,8 +47,7 @@ class AdminDashboardPage extends StatefulWidget {
|
||||
class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBindingObserver {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
// Liste des pages à afficher
|
||||
late final List<Widget> _pages;
|
||||
// Pages seront construites dynamiquement dans build()
|
||||
|
||||
// Référence à la boîte Hive pour les paramètres
|
||||
late Box _settingsBox;
|
||||
@@ -138,6 +137,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
List<NavigationDestination> _buildNavigationDestinations() {
|
||||
final destinations = <NavigationDestination>[];
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isMobile = size.width <= 900;
|
||||
|
||||
// Ajouter les éléments de base
|
||||
for (final item in _baseNavigationItems) {
|
||||
@@ -153,6 +154,12 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
// Ajouter les éléments admin si l'utilisateur a le rôle requis
|
||||
if (currentUser?.role == 2) {
|
||||
for (final item in _adminNavigationItems) {
|
||||
// En mobile, exclure "Amicale & membres" et "Opérations"
|
||||
if (isMobile &&
|
||||
(item.label == 'Amicale & membres' || item.label == 'Opérations')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.requiredRole == null || item.requiredRole == 2) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
@@ -172,6 +179,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
List<Widget> _buildPages() {
|
||||
final pages = <Widget>[];
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isMobile = size.width <= 900;
|
||||
|
||||
// Ajouter les pages de base
|
||||
for (final item in _baseNavigationItems) {
|
||||
@@ -181,6 +190,12 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
// Ajouter les pages admin si l'utilisateur a le rôle requis
|
||||
if (currentUser?.role == 2) {
|
||||
for (final item in _adminNavigationItems) {
|
||||
// En mobile, exclure "Amicale & membres" et "Opérations"
|
||||
if (isMobile &&
|
||||
(item.label == 'Amicale & membres' || item.label == 'Opérations')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.requiredRole == null || item.requiredRole == 2) {
|
||||
pages.add(_buildPage(item.pageType));
|
||||
}
|
||||
@@ -208,8 +223,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
}
|
||||
userRepository.addListener(_handleUserRepositoryChanges);
|
||||
|
||||
// Initialiser les pages et les destinations
|
||||
_pages = _buildPages();
|
||||
// Les pages seront construites dynamiquement dans build()
|
||||
|
||||
// Initialiser et charger les paramètres
|
||||
_initSettings();
|
||||
@@ -257,19 +271,11 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
if (savedIndex != null && savedIndex is int) {
|
||||
debugPrint('Index sauvegardé trouvé: $savedIndex');
|
||||
|
||||
// S'assurer que l'index est dans les limites valides
|
||||
if (savedIndex >= 0 && savedIndex < _pages.length) {
|
||||
setState(() {
|
||||
_selectedIndex = savedIndex;
|
||||
});
|
||||
debugPrint('Index sauvegardé valide, utilisé: $_selectedIndex');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Index sauvegardé invalide ($savedIndex), utilisation de l\'index par défaut: 0',
|
||||
);
|
||||
// Réinitialiser l'index sauvegardé à 0 si invalide
|
||||
_settingsBox.put('adminSelectedPageIndex', 0);
|
||||
}
|
||||
// La validation de l'index sera faite dans build()
|
||||
setState(() {
|
||||
_selectedIndex = savedIndex;
|
||||
});
|
||||
debugPrint('Index sauvegardé utilisé: $_selectedIndex');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Aucun index sauvegardé trouvé, utilisation de l\'index par défaut: 0',
|
||||
@@ -292,6 +298,19 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Construire les pages et destinations dynamiquement
|
||||
final pages = _buildPages();
|
||||
final destinations = _buildNavigationDestinations();
|
||||
|
||||
// Valider et ajuster l'index si nécessaire
|
||||
if (_selectedIndex >= pages.length) {
|
||||
_selectedIndex = 0;
|
||||
// Sauvegarder le nouvel index
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
@@ -318,10 +337,10 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
_saveSettings(); // Sauvegarder l'index de page sélectionné
|
||||
});
|
||||
},
|
||||
destinations: _buildNavigationDestinations(),
|
||||
destinations: destinations,
|
||||
showNewPassageButton: false,
|
||||
isAdmin: true,
|
||||
body: _pages[_selectedIndex],
|
||||
body: pages[_selectedIndex],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
5
app/lib/presentation/admin/admin_debug_info_widget.dart
Normal file → Executable file
5
app/lib/presentation/admin/admin_debug_info_widget.dart
Normal file → Executable file
@@ -4,7 +4,7 @@ import 'package:geosector_app/presentation/widgets/environment_info_widget.dart'
|
||||
/// Widget d'information de débogage pour l'administrateur
|
||||
/// À intégrer où nécessaire dans l'interface administrateur
|
||||
class AdminDebugInfoWidget extends StatelessWidget {
|
||||
const AdminDebugInfoWidget({Key? key}) : super(key: key);
|
||||
const AdminDebugInfoWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -30,7 +30,8 @@ class AdminDebugInfoWidget extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Environnement'),
|
||||
subtitle: const Text('Afficher les informations sur l\'environnement actuel'),
|
||||
subtitle: const Text(
|
||||
'Afficher les informations sur l\'environnement actuel'),
|
||||
onTap: () => EnvironmentInfoWidget.show(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
|
||||
715
app/lib/presentation/admin/admin_history_page.dart
Normal file → Executable file
715
app/lib/presentation/admin/admin_history_page.dart
Normal file → Executable file
@@ -1,15 +1,15 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:geosector_app/core/constants/app_keys.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_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/repositories/membre_repository.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Class pour dessiner les petits points blancs sur le fond
|
||||
@@ -36,7 +36,7 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminHistoryPage extends StatefulWidget {
|
||||
const AdminHistoryPage({Key? key}) : super(key: key);
|
||||
const AdminHistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminHistoryPage> createState() => _AdminHistoryPageState();
|
||||
@@ -49,25 +49,32 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
String selectedUser = 'Tous';
|
||||
String selectedType = 'Tous';
|
||||
String selectedPaymentMethod = 'Tous';
|
||||
String selectedPeriod = 'Dernier mois'; // Période par défaut
|
||||
String selectedPeriod = 'Tous'; // Période par défaut
|
||||
DateTimeRange? selectedDateRange;
|
||||
|
||||
// Contrôleur pour la recherche
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
// IDs pour les filtres
|
||||
int? selectedSectorId;
|
||||
int? selectedUserId;
|
||||
|
||||
// Listes pour les filtres
|
||||
List<SectorModel> _sectors = [];
|
||||
List<UserModel> _users = [];
|
||||
List<MembreModel> _membres = [];
|
||||
|
||||
// Repositories
|
||||
late PassageRepository _passageRepository;
|
||||
late SectorRepository _sectorRepository;
|
||||
late UserRepository _userRepository;
|
||||
late MembreRepository _membreRepository;
|
||||
|
||||
// Passages formatés
|
||||
// Passages formatés pour l'affichage
|
||||
List<Map<String, dynamic>> _formattedPassages = [];
|
||||
|
||||
// Passages originaux pour l'édition
|
||||
List<PassageModel> _originalPassages = [];
|
||||
|
||||
// État de chargement
|
||||
bool _isLoading = true;
|
||||
String _errorMessage = '';
|
||||
@@ -93,9 +100,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_passageRepository = passageRepository;
|
||||
_userRepository = userRepository;
|
||||
_sectorRepository = sectorRepository;
|
||||
_membreRepository = membreRepository;
|
||||
|
||||
// Charger les secteurs et les utilisateurs
|
||||
_loadSectorsAndUsers();
|
||||
// Charger les secteurs et les membres
|
||||
_loadSectorsAndMembres();
|
||||
|
||||
// Charger les passages
|
||||
_loadPassages();
|
||||
@@ -107,18 +115,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les secteurs et les utilisateurs
|
||||
void _loadSectorsAndUsers() {
|
||||
// Charger les secteurs et les membres
|
||||
void _loadSectorsAndMembres() {
|
||||
try {
|
||||
// Récupérer la liste des secteurs
|
||||
_sectors = _sectorRepository.getAllSectors();
|
||||
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
|
||||
|
||||
// Récupérer la liste des utilisateurs
|
||||
_users = _userRepository.getAllUsers();
|
||||
debugPrint('Nombre d\'utilisateurs récupérés: ${_users.length}');
|
||||
// Récupérer la liste des membres
|
||||
_membres = _membreRepository.getAllMembres();
|
||||
debugPrint('Nombre de membres récupérés: ${_membres.length}');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des secteurs et utilisateurs: $e');
|
||||
debugPrint('Erreur lors du chargement des secteurs et membres: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +141,12 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
final List<PassageModel> allPassages =
|
||||
_passageRepository.getAllPassages();
|
||||
|
||||
// Stocker les passages originaux pour l'édition
|
||||
_originalPassages = allPassages;
|
||||
|
||||
// Convertir les passages en format attendu par PassagesListWidget
|
||||
_formattedPassages = _formatPassagesForWidget(
|
||||
allPassages, _sectorRepository, _userRepository);
|
||||
allPassages, _sectorRepository, _membreRepository);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@@ -154,13 +165,137 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
selectedSectorId = null;
|
||||
selectedUserId = null;
|
||||
|
||||
// Période par défaut : dernier mois
|
||||
selectedPeriod = 'Dernier mois';
|
||||
// Période par défaut : toutes les périodes
|
||||
selectedPeriod = 'Tous';
|
||||
|
||||
// Plage de dates par défaut : dernier mois
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime oneMonthAgo = DateTime(now.year, now.month - 1, now.day);
|
||||
selectedDateRange = DateTimeRange(start: oneMonthAgo, end: now);
|
||||
// Plage de dates par défaut : aucune restriction
|
||||
selectedDateRange = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Méthode pour appliquer tous les filtres
|
||||
List<Map<String, dynamic>> _getFilteredPassages() {
|
||||
try {
|
||||
var filtered = _formattedPassages.where((passage) {
|
||||
try {
|
||||
// Ne plus exclure automatiquement les passages de type 2
|
||||
// car on propose maintenant un filtre par type dans les "Filtres avancés"
|
||||
|
||||
// Filtrer par utilisateur
|
||||
if (selectedUserId != null &&
|
||||
passage.containsKey('fkUser') &&
|
||||
passage['fkUser'] != selectedUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par secteur
|
||||
if (selectedSectorId != null &&
|
||||
passage.containsKey('fkSector') &&
|
||||
passage['fkSector'] != selectedSectorId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par type de passage
|
||||
if (selectedType != 'Tous') {
|
||||
try {
|
||||
final int? selectedTypeId = int.tryParse(selectedType);
|
||||
if (selectedTypeId != null) {
|
||||
if (!passage.containsKey('type') ||
|
||||
passage['type'] != selectedTypeId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par type: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par mode de règlement
|
||||
if (selectedPaymentMethod != 'Tous') {
|
||||
try {
|
||||
final int? selectedPaymentId =
|
||||
int.tryParse(selectedPaymentMethod);
|
||||
if (selectedPaymentId != null) {
|
||||
if (!passage.containsKey('payment') ||
|
||||
passage['payment'] != selectedPaymentId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par mode de règlement: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par recherche
|
||||
if (searchQuery.isNotEmpty) {
|
||||
try {
|
||||
final query = searchQuery.toLowerCase();
|
||||
final address = passage.containsKey('address')
|
||||
? passage['address']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
final name = passage.containsKey('name')
|
||||
? passage['name']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
final notes = passage.containsKey('notes')
|
||||
? passage['notes']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
|
||||
if (!address.contains(query) &&
|
||||
!name.contains(query) &&
|
||||
!notes.contains(query)) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par recherche: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par période/date
|
||||
if (selectedDateRange != null) {
|
||||
try {
|
||||
if (passage.containsKey('date') && passage['date'] is DateTime) {
|
||||
final DateTime passageDate = passage['date'] as DateTime;
|
||||
if (passageDate.isBefore(selectedDateRange!.start) ||
|
||||
passageDate.isAfter(selectedDateRange!.end)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par date: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du filtrage d\'un passage: $e');
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// Trier par date décroissante (plus récent en premier)
|
||||
filtered.sort((a, b) {
|
||||
try {
|
||||
final DateTime dateA = a['date'] as DateTime;
|
||||
final DateTime dateB = b['date'] as DateTime;
|
||||
return dateB.compareTo(dateA);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint(
|
||||
'Passages filtrés: ${filtered.length}/${_formattedPassages.length}');
|
||||
return filtered;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur globale lors du filtrage: $e');
|
||||
return _formattedPassages;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le filtre par secteur
|
||||
@@ -230,7 +365,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(
|
||||
width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
const Center(
|
||||
@@ -258,67 +394,71 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child:
|
||||
const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
// Contenu de la page
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Historique des passages',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final passages = _getFilteredPassages();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 32, // Moins le padding
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Historique des passages',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtres supplémentaires (secteur, utilisateur, période)
|
||||
_buildAdditionalFilters(context),
|
||||
// Filtres supplémentaires (secteur, utilisateur, période)
|
||||
_buildAdditionalFilters(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Widget de liste des passages
|
||||
Expanded(
|
||||
child: PassagesListWidget(
|
||||
passages: _formattedPassages,
|
||||
showFilters: true,
|
||||
showSearch: true,
|
||||
showActions: true,
|
||||
initialSearchQuery: searchQuery,
|
||||
initialTypeFilter: selectedType,
|
||||
initialPaymentFilter: selectedPaymentMethod,
|
||||
// Exclure les passages de type 2 (À finaliser)
|
||||
excludePassageTypes: [2],
|
||||
// Filtres par utilisateur et secteur
|
||||
filterByUserId: selectedUserId,
|
||||
filterBySectorId: selectedSectorId,
|
||||
// Période par défaut (dernier mois)
|
||||
periodFilter: 'lastMonth',
|
||||
// Plage de dates personnalisée si définie
|
||||
dateRange: selectedDateRange,
|
||||
onPassageSelected: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onReceiptView: (passage) {
|
||||
_showReceiptDialog(context, passage);
|
||||
},
|
||||
onDetailsView: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onPassageEdit: (passage) {
|
||||
// Action pour modifier le passage
|
||||
// Cette fonctionnalité pourrait être implémentée ultérieurement
|
||||
},
|
||||
// Widget de liste des passages avec hauteur fixe
|
||||
SizedBox(
|
||||
height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible
|
||||
child: PassagesListWidget(
|
||||
passages: passages,
|
||||
showFilters:
|
||||
false, // Désactivé car les filtres sont maintenant dans la card "Filtres avancés"
|
||||
showSearch:
|
||||
false, // Désactivé car la recherche est maintenant dans la card "Filtres avancés"
|
||||
showActions: true,
|
||||
// Ne plus passer les filtres individuels car ils sont maintenant appliqués dans _getFilteredPassages()
|
||||
onPassageSelected: (passage) {
|
||||
_openPassageEditDialog(context, passage);
|
||||
},
|
||||
onReceiptView: (passage) {
|
||||
_showReceiptDialog(context, passage);
|
||||
},
|
||||
onDetailsView: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onPassageEdit: (passage) {
|
||||
// Action pour modifier le passage
|
||||
// Cette fonctionnalité pourrait être implémentée ultérieurement
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -339,7 +479,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child:
|
||||
const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
@@ -388,14 +529,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
List<Map<String, dynamic>> _formatPassagesForWidget(
|
||||
List<PassageModel> passages,
|
||||
SectorRepository sectorRepository,
|
||||
UserRepository userRepository) {
|
||||
MembreRepository membreRepository) {
|
||||
return passages.map((passage) {
|
||||
// Récupérer le secteur associé au passage
|
||||
final SectorModel? sector =
|
||||
sectorRepository.getSectorById(passage.fkSector);
|
||||
// Récupérer le secteur associé au passage (si fkSector n'est pas null)
|
||||
final SectorModel? sector = passage.fkSector != null
|
||||
? sectorRepository.getSectorById(passage.fkSector!)
|
||||
: null;
|
||||
|
||||
// Récupérer l'utilisateur associé au passage
|
||||
final UserModel? user = userRepository.getUserById(passage.fkUser);
|
||||
// Récupérer le membre associé au passage
|
||||
final MembreModel? membre =
|
||||
membreRepository.getMembreById(passage.fkUser);
|
||||
|
||||
// Construire l'adresse complète
|
||||
final String address =
|
||||
@@ -406,12 +549,21 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
|
||||
return {
|
||||
'id': passage.id,
|
||||
'date': passage.passedAt,
|
||||
'address': address,
|
||||
if (passage.passedAt != null) 'date': passage.passedAt!,
|
||||
'address': address, // Adresse complète pour l'affichage
|
||||
// Champs séparés pour l'édition
|
||||
'numero': passage.numero,
|
||||
'rueBis': passage.rueBis,
|
||||
'rue': passage.rue,
|
||||
'ville': passage.ville,
|
||||
'residence': passage.residence,
|
||||
'appt': passage.appt,
|
||||
'niveau': passage.niveau,
|
||||
'fkHabitat': passage.fkHabitat,
|
||||
'fkSector': passage.fkSector,
|
||||
'sector': sector?.libelle ?? 'Secteur inconnu',
|
||||
'fkUser': passage.fkUser,
|
||||
'user': user?.name ?? 'Utilisateur inconnu',
|
||||
'user': membre?.name ?? 'Membre inconnu',
|
||||
'type': passage.fkType,
|
||||
'amount': double.tryParse(passage.montant) ?? 0.0,
|
||||
'payment': passage.fkTypeReglement,
|
||||
@@ -421,7 +573,14 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
'notes': passage.remarque,
|
||||
'name': passage.name,
|
||||
'phone': passage.phone,
|
||||
// Ajouter d'autres champs nécessaires pour le widget
|
||||
'montant': passage.montant,
|
||||
'remarque': passage.remarque,
|
||||
// Autres champs utiles
|
||||
'fkOperation': passage.fkOperation,
|
||||
'passedAt': passage.passedAt,
|
||||
'lastSyncedAt': passage.lastSyncedAt,
|
||||
'isActive': passage.isActive,
|
||||
'isSynced': passage.isSynced,
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
@@ -552,6 +711,63 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openPassageEditDialog(
|
||||
BuildContext context, Map<String, dynamic> passage) async {
|
||||
try {
|
||||
debugPrint('=== DEBUT _openPassageEditDialog ===');
|
||||
|
||||
// Récupérer l'ID du passage
|
||||
final int passageId = passage['id'] as int;
|
||||
debugPrint('Recherche du passage ID: $passageId');
|
||||
|
||||
// Trouver le PassageModel original dans la liste
|
||||
final PassageModel? passageModel =
|
||||
_originalPassages.where((p) => p.id == passageId).firstOrNull;
|
||||
|
||||
if (passageModel == null) {
|
||||
throw Exception('Passage original introuvable avec l\'ID: $passageId');
|
||||
}
|
||||
|
||||
debugPrint('PassageModel original trouvé');
|
||||
if (!mounted) {
|
||||
debugPrint('Widget non monté, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Ouverture du dialog...');
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PassageFormDialog(
|
||||
passage: passageModel,
|
||||
title: 'Modifier le passage',
|
||||
passageRepository: _passageRepository,
|
||||
userRepository: _userRepository,
|
||||
operationRepository: operationRepository,
|
||||
onSuccess: () {
|
||||
debugPrint('Dialog fermé avec succès');
|
||||
// Recharger les données après modification
|
||||
_loadPassages();
|
||||
},
|
||||
),
|
||||
);
|
||||
debugPrint('=== FIN _openPassageEditDialog ===');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR _openPassageEditDialog ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de l\'ouverture du formulaire: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
@@ -616,25 +832,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Champ de recherche
|
||||
_buildSearchField(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Disposition des filtres en fonction de la taille de l'écran
|
||||
isDesktop
|
||||
? Row(
|
||||
? Column(
|
||||
children: [
|
||||
// Filtre par secteur
|
||||
Expanded(
|
||||
child: _buildSectorFilter(theme, _sectors),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Première ligne : Secteur, Utilisateur, Période
|
||||
Row(
|
||||
children: [
|
||||
// Filtre par secteur
|
||||
Expanded(
|
||||
child: _buildSectorFilter(theme, _sectors),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par utilisateur
|
||||
Expanded(
|
||||
child: _buildUserFilter(theme, _users),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Filtre par membre
|
||||
Expanded(
|
||||
child: _buildMembreFilter(theme, _membres),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par période
|
||||
Expanded(
|
||||
child: _buildPeriodFilter(theme),
|
||||
// Filtre par période
|
||||
Expanded(
|
||||
child: _buildPeriodFilter(theme),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Deuxième ligne : Type de passage, Mode de règlement
|
||||
Row(
|
||||
children: [
|
||||
// Filtre par type de passage
|
||||
Expanded(
|
||||
child: _buildTypeFilter(theme),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par mode de règlement
|
||||
Expanded(
|
||||
child: _buildPaymentFilter(theme),
|
||||
),
|
||||
// Espacement pour équilibrer avec la ligne du dessus (3 colonnes)
|
||||
const Expanded(child: SizedBox()),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -644,12 +887,20 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_buildSectorFilter(theme, _sectors),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par utilisateur
|
||||
_buildUserFilter(theme, _users),
|
||||
// Filtre par membre
|
||||
_buildMembreFilter(theme, _membres),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par période
|
||||
_buildPeriodFilter(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par type de passage
|
||||
_buildTypeFilter(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par mode de règlement
|
||||
_buildPaymentFilter(theme),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -714,7 +965,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
@@ -745,11 +996,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par utilisateur
|
||||
Widget _buildUserFilter(ThemeData theme, List<UserModel> users) {
|
||||
// Vérifier si la liste des utilisateurs est vide ou si selectedUser n'est pas dans la liste
|
||||
bool isSelectedUserValid = selectedUser == 'Tous' ||
|
||||
users.any((u) => (u.name ?? 'Utilisateur inconnu') == selectedUser);
|
||||
// Construction du filtre par membre
|
||||
Widget _buildMembreFilter(ThemeData theme, List<MembreModel> membres) {
|
||||
// Fonction pour formater le nom d'affichage d'un membre
|
||||
String formatMembreDisplayName(MembreModel membre) {
|
||||
final String firstName = membre.firstName ?? '';
|
||||
final String name = membre.name ?? '';
|
||||
final String sectName = membre.sectName ?? '';
|
||||
|
||||
// Construire le nom de base
|
||||
String displayName = '';
|
||||
if (firstName.isNotEmpty && name.isNotEmpty) {
|
||||
displayName = '$firstName $name';
|
||||
} else if (name.isNotEmpty) {
|
||||
displayName = name;
|
||||
} else if (firstName.isNotEmpty) {
|
||||
displayName = firstName;
|
||||
} else {
|
||||
displayName = 'Membre inconnu';
|
||||
}
|
||||
|
||||
// Ajouter le sectName entre parenthèses s'il existe
|
||||
if (sectName.isNotEmpty) {
|
||||
displayName = '$displayName ($sectName)';
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
// Trier les membres par nom de famille
|
||||
final List<MembreModel> sortedMembres = [...membres];
|
||||
sortedMembres.sort((a, b) {
|
||||
final String nameA = a.name ?? '';
|
||||
final String nameB = b.name ?? '';
|
||||
return nameA.compareTo(nameB);
|
||||
});
|
||||
|
||||
// Créer une map pour retrouver les membres par leur nom d'affichage
|
||||
final Map<String, MembreModel> membreDisplayMap = {};
|
||||
for (final membre in sortedMembres) {
|
||||
final displayName = formatMembreDisplayName(membre);
|
||||
membreDisplayMap[displayName] = membre;
|
||||
}
|
||||
|
||||
// Vérifier si la liste des membres est vide ou si selectedUser n'est pas dans la liste
|
||||
bool isSelectedUserValid =
|
||||
selectedUser == 'Tous' || membreDisplayMap.containsKey(selectedUser);
|
||||
|
||||
// Si selectedUser n'est pas valide, le réinitialiser à 'Tous'
|
||||
if (!isSelectedUserValid) {
|
||||
@@ -767,7 +1059,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Utilisateur',
|
||||
'Membre',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -788,19 +1080,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les utilisateurs'),
|
||||
child: Text('Tous les membres'),
|
||||
),
|
||||
...users.map((user) {
|
||||
// S'assurer que user.name n'est pas null
|
||||
final String userName = user.name ?? 'Utilisateur inconnu';
|
||||
...membreDisplayMap.entries.map((entry) {
|
||||
final String displayName = entry.key;
|
||||
return DropdownMenuItem<String>(
|
||||
value: userName,
|
||||
value: displayName,
|
||||
child: Text(
|
||||
userName,
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
@@ -808,21 +1099,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_updateUserFilter('Tous', null);
|
||||
} else {
|
||||
try {
|
||||
// Trouver l'utilisateur correspondant
|
||||
final user = users.firstWhere(
|
||||
(u) => (u.name ?? 'Utilisateur inconnu') == value,
|
||||
orElse: () => users.isNotEmpty
|
||||
? users.first
|
||||
: throw Exception('Liste d\'utilisateurs vide'),
|
||||
);
|
||||
// S'assurer que user.name et user.id ne sont pas null
|
||||
final String userName =
|
||||
user.name ?? 'Utilisateur inconnu';
|
||||
final int? userId = user.id;
|
||||
_updateUserFilter(userName, userId);
|
||||
// Trouver le membre correspondant dans la map
|
||||
final membre = membreDisplayMap[value];
|
||||
if (membre != null) {
|
||||
final int membreId = membre.id;
|
||||
_updateUserFilter(value, membreId);
|
||||
} else {
|
||||
throw Exception('Membre non trouvé: $value');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Erreur lors de la sélection de l\'utilisateur: $e');
|
||||
debugPrint('Erreur lors de la sélection du membre: $e');
|
||||
_updateUserFilter('Tous', null);
|
||||
}
|
||||
}
|
||||
@@ -912,34 +1198,155 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showResendConfirmation(BuildContext context, int passageId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Renvoyer le reçu'),
|
||||
content: Text(
|
||||
'Êtes-vous sûr de vouloir renvoyer le reçu du passage #$passageId ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
// Construction du champ de recherche
|
||||
Widget _buildSearchField(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Recherche',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Action pour renvoyer le reçu
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Reçu du passage #$passageId renvoyé avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par adresse ou nom...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
searchQuery = '';
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
searchQuery = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par type de passage
|
||||
Widget _buildTypeFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Type de passage',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedType,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les types'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Renvoyer'),
|
||||
...AppKeys.typesPassages.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key.toString(),
|
||||
child: Text(
|
||||
entry.value['titre'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
selectedType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par mode de règlement
|
||||
Widget _buildPaymentFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Mode de règlement',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedPaymentMethod,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les modes'),
|
||||
),
|
||||
...AppKeys.typesReglements.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key.toString(),
|
||||
child: Text(
|
||||
entry.value['titre'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
selectedPaymentMethod = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3585
app/lib/presentation/admin/admin_map_page.dart
Normal file → Executable file
3585
app/lib/presentation/admin/admin_map_page.dart
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
0
app/lib/presentation/admin/admin_operations_page.dart
Normal file → Executable file
0
app/lib/presentation/admin/admin_operations_page.dart
Normal file → Executable file
17
app/lib/presentation/admin/admin_statistics_page.dart
Normal file → Executable file
17
app/lib/presentation/admin/admin_statistics_page.dart
Normal file → Executable file
@@ -27,7 +27,7 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminStatisticsPage extends StatefulWidget {
|
||||
const AdminStatisticsPage({Key? key}) : super(key: key);
|
||||
const AdminStatisticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminStatisticsPage> createState() => _AdminStatisticsPageState();
|
||||
@@ -80,7 +80,8 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child:
|
||||
const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
// Contenu de la page
|
||||
@@ -228,21 +229,21 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
PaymentData(
|
||||
typeId: 1,
|
||||
amount: 1500.0,
|
||||
color: const Color(0xFFFFC107),
|
||||
color: Color(0xFFFFC107),
|
||||
icon: Icons.toll,
|
||||
title: 'Espèce',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 2,
|
||||
amount: 2500.0,
|
||||
color: const Color(0xFF8BC34A),
|
||||
color: Color(0xFF8BC34A),
|
||||
icon: Icons.wallet,
|
||||
title: 'Chèque',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 3,
|
||||
amount: 1000.0,
|
||||
color: const Color(0xFF00B0FF),
|
||||
color: Color(0xFF00B0FF),
|
||||
icon: Icons.credit_card,
|
||||
title: 'CB',
|
||||
),
|
||||
@@ -281,21 +282,21 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
PaymentData(
|
||||
typeId: 1,
|
||||
amount: 1500.0,
|
||||
color: const Color(0xFFFFC107),
|
||||
color: Color(0xFFFFC107),
|
||||
icon: Icons.toll,
|
||||
title: 'Espèce',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 2,
|
||||
amount: 2500.0,
|
||||
color: const Color(0xFF8BC34A),
|
||||
color: Color(0xFF8BC34A),
|
||||
icon: Icons.wallet,
|
||||
title: 'Chèque',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 3,
|
||||
amount: 1000.0,
|
||||
color: const Color(0xFF00B0FF),
|
||||
color: Color(0xFF00B0FF),
|
||||
icon: Icons.credit_card,
|
||||
title: 'CB',
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user