feat: synchronisation mode deconnecte fin chat et stats

This commit is contained in:
2025-08-31 18:21:20 +02:00
parent f5bef999df
commit 96af94ad13
129 changed files with 125731 additions and 110375 deletions

View File

@@ -134,10 +134,14 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
password: password,
);
if (success && mounted) {
Navigator.of(context).pop();
ApiException.showSuccess(context,
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
if (success) {
if (context.mounted) {
Navigator.of(context).pop();
}
if (mounted) {
ApiException.showSuccess(context,
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
}
}
} catch (e) {
debugPrint('❌ Erreur mise à jour membre: $e');
@@ -641,13 +645,17 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
password: password,
);
if (createdMembre != null && mounted) {
if (createdMembre != null) {
// Fermer le dialog
Navigator.of(context).pop();
if (context.mounted) {
Navigator.of(context).pop();
}
// Afficher le message de succès avec les informations du membre créé
ApiException.showSuccess(context,
'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
if (mounted) {
ApiException.showSuccess(context,
'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
}
} else if (mounted) {
// En cas d'échec, ne pas fermer le dialog pour permettre la correction
ApiException.showError(

View File

@@ -1,251 +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/chat/pages/rooms_page_embedded.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 AdminCommunicationPage extends StatefulWidget {
const AdminCommunicationPage({super.key});
@override
State<AdminCommunicationPage> createState() => _AdminCommunicationPageState();
}
class _AdminCommunicationPageState extends State<AdminCommunicationPage> {
bool _isChatInitialized = false;
bool _isInitializing = false;
String? _initError;
GlobalKey<RoomsPageEmbeddedState>? _roomsPageKey;
@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('Administrateur non connecté');
}
// Initialiser le module chat avec les informations de l'administrateur
await ChatModule.init(
apiUrl: apiService.baseUrl,
userId: currentUser.currentUser!.id,
userName: currentUser.userName ?? currentUser.userEmail ?? 'Administrateur',
userRole: currentUser.currentUser!.role,
userEntite: currentUser.fkEntite ?? currentAmicale?.id,
authToken: currentUser.sessionId,
);
setState(() {
_isChatInitialized = true;
_isInitializing = false;
_roomsPageKey = GlobalKey<RoomsPageEmbeddedState>();
});
} catch (e) {
setState(() {
_initError = e.toString();
_isInitializing = false;
});
debugPrint('Erreur initialisation chat admin: $e');
}
}
void _refreshRooms() {
_roomsPageKey?.currentState?.refresh();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return 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 administrateur...',
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 chat',
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'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.error,
foregroundColor: Colors.white,
),
),
],
),
);
}
if (_isChatInitialized) {
// Afficher le module chat avec un header simplifié
return Column(
children: [
// En-tête simplifié avec boutons intégrés
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.red.shade50,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
Icon(
Icons.admin_panel_settings,
color: Colors.red.shade600,
size: 24,
),
const SizedBox(width: 12),
Text(
'Messages Administration',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.red.shade600,
),
),
const Spacer(),
// Boutons d'action
IconButton(
icon: Icon(Icons.add, color: Colors.red.shade600),
onPressed: () {
// Déclencher la création d'une nouvelle conversation
// Cela sera géré par RoomsPageEmbedded
_roomsPageKey?.currentState?.createNewConversation();
},
tooltip: 'Nouvelle conversation',
),
IconButton(
icon: Icon(Icons.refresh, color: Colors.red.shade600),
onPressed: _refreshRooms,
tooltip: 'Actualiser',
),
],
),
),
// Module chat sans AppBar
Expanded(
child: RoomsPageEmbedded(
key: _roomsPageKey,
onRefreshPressed: () {
// Callback optionnel après refresh
debugPrint('Conversations actualisées');
},
),
),
],
);
}
// É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 administrateur non initialisé',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initializeChat,
icon: const Icon(Icons.power_settings_new),
label: const Text('Initialiser le chat'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
),
);
}
}

View File

