Files
geo/app/lib/presentation/pages/history_page.dart
pierre 2f5946a184 feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 18:26:27 +01:00

1787 lines
67 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/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:geosector_app/presentation/widgets/btn_passages.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> with SingleTickerProviderStateMixin {
// Détection du rôle et permissions
late final bool isAdmin;
late final int currentUserId; // users.id (table centrale)
late final int? currentOpeUserId; // ope_users.id (pour comparaisons avec passages)
late final bool canDeletePassages; // Permission de suppression pour les users
// TabController pour les onglets Filtres / Statistiques
late TabController _tabController;
// 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 = []; // Liste des membres de l'opération
// Passages originaux pour l'édition
List<PassageModel> _originalPassages = [];
List<PassageModel> _filteredPassages = [];
// État de chargement
bool _isLoading = true;
String _errorMessage = '';
// État de la section graphiques
bool _isGraphicsExpanded = true;
// Hauteur dynamique du TabBarView selon l'onglet actif
double _tabBarViewHeight = 280.0; // Hauteur par défaut (Filtres)
// Onglet précédemment sélectionné (pour détecter les clics sur le même onglet)
int _previousTabIndex = 0;
// Listener pour les changements de secteur depuis map_page
late final Box _settingsBox;
@override
void initState() {
super.initState();
// Initialiser le TabController (2 onglets)
_tabController = TabController(length: 2, vsync: this);
// 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;
currentOpeUserId = currentUser?.opeUserId; // ID dans ope_users pour comparaisons avec passages
// 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() {
_tabController.dispose();
_startDateController.dispose();
_endDateController.dispose();
_searchController.dispose();
// Pas besoin de fermer _settingsBox car c'est une box partagée
super.dispose();
}
// Callback pour gérer les clics sur les onglets
void _onTabTapped(int index) {
setState(() {
// Si on clique sur le même onglet alors que l'ExpansionTile est ouvert → le fermer
if (index == _previousTabIndex && _isGraphicsExpanded) {
_isGraphicsExpanded = false;
_saveGraphicsExpandedState();
}
// Sinon, ouvrir l'ExpansionTile et ajuster la hauteur
else {
if (!_isGraphicsExpanded) {
_isGraphicsExpanded = true;
_saveGraphicsExpandedState();
}
// Onglet 0 = Filtres (hauteur plus petite)
// Onglet 1 = Statistiques (hauteur plus grande)
_tabBarViewHeight = index == 0 ? 280.0 : 800.0;
}
_previousTabIndex = index;
});
}
// Callback pour gérer les clics sur les boutons de type de passage
void _handleTypeSelected(int? typeId) {
setState(() {
// Réinitialiser tous les filtres
_selectedPaymentFilter = 'Tous les règlements';
_selectedPaymentTypeId = null;
selectedPaymentTypeId = null;
startDate = null;
endDate = null;
_startDateController.clear();
_endDateController.clear();
_searchQuery = '';
_searchController.clear();
_selectedSectorId = null;
selectedSectorId = null;
if (isAdmin) {
_selectedUserId = null;
selectedMemberId = null;
}
// Appliquer le filtre de type
if (typeId == null) {
// Tous les passages
_selectedTypeFilter = 'Tous les types';
selectedTypeId = null;
} else {
// Type spécifique
selectedTypeId = typeId;
final typeInfo = AppKeys.typesPassages[typeId];
if (typeInfo != null) {
_selectedTypeFilter = typeInfo['titre'] as String;
}
}
});
// Appliquer les filtres
_notifyFiltersChanged();
}
// 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 section TabBar + ExpansionTile
Widget _buildTabBarSection() {
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(
key: ValueKey('expansion_tile_$_isGraphicsExpanded'),
initiallyExpanded: _isGraphicsExpanded,
trailing: const SizedBox.shrink(), // Masquer la flèche d'expansion
onExpansionChanged: (expanded) {
setState(() {
_isGraphicsExpanded = expanded;
// Réinitialiser _previousTabIndex quand on ferme manuellement
// pour permettre de rouvrir en cliquant sur l'onglet actif
if (!expanded) {
_previousTabIndex = -1;
}
});
_saveGraphicsExpandedState();
},
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: TabBar(
controller: _tabController,
labelColor: AppTheme.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppTheme.primaryColor,
indicatorWeight: 3,
onTap: _onTabTapped,
tabs: const [
Tab(
icon: Icon(Icons.filter_list, size: 20),
text: 'Filtres',
),
Tab(
icon: Icon(Icons.analytics_outlined, size: 20),
text: 'Statistiques',
),
],
),
),
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: _tabBarViewHeight,
child: TabBarView(
controller: _tabController,
children: [
// Onglet 1 : Filtres
_buildFiltersContent(),
// Onglet 2 : Statistiques
_buildGraphicsContent(),
],
),
),
],
),
),
);
}
/// Construire le contenu des filtres (ancien _buildFiltersCard sans la Card)
Widget _buildFiltersContent() {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return SingleChildScrollView(
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'),
),
..._membres.map((MembreModel membre) {
return DropdownMenuItem<int?>(
value: membre.opeUserId,
child: Text('${membre.firstName ?? ''} ${membre.name ?? ''}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}'),
);
}),
],
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) {
// Réinitialiser TOUS les filtres avant d'appliquer le type
setState(() {
// Réinitialiser les filtres de type et paiement
_selectedPaymentFilter = 'Tous les règlements';
_selectedPaymentTypeId = null;
selectedPaymentTypeId = null;
// Réinitialiser les dates
startDate = null;
endDate = null;
_startDateController.clear();
_endDateController.clear();
// Réinitialiser la recherche
_searchQuery = '';
_searchController.clear();
// Réinitialiser le secteur
_selectedSectorId = null;
selectedSectorId = null;
// Réinitialiser le membre (admin seulement)
if (isAdmin) {
_selectedUserId = null;
selectedMemberId = null;
}
// Appliquer le type de passage sélectionné
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;
}
});
// Supprimer le typeId de Hive après l'avoir utilisé
settingsBox.delete('history_selectedTypeId');
debugPrint('HistoryPage: Type de passage présélectionné: $typeId (tous les autres filtres réinitialisés)');
}
// 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();
}
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
// Charger les membres de l'opération (admin seulement)
if (isAdmin) {
// Charger directement depuis MembreModel (déjà unique, pas de déduplication nécessaire)
final membreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
_membres = membreBox.values.whereType<MembreModel>().toList();
debugPrint('Nombre de membres de l\'opération récupérés: ${_membres.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 == currentOpeUserId;
}).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');
}
}
@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() {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
vertical: AppTheme.spacingL,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 0. BtnPassages
BtnPassages(
onTypeSelected: _handleTypeSelected,
selectedTypeId: selectedTypeId,
),
const SizedBox(height: AppTheme.spacingL),
// 1. TabBar + ExpansionTile (Filtres / Statistiques) - FIXE EN HAUT
_buildTabBarSection(),
SizedBox(height: _isGraphicsExpanded ? 8 : 16),
// 2. Liste des passages - EXPANDED pour prendre tout l'espace restant
Expanded(
child: Card(
elevation: 2,
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);
},
filteredPassageType: _selectedTypeFilter != 'Tous les types' ? _selectedTypeFilter : null,
),
),
),
],
),
);
}
// Contenu des statistiques adaptatif selon la taille d'écran
Widget _buildGraphicsContent() {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
return SingleChildScrollView(
child: 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 == currentOpeUserId || 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 == currentOpeUserId)) {
_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,
),
);
}
}
}
}
}