feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user