feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/chat/chat_module.dart';
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
|
||||
class UserCommunicationPage extends StatefulWidget {
|
||||
const UserCommunicationPage({super.key});
|
||||
|
||||
@override
|
||||
State<UserCommunicationPage> createState() => _UserCommunicationPageState();
|
||||
}
|
||||
|
||||
class _UserCommunicationPageState extends State<UserCommunicationPage> {
|
||||
bool _isChatInitialized = false;
|
||||
bool _isInitializing = false;
|
||||
String? _initError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeChat();
|
||||
}
|
||||
|
||||
Future<void> _initializeChat() async {
|
||||
if (_isInitializing) return;
|
||||
|
||||
setState(() {
|
||||
_isInitializing = true;
|
||||
_initError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
// Récupérer les informations utilisateur
|
||||
final currentUser = CurrentUserService.instance;
|
||||
final apiService = ApiService.instance;
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
if (currentUser.currentUser == null) {
|
||||
throw Exception('Utilisateur non connecté');
|
||||
}
|
||||
|
||||
// Initialiser le module chat avec les informations de l'utilisateur
|
||||
await ChatModule.init(
|
||||
apiUrl: apiService.baseUrl,
|
||||
userId: currentUser.currentUser!.id,
|
||||
userName: currentUser.userName ?? currentUser.userEmail ?? 'Utilisateur',
|
||||
userRole: currentUser.currentUser!.role,
|
||||
userEntite: currentUser.fkEntite ?? currentAmicale?.id,
|
||||
authToken: currentUser.sessionId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isChatInitialized = true;
|
||||
_isInitializing = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_initError = e.toString();
|
||||
_isInitializing = false;
|
||||
});
|
||||
debugPrint('Erreur initialisation chat: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Container(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 1,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: _buildContent(theme),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ThemeData theme) {
|
||||
if (_isInitializing) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Initialisation du chat...',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_initError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur d\'initialisation',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_initError!,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initializeChat,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_isChatInitialized) {
|
||||
// Afficher directement le module chat
|
||||
return ChatModule.getRoomsPage();
|
||||
}
|
||||
|
||||
// État initial
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 80,
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Chat non initialisé',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _initializeChat,
|
||||
child: const Text('Initialiser le chat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Ne pas disposer le chat ici car il est partagé
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import 'package:geosector_app/presentation/widgets/badged_navigation_destination
|
||||
import 'user_dashboard_home_page.dart';
|
||||
import 'user_statistics_page.dart';
|
||||
import 'user_history_page.dart';
|
||||
import 'user_communication_page.dart';
|
||||
import '../chat/chat_communication_page.dart';
|
||||
import 'user_map_page.dart';
|
||||
import 'user_field_mode_page.dart';
|
||||
|
||||
@@ -36,7 +36,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
|
||||
const UserDashboardHomePage(),
|
||||
const UserStatisticsPage(),
|
||||
const UserHistoryPage(),
|
||||
const UserCommunicationPage(),
|
||||
const ChatCommunicationPage(),
|
||||
const UserMapPage(),
|
||||
const UserFieldModePage(),
|
||||
];
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
// Pour accéder aux instances globales
|
||||
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/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
||||
|
||||
class UserHistoryPage extends StatefulWidget {
|
||||
const UserHistoryPage({super.key});
|
||||
@@ -37,11 +39,206 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
// État du tri actuel
|
||||
PassageSortType _currentSort = PassageSortType.dateDesc;
|
||||
|
||||
// État des filtres
|
||||
String selectedSector = 'Tous';
|
||||
String selectedPeriod = 'Tous';
|
||||
String selectedType = 'Tous';
|
||||
String selectedPaymentMethod = 'Tous';
|
||||
DateTimeRange? selectedDateRange;
|
||||
|
||||
// IDs pour les filtres
|
||||
int? selectedSectorId;
|
||||
|
||||
// Repository pour les secteurs
|
||||
late SectorRepository _sectorRepository;
|
||||
|
||||
// Liste des secteurs disponibles pour l'utilisateur
|
||||
List<SectorModel> _userSectors = [];
|
||||
|
||||
// Box des settings pour sauvegarder les préférences
|
||||
late Box _settingsBox;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Charger les passages depuis la box Hive au démarrage
|
||||
_loadPassages();
|
||||
// Initialiser le repository
|
||||
_sectorRepository = sectorRepository;
|
||||
// Initialiser les settings et charger les données
|
||||
_initSettingsAndLoad();
|
||||
}
|
||||
|
||||
// Initialiser les settings et charger les préférences
|
||||
Future<void> _initSettingsAndLoad() async {
|
||||
try {
|
||||
// Ouvrir la box des settings
|
||||
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
_settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
|
||||
} else {
|
||||
_settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
}
|
||||
|
||||
// Charger les préférences présélectionnées
|
||||
_loadPreselectedFilters();
|
||||
|
||||
// Charger les secteurs de l'utilisateur
|
||||
_loadUserSectors();
|
||||
|
||||
// Charger les passages
|
||||
await _loadPassages();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de l\'initialisation: $e');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'Erreur lors de l\'initialisation: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les secteurs de l'utilisateur
|
||||
void _loadUserSectors() {
|
||||
try {
|
||||
// Récupérer l'ID de l'utilisateur courant
|
||||
final currentUserId = userRepository.getCurrentUser()?.id;
|
||||
|
||||
if (currentUserId != null) {
|
||||
// Récupérer tous les secteurs
|
||||
final allSectors = _sectorRepository.getAllSectors();
|
||||
|
||||
// Filtrer les secteurs où l'utilisateur a des passages
|
||||
final userSectorIds = <int>{};
|
||||
final allPassages = passageRepository.passages;
|
||||
|
||||
for (var passage in allPassages) {
|
||||
if (passage.fkUser == currentUserId && passage.fkSector != null) {
|
||||
userSectorIds.add(passage.fkSector!);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les secteurs correspondants
|
||||
_userSectors = allSectors.where((sector) => userSectorIds.contains(sector.id)).toList();
|
||||
|
||||
debugPrint('Nombre de secteurs pour l\'utilisateur: ${_userSectors.length}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des secteurs utilisateur: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les filtres présélectionnés depuis Hive
|
||||
void _loadPreselectedFilters() {
|
||||
try {
|
||||
// Charger le secteur présélectionné
|
||||
final int? preselectedSectorId = _settingsBox.get('history_selectedSectorId');
|
||||
final String? preselectedSectorName = _settingsBox.get('history_selectedSectorName');
|
||||
final int? preselectedTypeId = _settingsBox.get('history_selectedTypeId');
|
||||
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
|
||||
final int? preselectedPaymentId = _settingsBox.get('history_selectedPaymentId');
|
||||
|
||||
if (preselectedSectorId != null && preselectedSectorName != null) {
|
||||
selectedSectorId = preselectedSectorId;
|
||||
selectedSector = preselectedSectorName;
|
||||
debugPrint('Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)');
|
||||
}
|
||||
|
||||
if (preselectedTypeId != null) {
|
||||
selectedType = preselectedTypeId.toString();
|
||||
debugPrint('Type de passage présélectionné: $preselectedTypeId');
|
||||
}
|
||||
|
||||
if (preselectedPeriod != null) {
|
||||
selectedPeriod = preselectedPeriod;
|
||||
_updatePeriodFilter(preselectedPeriod);
|
||||
debugPrint('Période présélectionnée: $preselectedPeriod');
|
||||
}
|
||||
|
||||
if (preselectedPaymentId != null) {
|
||||
selectedPaymentMethod = preselectedPaymentId.toString();
|
||||
debugPrint('Mode de règlement présélectionné: $preselectedPaymentId');
|
||||
}
|
||||
|
||||
// Nettoyer les valeurs après utilisation
|
||||
_settingsBox.delete('history_selectedSectorId');
|
||||
_settingsBox.delete('history_selectedSectorName');
|
||||
_settingsBox.delete('history_selectedTypeId');
|
||||
_settingsBox.delete('history_selectedPeriod');
|
||||
_settingsBox.delete('history_selectedPaymentId');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des filtres présélectionnés: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Sauvegarder les préférences de filtres
|
||||
void _saveFilterPreferences() {
|
||||
try {
|
||||
if (selectedSectorId != null) {
|
||||
_settingsBox.put('history_selectedSectorId', selectedSectorId);
|
||||
_settingsBox.put('history_selectedSectorName', selectedSector);
|
||||
}
|
||||
|
||||
if (selectedType != 'Tous') {
|
||||
final typeId = int.tryParse(selectedType);
|
||||
if (typeId != null) {
|
||||
_settingsBox.put('history_selectedTypeId', typeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedPeriod != 'Tous') {
|
||||
_settingsBox.put('history_selectedPeriod', selectedPeriod);
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod != 'Tous') {
|
||||
final paymentId = int.tryParse(selectedPaymentMethod);
|
||||
if (paymentId != null) {
|
||||
_settingsBox.put('history_selectedPaymentId', paymentId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la sauvegarde des préférences: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le filtre par secteur
|
||||
void _updateSectorFilter(String sectorName, int? sectorId) {
|
||||
setState(() {
|
||||
selectedSector = sectorName;
|
||||
selectedSectorId = sectorId;
|
||||
});
|
||||
_saveFilterPreferences();
|
||||
}
|
||||
|
||||
// Mettre à jour le filtre par période
|
||||
void _updatePeriodFilter(String period) {
|
||||
setState(() {
|
||||
selectedPeriod = period;
|
||||
|
||||
// Mettre à jour la plage de dates en fonction de la période
|
||||
final DateTime now = DateTime.now();
|
||||
|
||||
switch (period) {
|
||||
case 'Derniers 15 jours':
|
||||
selectedDateRange = DateTimeRange(
|
||||
start: now.subtract(const Duration(days: 15)),
|
||||
end: now,
|
||||
);
|
||||
break;
|
||||
case 'Dernière semaine':
|
||||
selectedDateRange = DateTimeRange(
|
||||
start: now.subtract(const Duration(days: 7)),
|
||||
end: now,
|
||||
);
|
||||
break;
|
||||
case 'Dernier mois':
|
||||
selectedDateRange = DateTimeRange(
|
||||
start: DateTime(now.year, now.month - 1, now.day),
|
||||
end: now,
|
||||
);
|
||||
break;
|
||||
case 'Tous':
|
||||
selectedDateRange = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
_saveFilterPreferences();
|
||||
}
|
||||
|
||||
// Méthode pour charger les passages depuis le repository
|
||||
@@ -53,16 +250,15 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
|
||||
try {
|
||||
// Utiliser l'instance globale définie dans app.dart
|
||||
|
||||
// Utiliser la propriété passages qui gère déjà l'ouverture de la box
|
||||
final List<PassageModel> allPassages = passageRepository.passages;
|
||||
|
||||
debugPrint('Nombre total de passages dans la box: ${allPassages.length}');
|
||||
|
||||
// Ne plus filtrer les passages de type 2 - laisser le widget gérer le filtrage
|
||||
List<PassageModel> filtered = allPassages;
|
||||
// Filtrer les passages de l'utilisateur courant
|
||||
final currentUserId = userRepository.getCurrentUser()?.id;
|
||||
List<PassageModel> filtered = allPassages.where((p) => p.fkUser == currentUserId).toList();
|
||||
|
||||
debugPrint('Nombre total de passages disponibles: ${filtered.length}');
|
||||
debugPrint('Nombre de passages de l\'utilisateur: ${filtered.length}');
|
||||
|
||||
// Afficher la distribution des types de passages pour le débogage
|
||||
final Map<int, int> typeCount = {};
|
||||
@@ -73,96 +269,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
debugPrint('Type de passage $type: $count passages');
|
||||
});
|
||||
|
||||
// Afficher la plage de dates pour le débogage
|
||||
if (filtered.isNotEmpty) {
|
||||
// Trier par date pour trouver min et max (exclure les passages sans date)
|
||||
final sortedByDate =
|
||||
List<PassageModel>.from(filtered.where((p) => p.passedAt != null));
|
||||
if (sortedByDate.isNotEmpty) {
|
||||
sortedByDate.sort((a, b) => a.passedAt!.compareTo(b.passedAt!));
|
||||
|
||||
final DateTime minDate = sortedByDate.first.passedAt!;
|
||||
final DateTime maxDate = sortedByDate.last.passedAt!;
|
||||
|
||||
// Log détaillé pour débogage
|
||||
debugPrint(
|
||||
'Plage de dates des passages: ${minDate.toString()} à ${maxDate.toString()}');
|
||||
|
||||
// Afficher les 5 passages les plus anciens et les 5 plus récents pour débogage
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS ANCIENS ---');
|
||||
for (int i = 0; i < sortedByDate.length && i < 5; i++) {
|
||||
final p = sortedByDate[i];
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS RÉCENTS ---');
|
||||
for (int i = sortedByDate.length - 1;
|
||||
i >= 0 && i >= sortedByDate.length - 5;
|
||||
i--) {
|
||||
final p = sortedByDate[i];
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
|
||||
// Vérifier la distribution des passages par mois
|
||||
final Map<String, int> monthCount = {};
|
||||
for (var passage in filtered) {
|
||||
// Ignorer les passages sans date
|
||||
if (passage.passedAt != null) {
|
||||
final String monthKey =
|
||||
'${passage.passedAt!.year}-${passage.passedAt!.month.toString().padLeft(2, '0')}';
|
||||
monthCount[monthKey] = (monthCount[monthKey] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('\n--- DISTRIBUTION PAR MOIS ---');
|
||||
final sortedMonths = monthCount.keys.toList()..sort();
|
||||
for (var month in sortedMonths) {
|
||||
debugPrint('$month: ${monthCount[month]} passages');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir les modèles en Maps pour l'affichage avec gestion d'erreurs
|
||||
List<Map<String, dynamic>> passagesMap = [];
|
||||
for (var passage in filtered) {
|
||||
try {
|
||||
final Map<String, dynamic> passageMap =
|
||||
_convertPassageModelToMap(passage);
|
||||
passagesMap.add(passageMap);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la conversion du passage en map: $e');
|
||||
// Ignorer ce passage et continuer
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Nombre de passages après conversion: ${passagesMap.length}');
|
||||
|
||||
// Trier par date (plus récent en premier) avec gestion d'erreurs
|
||||
try {
|
||||
passagesMap.sort((a, b) {
|
||||
try {
|
||||
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la comparaison des dates: $e');
|
||||
return 0; // Garder l'ordre actuel en cas d'erreur
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du tri des passages: $e');
|
||||
// Continuer sans tri en cas d'erreur
|
||||
}
|
||||
|
||||
// Debug: vérifier la plage de dates après conversion et tri
|
||||
if (passagesMap.isNotEmpty) {
|
||||
debugPrint('\n--- PLAGE DE DATES APRÈS CONVERSION ET TRI ---');
|
||||
final firstDate = passagesMap.last['date'] as DateTime;
|
||||
final lastDate = passagesMap.first['date'] as DateTime;
|
||||
debugPrint('Premier passage: ${firstDate.toString()}');
|
||||
debugPrint('Dernier passage: ${lastDate.toString()}');
|
||||
}
|
||||
|
||||
// Calculer le nombre de secteurs uniques
|
||||
final Set<int> uniqueSectors = {};
|
||||
for (var passage in filtered) {
|
||||
@@ -174,18 +280,30 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
// Compter les membres partagés (autres membres dans la même amicale)
|
||||
int sharedMembers = 0;
|
||||
try {
|
||||
// Utiliser l'instance globale définie dans app.dart
|
||||
final currentUserId = userRepository.getCurrentUser()?.id;
|
||||
final allMembers = membreRepository.membres; // Utiliser la propriété membres
|
||||
|
||||
final allMembers = membreRepository.membres;
|
||||
// Compter les membres autres que l'utilisateur courant
|
||||
sharedMembers = allMembers.where((membre) => membre.id != currentUserId).length;
|
||||
|
||||
debugPrint('Nombre de membres partagés: $sharedMembers');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du comptage des membres: $e');
|
||||
}
|
||||
|
||||
// Convertir les modèles en Maps pour l'affichage
|
||||
List<Map<String, dynamic>> passagesMap = [];
|
||||
for (var passage in filtered) {
|
||||
try {
|
||||
final Map<String, dynamic> passageMap = _convertPassageModelToMap(passage);
|
||||
passagesMap.add(passageMap);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la conversion du passage en map: $e');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Nombre de passages après conversion: ${passagesMap.length}');
|
||||
|
||||
// Trier par date (plus récent en premier)
|
||||
passagesMap = _sortPassages(passagesMap);
|
||||
|
||||
setState(() {
|
||||
_convertedPassages = passagesMap;
|
||||
_totalSectors = uniqueSectors.length;
|
||||
@@ -200,139 +318,121 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
debugPrint(_errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les passages selon les critères sélectionnés
|
||||
List<Map<String, dynamic>> _getFilteredPassages(List<Map<String, dynamic>> passages) {
|
||||
return passages.where((passage) {
|
||||
// Filtrer par secteur
|
||||
if (selectedSectorId != null && passage['fkSector'] != selectedSectorId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par type
|
||||
if (selectedType != 'Tous') {
|
||||
final typeId = int.tryParse(selectedType);
|
||||
if (typeId != null && passage['type'] != typeId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par mode de règlement
|
||||
if (selectedPaymentMethod != 'Tous') {
|
||||
final paymentId = int.tryParse(selectedPaymentMethod);
|
||||
if (paymentId != null && passage['payment'] != paymentId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par période/date
|
||||
if (selectedDateRange != null && passage['date'] is DateTime) {
|
||||
final DateTime passageDate = passage['date'] as DateTime;
|
||||
if (passageDate.isBefore(selectedDateRange!.start) ||
|
||||
passageDate.isAfter(selectedDateRange!.end)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Convertir un modèle de passage en Map pour l'affichage avec gestion renforcée des erreurs
|
||||
// Convertir un modèle de passage en Map pour l'affichage
|
||||
Map<String, dynamic> _convertPassageModelToMap(PassageModel passage) {
|
||||
try {
|
||||
// Le passage ne peut pas être null en Dart non-nullable,
|
||||
// mais nous gardons cette structure pour faciliter la gestion des erreurs
|
||||
// Construire l'adresse complète
|
||||
String address = _buildFullAddress(passage);
|
||||
|
||||
// Construire l'adresse complète avec gestion des erreurs
|
||||
String address = 'Adresse non disponible';
|
||||
try {
|
||||
address = _buildFullAddress(passage);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la construction de l\'adresse: $e');
|
||||
}
|
||||
|
||||
// Convertir le montant en double avec sécurité
|
||||
// Convertir le montant en double
|
||||
double amount = 0.0;
|
||||
try {
|
||||
if (passage.montant.isNotEmpty) {
|
||||
amount = double.parse(passage.montant);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de conversion du montant: ${passage.montant}: $e');
|
||||
if (passage.montant.isNotEmpty) {
|
||||
amount = double.tryParse(passage.montant) ?? 0.0;
|
||||
}
|
||||
|
||||
// Récupérer la date avec gestion d'erreur
|
||||
DateTime date;
|
||||
try {
|
||||
date = passage.passedAt ?? DateTime.now();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération de la date: $e');
|
||||
date = DateTime.now();
|
||||
// Récupérer la date
|
||||
DateTime date = passage.passedAt ?? DateTime.now();
|
||||
|
||||
// Récupérer le type
|
||||
int type = passage.fkType;
|
||||
if (!AppKeys.typesPassages.containsKey(type)) {
|
||||
type = 1; // Type 1 par défaut (Effectué)
|
||||
}
|
||||
|
||||
// Récupérer le type avec gestion d'erreur
|
||||
int type;
|
||||
try {
|
||||
type = passage.fkType;
|
||||
// Si le type n'est pas dans les types connus, utiliser 1 comme valeur par défaut
|
||||
if (!AppKeys.typesPassages.containsKey(type)) {
|
||||
type = 1; // Type 1 par défaut (Effectué)
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération du type: $e');
|
||||
type = 1; // Type 1 par défaut
|
||||
// Récupérer le type de règlement
|
||||
int payment = passage.fkTypeReglement;
|
||||
if (!AppKeys.typesReglements.containsKey(payment)) {
|
||||
payment = 0; // Type de règlement inconnu
|
||||
}
|
||||
|
||||
// Récupérer le type de règlement avec gestion d'erreur
|
||||
int payment;
|
||||
try {
|
||||
payment = passage.fkTypeReglement;
|
||||
// Si le type de règlement n'est pas dans les types connus, utiliser 0 comme valeur par défaut
|
||||
if (!AppKeys.typesReglements.containsKey(payment)) {
|
||||
payment = 0; // Type de règlement inconnu
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération du type de règlement: $e');
|
||||
payment = 0;
|
||||
}
|
||||
// Vérifier si un reçu est disponible
|
||||
bool hasReceipt = amount > 0 && type == 1 && passage.nomRecu.isNotEmpty;
|
||||
|
||||
// Gérer les champs optionnels
|
||||
String name = '';
|
||||
try {
|
||||
name = passage.name;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération du nom: $e');
|
||||
// Vérifier s'il y a une erreur
|
||||
bool hasError = passage.emailErreur.isNotEmpty;
|
||||
|
||||
// Récupérer le secteur
|
||||
SectorModel? sector;
|
||||
if (passage.fkSector != null) {
|
||||
sector = _sectorRepository.getSectorById(passage.fkSector!);
|
||||
}
|
||||
|
||||
String notes = '';
|
||||
try {
|
||||
notes = passage.remarque;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération des remarques: $e');
|
||||
}
|
||||
|
||||
// Vérifier si un reçu est disponible avec gestion d'erreur
|
||||
bool hasReceipt = false;
|
||||
try {
|
||||
hasReceipt = amount > 0 && type == 1 && passage.nomRecu.isNotEmpty;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la vérification du reçu: $e');
|
||||
}
|
||||
|
||||
// Vérifier s'il y a une erreur avec gestion d'erreur
|
||||
bool hasError = false;
|
||||
try {
|
||||
hasError = passage.emailErreur.isNotEmpty;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la vérification des erreurs: $e');
|
||||
}
|
||||
|
||||
// Log pour débogage
|
||||
debugPrint(
|
||||
'Conversion passage ID: ${passage.id}, Type: $type, Date: $date');
|
||||
|
||||
return {
|
||||
'id': passage.id, // Garder l'ID comme int, pas besoin de toString()
|
||||
'id': passage.id,
|
||||
'address': address,
|
||||
'amount': amount,
|
||||
'date': date,
|
||||
'type': type,
|
||||
'payment': payment,
|
||||
'name': name,
|
||||
'notes': notes,
|
||||
'name': passage.name,
|
||||
'notes': passage.remarque,
|
||||
'hasReceipt': hasReceipt,
|
||||
'hasError': hasError,
|
||||
'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur
|
||||
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
|
||||
// Ajouter les composants de l'adresse pour le tri
|
||||
'fkUser': passage.fkUser,
|
||||
'fkSector': passage.fkSector,
|
||||
'sector': sector?.libelle ?? 'Secteur inconnu',
|
||||
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id,
|
||||
// Composants de l'adresse pour le tri
|
||||
'rue': passage.rue,
|
||||
'numero': passage.numero,
|
||||
'rueBis': passage.rueBis,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('ERREUR CRITIQUE lors de la conversion du passage: $e');
|
||||
// Retourner un objet valide par défaut pour éviter les erreurs
|
||||
// Récupérer l'ID de l'utilisateur courant pour l'objet par défaut
|
||||
// Utiliser l'instance globale définie dans app.dart
|
||||
debugPrint('Erreur lors de la conversion du passage: $e');
|
||||
// Retourner un objet valide par défaut
|
||||
final currentUserId = userRepository.getCurrentUser()?.id;
|
||||
|
||||
return {
|
||||
'id': 'error',
|
||||
'id': 0,
|
||||
'address': 'Adresse non disponible',
|
||||
'amount': 0.0,
|
||||
'date': DateTime.now(),
|
||||
'type': 1, // Type 1 par défaut au lieu de 0
|
||||
'payment': 1, // Payment 1 par défaut au lieu de 0
|
||||
'type': 1,
|
||||
'payment': 1,
|
||||
'name': 'Nom non disponible',
|
||||
'notes': '',
|
||||
'hasReceipt': false,
|
||||
'hasError': true,
|
||||
'fkUser': currentUserId, // Ajouter l'ID de l'utilisateur courant
|
||||
// Composants de l'adresse pour le tri
|
||||
'fkUser': currentUserId,
|
||||
'fkSector': null,
|
||||
'sector': 'Secteur inconnu',
|
||||
'rue': '',
|
||||
'numero': '',
|
||||
'rueBis': '',
|
||||
@@ -366,7 +466,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
case PassageSortType.addressAsc:
|
||||
sortedPassages.sort((a, b) {
|
||||
try {
|
||||
// Tri intelligent par rue, numéro (numérique), rueBis
|
||||
// Tri intelligent par rue, numéro, rueBis
|
||||
final String rueA = a['rue'] ?? '';
|
||||
final String rueB = b['rue'] ?? '';
|
||||
final String numeroA = a['numero'] ?? '';
|
||||
@@ -394,7 +494,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
case PassageSortType.addressDesc:
|
||||
sortedPassages.sort((a, b) {
|
||||
try {
|
||||
// Tri intelligent inversé par rue, numéro (numérique), rueBis
|
||||
// Tri intelligent inversé
|
||||
final String rueA = a['rue'] ?? '';
|
||||
final String rueB = b['rue'] ?? '';
|
||||
final String numeroA = a['numero'] ?? '';
|
||||
@@ -406,7 +506,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
|
||||
if (rueCompare != 0) return rueCompare;
|
||||
|
||||
// Si les rues sont identiques, comparer les numéros (inversé numériquement)
|
||||
// Si les rues sont identiques, comparer les numéros (inversé)
|
||||
int numA = int.tryParse(numeroA) ?? 0;
|
||||
int numB = int.tryParse(numeroB) ?? 0;
|
||||
int numCompare = numB.compareTo(numA);
|
||||
@@ -463,18 +563,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
return addressParts.join(', ');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Méthode pour afficher les détails d'un passage
|
||||
void _showPassageDetails(Map<String, dynamic> passage) {
|
||||
// Récupérer les informations du type de passage et du type de règlement
|
||||
final typePassage =
|
||||
AppKeys.typesPassages[passage['type']] as Map<String, dynamic>;
|
||||
final typeReglement =
|
||||
AppKeys.typesReglements[passage['payment']] as Map<String, dynamic>;
|
||||
final typePassage = AppKeys.typesPassages[passage['type']] as Map<String, dynamic>;
|
||||
final typeReglement = AppKeys.typesReglements[passage['payment']] as Map<String, dynamic>;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -492,8 +585,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
_buildDetailRow('Type', typePassage['titre']),
|
||||
_buildDetailRow('Règlement', typeReglement['titre']),
|
||||
_buildDetailRow('Montant', '${passage['amount']}€'),
|
||||
if (passage['notes'] != null &&
|
||||
passage['notes'].toString().isNotEmpty)
|
||||
if (passage['sector'] != null)
|
||||
_buildDetailRow('Secteur', passage['sector']),
|
||||
if (passage['notes'] != null && passage['notes'].toString().isNotEmpty)
|
||||
_buildDetailRow('Notes', passage['notes']),
|
||||
if (passage['hasReceipt'] == true)
|
||||
_buildDetailRow('Reçu', 'Disponible'),
|
||||
@@ -529,18 +623,12 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
|
||||
// Méthode pour éditer un passage
|
||||
void _editPassage(Map<String, dynamic> passage) {
|
||||
// Implémenter l'ouverture d'un formulaire d'édition
|
||||
// Cette méthode pourrait naviguer vers une page d'édition
|
||||
debugPrint('Édition du passage ${passage['id']}');
|
||||
// Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => EditPassagePage(passage: passage)));
|
||||
}
|
||||
|
||||
// Méthode pour afficher un reçu
|
||||
void _showReceipt(Map<String, dynamic> passage) {
|
||||
// Implémenter l'affichage ou la génération d'un reçu
|
||||
// Cette méthode pourrait générer un PDF et l'afficher
|
||||
debugPrint('Affichage du reçu pour le passage ${passage['id']}');
|
||||
// Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => ReceiptPage(passage: passage)));
|
||||
}
|
||||
|
||||
// Helper pour construire une ligne de détails
|
||||
@@ -564,9 +652,211 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Variable pour gérer la recherche
|
||||
final String _searchQuery = '';
|
||||
|
||||
// Construction des filtres
|
||||
Widget _buildFilters(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isDesktop = size.width > 900;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Filtres',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isDesktop)
|
||||
Row(
|
||||
children: [
|
||||
// Filtre par secteur (si plusieurs secteurs)
|
||||
if (_userSectors.length > 1)
|
||||
Expanded(
|
||||
child: _buildSectorFilter(theme),
|
||||
),
|
||||
if (_userSectors.length > 1)
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par période
|
||||
Expanded(
|
||||
child: _buildPeriodFilter(theme),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
// Filtre par secteur (si plusieurs secteurs)
|
||||
if (_userSectors.length > 1) ...[
|
||||
_buildSectorFilter(theme),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Filtre par période
|
||||
_buildPeriodFilter(theme),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par secteur
|
||||
Widget _buildSectorFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Secteur',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedSector,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les secteurs'),
|
||||
),
|
||||
..._userSectors.map((sector) {
|
||||
final String libelle = sector.libelle.isNotEmpty
|
||||
? sector.libelle
|
||||
: 'Secteur ${sector.id}';
|
||||
return DropdownMenuItem<String>(
|
||||
value: libelle,
|
||||
child: Text(
|
||||
libelle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
if (value == 'Tous') {
|
||||
_updateSectorFilter('Tous', null);
|
||||
} else {
|
||||
try {
|
||||
final sector = _userSectors.firstWhere(
|
||||
(s) => s.libelle == value,
|
||||
);
|
||||
_updateSectorFilter(value, sector.id);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la sélection du secteur: $e');
|
||||
_updateSectorFilter('Tous', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par période
|
||||
Widget _buildPeriodFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Période',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedPeriod,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: const [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Toutes les périodes'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Derniers 15 jours',
|
||||
child: Text('Derniers 15 jours'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Dernière semaine',
|
||||
child: Text('Dernière semaine'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Dernier mois',
|
||||
child: Text('Dernier mois'),
|
||||
),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
_updatePeriodFilter(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Afficher la plage de dates sélectionnée si elle existe
|
||||
if (selectedDateRange != null && selectedPeriod != 'Tous')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.date_range,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -621,6 +911,13 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Filtres (secteur et période)
|
||||
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: _buildFilters(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -670,7 +967,10 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||
builder: (context, Box<PassageModel> passagesBox, child) {
|
||||
// Reconvertir les passages à chaque changement
|
||||
final List<PassageModel> allPassages = passagesBox.values.toList();
|
||||
final currentUserId = userRepository.getCurrentUser()?.id;
|
||||
final List<PassageModel> allPassages = passagesBox.values
|
||||
.where((p) => p.fkUser == currentUserId)
|
||||
.toList();
|
||||
|
||||
// Appliquer le même filtrage et conversion
|
||||
List<Map<String, dynamic>> passagesMap = [];
|
||||
@@ -683,6 +983,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer les filtres
|
||||
passagesMap = _getFilteredPassages(passagesMap);
|
||||
|
||||
// Appliquer le tri sélectionné
|
||||
passagesMap = _sortPassages(passagesMap);
|
||||
|
||||
@@ -781,13 +1084,12 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
showFilters: true,
|
||||
showSearch: true,
|
||||
showActions: true,
|
||||
initialSearchQuery: _searchQuery,
|
||||
initialTypeFilter: 'Tous',
|
||||
initialPaymentFilter: 'Tous',
|
||||
initialSearchQuery: '',
|
||||
initialTypeFilter: selectedType,
|
||||
initialPaymentFilter: selectedPaymentMethod,
|
||||
excludePassageTypes: const [],
|
||||
filterByUserId: userRepository.getCurrentUser()?.id,
|
||||
filterByUserId: null, // Déjà filtré en amont
|
||||
key: const ValueKey('user_passages_list'),
|
||||
// Le widget gère maintenant le flux conditionnel par défaut
|
||||
onPassageSelected: null,
|
||||
onDetailsView: (passage) {
|
||||
debugPrint('Affichage des détails: ${passage['id']}');
|
||||
@@ -818,4 +1120,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user