@@ -11,7 +11,7 @@ import 'dart:math' as math;
import 'admin_dashboard_home_page.dart';
import 'admin_statistics_page.dart';
import 'admin_history_page.dart';
import 'admin_communication_page.dart';
import '../chat/chat_communication_page.dart';
import 'admin_map_page.dart';
import 'admin_amicale_page.dart';
import 'admin_operations_page.dart';
@@ -119,7 +119,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
case _PageType.history:
return const AdminHistoryPage();
case _PageType.communication:
return const AdminCommunicationPage();
return const ChatCommunicationPage();
case _PageType.map:
return const AdminMapPage();
case _PageType.amicale:
@@ -257,7 +257,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
// Initialiser et charger les paramètres
_initSettings().then((_) {
// Écouter les changements de la boîte de paramètres après l'initialisation
_settingsListenable = _settingsBox.listenable(keys: ['adminSelectedPageIndex']);
_settingsListenable = _settingsBox.listenable(keys: ['selectedPageIndex']);
_settingsListenable.addListener(_onSettingsChanged);
});
@@ -285,7 +285,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
// Méthode pour gérer les changements de paramètres
void _onSettingsChanged() {
final newIndex = _settingsBox.get('adminSelectedPageIndex');
final newIndex = _settingsBox.get('selectedPageIndex');
if (newIndex != null && newIndex is int && newIndex != _selectedIndex) {
setState(() {
_selectedIndex = newIndex;
@@ -309,7 +309,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
}
// Charger l'index de page sélectionné
final savedIndex = _settingsBox.get('adminSelectedPageIndex');
final savedIndex = _settingsBox.get('selectedPageIndex');
// Vérifier si l'index sauvegardé est valide
if (savedIndex != null && savedIndex is int) {
@@ -334,7 +334,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
void _saveSettings() {
try {
// Sauvegarder l'index de page sélectionné
_settingsBox.put('adminSelectedPageIndex', _selectedIndex);
_settingsBox.put('selectedPageIndex', _selectedIndex);
} catch (e) {
debugPrint('Erreur lors de la sauvegarde des paramètres: $e');
}

View File

@@ -531,8 +531,6 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
// Contenu de la page
LayoutBuilder(
builder: (context, constraints) {
final passages = _getFilteredPassages();
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: ConstrainedBox(
@@ -1676,8 +1674,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
return;
}
final String streetNumber = passageModel.numero ?? '';
final String fullAddress = '${passageModel.numero ?? ''} ${passageModel.rueBis ?? ''} ${passageModel.rue ?? ''}'.trim();
final String streetNumber = passageModel.numero;
final String fullAddress = '${passageModel.numero} ${passageModel.rueBis} ${passageModel.rue}'.trim();
showDialog(
context: context,

View File

@@ -16,10 +16,8 @@ import 'package:geosector_app/presentation/dialogs/sector_dialog.dart';
import 'package:geosector_app/core/repositories/sector_repository.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
import 'package:geosector_app/core/services/data_loading_service.dart';
import 'package:geosector_app/presentation/widgets/passage_map_dialog.dart';
class AdminMapPage extends StatefulWidget {
@@ -98,7 +96,7 @@ class _AdminMapPageState extends State<AdminMapPage> {
_loadPassages();
// Écouter les changements du secteur sélectionné
_settingsListenable = _settingsBox.listenable(keys: ['admin_selectedSectorId']);
_settingsListenable = _settingsBox.listenable(keys: ['selectedSectorId']);
_settingsListenable.addListener(_onSectorSelectionChanged);
// Centrer la carte une seule fois après le chargement initial
@@ -122,12 +120,12 @@ class _AdminMapPageState extends State<AdminMapPage> {
}
// Charger le secteur sélectionné
_selectedSectorId = _settingsBox.get('admin_selectedSectorId');
_selectedSectorId = _settingsBox.get('selectedSectorId');
// Charger la position et le zoom
final double? savedLat = _settingsBox.get('admin_mapLat');
final double? savedLng = _settingsBox.get('admin_mapLng');
final double? savedZoom = _settingsBox.get('admin_mapZoom');
final double? savedLat = _settingsBox.get('mapLat');
final double? savedLng = _settingsBox.get('mapLng');
final double? savedZoom = _settingsBox.get('mapZoom');
if (savedLat != null && savedLng != null) {
_currentPosition = LatLng(savedLat, savedLng);
@@ -140,7 +138,7 @@ class _AdminMapPageState extends State<AdminMapPage> {
// Méthode pour gérer les changements de sélection de secteur
void _onSectorSelectionChanged() {
final newSectorId = _settingsBox.get('admin_selectedSectorId');
final newSectorId = _settingsBox.get('selectedSectorId');
if (newSectorId != null && newSectorId != _selectedSectorId) {
setState(() {
_selectedSectorId = newSectorId;
@@ -169,13 +167,13 @@ class _AdminMapPageState extends State<AdminMapPage> {
void _saveSettings() {
// Sauvegarder le secteur sélectionné
if (_selectedSectorId != null) {
_settingsBox.put('admin_selectedSectorId', _selectedSectorId);
_settingsBox.put('selectedSectorId', _selectedSectorId);
}
// Sauvegarder la position et le zoom actuels
_settingsBox.put('admin_mapLat', _currentPosition.latitude);
_settingsBox.put('admin_mapLng', _currentPosition.longitude);
_settingsBox.put('admin_mapZoom', _currentZoom);
_settingsBox.put('mapLat', _currentPosition.latitude);
_settingsBox.put('mapLng', _currentPosition.longitude);
_settingsBox.put('mapZoom', _currentZoom);
}
// Charger les secteurs depuis la boîte (pour ValueListenableBuilder)
@@ -622,8 +620,8 @@ class _AdminMapPageState extends State<AdminMapPage> {
_updateMapPosition(position, zoom: 17);
// Sauvegarder la nouvelle position
_settingsBox.put('admin_mapLat', position.latitude);
_settingsBox.put('admin_mapLng', position.longitude);
_settingsBox.put('mapLat', position.latitude);
_settingsBox.put('mapLng', position.longitude);
// Informer l'utilisateur
if (mounted) {
@@ -2776,7 +2774,9 @@ class _AdminMapPageState extends State<AdminMapPage> {
final sectorRepository = SectorRepository();
final result = await sectorRepository.deleteSectorFromApi(_sectorToDeleteId!);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
if (mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
if (result['status'] == 'success') {
// Si le secteur supprimé était sélectionné, réinitialiser la sélection
@@ -2805,21 +2805,25 @@ class _AdminMapPageState extends State<AdminMapPage> {
}
} else {
final errorMessage = result['message'] ?? 'Erreur lors de la suppression du secteur';
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_mapMode = MapMode.view;
@@ -2863,7 +2867,8 @@ class _AdminMapPageState extends State<AdminMapPage> {
try {
// Afficher un indicateur de chargement
ScaffoldMessenger.of(parentContext).showSnackBar(
if (parentContext.mounted) {
ScaffoldMessenger.of(parentContext).showSnackBar(
const SnackBar(
content: Row(
children: [
@@ -2879,6 +2884,7 @@ class _AdminMapPageState extends State<AdminMapPage> {
duration: Duration(seconds: 30),
),
);
}
final sectorRepository = SectorRepository();
int passagesCreated = 0;
@@ -2960,10 +2966,12 @@ class _AdminMapPageState extends State<AdminMapPage> {
});
}
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
if (parentContext.mounted) {
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
}
// Message de succès simple pour la création
if (mounted) {
if (mounted && parentContext.mounted) {
String message = 'Secteur "$name" créé avec succès. ';
if (passagesCreated > 0) {
message += '$passagesCreated passages créés.';
@@ -3012,10 +3020,12 @@ class _AdminMapPageState extends State<AdminMapPage> {
_loadSectors();
_loadPassages();
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
if (parentContext.mounted) {
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
}
// Message de succès simple pour la modification
if (mounted) {
if (mounted && parentContext.mounted) {
String message = 'Secteur "$name" modifié avec succès. ';
final passagesUpdated = result['passages_updated'] ?? 0;
final passagesCreated = result['passages_created'] ?? 0;

View File

@@ -1,5 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/constants/app_keys.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_sector_model.dart';
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
import 'dart:math' as math;
@@ -36,37 +41,120 @@ class AdminStatisticsPage extends StatefulWidget {
class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
// Filtres
String _selectedPeriod = 'Jour';
String _selectedFilterType = 'Secteur';
String _selectedSector = 'Tous';
String _selectedUser = 'Tous';
String _selectedMember = 'Tous';
int _daysToShow = 15;
// Liste des périodes et types de filtre
// Liste des périodes
final List<String> _periods = ['Jour', 'Semaine', 'Mois', 'Année'];
final List<String> _filterTypes = ['Secteur', 'Membre'];
// Données simulées pour les secteurs et membres (à remplacer par des données réelles)
final List<String> _sectors = [
'Tous',
'Secteur Nord',
'Secteur Sud',
'Secteur Est',
'Secteur Ouest'
];
final List<String> _members = [
'Tous',
'Jean Dupont',
'Marie Martin',
'Pierre Legrand',
'Sophie Petit',
'Lucas Moreau'
];
// Listes dynamiques pour les secteurs et membres
List<String> _sectors = ['Tous'];
List<String> _members = ['Tous'];
// Listes complètes (non filtrées) pour réinitialisation
List<SectorModel> _allSectors = [];
List<MembreModel> _allMembers = [];
List<UserSectorModel> _userSectors = [];
// Map pour stocker les IDs correspondants
final Map<String, int> _sectorIds = {};
final Map<String, int> _memberIds = {};
@override
void initState() {
super.initState();
_loadData();
}
void _loadData() {
// Charger les secteurs depuis Hive
if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
_allSectors = sectorsBox.values.toList();
}
// Charger les membres depuis Hive
if (Hive.isBoxOpen(AppKeys.membresBoxName)) {
final membresBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
_allMembers = membresBox.values.toList();
}
// Charger les associations user-sector depuis Hive
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
final userSectorBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
_userSectors = userSectorBox.values.toList();
}
// Initialiser les listes avec toutes les données
_updateSectorsList();
_updateMembersList();
}
// Mettre à jour la liste des secteurs (filtrée ou complète)
void _updateSectorsList({int? forMemberId}) {
setState(() {
_sectors = ['Tous'];
_sectorIds.clear();
List<SectorModel> sectorsToShow = _allSectors;
// Si un membre est sélectionné, filtrer les secteurs
if (forMemberId != null) {
final memberSectorIds = _userSectors
.where((us) => us.id == forMemberId)
.map((us) => us.fkSector)
.toSet();
sectorsToShow = _allSectors
.where((sector) => memberSectorIds.contains(sector.id))
.toList();
}
// Ajouter les secteurs à la liste
for (final sector in sectorsToShow) {
_sectors.add(sector.libelle);
_sectorIds[sector.libelle] = sector.id;
}
});
}
// Mettre à jour la liste des membres (filtrée ou complète)
void _updateMembersList({int? forSectorId}) {
setState(() {
_members = ['Tous'];
_memberIds.clear();
List<MembreModel> membersToShow = _allMembers;
// Si un secteur est sélectionné, filtrer les membres
if (forSectorId != null) {
final sectorMemberIds = _userSectors
.where((us) => us.fkSector == forSectorId)
.map((us) => us.id)
.toSet();
membersToShow = _allMembers
.where((member) => sectorMemberIds.contains(member.id))
.toList();
}
// Ajouter les membres à la liste
for (final membre in membersToShow) {
final fullName = '${membre.firstName} ${membre.name}'.trim();
_members.add(fullName);
_memberIds[fullName] = membre.id;
}
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
// Utiliser un Builder simple avec listeners pour les boxes
// On écoute les changements et on reconstruit le widget
return Stack(
children: [
// Fond dégradé avec petits points blancs
@@ -128,15 +216,23 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
),
const SizedBox(height: AppTheme.spacingM),
isDesktop
? Row(
? Column(
children: [
Expanded(child: _buildPeriodDropdown()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildDaysDropdown()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildFilterTypeDropdown()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildFilterDropdown()),
Row(
children: [
Expanded(child: _buildPeriodDropdown()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildDaysDropdown()),
],
),
const SizedBox(height: AppTheme.spacingM),
Row(
children: [
Expanded(child: _buildSectorDropdown()),
const SizedBox(width: AppTheme.spacingM),
Expanded(child: _buildMemberDropdown()),
],
),
],
)
: Column(
@@ -145,9 +241,9 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
const SizedBox(height: AppTheme.spacingM),
_buildDaysDropdown(),
const SizedBox(height: AppTheme.spacingM),
_buildFilterTypeDropdown(),
_buildSectorDropdown(),
const SizedBox(height: AppTheme.spacingM),
_buildFilterDropdown(),
_buildMemberDropdown(),
],
),
],
@@ -179,14 +275,15 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
const SizedBox(height: AppTheme.spacingM),
ActivityChart(
height: 350,
showAllPassages: true,
showAllPassages: _selectedMember == 'Tous', // Afficher tous les passages seulement si "Tous" est sélectionné
title: '',
daysToShow: _daysToShow,
periodType: _selectedPeriod,
userId: _selectedUser != 'Tous'
? _getUserIdFromName(_selectedUser)
userId: _selectedMember != 'Tous'
? _getMemberIdFromName(_selectedMember)
: null,
// Si on filtre par secteur, on devrait passer l'ID du secteur
// Note: Le filtre par secteur nécessite une modification du widget ActivityChart
// Pour filtrer par secteur, il faudrait ajouter un paramètre sectorId au widget
),
],
),
@@ -208,13 +305,14 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
titleIcon: Icons.pie_chart,
height: 300,
useValueListenable: true,
showAllPassages: true,
showAllPassages: _selectedMember == 'Tous',
excludePassageTypes: const [
2
], // Exclure "À finaliser"
userId: _selectedUser != 'Tous'
? _getUserIdFromName(_selectedUser)
userId: _selectedMember != 'Tous'
? _getMemberIdFromName(_selectedMember)
: null,
// Note: Le filtre par secteur nécessite une modification du widget PassageSummaryCard
isDesktop:
MediaQuery.of(context).size.width > 800,
),
@@ -224,30 +322,12 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
Expanded(
child: _buildChartCard(
'Répartition par mode de paiement',
const PaymentPieChart(
payments: [
PaymentData(
typeId: 1,
amount: 1500.0,
color: Color(0xFFFFC107),
icon: Icons.toll,
title: 'Espèce',
),
PaymentData(
typeId: 2,
amount: 2500.0,
color: Color(0xFF8BC34A),
icon: Icons.wallet,
title: 'Chèque',
),
PaymentData(
typeId: 3,
amount: 1000.0,
color: Color(0xFF00B0FF),
icon: Icons.credit_card,
title: 'CB',
),
],
PaymentPieChart(
useValueListenable: true,
showAllPassages: _selectedMember == 'Tous',
userId: _selectedMember != 'Tous'
? _getMemberIdFromName(_selectedMember)
: null,
size: 300,
),
),
@@ -264,127 +344,31 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
titleIcon: Icons.pie_chart,
height: 300,
useValueListenable: true,
showAllPassages: true,
showAllPassages: _selectedMember == 'Tous',
excludePassageTypes: const [
2
], // Exclure "À finaliser"
userId: _selectedUser != 'Tous'
? _getUserIdFromName(_selectedUser)
userId: _selectedMember != 'Tous'
? _getMemberIdFromName(_selectedMember)
: null,
// Note: Le filtre par secteur nécessite une modification du widget PassageSummaryCard
isDesktop: MediaQuery.of(context).size.width > 800,
),
),
const SizedBox(height: AppTheme.spacingM),
_buildChartCard(
'Répartition par mode de paiement',
const PaymentPieChart(
payments: [
PaymentData(
typeId: 1,
amount: 1500.0,
color: Color(0xFFFFC107),
icon: Icons.toll,
title: 'Espèce',
),
PaymentData(
typeId: 2,
amount: 2500.0,
color: Color(0xFF8BC34A),
icon: Icons.wallet,
title: 'Chèque',
),
PaymentData(
typeId: 3,
amount: 1000.0,
color: Color(0xFF00B0FF),
icon: Icons.credit_card,
title: 'CB',
),
],
PaymentPieChart(
useValueListenable: true,
showAllPassages: _selectedMember == 'Tous',
userId: _selectedMember != 'Tous'
? _getMemberIdFromName(_selectedMember)
: null,
size: 300,
),
),
],
),
const SizedBox(height: AppTheme.spacingL),
// Graphique combiné (si disponible)
_buildChartCard(
'Comparaison passages/montants',
const SizedBox(
height: 350,
child: Center(
child: Text('Graphique combiné à implémenter'),
),
),
),
const SizedBox(height: AppTheme.spacingL),
// Actions
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppTheme.borderRadiusMedium),
),
color: Colors.white, // Fond opaque
child: Padding(
padding: const EdgeInsets.all(AppTheme.spacingM),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions',
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppTheme.spacingM),
Wrap(
spacing: AppTheme.spacingM,
runSpacing: AppTheme.spacingM,
children: [
ElevatedButton.icon(
onPressed: () {
// Exporter les statistiques
},
icon: const Icon(Icons.file_download),
label: const Text('Exporter les statistiques'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () {
// Imprimer les statistiques
},
icon: const Icon(Icons.print),
label: const Text('Imprimer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () {
// Partager les statistiques
},
icon: const Icon(Icons.share),
label: const Text('Partager'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentColor,
foregroundColor: Colors.white,
),
),
],
),
],
),
),
),
],
),
),
@@ -464,11 +448,11 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
);
}
// Dropdown pour le type de filtre
Widget _buildFilterTypeDropdown() {
// Dropdown pour les secteurs
Widget _buildSectorDropdown() {
return InputDecorator(
decoration: InputDecoration(
labelText: 'Filtrer par',
labelText: 'Secteur',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
),
@@ -479,22 +463,40 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedFilterType,
value: _selectedSector,
isDense: true,
isExpanded: true,
items: _filterTypes.map((String type) {
items: _sectors.map((String sector) {
return DropdownMenuItem<String>(
value: type,
child: Text(type),
value: sector,
child: Text(sector),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedFilterType = newValue;
// Réinitialiser les filtres spécifiques
_selectedSector = 'Tous';
_selectedUser = 'Tous';
_selectedSector = newValue;
// Si "Tous" est sélectionné, réinitialiser la liste des membres
if (newValue == 'Tous') {
_updateMembersList();
// Garder le membre sélectionné s'il existe
} else {
// Sinon, filtrer les membres pour ce secteur
final sectorId = _getSectorIdFromName(newValue);
_updateMembersList(forSectorId: sectorId);
// Si le membre actuellement sélectionné n'est pas dans la liste filtrée
if (_selectedMember == 'Tous' || !_members.contains(_selectedMember)) {
// Auto-sélectionner le premier membre du secteur (après "Tous")
// Puisque chaque secteur a au moins un membre, il y aura toujours un membre à sélectionner
if (_members.length > 1) {
_selectedMember = _members[1]; // Index 1 car 0 est "Tous"
}
}
// Si le membre sélectionné est dans la liste, on le garde
// Les graphiques afficheront ses données
}
});
}
},
@@ -503,16 +505,11 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
);
}
// Dropdown pour le filtre spécifique (secteur ou membre)
Widget _buildFilterDropdown() {
final List<String> items =
_selectedFilterType == 'Secteur' ? _sectors : _members;
final String value =
_selectedFilterType == 'Secteur' ? _selectedSector : _selectedUser;
// Dropdown pour les membres
Widget _buildMemberDropdown() {
return InputDecorator(
decoration: InputDecoration(
labelText: _selectedFilterType,
labelText: 'Membre',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
),
@@ -523,22 +520,35 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: value,
value: _selectedMember,
isDense: true,
isExpanded: true,
items: items.map((String item) {
items: _members.map((String member) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
value: member,
child: Text(member),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
if (_selectedFilterType == 'Secteur') {
_selectedSector = newValue;
_selectedMember = newValue;
// Si "Tous" est sélectionné, réinitialiser la liste des secteurs
if (newValue == 'Tous') {
_updateSectorsList();
// On peut réinitialiser le secteur car "Tous" les membres = pas de filtre secteur pertinent
_selectedSector = 'Tous';
} else {
_selectedUser = newValue;
// Sinon, filtrer les secteurs pour ce membre
final memberId = _getMemberIdFromName(newValue);
_updateSectorsList(forMemberId: memberId);
// Si le secteur actuellement sélectionné n'est plus dans la liste, réinitialiser
if (_selectedSector != 'Tous' && !_sectors.contains(_selectedSector)) {
_selectedSector = 'Tous';
}
// Si le secteur est toujours dans la liste, on le garde sélectionné
}
});
}
@@ -575,15 +585,44 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
);
}
// Méthode utilitaire pour obtenir l'ID utilisateur à partir de son nom
int? _getUserIdFromName(String name) {
// Dans un cas réel, cela nécessiterait une requête au repository
// Pour l'exemple, on utilise une correspondance simple
if (name == 'Jean Dupont') return 1;
if (name == 'Marie Martin') return 2;
if (name == 'Pierre Legrand') return 3;
if (name == 'Sophie Petit') return 4;
if (name == 'Lucas Moreau') return 5;
return null;
// Méthode utilitaire pour obtenir l'ID membre à partir de son nom
int? _getMemberIdFromName(String name) {
if (name == 'Tous') return null;
return _memberIds[name];
}
// Méthode utilitaire pour obtenir l'ID du secteur à partir de son nom
int? _getSectorIdFromName(String name) {
if (name == 'Tous') return null;
return _sectorIds[name];
}
// Méthode pour obtenir tous les IDs des membres d'un secteur
List<int> _getMemberIdsForSector(int sectorId) {
return _userSectors
.where((us) => us.fkSector == sectorId)
.map((us) => us.id)
.toList();
}
// Méthode pour déterminer quel userId utiliser pour les graphiques
int? _getUserIdForCharts() {
// Si un membre spécifique est sélectionné, utiliser son ID
if (_selectedMember != 'Tous') {
return _getMemberIdFromName(_selectedMember);
}
// Si un secteur est sélectionné mais pas de membre spécifique
// Les widgets actuels ne supportent pas plusieurs userIds
// Donc on ne peut pas filtrer par secteur pour le moment
// TODO: Implémenter le support multi-users ou sectorId dans les widgets
return null; // Afficher tous les passages
}
// Méthode pour déterminer si on doit afficher tous les passages
bool _shouldShowAllPassages() {
// Afficher tous les passages seulement si aucun filtre n'est appliqué
return _selectedMember == 'Tous' && _selectedSector == 'Tous';
}
}

View File

@@ -6,7 +6,9 @@ import 'package:geosector_app/core/services/js_stub.dart'
if (dart.library.js) 'dart:js' as js;
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/custom_button.dart';
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
@@ -53,6 +55,7 @@ class _LoginPageState extends State<LoginPage> {
final _usernameFocusNode = FocusNode();
bool _obscurePassword = true;
String _appVersion = '';
bool _isCleaningCache = false;
// Type de connexion (utilisateur ou administrateur)
late String _loginType;
@@ -117,6 +120,46 @@ class _LoginPageState extends State<LoginPage> {
return; // IMPORTANT : Arrêter l'exécution du reste de initState
}
// NOUVELLE VÉRIFICATION : S'assurer que la réinitialisation complète a été effectuée
// Vérifier la clé 'hive_initialized' dans la box settings
try {
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
final isInitialized = settingsBox.get('hive_initialized', defaultValue: false);
if (isInitialized != true) {
debugPrint('⚠️ LoginPage: Réinitialisation Hive requise (hive_initialized=$isInitialized)');
// Construire les paramètres pour la redirection après initialisation
final loginType = widget.loginType ?? 'admin';
// Forcer une réinitialisation complète via SplashPage
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.go('/?action=login&type=$loginType');
}
});
// Initialiser avec des valeurs par défaut pour éviter les erreurs
_loginType = '';
return; // IMPORTANT : Arrêter l'exécution du reste de initState
}
debugPrint('✅ LoginPage: Hive correctement initialisé');
}
} catch (e) {
debugPrint('❌ LoginPage: Erreur lors de la vérification de hive_initialized: $e');
// En cas d'erreur, forcer la réinitialisation
final loginType = widget.loginType ?? 'admin';
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.go('/?action=login&type=$loginType');
}
});
_loginType = '';
return;
}
// Vérification du type de connexion (seulement si Hive est initialisé)
if (widget.loginType == null) {
// Si aucun type n'est spécifié, naviguer vers la splash page
@@ -314,6 +357,72 @@ class _LoginPageState extends State<LoginPage> {
width: double.infinity, height: double.infinity),
),
),
// Bouton "Nettoyer le cache" en bas à gauche
Positioned(
bottom: 20,
left: 20,
child: SafeArea(
child: TextButton.icon(
onPressed: _isCleaningCache ? null : () async {
// Confirmation avant nettoyage
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Nettoyer le cache ?'),
content: const Text(
'Cette action va :\n'
'• Supprimer toutes les données locales\n'
'• Forcer le rechargement de l\'application\n\n'
'Continuer ?'
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('Nettoyer'),
),
],
),
);
if (confirm == true) {
setState(() => _isCleaningCache = true);
debugPrint('👤 Utilisateur a demandé un nettoyage du cache');
// Nettoyer le cache Hive
await HiveService.instance.cleanDataOnLogout();
setState(() => _isCleaningCache = false);
// Rediriger vers la page splash pour réinitialiser
if (context.mounted) {
context.go('/');
}
}
},
icon: _isCleaningCache
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.cleaning_services, size: 18, color: Colors.black87),
label: Text(
_isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache',
style: const TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
@@ -481,14 +590,16 @@ class _LoginPageState extends State<LoginPage> {
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
}
return;
}
@@ -509,13 +620,17 @@ class _LoginPageState extends State<LoginPage> {
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
context.go('/admin');
if (context.mounted) {
context.go('/admin');
}
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
context.go('/user');
if (context.mounted) {
context.go('/user');
}
}
} else if (mounted) {
} else if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
@@ -563,38 +678,40 @@ class _LoginPageState extends State<LoginPage> {
if (!connectivityService
.isConnected) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
backgroundColor:
theme.colorScheme.error,
duration: const Duration(
seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
),
);
}
},
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
backgroundColor:
theme.colorScheme.error,
duration: const Duration(
seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
context.mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
),
);
}
},
),
),
),
);
);
}
return;
}
@@ -602,7 +719,9 @@ class _LoginPageState extends State<LoginPage> {
if (_loginType.isEmpty) {
print(
'Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/');
if (context.mounted) {
context.go('/');
}
return;
}
@@ -628,14 +747,16 @@ class _LoginPageState extends State<LoginPage> {
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
}
return;
}
@@ -656,13 +777,17 @@ class _LoginPageState extends State<LoginPage> {
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
context.go('/admin');
if (context.mounted) {
context.go('/admin');
}
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
context.go('/user');
if (context.mounted) {
context.go('/user');
}
}
} else if (mounted) {
} else if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
@@ -931,17 +1056,18 @@ class _LoginPageState extends State<LoginPage> {
});
// Remplacer le contenu de la boîte de dialogue par un message de succès
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
// Fermer automatiquement la boîte de dialogue après 2 secondes
Future.delayed(const Duration(seconds: 2),
() {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
});
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
// Fermer automatiquement la boîte de dialogue après 2 secondes
Future.delayed(const Duration(seconds: 2),
() {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
});
return const AlertDialog(
content: Column(
@@ -961,11 +1087,14 @@ class _LoginPageState extends State<LoginPage> {
],
),
);
},
);
},
);
}
} else {
// Fermer la boîte de dialogue actuelle
Navigator.of(context).pop();
if (context.mounted) {
Navigator.of(context).pop();
}
// Afficher un message d'erreur
final responseData = json.decode(response.body);
@@ -974,16 +1103,18 @@ class _LoginPageState extends State<LoginPage> {
}
} catch (e) {
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e
.toString()
.contains('Exception:')
? e.toString().split('Exception: ')[1]
: 'Erreur lors de la récupération du mot de passe'),
backgroundColor: Colors.red,
),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e
.toString()
.contains('Exception:')
? e.toString().split('Exception: ')[1]
: 'Erreur lors de la récupération du mot de passe'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {

View File

@@ -5,11 +5,14 @@ import 'package:go_router/go_router.dart';
import 'dart:math' as math;
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:geosector_app/presentation/widgets/custom_button.dart';
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/services/hive_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
@@ -112,6 +115,50 @@ class _RegisterPageState extends State<RegisterPage> {
void initState() {
super.initState();
// VÉRIFICATION CRITIQUE : S'assurer que Hive est initialisé correctement
// Vérifier la clé 'hive_initialized' dans la box settings
try {
// D'abord vérifier que les boxes sont disponibles
if (!HiveService.instance.areBoxesInitialized()) {
debugPrint('⚠️ RegisterPage: Boxes Hive non initialisées, redirection vers SplashPage');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.go('/?action=register');
}
});
return; // IMPORTANT : Arrêter l'exécution du reste de initState
}
// Ensuite vérifier la clé de réinitialisation
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
final isInitialized = settingsBox.get('hive_initialized', defaultValue: false);
if (isInitialized != true) {
debugPrint('⚠️ RegisterPage: Réinitialisation Hive requise (hive_initialized=$isInitialized)');
// Forcer une réinitialisation complète via SplashPage
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.go('/?action=register');
}
});
return; // IMPORTANT : Arrêter l'exécution du reste de initState
}
debugPrint('✅ RegisterPage: Hive correctement initialisé');
}
} catch (e) {
debugPrint('❌ RegisterPage: Erreur lors de la vérification de hive_initialized: $e');
// En cas d'erreur, forcer la réinitialisation
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.go('/?action=register');
}
});
return;
}
// Déterminer si l'application s'exécute sur mobile
_isMobile = !kIsWeb;

