Files
geo/app/lib/presentation/pages/history_page.dart

1747 lines
65 KiB
Dart

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/theme/app_theme.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/membre_model.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
import 'package:intl/intl.dart';
/// Page d'historique unifiée utilisant AppScaffold
class HistoryPage extends StatelessWidget {
/// ID du membre à filtrer (optionnel, pour les admins)
final int? memberId;
const HistoryPage({
super.key,
this.memberId,
});
@override
Widget build(BuildContext context) {
return AppScaffold(
selectedIndex: 1, // Index de la page History dans la navigation
pageTitle: 'Historique',
body: HistoryContent(memberId: memberId),
);
}
}
/// Contenu de la page historique unifié pour admin et user
class HistoryContent extends StatefulWidget {
/// ID du membre à filtrer (optionnel, pour les admins)
final int? memberId;
const HistoryContent({
super.key,
this.memberId,
});
@override
State<HistoryContent> createState() => _HistoryContentState();
}
// Enum pour gérer les types de tri
enum PassageSortType {
dateDesc, // Plus récent en premier (défaut)
dateAsc, // Plus ancien en premier
addressAsc, // Adresse A-Z
addressDesc, // Adresse Z-A
}
class _HistoryContentState extends State<HistoryContent> {
// Détection du rôle et permissions
late final bool isAdmin;
late final int currentUserId;
late final bool canDeletePassages; // Permission de suppression pour les users
// Filtres principaux (nouveaux)
String _selectedTypeFilter = 'Tous les types';
String _selectedPaymentFilter = 'Tous les règlements';
String _searchQuery = '';
int? _selectedSectorId;
int? _selectedUserId; // Pour admin seulement
int? _selectedPaymentTypeId; // ID du type de règlement sélectionné
// Contrôleurs
final TextEditingController _startDateController = TextEditingController();
final TextEditingController _endDateController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
// Anciens filtres (à supprimer progressivement)
int? selectedSectorId;
String selectedSector = 'Tous';
String selectedType = 'Tous';
int? selectedMemberId;
int? selectedTypeId;
int? selectedPaymentTypeId;
DateTime? startDate;
DateTime? endDate;
String selectedPeriod = 'Toutes';
DateTimeRange? selectedDateRange;
// Listes pour les filtres
List<SectorModel> _sectors = [];
List<MembreModel> _membres = [];
List<UserModel> _users = []; // Liste des users pour le filtre
// Passages originaux pour l'édition
List<PassageModel> _originalPassages = [];
List<PassageModel> _filteredPassages = [];
// État de chargement
bool _isLoading = true;
String _errorMessage = '';
// Statistiques pour l'affichage
int _totalSectors = 0;
int _sharedMembersCount = 0;
// État de la section graphiques
bool _isGraphicsExpanded = true;
// Listener pour les changements de secteur depuis map_page
late final Box _settingsBox;
@override
void initState() {
super.initState();
// Initialiser la box settings et écouter les changements de secteur
_initSettingsListener();
// Déterminer le rôle et les permissions de l'utilisateur (prend en compte le mode d'affichage)
final currentUser = userRepository.getCurrentUser();
isAdmin = CurrentUserService.instance.shouldShowAdminUI;
currentUserId = currentUser?.id ?? 0;
// Vérifier la permission de suppression pour les users
bool userCanDelete = false;
if (!isAdmin && currentUser != null && currentUser.fkEntite != null) {
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (userAmicale != null) {
userCanDelete = userAmicale.chkUserDeletePass;
}
}
canDeletePassages = isAdmin || userCanDelete;
// Si un memberId est passé en paramètre et que c'est un admin, l'utiliser
if (widget.memberId != null && isAdmin) {
selectedMemberId = widget.memberId;
debugPrint('HistoryPage: Filtre membre activé pour ID ${widget.memberId}');
// Sauvegarder aussi dans Hive pour la persistance
_saveMemberFilter(widget.memberId!);
} else {
// Pour tous les autres cas (admin et user), charger les filtres depuis Hive
_loadPreselectedFilters();
// Pour un user standard, toujours filtrer sur son propre ID
if (!isAdmin) {
selectedMemberId = currentUserId;
}
}
_initializeNewFilters();
_initializeFilters();
_updateDateControllers();
_loadGraphicsExpandedState();
}
@override
void dispose() {
_startDateController.dispose();
_endDateController.dispose();
_searchController.dispose();
// Pas besoin de fermer _settingsBox car c'est une box partagée
super.dispose();
}
// Initialiser le listener pour les changements de secteur
Future<void> _initSettingsListener() async {
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
_settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
} else {
_settingsBox = Hive.box(AppKeys.settingsBoxName);
}
// Charger le secteur depuis Hive au démarrage
final savedSectorId = _settingsBox.get('selectedSectorId');
if (savedSectorId != null && savedSectorId is int) {
if (mounted) {
setState(() {
_selectedSectorId = savedSectorId;
selectedSectorId = savedSectorId; // Sync avec l'ancien système
});
}
debugPrint('HistoryPage: Secteur chargé depuis Hive: $savedSectorId');
}
// Écouter les changements futurs
_settingsBox.listenable(keys: ['selectedSectorId']).addListener(_onSectorChanged);
} catch (e) {
debugPrint('HistoryPage: Erreur initialisation settings listener: $e');
}
}
// Callback quand le secteur change depuis map_page
void _onSectorChanged() {
final newSectorId = _settingsBox.get('selectedSectorId');
if (newSectorId != _selectedSectorId) {
if (mounted) {
setState(() {
_selectedSectorId = newSectorId;
selectedSectorId = newSectorId; // Sync avec l'ancien système
});
debugPrint('HistoryPage: Secteur mis à jour depuis map_page: $newSectorId');
_notifyFiltersChanged();
}
}
}
// Initialiser les nouveaux filtres
void _initializeNewFilters() {
// Initialiser le contrôleur de recherche
_searchController.text = _searchQuery;
// Initialiser les filtres selon le rôle
if (isAdmin) {
// Admin peut sélectionner un membre ou "Tous les membres"
_selectedUserId = selectedMemberId; // Utiliser l'ancien système pour la transition
} else {
// User : toujours filtré sur son ID
_selectedUserId = currentUserId;
}
// Initialiser le secteur
_selectedSectorId = selectedSectorId; // Utiliser l'ancien système pour la transition
debugPrint('HistoryPage: Nouveaux filtres initialisés');
debugPrint(' _selectedTypeFilter: $_selectedTypeFilter');
debugPrint(' _selectedPaymentFilter: $_selectedPaymentFilter');
debugPrint(' _selectedSectorId: $_selectedSectorId');
debugPrint(' _selectedUserId: $_selectedUserId');
// Appliquer les filtres initiaux (seulement si les passages sont déjà chargés)
if (_originalPassages.isNotEmpty) {
_notifyFiltersChanged();
}
}
// Vérifier si le type Lot doit être affiché (extrait de PassagesListWidget)
bool _shouldShowLotType() {
final currentUser = userRepository.getCurrentUser();
if (currentUser != null && currentUser.fkEntite != null) {
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (userAmicale != null) {
return userAmicale.chkLotActif;
}
}
return true; // Par défaut, on affiche
}
// Obtenir la liste filtrée des types de passages (extrait de PassagesListWidget)
List<String> _getFilteredPassageTypes() {
final showLotType = _shouldShowLotType();
final types = <String>[];
AppKeys.typesPassages.forEach((typeId, typeInfo) {
// Exclure le type Lot (5) si chkLotActif = false
if (typeId == 5 && !showLotType) {
return; // Skip ce type
}
types.add(typeInfo['titre'] as String);
});
return types;
}
/// Appliquer les filtres aux données et synchroniser GraphicsSection + liste
void _notifyFiltersChanged() {
debugPrint('HistoryPage: Application des filtres');
debugPrint(' Type: $_selectedTypeFilter');
debugPrint(' Paiement: $_selectedPaymentFilter');
debugPrint(' Recherche: $_searchQuery');
debugPrint(' Secteur: $_selectedSectorId');
debugPrint(' Utilisateur: $_selectedUserId');
debugPrint(' Dates: $startDate à $endDate');
// Appliquer les filtres aux passages originaux
List<PassageModel> filteredPassages = _originalPassages.where((passage) {
// Filtre par type de passage
if (_selectedTypeFilter != 'Tous les types') {
final typeInfo = AppKeys.typesPassages.entries.firstWhere(
(entry) => entry.value['titre'] == _selectedTypeFilter,
orElse: () => const MapEntry(-1, {'titre': '', 'couleur': ''}),
);
if (typeInfo.key == -1 || passage.fkType != typeInfo.key) {
return false;
}
}
// Filtre par type de règlement
if (_selectedPaymentTypeId != null && passage.fkTypeReglement != _selectedPaymentTypeId) {
return false;
}
// Filtre par secteur
if (_selectedSectorId != null && passage.fkSector != _selectedSectorId) {
return false;
}
// Filtre par utilisateur (admin seulement)
if (isAdmin && _selectedUserId != null && passage.fkUser != _selectedUserId) {
return false;
}
// Filtre par dates
// Si une date de début ou de fin est définie et que le passage est de type 2 (À finaliser)
// sans date, on l'exclut
if ((startDate != null || endDate != null) && passage.fkType == 2 && passage.passedAt == null) {
return false; // Exclure les passages "À finaliser" sans date quand une date est définie
}
// Filtre par date de début - ne filtrer que si le passage a une date
if (startDate != null) {
// Si le passage a une date, vérifier qu'elle est après la date de début
if (passage.passedAt != null) {
final passageDate = passage.passedAt!;
if (passageDate.isBefore(startDate!)) {
return false;
}
}
// Si le passage n'a pas de date ET n'est pas de type 2, on le garde
// (les passages type 2 sans date ont déjà été exclus au-dessus)
}
// Filtre par date de fin - ne filtrer que si le passage a une date
if (endDate != null) {
// Si le passage a une date, vérifier qu'elle est avant la date de fin
if (passage.passedAt != null) {
final passageDate = passage.passedAt!;
if (passageDate.isAfter(endDate!.add(const Duration(days: 1)))) {
return false;
}
}
// Si le passage n'a pas de date ET n'est pas de type 2, on le garde
// (les passages type 2 sans date ont déjà été exclus au-dessus)
}
// Filtre par recherche textuelle
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
// Construire l'adresse complète pour la recherche
final fullAddress = '${passage.numero} ${passage.rueBis} ${passage.rue} ${passage.residence} ${passage.ville}'
.toLowerCase()
.trim();
// Ajouter le nom et l'email
final name = passage.name.toLowerCase();
final email = passage.email.toLowerCase();
// Vérifier si la recherche correspond à l'adresse, au nom ou à l'email
if (!fullAddress.contains(query) &&
!name.contains(query) &&
!email.contains(query)) {
return false;
}
}
return true;
}).toList();
// Mettre à jour les données filtrées
setState(() {
_filteredPassages = filteredPassages;
});
debugPrint('HistoryPage: ${filteredPassages.length} passages filtrés sur ${_originalPassages.length}');
}
/// Construire la card de filtres intégrée
Widget _buildFiltersCard() {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return Card(
elevation: 2,
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Première ligne : Type de passage et Mode de paiement
Row(
children: [
// Filtre Type de passage
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedTypeFilter,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
items: [
const DropdownMenuItem<String>(
value: 'Tous les types',
child: Text('Passage'),
),
..._getFilteredPassageTypes().map((String type) {
return DropdownMenuItem<String>(
value: type,
child: Text(type),
);
}),
],
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedTypeFilter = newValue;
});
_notifyFiltersChanged();
}
},
),
),
const SizedBox(width: 12),
// Filtre Mode de règlement
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedPaymentFilter,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
items: [
const DropdownMenuItem<String>(
value: 'Tous les règlements',
child: Text('Règlements'),
),
...AppKeys.typesReglements.entries.map((entry) {
final typeInfo = entry.value;
final titre = typeInfo['titre'] as String;
return DropdownMenuItem<String>(
value: titre,
child: Text(titre),
);
}),
],
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedPaymentFilter = newValue;
// Trouver l'ID correspondant au type de règlement sélectionné
if (newValue == 'Tous les règlements') {
_selectedPaymentTypeId = null;
} else {
final entry = AppKeys.typesReglements.entries.firstWhere(
(e) => e.value['titre'] == newValue,
orElse: () => const MapEntry(-1, {}),
);
_selectedPaymentTypeId = entry.key != -1 ? entry.key : null;
}
});
_notifyFiltersChanged();
}
},
),
),
],
),
const SizedBox(height: 12),
// Deuxième ligne : Secteur et Membre (admin seulement)
Row(
children: [
// Filtre Secteur
Expanded(
child: ValueListenableBuilder<Box<SectorModel>>(
valueListenable: Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
builder: (context, sectorsBox, child) {
final sectors = sectorsBox.values.toList();
return DropdownButtonFormField<int?>(
value: _selectedSectorId,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Secteurs'),
),
...sectors.map((SectorModel sector) {
return DropdownMenuItem<int?>(
value: sector.id,
child: Text(sector.libelle),
);
}),
],
onChanged: (int? newValue) async {
setState(() {
_selectedSectorId = newValue;
});
// Sauvegarder dans Hive pour synchronisation avec map_page
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
if (newValue != null) {
await settingsBox.put('selectedSectorId', newValue);
} else {
await settingsBox.delete('selectedSectorId');
}
} catch (e) {
debugPrint('Erreur sauvegarde secteur: $e');
}
_notifyFiltersChanged();
},
);
},
),
),
// Espace ou filtre Membre (admin seulement)
const SizedBox(width: 12),
if (isAdmin)
Expanded(
child: DropdownButtonFormField<int?>(
value: _selectedUserId,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Membres'),
),
..._users.map((UserModel user) {
return DropdownMenuItem<int?>(
value: user.id,
child: Text('${user.firstName ?? ''} ${user.name ?? ''}'),
);
}),
],
onChanged: (int? newValue) {
setState(() {
_selectedUserId = newValue;
});
_notifyFiltersChanged();
},
),
)
else
const Expanded(child: SizedBox()),
],
),
const SizedBox(height: 12),
// Troisième ligne : Dates
Row(
children: [
// Date de début
Expanded(
child: TextFormField(
controller: _startDateController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
hintText: 'Début',
isDense: true,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_startDateController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
setState(() {
startDate = null;
_startDateController.clear();
});
_notifyFiltersChanged();
},
),
IconButton(
icon: const Icon(Icons.calendar_today, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: startDate ?? DateTime.now().subtract(const Duration(days: 30)),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
startDate = picked;
});
_updateDateControllers();
_notifyFiltersChanged();
}
},
),
const SizedBox(width: 8),
],
),
),
readOnly: false,
onChanged: (value) {
// Valider et parser la date au format JJ/MM/AAAA
if (value.length == 10) {
final parts = value.split('/');
if (parts.length == 3) {
try {
final day = int.parse(parts[0]);
final month = int.parse(parts[1]);
final year = int.parse(parts[2]);
final date = DateTime(year, month, day);
// Vérifier que la date est valide
if (date.year >= 2020 && date.isBefore(DateTime.now().add(const Duration(days: 1)))) {
setState(() {
startDate = date;
});
_notifyFiltersChanged();
}
} catch (e) {
// Date invalide, ignorer
}
}
} else if (value.isEmpty) {
setState(() {
startDate = null;
});
_notifyFiltersChanged();
}
},
),
),
const SizedBox(width: 12),
// Date de fin
Expanded(
child: TextFormField(
controller: _endDateController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
hintText: 'Fin',
isDense: true,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_endDateController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
setState(() {
endDate = null;
_endDateController.clear();
});
_notifyFiltersChanged();
},
),
IconButton(
icon: const Icon(Icons.calendar_today, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: endDate ?? DateTime.now(),
firstDate: startDate ?? DateTime(2020),
lastDate: DateTime.now(),
locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
endDate = picked;
});
_updateDateControllers();
_notifyFiltersChanged();
}
},
),
const SizedBox(width: 8),
],
),
),
readOnly: false,
onChanged: (value) {
// Valider et parser la date au format JJ/MM/AAAA
if (value.length == 10) {
final parts = value.split('/');
if (parts.length == 3) {
try {
final day = int.parse(parts[0]);
final month = int.parse(parts[1]);
final year = int.parse(parts[2]);
final date = DateTime(year, month, day);
// Vérifier que la date est valide et après la date de début si définie
if (date.year >= 2020 &&
date.isBefore(DateTime.now().add(const Duration(days: 1))) &&
(startDate == null || date.isAfter(startDate!) || date.isAtSameMomentAs(startDate!))) {
setState(() {
endDate = date;
});
_notifyFiltersChanged();
}
} catch (e) {
// Date invalide, ignorer
}
}
} else if (value.isEmpty) {
setState(() {
endDate = null;
});
_notifyFiltersChanged();
}
},
),
),
],
),
const SizedBox(height: 12),
// Quatrième ligne : Recherche et actions
Row(
children: [
// Barre de recherche
Expanded(
flex: 3,
child: TextFormField(
controller: _searchController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
hintText: '...',
prefixIcon: Icon(Icons.search, size: 20),
isDense: true,
),
onChanged: (String value) {
setState(() {
_searchQuery = value;
});
_notifyFiltersChanged();
},
),
),
const SizedBox(width: 12),
// Bouton Réinitialiser (adaptatif)
isDesktop
? OutlinedButton.icon(
onPressed: () {
setState(() {
_selectedTypeFilter = 'Tous les types';
_selectedPaymentFilter = 'Tous les règlements';
_selectedPaymentTypeId = null;
_selectedSectorId = null;
_selectedUserId = null;
_searchQuery = '';
startDate = null;
endDate = null;
_searchController.clear();
_startDateController.clear();
_endDateController.clear();
});
_notifyFiltersChanged();
},
icon: const Icon(Icons.filter_alt_off, size: 18),
label: const Text('Réinitialiser'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
)
: IconButton(
onPressed: () {
setState(() {
_selectedTypeFilter = 'Tous les types';
_selectedPaymentFilter = 'Tous les règlements';
_selectedPaymentTypeId = null;
_selectedSectorId = null;
_selectedUserId = null;
_searchQuery = '';
startDate = null;
endDate = null;
_searchController.clear();
_startDateController.clear();
_endDateController.clear();
});
_notifyFiltersChanged();
},
icon: const Icon(Icons.filter_alt_off, size: 20),
tooltip: 'Réinitialiser les filtres',
style: IconButton.styleFrom(
side: BorderSide(color: Theme.of(context).colorScheme.outline),
),
),
],
),
],
),
),
);
}
// Mettre à jour les contrôleurs de date
void _updateDateControllers() {
if (startDate != null) {
_startDateController.text = '${startDate!.day.toString().padLeft(2, '0')}/${startDate!.month.toString().padLeft(2, '0')}/${startDate!.year}';
}
if (endDate != null) {
_endDateController.text = '${endDate!.day.toString().padLeft(2, '0')}/${endDate!.month.toString().padLeft(2, '0')}/${endDate!.year}';
}
}
// Sauvegarder le filtre membre dans Hive (pour les admins)
void _saveMemberFilter(int memberId) async {
if (!isAdmin) return; // Seulement pour les admins
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.put('selectedMemberId', memberId);
await settingsBox.put('history_selectedMemberId', memberId);
debugPrint('HistoryPage: MemberId $memberId sauvegardé dans Hive');
} catch (e) {
debugPrint('Erreur lors de la sauvegarde du filtre membre: $e');
}
}
// Charger l'état de la section graphiques depuis Hive
void _loadGraphicsExpandedState() async {
try {
// Définir l'état par défaut selon la taille d'écran (mobile = rétracté)
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 800;
_isGraphicsExpanded = !isMobile; // true sur desktop, false sur mobile
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
final saved = settingsBox.get('history_graphics_expanded');
if (saved != null && saved is bool) {
setState(() {
_isGraphicsExpanded = saved;
});
debugPrint('HistoryPage: État graphics chargé depuis Hive: $_isGraphicsExpanded');
}
} catch (e) {
debugPrint('Erreur lors du chargement de l\'état graphics: $e');
}
}
// Sauvegarder l'état de la section graphiques dans Hive
void _saveGraphicsExpandedState() async {
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.put('history_graphics_expanded', _isGraphicsExpanded);
debugPrint('HistoryPage: État graphics sauvegardé: $_isGraphicsExpanded');
} catch (e) {
debugPrint('Erreur lors de la sauvegarde de l\'état graphics: $e');
}
}
// Charger les filtres présélectionnés depuis Hive
void _loadPreselectedFilters() async {
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
// Charger le membre sélectionné (admins seulement)
if (isAdmin) {
final memberId = settingsBox.get('history_selectedMemberId') ??
settingsBox.get('selectedMemberId');
if (memberId != null && memberId is int) {
setState(() {
selectedMemberId = memberId;
_selectedUserId = memberId; // Synchroniser avec le nouveau filtre
});
debugPrint('HistoryPage: Membre présélectionné chargé: $memberId');
}
}
// Charger le secteur sélectionné
final sectorId = settingsBox.get('history_selectedSectorId');
if (sectorId != null && sectorId is int) {
setState(() {
selectedSectorId = sectorId;
_selectedSectorId = sectorId; // Synchroniser avec le nouveau filtre
});
debugPrint('HistoryPage: Secteur présélectionné chargé: $sectorId');
}
// Charger le type de passage sélectionné
final typeId = settingsBox.get('history_selectedTypeId');
if (typeId != null && typeId is int) {
setState(() {
selectedTypeId = typeId;
final typeInfo = AppKeys.typesPassages[typeId];
selectedType = typeInfo != null ? typeInfo['titre'] as String : 'Inconnu';
// Synchroniser avec le nouveau filtre
if (typeInfo != null) {
_selectedTypeFilter = typeInfo['titre'] as String;
}
});
debugPrint('HistoryPage: Type de passage présélectionné: $typeId');
}
// Charger le type de règlement sélectionné
final paymentTypeId = settingsBox.get('history_selectedPaymentTypeId');
if (paymentTypeId != null && paymentTypeId is int) {
setState(() {
selectedPaymentTypeId = paymentTypeId;
_selectedPaymentTypeId = paymentTypeId; // Synchroniser avec le nouveau filtre
// Mettre à jour aussi le label du filtre
final paymentInfo = AppKeys.typesReglements[paymentTypeId];
if (paymentInfo != null) {
_selectedPaymentFilter = paymentInfo['titre'] as String;
}
});
debugPrint('HistoryPage: Type de règlement présélectionné: $paymentTypeId');
}
// Charger les dates de période si disponibles
final startDateMs = settingsBox.get('history_startDate');
final endDateMs = settingsBox.get('history_endDate');
if (startDateMs != null && startDateMs is int) {
setState(() {
startDate = DateTime.fromMillisecondsSinceEpoch(startDateMs);
});
debugPrint('HistoryPage: Date de début chargée: $startDate');
}
if (endDateMs != null && endDateMs is int) {
setState(() {
endDate = DateTime.fromMillisecondsSinceEpoch(endDateMs);
});
debugPrint('HistoryPage: Date de fin chargée: $endDate');
}
} catch (e) {
debugPrint('Erreur lors du chargement des filtres: $e');
}
}
// Initialiser les listes de filtres
void _initializeFilters() {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
// Charger les secteurs
if (isAdmin) {
// Admin : tous les secteurs
_sectors = sectorRepository.getAllSectors();
} else {
// User : seulement ses secteurs assignés
final userSectors = userRepository.getUserSectors();
final userSectorIds = userSectors.map((us) => us.id).toSet();
_sectors = sectorRepository.getAllSectors()
.where((s) => userSectorIds.contains(s.id))
.toList();
// Calculer les statistiques pour l'utilisateur
_totalSectors = _sectors.length;
// Compter les membres partageant les mêmes secteurs
final allUserSectors = userRepository.getUserSectors();
final sharedMembers = <int>{};
for (final userSector in allUserSectors) {
if (userSectorIds.contains(userSector.id) && userSector.id != currentUserId) {
sharedMembers.add(userSector.id);
}
}
_sharedMembersCount = sharedMembers.length;
}
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
// Charger les membres (admin seulement)
if (isAdmin) {
_membres = membreRepository.getAllMembres();
debugPrint('Nombre de membres récupérés: ${_membres.length}');
// Convertir les membres en users pour le filtre
_users = _convertMembresToUsers();
debugPrint('Nombre d\'utilisateurs pour le filtre: ${_users.length}');
}
// Charger les passages
final currentOperation = userRepository.getCurrentOperation();
if (currentOperation != null) {
if (isAdmin) {
// Admin : tous les passages de l'opération
_originalPassages = passageRepository.getPassagesByOperation(currentOperation.id);
} else {
// User : logique spéciale selon le type de passage
final allPassages = passageRepository.getPassagesByOperation(currentOperation.id);
_originalPassages = allPassages.where((p) {
// Type 2 (À finaliser) : afficher TOUS les passages
if (p.fkType == 2) {
return true;
}
// Autres types : seulement les passages de l'utilisateur
return p.fkUser == currentUserId;
}).toList();
}
debugPrint('Nombre de passages récupérés: ${_originalPassages.length}');
// Initialiser les passages filtrés avec tous les passages
_filteredPassages = List.from(_originalPassages);
// Appliquer les filtres initiaux après le chargement
_notifyFiltersChanged();
}
setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Erreur lors du chargement des données: $e';
});
debugPrint('Erreur lors de l\'initialisation des filtres: $e');
}
}
// Convertir les MembreModel en UserModel pour le filtre (admin seulement)
List<UserModel> _convertMembresToUsers() {
final users = <UserModel>[];
for (final membre in _membres) {
// Utiliser l'ID du membre pour récupérer l'utilisateur associé
final user = userRepository.getUserById(membre.id);
if (user != null) {
// Si l'utilisateur existe, copier avec le sectName du membre
users.add(user.copyWith(
sectName: membre.sectName ?? user.sectName,
));
} else {
// Créer un UserModel temporaire si l'utilisateur n'existe pas
users.add(UserModel(
id: membre.id,
username: membre.username ?? 'membre_${membre.id}',
name: membre.name,
firstName: membre.firstName,
email: membre.email,
role: membre.role,
isActive: membre.isActive,
createdAt: membre.createdAt,
lastSyncedAt: DateTime.now(),
sectName: membre.sectName,
));
}
}
// Trier par nom complet
users.sort((a, b) {
final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim().toLowerCase();
final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim().toLowerCase();
return nameA.compareTo(nameB);
});
return users;
}
@override
Widget build(BuildContext context) {
// Le contenu sans scaffold (AppScaffold est déjà dans HistoryPage)
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_errorMessage.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(_errorMessage, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeFilters,
child: const Text('Réessayer'),
),
],
),
);
}
return _buildContent();
}
Widget _buildContent() {
// Titre unique pour tous
const pageTitle = 'Historique des passages';
// Statistiques pour les users
final statsText = !isAdmin
? '$_totalSectors secteur${_totalSectors > 1 ? 's' : ''} | $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''} en partage'
: null;
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
vertical: AppTheme.spacingL,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec titre
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre
Text(
pageTitle,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (statsText != null) ...[
const SizedBox(height: 8),
Text(
statsText,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
],
),
const SizedBox(height: 16),
// 1. Card de filtres intégrée
_buildFiltersCard(),
const SizedBox(height: 16),
// 2. Section graphiques (rétractable)
_buildGraphicsSection(),
const SizedBox(height: 16),
// 3. Liste des passages avec hauteur maximale
Card(
elevation: 2,
child: Container(
constraints: const BoxConstraints(
maxHeight: 700,
),
child: PassagesListWidget(
passages: _convertPassagesToMaps(),
showActions: true, // Actions disponibles pour tous (avec vérification des droits)
showAddButton: true, // Bouton + pour tous
onPassageEdit: _handlePassageEditMap, // Tous peuvent éditer (avec vérification)
onPassageDelete: canDeletePassages ? _handlePassageDeleteMap : null, // Selon permissions
onAddPassage: () async {
await _showPassageFormDialog(context);
},
),
),
),
],
),
),
);
}
// Construction de la section graphiques rétractable (pour intégration dans PassagesListWidget)
Widget _buildGraphicsSection() {
// final screenWidth = MediaQuery.of(context).size.width; // Non utilisé actuellement
return Card(
elevation: 0,
color: Colors.transparent,
child: Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
colorScheme: Theme.of(context).colorScheme.copyWith(
primary: AppTheme.primaryColor,
),
),
child: ExpansionTile(
title: Row(
children: [
Icon(Icons.analytics_outlined, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 8),
Text(
'Statistiques graphiques',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
subtitle: !_isGraphicsExpanded ? Text(
isAdmin ? "Tous les passages de l'opération" : "Mes passages de l'opération",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
) : null,
initiallyExpanded: _isGraphicsExpanded,
onExpansionChanged: (expanded) {
setState(() {
_isGraphicsExpanded = expanded;
});
_saveGraphicsExpandedState();
},
tilePadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
childrenPadding: const EdgeInsets.only(top: 0, bottom: 16.0),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGraphicsContent(),
],
),
),
);
}
// Contenu des statistiques adaptatif selon la taille d'écran
Widget _buildGraphicsContent() {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return Column(
children: [
// Graphiques en camembert (côte à côte sur desktop)
isDesktop
? Row(
children: [
Expanded(child: _buildPassageSummaryCard()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildPaymentSummaryCard()),
],
)
: Column(
children: [
_buildPassageSummaryCard(),
const SizedBox(height: AppTheme.spacingM),
_buildPaymentSummaryCard(),
],
),
const SizedBox(height: AppTheme.spacingL),
// Graphique d'activité
_buildActivityChart(),
],
);
}
// Graphique camembert des types de passage
Widget _buildPassageSummaryCard() {
// Calculer les données filtrées pour le graphique
final passagesByType = _calculatePassagesByType();
return PassageSummaryCard(
title: isAdmin ? 'Types de passage' : 'Mes types de passage',
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
useValueListenable: false, // Utiliser les données filtrées directement
passagesByType: passagesByType, // Passer les données calculées
showAllPassages: isAdmin,
userId: isAdmin ? _selectedUserId : currentUserId,
excludePassageTypes: const [], // Ne pas exclure "À finaliser" ici, c'est déjà filtré
isDesktop: MediaQuery.of(context).size.width > 800,
backgroundIcon: Icons.route,
backgroundIconColor: AppTheme.primaryColor,
backgroundIconOpacity: 0.07,
backgroundIconSize: 120,
);
}
// Calculer la répartition des passages par type depuis les données filtrées
Map<int, int> _calculatePassagesByType() {
final counts = <int, int>{};
for (final passage in _filteredPassages) {
final typeId = passage.fkType;
counts[typeId] = (counts[typeId] ?? 0) + 1;
}
return counts;
}
// Graphique camembert des modes de paiement
Widget _buildPaymentSummaryCard() {
// Calculer les données filtrées pour le graphique
final paymentsByType = _calculatePaymentsByType();
return PaymentSummaryCard(
title: isAdmin ? 'Modes de règlement' : 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.euro_symbol,
height: 300,
useValueListenable: false, // Utiliser les données filtrées directement
paymentsByType: paymentsByType, // Passer les données calculées
showAllPayments: isAdmin,
userId: isAdmin ? _selectedUserId : currentUserId,
isDesktop: MediaQuery.of(context).size.width > 800,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: AppTheme.accentColor,
backgroundIconOpacity: 0.07,
backgroundIconSize: 120,
);
}
// Calculer la répartition des montants par type de règlement depuis les données filtrées
Map<int, double> _calculatePaymentsByType() {
final amounts = <int, double>{};
for (final passage in _filteredPassages) {
// Récupérer le montant du passage
final montantStr = _safeString(passage.montant);
final montantDouble = double.tryParse(montantStr) ?? 0.0;
// Ne prendre en compte que les passages payés (montant > 0)
if (montantDouble > 0.0) {
// Utiliser le type de règlement pour catégoriser
final typeReglement = passage.fkTypeReglement;
amounts[typeReglement] = (amounts[typeReglement] ?? 0.0) + montantDouble;
}
// Les passages non payés sont ignorés (ne comptent ni dans le total ni dans le graphique)
}
return amounts;
}
// Graphique d'évolution temporelle des passages
Widget _buildActivityChart() {
// Calculer les données d'activité depuis les passages filtrés
final activityData = _calculateActivityData();
// Calculer le titre dynamique basé sur la plage de dates
final dateRange = _getDateRangeForActivityChart();
String title;
if (dateRange != null) {
final dateFormat = DateFormat('dd/MM/yyyy');
title = isAdmin
? 'Évolution des passages (${dateFormat.format(dateRange.start)} - ${dateFormat.format(dateRange.end)})'
: 'Évolution de mes passages (${dateFormat.format(dateRange.start)} - ${dateFormat.format(dateRange.end)})';
} else {
title = isAdmin
? 'Évolution des passages'
: 'Évolution de mes passages';
}
return Card(
elevation: 2,
child: ActivityChart(
height: 350,
useValueListenable: false, // Utiliser les données filtrées directement
passageData: activityData, // Passer les données calculées
showAllPassages: isAdmin,
title: title,
daysToShow: 0, // 0 = utiliser toutes les données fournies
userId: isAdmin ? _selectedUserId : currentUserId,
excludePassageTypes: const [], // Ne pas exclure ici, c'est déjà filtré
),
);
}
// Obtenir la plage de dates pour le graphique d'activité
DateTimeRange? _getDateRangeForActivityChart() {
// Si des dates sont explicitement définies par l'utilisateur, les utiliser
if (startDate != null || endDate != null) {
final start = startDate ?? DateTime.now().subtract(const Duration(days: 90));
final end = endDate ?? DateTime.now();
return DateTimeRange(start: start, end: end);
}
// Par défaut : utiliser les 90 derniers jours
final now = DateTime.now();
return DateTimeRange(
start: now.subtract(const Duration(days: 90)),
end: now,
);
}
// Calculer les données d'activité pour le graphique temporel
List<Map<String, dynamic>> _calculateActivityData() {
final data = <Map<String, dynamic>>[];
// Obtenir la plage de dates à afficher (ne peut plus être null avec la nouvelle logique)
final dateRange = _getDateRangeForActivityChart();
if (dateRange == null) {
// Fallback : retourner des données vides pour les 90 derniers jours
final now = DateTime.now();
final dateFormat = DateFormat('yyyy-MM-dd');
for (int i = 89; i >= 0; i--) {
final date = now.subtract(Duration(days: i));
data.add({
'date': dateFormat.format(date),
'type_passage': 0,
'nb': 0,
});
}
return data;
}
final dateFormat = DateFormat('yyyy-MM-dd');
final dataByDate = <String, Map<int, int>>{};
// Parcourir les passages filtrés pour compter par date et type
// Exclure les passages de type 2 (À finaliser) du graphique
for (final passage in _filteredPassages) {
// Exclure les passages de type 2
if (passage.fkType == 2) {
continue;
}
if (passage.passedAt != null) {
// Ne compter que les passages dans la plage de dates
if (passage.passedAt!.isBefore(dateRange.start) ||
passage.passedAt!.isAfter(dateRange.end.add(const Duration(days: 1)))) {
continue;
}
final dateKey = dateFormat.format(passage.passedAt!);
if (!dataByDate.containsKey(dateKey)) {
dataByDate[dateKey] = {};
}
final typeId = passage.fkType;
dataByDate[dateKey]![typeId] = (dataByDate[dateKey]![typeId] ?? 0) + 1;
}
}
// Calculer le nombre de jours dans la plage
final daysDiff = dateRange.end.difference(dateRange.start).inDays + 1;
// Limiter l'affichage à 90 jours maximum pour la lisibilité
final daysToShow = daysDiff > 90 ? 90 : daysDiff;
final startDate = daysDiff > 90
? dateRange.end.subtract(Duration(days: 89))
: dateRange.start;
// Créer une entrée pour chaque jour de la plage
for (int i = 0; i < daysToShow; i++) {
final date = startDate.add(Duration(days: i));
final dateKey = dateFormat.format(date);
final passagesByType = dataByDate[dateKey] ?? {};
// Ajouter les données pour chaque type de passage présent ce jour (sauf type 2)
bool hasDataForDay = false;
if (passagesByType.isNotEmpty) {
for (final entry in passagesByType.entries) {
if (entry.key != 2) { // Double vérification pour exclure type 2
data.add({
'date': dateKey,
'type_passage': entry.key,
'nb': entry.value,
});
hasDataForDay = true;
}
}
}
// Si aucune donnée pour ce jour, ajouter une entrée vide pour maintenir la continuité
if (!hasDataForDay) {
data.add({
'date': dateKey,
'type_passage': 0,
'nb': 0,
});
}
}
return data;
}
// Afficher le formulaire de création de passage (users seulement)
Future<void> _showPassageFormDialog(BuildContext context) async {
await showDialog<PassageModel>(
context: context,
builder: (context) => PassageFormDialog(
title: 'Nouveau passage',
readOnly: false,
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
amicaleRepository: amicaleRepository,
onSuccess: () {
_initializeFilters(); // Recharger les données
},
),
);
}
// Méthode helper pour convertir une valeur potentiellement null en String
String _safeString(dynamic value) {
if (value == null) return '';
return value.toString();
}
// Convertir les passages en Map pour PassagesListWidget
List<Map<String, dynamic>> _convertPassagesToMaps() {
try {
// Utiliser les passages filtrés (filtrage déjà appliqué par _notifyFiltersChanged)
// Ne PAS filtrer par passedAt ici car les passages "À finaliser" n'ont pas de date
var passages = _filteredPassages.toList();
// Convertir chaque passage en Map
final maps = <Map<String, dynamic>>[];
for (final passage in passages) {
try {
// Construire l'adresse complète
final fullAddress = '${_safeString(passage.numero)} ${_safeString(passage.rue)} ${_safeString(passage.ville)}'.trim();
// Convertir le montant en double (normalement un String dans PassageModel)
double? montantDouble = 0.0;
final montantStr = _safeString(passage.montant);
if (montantStr.isNotEmpty) {
montantDouble = double.tryParse(montantStr) ?? 0.0;
}
// Gestion de la date selon le type de passage
// Pour les passages "À finaliser" (type 2), la date peut être null
DateTime? passageDate = passage.passedAt;
final map = <String, dynamic>{
'id': passage.id,
'fk_operation': passage.fkOperation,
'fk_sector': passage.fkSector,
'fk_user': passage.fkUser,
'fkUser': passage.fkUser,
'type': passage.fkType,
'fk_type': passage.fkType,
'payment': passage.fkTypeReglement,
'fk_type_reglement': passage.fkTypeReglement,
'fk_adresse': _safeString(passage.fkAdresse),
'address': fullAddress.isNotEmpty ? fullAddress : 'Adresse non renseignée',
'date': passageDate,
'datePassage': passageDate, // Ajouter aussi datePassage pour compatibilité
'passed_at': passage.passedAt?.toIso8601String(),
'passedAt': passageDate, // Ajouter aussi passedAt comme DateTime
'numero': _safeString(passage.numero),
'rue': _safeString(passage.rue),
'rue_bis': _safeString(passage.rueBis),
'ville': _safeString(passage.ville),
'residence': _safeString(passage.residence),
'fk_habitat': passage.fkHabitat,
'appt': _safeString(passage.appt),
'niveau': _safeString(passage.niveau),
'gps_lat': _safeString(passage.gpsLat),
'gps_lng': _safeString(passage.gpsLng),
'nom_recu': _safeString(passage.nomRecu),
'remarque': _safeString(passage.remarque),
'notes': _safeString(passage.remarque),
'montant': _safeString(passage.montant),
'amount': montantDouble,
'email_erreur': _safeString(passage.emailErreur),
'nb_passages': passage.nbPassages,
'name': _safeString(passage.name),
'email': _safeString(passage.email),
'phone': _safeString(passage.phone),
'fullAddress': fullAddress,
};
maps.add(map);
} catch (e) {
debugPrint('Erreur lors de la conversion du passage ${passage.id}: $e');
}
}
debugPrint('${maps.length} passages convertis avec succès');
return maps;
} catch (e) {
debugPrint('Erreur lors de la conversion des passages en Maps: $e');
return [];
}
}
// Gérer l'édition d'un passage depuis le Map
void _handlePassageEditMap(Map<String, dynamic> passageMap) {
final passageId = passageMap['id'] as int;
final passage = _originalPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => PassageModel.fromJson(passageMap),
);
// Vérifier les permissions :
// - Admin peut tout éditer
// - User peut éditer ses propres passages
// - Type 2 (À finaliser) : éditable par tous les utilisateurs
if (isAdmin || passage.fkUser == currentUserId || passage.fkType == 2) {
_handlePassageEdit(passage);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Vous ne pouvez éditer que vos propres passages'),
backgroundColor: Colors.orange,
),
);
}
}
// Gérer la suppression d'un passage depuis le Map
void _handlePassageDeleteMap(Map<String, dynamic> passageMap) {
final passageId = passageMap['id'] as int;
final passage = _originalPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => PassageModel.fromJson(passageMap),
);
// Vérifier les permissions : admin peut tout supprimer, user seulement ses propres passages si autorisé
if (isAdmin || (canDeletePassages && passage.fkUser == currentUserId)) {
_handlePassageDelete(passage);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
!canDeletePassages
? 'Vous n\'êtes pas autorisé à supprimer des passages'
: 'Vous ne pouvez supprimer que vos propres passages'
),
backgroundColor: Colors.orange,
),
);
}
}
// Sauvegarder les filtres dans Hive
// NOTE: Méthode non utilisée pour le moment - conservée pour référence future
// void _saveFiltersToHive() async {
// try {
// if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
// await Hive.openBox(AppKeys.settingsBoxName);
// }
// final settingsBox = Hive.box(AppKeys.settingsBoxName);
//
// // Sauvegarder tous les filtres
// await settingsBox.put('history_selectedSectorId', selectedSectorId);
// if (isAdmin) {
// await settingsBox.put('history_selectedMemberId', selectedMemberId);
// }
// await settingsBox.put('history_selectedTypeId', selectedTypeId);
// await settingsBox.put('history_selectedPaymentTypeId', selectedPaymentTypeId);
//
// // Sauvegarder les dates
// if (startDate != null) {
// await settingsBox.put('history_startDate', startDate!.millisecondsSinceEpoch);
// } else {
// await settingsBox.delete('history_startDate');
// }
// if (endDate != null) {
// await settingsBox.put('history_endDate', endDate!.millisecondsSinceEpoch);
// } else {
// await settingsBox.delete('history_endDate');
// }
//
// debugPrint('Filtres sauvegardés dans Hive');
// } catch (e) {
// debugPrint('Erreur lors de la sauvegarde des filtres: $e');
// }
// }
// Gérer l'édition d'un passage
void _handlePassageEdit(PassageModel passage) async {
await showDialog<PassageModel>(
context: context,
builder: (context) => PassageFormDialog(
passage: passage,
title: 'Modifier le passage',
readOnly: false,
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
amicaleRepository: amicaleRepository,
onSuccess: () {
_initializeFilters(); // Recharger les données
},
),
);
}
// Gérer la suppression d'un passage
void _handlePassageDelete(PassageModel passage) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Êtes-vous sûr de vouloir supprimer le passage chez ${passage.name} ?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
try {
await passageRepository.deletePassage(passage.id);
_initializeFilters(); // Recharger les données
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Passage supprimé avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la suppression: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
}