View File

@@ -3,11 +3,16 @@ import 'package:go_router/go_router.dart';
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/services/hive_service.dart';
import 'package:geosector_app/core/services/location_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html if (dart.library.io) '';
class SplashPage extends StatefulWidget {
/// Action à effectuer après l'initialisation (login ou register)
@@ -55,6 +60,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
String _appVersion = '';
bool _showLocationError = false;
String? _locationErrorMessage;
bool _isCleaningCache = false;
Future<void> _getAppVersion() async {
try {
@@ -74,6 +80,216 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
}
}
/// Effectue un nettoyage sélectif du cache
/// Préserve la box pending_requests et les données critiques
Future<void> _performSelectiveCleanup({bool manual = false}) async {
debugPrint('🧹 === DÉBUT DU NETTOYAGE DU CACHE === 🧹');
debugPrint('📌 Type: ${manual ? "MANUEL" : "AUTOMATIQUE"}');
debugPrint('📱 Platform: ${kIsWeb ? "WEB" : "MOBILE"}');
debugPrint('📦 Version actuelle: $_appVersion');
try {
if (mounted) {
setState(() {
_isCleaningCache = true;
_statusMessage = "Nettoyage du cache en cours...";
_progress = 0.1;
});
}
// Étape 1: Nettoyer le Service Worker (Web uniquement)
if (kIsWeb) {
debugPrint('🔄 Nettoyage du Service Worker...');
try {
// Désenregistrer tous les service workers
final registrations = await html.window.navigator.serviceWorker?.getRegistrations();
if (registrations != null) {
for (final registration in registrations) {
await registration.unregister();
debugPrint('✅ Service Worker désenregistré');
}
}
// Nettoyer les caches du navigateur
if (html.window.caches != null) {
final cacheNames = await html.window.caches!.keys();
for (final cacheName in cacheNames) {
await html.window.caches!.delete(cacheName);
debugPrint('✅ Cache "$cacheName" supprimé');
}
}
} catch (e) {
debugPrint('⚠️ Erreur lors du nettoyage Service Worker: $e');
}
}
if (mounted) {
setState(() {
_statusMessage = "Fermeture des bases de données...";
_progress = 0.3;
});
}
// Étape 2: Sauvegarder les données de pending_requests
debugPrint('💾 Sauvegarde des requêtes en attente...');
List<dynamic>? pendingRequests;
try {
if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName);
pendingRequests = pendingBox.values.toList();
debugPrint('📊 ${pendingRequests.length} requêtes en attente sauvegardées');
await pendingBox.close();
}
} catch (e) {
debugPrint('⚠️ Erreur lors de la sauvegarde des requêtes: $e');
}
if (mounted) {
setState(() {
_statusMessage = "Nettoyage des données locales...";
_progress = 0.5;
});
}
// Étape 3: Lister toutes les boxes à nettoyer (SAUF pending_requests)
final boxesToClean = [
AppKeys.userBoxName,
AppKeys.operationsBoxName,
AppKeys.passagesBoxName,
AppKeys.sectorsBoxName,
AppKeys.membresBoxName,
AppKeys.amicaleBoxName,
AppKeys.clientsBoxName,
AppKeys.userSectorBoxName,
AppKeys.settingsBoxName,
AppKeys.chatRoomsBoxName,
AppKeys.chatMessagesBoxName,
];
// Étape 4: Fermer et supprimer les boxes
debugPrint('🗑️ Nettoyage des boxes Hive...');
for (final boxName in boxesToClean) {
try {
if (Hive.isBoxOpen(boxName)) {
await Hive.box(boxName).close();
debugPrint('📦 Box "$boxName" fermée');
}
await Hive.deleteBoxFromDisk(boxName);
debugPrint('✅ Box "$boxName" supprimée');
} catch (e) {
debugPrint('⚠️ Erreur lors du nettoyage de "$boxName": $e');
}
}
if (mounted) {
setState(() {
_statusMessage = "Réinitialisation de Hive...";
_progress = 0.7;
});
}
// Étape 5: Réinitialiser Hive proprement
debugPrint('🔄 Réinitialisation de Hive...');
await Hive.close();
await Future.delayed(const Duration(milliseconds: 500));
await Hive.initFlutter();
// Étape 6: Restaurer les requêtes en attente
if (pendingRequests != null && pendingRequests.isNotEmpty) {
debugPrint('♻️ Restauration des requêtes en attente...');
final pendingBox = await Hive.openBox(AppKeys.pendingRequestsBoxName);
for (final request in pendingRequests) {
await pendingBox.add(request);
}
debugPrint('${pendingRequests.length} requêtes restaurées');
}
if (mounted) {
setState(() {
_statusMessage = "Nettoyage terminé !";
_progress = 1.0;
});
}
// Étape 7: Sauvegarder la nouvelle version
if (!manual && kIsWeb) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_version', _appVersion);
debugPrint('💾 Version $_appVersion sauvegardée');
}
debugPrint('🎉 === NETTOYAGE TERMINÉ AVEC SUCCÈS === 🎉');
// Petit délai pour voir le message de succès
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() {
_isCleaningCache = false;
_progress = 0.0;
});
}
} catch (e) {
debugPrint('❌ ERREUR CRITIQUE lors du nettoyage: $e');
if (mounted) {
setState(() {
_isCleaningCache = false;
_statusMessage = "Erreur lors du nettoyage";
_progress = 0.0;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du nettoyage: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
}
}
/// Vérifie si une nouvelle version est disponible et nettoie si nécessaire
Future<void> _checkVersionAndCleanIfNeeded() async {
if (!kIsWeb) {
debugPrint('📱 Plateforme mobile - pas de nettoyage automatique');
return;
}
try {
final prefs = await SharedPreferences.getInstance();
final lastVersion = prefs.getString('app_version') ?? '';
debugPrint('🔍 Vérification de version:');
debugPrint(' Version stockée: $lastVersion');
debugPrint(' Version actuelle: $_appVersion');
// Si changement de version détecté
if (lastVersion.isNotEmpty && lastVersion != _appVersion) {
debugPrint('🆕 NOUVELLE VERSION DÉTECTÉE !');
debugPrint(' Migration de $lastVersion vers $_appVersion');
if (mounted) {
setState(() {
_statusMessage = "Nouvelle version détectée, mise à jour...";
});
}
// Effectuer le nettoyage automatique
await _performSelectiveCleanup(manual: false);
} else if (lastVersion.isEmpty) {
// Première installation
debugPrint('🎉 Première installation détectée');
await prefs.setString('app_version', _appVersion);
} else {
debugPrint('✅ Même version - pas de nettoyage nécessaire');
}
} catch (e) {
debugPrint('⚠️ Erreur lors de la vérification de version: $e');
}
}
@override
void initState() {
super.initState();
@@ -109,7 +325,10 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
try {
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
// Étape 0: Vérification des permissions GPS (obligatoire) - 0 à 10%
// Étape 0: Vérifier et nettoyer si nouvelle version (Web uniquement)
await _checkVersionAndCleanIfNeeded();
// Étape 1: Vérification des permissions GPS (obligatoire) - 0 à 10%
if (!kIsWeb) {
if (mounted) {
setState(() {
@@ -183,6 +402,26 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
// Étape 3: Ouverture des Box - 60 à 80%
await HiveService.instance.ensureBoxesAreOpen();
// Gérer la box pending_requests séparément pour préserver les données
try {
debugPrint('📦 Gestion de la box pending_requests...');
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
// Importer PendingRequest si nécessaire
final pendingRequestBox = await Hive.openBox(AppKeys.pendingRequestsBoxName);
final pendingCount = pendingRequestBox.length;
if (pendingCount > 0) {
debugPrint('$pendingCount requêtes en attente trouvées dans la box');
} else {
debugPrint('✅ Box pending_requests ouverte (vide)');
}
} else {
debugPrint('✅ Box pending_requests déjà ouverte');
}
} catch (e) {
debugPrint('⚠️ Erreur lors de l\'ouverture de la box pending_requests: $e');
// On continue quand même, ce n'est pas critique pour le démarrage
}
if (mounted) {
setState(() {
@@ -215,6 +454,18 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
_progress = 1.0;
});
// Marquer dans settings que l'initialisation complète de Hive a été effectuée
try {
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.put('hive_initialized', true);
await settingsBox.put('hive_initialized_at', DateTime.now().toIso8601String());
debugPrint('✅ Clé hive_initialized définie à true dans settings');
}
} catch (e) {
debugPrint('⚠️ Impossible de définir la clé hive_initialized: $e');
}
// Attendre un court instant pour que l'utilisateur voie "Application prête !"
await Future.delayed(const Duration(milliseconds: 400));
@@ -375,7 +626,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
const Spacer(flex: 1),
// Indicateur de chargement
if (_isInitializing && !_showLocationError) ...[
if ((_isInitializing || _isCleaningCache) && !_showLocationError) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
@@ -651,6 +902,65 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
),
),
),
const SizedBox(height: 8),
// Bouton de nettoyage du cache (en noir)
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton.icon(
onPressed: _isCleaningCache ? null : () async {
// Confirmation avant nettoyage
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Nettoyer le cache ?'),
content: const Text(
'Cette action va :\n'
'• Supprimer toutes les données locales\n'
'• Préserver les requêtes en attente\n'
'• Forcer le rechargement de l\'application\n\n'
'Continuer ?'
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('Nettoyer'),
),
],
),
);
if (confirm == true) {
debugPrint('👤 Utilisateur a demandé un nettoyage manuel');
await _performSelectiveCleanup(manual: true);
// Après le nettoyage, relancer l'initialisation
_startInitialization();
}
},
icon: Icon(
Icons.cleaning_services,
size: 18,
color: _isCleaningCache ? Colors.grey : Colors.black87,
),
label: Text(
_isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache',
style: TextStyle(
color: _isCleaningCache ? Colors.grey : Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
),
],
const Spacer(flex: 1),

View File

@@ -0,0 +1,319 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geosector_app/chat/pages/rooms_page_embedded.dart';
import 'package:geosector_app/chat/chat_module.dart';
import 'package:geosector_app/core/services/chat_manager.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
/// Page de communication adaptative qui s'ajuste selon le rôle de l'utilisateur
/// et la plateforme (Web vs Mobile)
class ChatCommunicationPage extends StatefulWidget {
const ChatCommunicationPage({super.key});
@override
State<ChatCommunicationPage> createState() => _ChatCommunicationPageState();
}
class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
final GlobalKey<RoomsPageEmbeddedState> _roomsPageKey = GlobalKey<RoomsPageEmbeddedState>();
// Récupération du rôle de l'utilisateur
int get _userRole => CurrentUserService.instance.currentUser?.role ?? 1;
String get _userName => CurrentUserService.instance.userName ?? 'Utilisateur';
// Configuration selon le rôle
MaterialColor get _themeColor {
switch (_userRole) {
case 1: return Colors.green; // Membre
case 2: return Colors.red; // Admin Amicale
case 9: return Colors.blue; // Super Admin
default: return Colors.grey;
}
}
Color get _backgroundColor {
switch (_userRole) {
case 1: return Colors.green.shade50;
case 2: return Colors.red.shade50;
case 9: return Colors.blue.shade50;
default: return Colors.grey.shade50;
}
}
String get _pageTitle {
switch (_userRole) {
case 1: return 'Messages';
case 2: return 'Messages Administration';
case 9: return 'Centre de Communication GEOSECTOR';
default: return 'Messages';
}
}
IconData get _roleIcon {
switch (_userRole) {
case 1: return Icons.person;
case 2: return Icons.admin_panel_settings;
case 9: return Icons.shield;
default: return Icons.chat;
}
}
bool get _showStatsButton => _userRole == 9; // Super Admin uniquement
@override
Widget build(BuildContext context) {
// Détection de la plateforme
final isWeb = kIsWeb;
final isMobile = !isWeb;
// Construction adaptative
if (isWeb) {
return _buildWebLayout(context);
} else {
return _buildMobileLayout(context);
}
}
/// Layout pour Web (Desktop)
Widget _buildWebLayout(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, isWeb: true),
),
),
);
}
/// Layout pour Mobile (iOS/Android)
Widget _buildMobileLayout(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(_pageTitle),
backgroundColor: _themeColor,
foregroundColor: Colors.white,
elevation: 2,
actions: _buildAppBarActions(),
),
body: _buildContent(theme, isWeb: false),
floatingActionButton: FloatingActionButton(
onPressed: _handleNewConversation,
backgroundColor: _themeColor,
foregroundColor: Colors.white,
child: const Icon(Icons.add),
tooltip: 'Nouvelle conversation',
),
);
}
/// Contenu principal commun
Widget _buildContent(ThemeData theme, {required bool isWeb}) {
// Vérifier si le chat est initialisé
if (!ChatManager.instance.isReady) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_bubble_outline,
size: 80,
color: _themeColor.withOpacity(0.3),
),
const SizedBox(height: 24),
Text(
'Module de communication non disponible',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_getUnavailableMessage(),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
),
textAlign: TextAlign.center,
),
if (_userRole == 9) ...[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _handleRetryInit,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: _themeColor,
foregroundColor: Colors.white,
),
),
],
],
),
),
);
}
// Le chat est initialisé
if (isWeb) {
// Version Web avec en-tête personnalisé
return Column(
children: [
_buildWebHeader(theme),
Expanded(
child: RoomsPageEmbedded(
key: _roomsPageKey,
onRefreshPressed: () {
debugPrint('Conversations actualisées');
},
),
),
],
);
} else {
// Version Mobile, contenu direct
return RoomsPageEmbedded(
key: _roomsPageKey,
onRefreshPressed: () {
debugPrint('Conversations actualisées');
},
);
}
}
/// En-tête personnalisé pour Web
Widget _buildWebHeader(ThemeData theme) {
return Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: _backgroundColor,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
Icon(
_roleIcon,
color: _themeColor.shade600,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_pageTitle,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: _themeColor.shade700,
),
),
if (_userRole == 9)
Text(
'Connecté en tant que $_userName',
style: theme.textTheme.bodySmall?.copyWith(
color: _themeColor.shade600,
),
),
],
),
),
// Boutons d'action
if (_userRole == 9) ...[
// Super Admin : Statistiques
TextButton.icon(
icon: Icon(Icons.analytics, color: _themeColor.shade600),
label: Text(
'Statistiques',
style: TextStyle(color: _themeColor.shade600),
),
onPressed: _handleShowStats,
),
],
],
),
);
}
/// Actions pour l'AppBar mobile
List<Widget> _buildAppBarActions() {
final actions = <Widget>[];
if (_showStatsButton) {
actions.add(
IconButton(
icon: const Icon(Icons.analytics),
onPressed: _handleShowStats,
tooltip: 'Statistiques',
),
);
}
return actions;
}
/// Message personnalisé selon le rôle quand le chat n'est pas disponible
String _getUnavailableMessage() {
switch (_userRole) {
case 1:
return 'Le service de messagerie n\'est pas disponible actuellement.\nVous pourrez bientôt contacter les membres de votre amicale.';
case 2:
return 'Le service de messagerie administration n\'est pas disponible.\nVous pourrez bientôt gérer les communications de votre amicale.';
case 9:
return 'Le centre de communication GEOSECTOR est temporairement indisponible.\nVérifiez la connexion au serveur.';
default:
return 'Le service de messagerie n\'est pas disponible actuellement.';
}
}
/// Gestionnaires d'événements
void _handleNewConversation() {
_roomsPageKey.currentState?.createNewConversation();
}
void _handleShowStats() {
// TODO: Implémenter l'affichage des statistiques pour Super Admin
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Statistiques à venir...'),
backgroundColor: _themeColor,
),
);
}
void _handleRetryInit() async {
// Réessayer l'initialisation du chat (pour Super Admin)
await ChatManager.instance.reinitialize();
if (mounted) {
setState(() {});
}
}
}

View File

@@ -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();
}
}

View File

@@ -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(),
];

View File

@@ -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();
}
}

View File

@@ -20,11 +20,15 @@ NavigationDestination createBadgedNavigationDestination({
final badgedIcon = BadgedIcon(
icon: icon.icon!,
showBadge: true,
color: icon.color,
size: icon.size,
);
final badgedSelectedIcon = BadgedIcon(
icon: selectedIcon.icon!,
showBadge: true,
color: selectedIcon.color,
size: selectedIcon.size,
);
return NavigationDestination(

View File

@@ -52,6 +52,9 @@ class PaymentPieChart extends StatefulWidget {
/// ID de l'utilisateur pour filtrer les passages
final int? userId;
/// Afficher tous les passages (admin) ou seulement ceux de l'utilisateur
final bool showAllPassages;
const PaymentPieChart({
super.key,
this.payments = const [],
@@ -68,6 +71,7 @@ class PaymentPieChart extends StatefulWidget {
this.useGradient = false,
this.useValueListenable = true,
this.userId,
this.showAllPassages = false,
});
@override
@@ -97,7 +101,8 @@ class _PaymentPieChartState extends State<PaymentPieChart>
bool shouldResetAnimation = false;
if (widget.useValueListenable != oldWidget.useValueListenable ||
widget.userId != oldWidget.userId) {
widget.userId != oldWidget.userId ||
widget.showAllPassages != oldWidget.showAllPassages) {
shouldResetAnimation = true;
} else if (!widget.useValueListenable) {
// Pour les données statiques, comparer les éléments
@@ -158,7 +163,11 @@ class _PaymentPieChartState extends State<PaymentPieChart>
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = widget.userId ?? currentUser?.id;
// Déterminer l'utilisateur cible selon les filtres
final int? targetUserId = widget.showAllPassages
? null
: (widget.userId ?? currentUser?.id);
// Initialiser les montants par type de règlement
final Map<int, double> paymentAmounts = {
@@ -170,8 +179,13 @@ class _PaymentPieChartState extends State<PaymentPieChart>
// Parcourir les passages et calculer les montants par type de règlement
for (final passage in passages) {
// Vérifier si le passage appartient à l'utilisateur actuel
if (currentUserId != null && passage.fkUser == currentUserId) {
// Appliquer le filtre utilisateur si nécessaire
bool shouldInclude = true;
if (targetUserId != null && passage.fkUser != targetUserId) {
shouldInclude = false;
}
if (shouldInclude) {
final int typeReglement = passage.fkTypeReglement;
// Convertir la chaîne de montant en double

View File

@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/pending_request.dart';
/// Widget qui affiche l'état de la connexion Internet
class ConnectivityIndicator extends StatelessWidget {
/// Widget qui affiche l'état de la connexion Internet et le nombre de requêtes en attente
class ConnectivityIndicator extends StatefulWidget {
/// Si true, affiche un message d'erreur lorsque l'appareil est déconnecté
final bool showErrorMessage;
@@ -20,6 +23,52 @@ class ConnectivityIndicator extends StatelessWidget {
this.onConnectivityChanged,
});
@override
State<ConnectivityIndicator> createState() => _ConnectivityIndicatorState();
}
class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// Configuration de l'animation de clignotement
_animationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_animation = Tween<double>(
begin: 1.0,
end: 0.3,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _updateAnimation(int pendingCount) {
if (pendingCount > 0) {
// Démarrer l'animation de clignotement si des requêtes sont en attente
if (!_animationController.isAnimating) {
_animationController.repeat(reverse: true);
}
} else {
// Arrêter l'animation quand il n'y a plus de requêtes
_animationController.stop();
_animationController.value = 1.0;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -32,12 +81,159 @@ class ConnectivityIndicator extends StatelessWidget {
// Appeler le callback si fourni, mais pas directement dans le build
// pour éviter les problèmes de rendu
WidgetsBinding.instance.addPostFrameCallback((_) {
if (onConnectivityChanged != null) {
onConnectivityChanged!(isConnected);
if (widget.onConnectivityChanged != null) {
widget.onConnectivityChanged!(isConnected);
}
});
if (!isConnected && showErrorMessage) {
// Vérifier si la box des requêtes en attente est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return _buildBasicIndicator(context, isConnected, connectionType, connectionStatus, theme, 0);
}
// Utiliser ValueListenableBuilder pour surveiller les requêtes en attente
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final pendingCount = box.length;
// Mettre à jour l'animation en fonction du nombre de requêtes
_updateAnimation(pendingCount);
if (!isConnected && widget.showErrorMessage) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.error.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.wifi_off,
color: theme.colorScheme.error,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
pendingCount > 0
? 'Hors ligne - $pendingCount requête${pendingCount > 1 ? 's' : ''} en attente'
: 'Aucune connexion Internet. Certaines fonctionnalités peuvent être limitées.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
),
if (pendingCount > 0)
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Text(
pendingCount.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
],
),
);
} else if (isConnected && widget.showConnectionType) {
return _buildConnectedIndicator(
context,
connectionStatus,
connectionType,
theme,
pendingCount
);
}
// Si aucune condition n'est remplie
return const SizedBox.shrink();
},
);
}
Widget _buildConnectedIndicator(
BuildContext context,
List<ConnectivityResult> connectionStatus,
String connectionType,
ThemeData theme,
int pendingCount,
) {
// Obtenir la couleur et l'icône en fonction du type de connexion
final color = _getConnectionColor(connectionStatus, theme);
final icon = _getConnectionIcon(connectionStatus);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: pendingCount > 0
? Colors.orange.withOpacity(0.1 * _animation.value)
: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: pendingCount > 0
? Colors.orange.withOpacity(0.3 * _animation.value)
: color.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
pendingCount > 0 ? Icons.sync : icon,
color: pendingCount > 0 ? Colors.orange : color,
size: 14,
),
const SizedBox(width: 4),
Text(
pendingCount > 0
? '$pendingCount en attente'
: connectionType,
style: theme.textTheme.bodySmall?.copyWith(
color: pendingCount > 0 ? Colors.orange : color,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
);
}
Widget _buildBasicIndicator(
BuildContext context,
bool isConnected,
String connectionType,
List<ConnectivityResult> connectionStatus,
ThemeData theme,
int pendingCount,
) {
if (!isConnected && widget.showErrorMessage) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
@@ -67,8 +263,7 @@ class ConnectivityIndicator extends StatelessWidget {
],
),
);
} else if (isConnected && showConnectionType) {
// Obtenir la couleur et l'icône en fonction du type de connexion
} else if (isConnected && widget.showConnectionType) {
final color = _getConnectionColor(connectionStatus, theme);
final icon = _getConnectionIcon(connectionStatus);
@@ -102,7 +297,6 @@ class ConnectivityIndicator extends StatelessWidget {
);
}
// Si aucune condition n'est remplie ou si showErrorMessage et showConnectionType sont false
return const SizedBox.shrink();
}

View File

@@ -0,0 +1,286 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:uuid/uuid.dart';
/// Widget de test pour vérifier le fonctionnement de la file d'attente offline
/// À utiliser uniquement en développement
class OfflineTestButton extends StatefulWidget {
const OfflineTestButton({Key? key}) : super(key: key);
@override
State<OfflineTestButton> createState() => _OfflineTestButtonState();
}
class _OfflineTestButtonState extends State<OfflineTestButton> {
final _uuid = const Uuid();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Test de synchronisation offline',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Utilisez ces boutons pour tester la mise en file d\'attente des requêtes',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
// Indicateur de connectivité
ListenableBuilder(
listenable: ConnectivityService(),
builder: (context, child) {
final isConnected = ConnectivityService().isConnected;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isConnected ? Colors.green.shade100 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isConnected ? Icons.wifi : Icons.wifi_off,
color: isConnected ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
isConnected ? 'Connecté' : 'Hors ligne',
style: TextStyle(
color: isConnected ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
const SizedBox(height: 16),
// Boutons de test
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testGetRequest,
icon: const Icon(Icons.download),
label: const Text('Test GET'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testPostRequest,
icon: const Icon(Icons.upload),
label: const Text('Test POST'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testPutRequest,
icon: const Icon(Icons.edit),
label: const Text('Test PUT'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testDeleteRequest,
icon: const Icon(Icons.delete),
label: const Text('Test DELETE'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _processQueue,
icon: const Icon(Icons.sync),
label: const Text('Traiter la file'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
),
),
],
),
if (_isProcessing)
const Padding(
padding: EdgeInsets.only(top: 16),
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
);
}
Future<void> _testGetRequest() async {
setState(() => _isProcessing = true);
try {
debugPrint('🧪 Test GET request');
final response = await ApiService.instance.get('/test/endpoint');
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête GET mise en file d\'attente');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête GET exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testPostRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test POST request avec tempId: $tempId');
final testData = {
'name': 'Test User ${DateTime.now().millisecondsSinceEpoch}',
'email': 'test@example.com',
'timestamp': DateTime.now().toIso8601String(),
};
final response = await ApiService.instance.post(
'/test/create',
data: testData,
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête POST mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête POST exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testPutRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test PUT request avec tempId: $tempId');
final testData = {
'id': 123,
'name': 'Updated User ${DateTime.now().millisecondsSinceEpoch}',
'email': 'updated@example.com',
'timestamp': DateTime.now().toIso8601String(),
};
final response = await ApiService.instance.put(
'/test/update/123',
data: testData,
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête PUT mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête PUT exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testDeleteRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test DELETE request avec tempId: $tempId');
final response = await ApiService.instance.delete(
'/test/delete/123',
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête DELETE mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête DELETE exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _processQueue() async {
setState(() => _isProcessing = true);
try {
debugPrint('🧪 Traitement manuel de la file d\'attente');
await ApiService.instance.processPendingRequests();
if (mounted) {
ApiException.showSuccess(context, 'File d\'attente traitée');
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
}

View File

@@ -0,0 +1,269 @@
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/data/models/pending_request.dart';
/// Widget qui affiche le nombre de requêtes en attente de synchronisation
/// S'affiche uniquement quand il y a au moins une requête en attente
/// Se met à jour automatiquement grâce au ValueListenableBuilder
class PendingRequestsCounter extends StatelessWidget {
final bool showDetails;
final Color? backgroundColor;
final Color? textColor;
const PendingRequestsCounter({
Key? key,
this.showDetails = false,
this.backgroundColor,
this.textColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Vérifier si la box est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return const SizedBox.shrink();
}
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final count = box.length;
// Ne rien afficher s'il n'y a pas de requêtes en attente
if (count == 0) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.orange.shade100,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.orange.shade300,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.sync,
size: 16,
color: textColor ?? Colors.orange.shade700,
),
const SizedBox(width: 6),
Text(
count == 1
? '1 requête en attente'
: '$count requêtes en attente',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textColor ?? Colors.orange.shade700,
),
),
if (showDetails) ...[
const SizedBox(width: 6),
InkWell(
onTap: () => _showPendingRequestsDialog(context, box),
child: Icon(
Icons.info_outline,
size: 16,
color: textColor ?? Colors.orange.shade700,
),
),
],
],
),
);
},
);
}
void _showPendingRequestsDialog(BuildContext context, Box<PendingRequest> box) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Requêtes en attente'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: box.length,
itemBuilder: (context, index) {
final request = box.getAt(index);
if (request == null) return const SizedBox.shrink();
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: _getMethodColor(request.method),
borderRadius: BorderRadius.circular(4),
),
child: Text(
request.method,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
request.path,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 4),
Text(
'Créée: ${_formatDateTime(request.createdAt)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
if (request.retryCount > 0) ...[
const SizedBox(height: 2),
Text(
'Tentatives: ${request.retryCount}',
style: TextStyle(
fontSize: 11,
color: Colors.orange.shade700,
),
),
],
if (request.errorMessage != null) ...[
const SizedBox(height: 2),
Text(
'Erreur: ${request.errorMessage}',
style: const TextStyle(
fontSize: 11,
color: Colors.red,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
Color _getMethodColor(String method) {
switch (method.toUpperCase()) {
case 'GET':
return Colors.blue;
case 'POST':
return Colors.green;
case 'PUT':
return Colors.orange;
case 'DELETE':
return Colors.red;
default:
return Colors.grey;
}
}
String _formatDateTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inSeconds < 60) {
return 'Il y a ${difference.inSeconds}s';
} else if (difference.inMinutes < 60) {
return 'Il y a ${difference.inMinutes}min';
} else if (difference.inHours < 24) {
return 'Il y a ${difference.inHours}h';
} else {
return 'Il y a ${difference.inDays}j';
}
}
}
/// Version compacte du compteur pour les barres d'outils
class PendingRequestsCounterCompact extends StatelessWidget {
final Color? color;
const PendingRequestsCounterCompact({
Key? key,
this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Vérifier si la box est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return const SizedBox.shrink();
}
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final count = box.length;
// Ne rien afficher s'il n'y a pas de requêtes en attente
if (count == 0) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color ?? Colors.orange,
shape: BoxShape.circle,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.sync,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
count.toString(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
);
},
);
}
}

View File

@@ -356,11 +356,25 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
Widget _buildNavItem(int index, String title, Widget icon) {
final theme = Theme.of(context);
final isSelected = widget.selectedIndex == index;
final IconData? iconData = (icon is Icon) ? (icon).icon : null;
// Définir les couleurs selon le rôle (admin = rouge, user = vert)
final Color selectedColor = widget.isAdmin ? Colors.red : Colors.green;
final Color unselectedColor = theme.colorScheme.onSurface.withOpacity(0.6);
// Gérer le cas où l'icône est un BadgedIcon ou autre widget composite
Widget iconWidget;
if (icon is Icon) {
// Si c'est une Icon simple, on peut appliquer les couleurs
iconWidget = Icon(
icon.icon,
color: isSelected ? selectedColor : unselectedColor,
size: 24,
);
} else {
// Si c'est un BadgedIcon ou autre widget, on le garde tel quel
// Le BadgedIcon gère ses propres couleurs
iconWidget = icon;
}
// Remplacer certains titres si l'interface est de type "user"
String displayTitle = title;
@@ -391,13 +405,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: iconData != null
? Icon(
iconData,
color: isSelected ? selectedColor : unselectedColor,
size: 24,
)
: icon,
child: Center(child: iconWidget),
),
),
),
@@ -405,12 +413,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
} else {
// Version normale avec texte et icône
return ListTile(
leading: iconData != null
? Icon(
iconData,
color: isSelected ? selectedColor : unselectedColor,
)
: icon,
leading: iconWidget,
title: Text(
displayTitle,
style: TextStyle(
@@ -432,8 +435,6 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
class _SettingsItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? trailing;
final VoidCallback onTap;
final bool isSidebarMinimized;
@@ -442,8 +443,6 @@ class _SettingsItem extends StatelessWidget {
required this.title,
required this.onTap,
required this.isSidebarMinimized,
this.subtitle,
this.trailing,
});
@override
@@ -482,8 +481,6 @@ class _SettingsItem extends StatelessWidget {
color: theme.colorScheme.primary,
),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: trailing,
onTap: onTap,
);
}

View File

@@ -53,7 +53,6 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
Widget _buildSortButton(String label, SortType sortType) {
final isActive = _currentSortType == sortType && _currentSortOrder != SortOrder.none;
final isAsc = _currentSortType == sortType && _currentSortOrder == SortOrder.asc;
final isDesc = _currentSortType == sortType && _currentSortOrder == SortOrder.desc;
return InkWell(
onTap: () => _onSortPressed(sortType),
@@ -320,8 +319,8 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
onTap: () {
// Sauvegarder le secteur sélectionné et l'index de la page carte dans Hive
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('admin_selectedSectorId', sectorId);
settingsBox.put('adminSelectedPageIndex', 4); // Index de la page carte
settingsBox.put('selectedSectorId', sectorId);
settingsBox.put('selectedPageIndex', 4); // Index de la page carte
// Naviguer vers le dashboard admin qui chargera la page carte
context.go('/admin');
@@ -426,7 +425,7 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
settingsBox.put('history_selectedSectorId', sectorId);
settingsBox.put('history_selectedSectorName', sectorName);
settingsBox.put('history_selectedTypeId', typeId);
settingsBox.put('adminSelectedPageIndex', 2); // Index de la page historique
settingsBox.put('selectedPageIndex', 2); // Index de la page historique
// Naviguer vers le dashboard admin qui chargera la page historique
context.go('/admin');

View File

@@ -147,7 +147,6 @@ class ThemeSwitcher extends StatelessWidget {
/// Boutons à bascule
Widget _buildToggleButtons(BuildContext context) {
final themeService = ThemeService.instance;
final theme = Theme.of(context);
return ToggleButtons(
borderRadius: BorderRadius.circular(8),

View File

@@ -989,7 +989,6 @@ class _UserFormState extends State<UserForm> {
required Function(int?)? onChanged,
}) {
final theme = Theme.of(context);
final isSelected = value == groupValue;
return Row(
children: [