import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/presentation/widgets/mapbox_map.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/core/data/models/sector_model.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; class AdminMapPage extends StatefulWidget { const AdminMapPage({Key? key}) : super(key: key); @override State createState() => _AdminMapPageState(); } class _AdminMapPageState extends State { // Contrôleur de carte final MapController _mapController = MapController(); // Position actuelle et zoom LatLng _currentPosition = const LatLng(48.117266, -1.6777926); // Position initiale sur Rennes double _currentZoom = 12.0; // Zoom initial // Données des secteurs et passages final List> _sectors = []; final List> _passages = []; // États bool _editMode = false; int? _selectedSectorId; List> _sectorItems = []; // Référence à la boîte Hive pour les paramètres late Box _settingsBox; @override void initState() { super.initState(); _initSettings().then((_) { _loadSectors(); _loadPassages(); }); } // Initialiser la boîte de paramètres et charger les préférences Future _initSettings() async { // Ouvrir la boîte de paramètres si elle n'est pas déjà ouverte if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { _settingsBox = await Hive.openBox(AppKeys.settingsBoxName); } else { _settingsBox = Hive.box(AppKeys.settingsBoxName); } // Charger le secteur sélectionné _selectedSectorId = _settingsBox.get('admin_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'); if (savedLat != null && savedLng != null) { _currentPosition = LatLng(savedLat, savedLng); } if (savedZoom != null) { _currentZoom = savedZoom; } } // Sauvegarder les paramètres utilisateur void _saveSettings() { // Sauvegarder le secteur sélectionné if (_selectedSectorId != null) { _settingsBox.put('admin_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); } // Charger les secteurs depuis la boîte Hive void _loadSectors() { try { final sectorsBox = Hive.box(AppKeys.sectorsBoxName); final sectors = sectorsBox.values.toList(); setState(() { _sectors.clear(); for (final sector in sectors) { final List> coordinates = sector.getCoordinates(); final List points = coordinates.map((coord) => LatLng(coord[0], coord[1])).toList(); if (points.isNotEmpty) { _sectors.add({ 'id': sector.id, 'name': sector.libelle, 'color': _hexToColor(sector.color), 'points': points, }); } } // Si un secteur était sélectionné précédemment, le centrer // Mettre à jour les items de la combobox de secteurs _updateSectorItems(); if (_selectedSectorId != null && _sectors.any((s) => s['id'] == _selectedSectorId)) { _centerMapOnSpecificSector(_selectedSectorId!); } // Sinon, centrer la carte sur tous les secteurs else if (_sectors.isNotEmpty) { _centerMapOnSectors(); } }); } catch (e) { debugPrint('Erreur lors du chargement des secteurs: $e'); } } // Charger les passages depuis la boîte Hive void _loadPassages() { try { // Récupérer la boîte des passages final passagesBox = Hive.box(AppKeys.passagesBoxName); // Créer une nouvelle liste temporaire final List> newPassages = []; // Parcourir tous les passages dans la boîte for (var i = 0; i < passagesBox.length; i++) { final passage = passagesBox.getAt(i); if (passage != null) { // Vérifier si les coordonnées GPS sont valides final lat = double.tryParse(passage.gpsLat); final lng = double.tryParse(passage.gpsLng); // Filtrer par secteur si un secteur est sélectionné if (_selectedSectorId != null && passage.fkSector != _selectedSectorId) { continue; } if (lat != null && lng != null) { // Obtenir la couleur du type de passage Color passageColor = Colors.grey; // Couleur par défaut // Vérifier si le type de passage existe dans AppKeys.typesPassages if (AppKeys.typesPassages.containsKey(passage.fkType)) { // Utiliser la couleur1 du type de passage final colorValue = AppKeys.typesPassages[passage.fkType]!['couleur1'] as int; passageColor = Color(colorValue); // Ajouter le passage à la liste temporaire newPassages.add({ 'id': passage.id, 'position': LatLng(lat, lng), 'type': passage.fkType, 'color': passageColor, 'model': passage, // Ajouter le modèle complet }); } } } } // Mettre à jour la liste des passages dans l'état setState(() { _passages.clear(); _passages.addAll(newPassages); }); // Sauvegarder les paramètres après chargement des passages _saveSettings(); } catch (e) { debugPrint('Erreur lors du chargement des passages: $e'); } } // Convertir une couleur hexadécimale en Color Color _hexToColor(String hexColor) { // Supprimer le # si présent final String colorStr = hexColor.startsWith('#') ? hexColor.substring(1) : hexColor; // Ajouter FF pour l'opacité si nécessaire (6 caractères -> 8 caractères) final String fullColorStr = colorStr.length == 6 ? 'FF$colorStr' : colorStr; // Convertir en entier et créer la couleur return Color(int.parse(fullColorStr, radix: 16)); } // Centrer la carte sur tous les secteurs void _centerMapOnSectors() { if (_sectors.isEmpty) return; // Trouver les limites de tous les secteurs double minLat = 90.0; double maxLat = -90.0; double minLng = 180.0; double maxLng = -180.0; for (final sector in _sectors) { final points = sector['points'] as List; for (final point in points) { minLat = point.latitude < minLat ? point.latitude : minLat; maxLat = point.latitude > maxLat ? point.latitude : maxLat; minLng = point.longitude < minLng ? point.longitude : minLng; maxLng = point.longitude > maxLng ? point.longitude : maxLng; } } // Ajouter un padding aux limites pour s'assurer que tous les secteurs sont entièrement visibles // avec une marge autour (5% de la taille totale) final latPadding = (maxLat - minLat) * 0.05; final lngPadding = (maxLng - minLng) * 0.05; minLat -= latPadding; maxLat += latPadding; minLng -= lngPadding; maxLng += lngPadding; // Calculer le centre final centerLat = (minLat + maxLat) / 2; final centerLng = (minLng + maxLng) / 2; // Calculer le zoom approprié en tenant compte des dimensions de l'écran final mapWidth = MediaQuery.of(context).size.width; final mapHeight = MediaQuery.of(context).size.height * 0.7; // Estimation de la hauteur de la carte final zoom = _calculateOptimalZoom( minLat, maxLat, minLng, maxLng, mapWidth, mapHeight); // Centrer la carte sur ces limites avec animation _mapController.move(LatLng(centerLat, centerLng), zoom); // Mettre à jour l'état pour refléter la nouvelle position setState(() { _currentPosition = LatLng(centerLat, centerLng); _currentZoom = zoom; }); debugPrint('Carte centrée sur tous les secteurs avec zoom: $zoom'); } // Mettre à jour les items de la combobox de secteurs void _updateSectorItems() { // Créer l'item "Tous les secteurs" final List> items = [ const DropdownMenuItem( value: null, child: Text('Tous les secteurs'), ), ]; // Ajouter tous les secteurs for (final sector in _sectors) { items.add( DropdownMenuItem( value: sector['id'] as int, child: Text(sector['name'] as String), ), ); } setState(() { _sectorItems = items; }); } // Centrer la carte sur un secteur spécifique void _centerMapOnSpecificSector(int sectorId) { final sectorIndex = _sectors.indexWhere((s) => s['id'] == sectorId); if (sectorIndex == -1) return; // Mettre à jour le secteur sélectionné _selectedSectorId = sectorId; final sector = _sectors[sectorIndex]; final points = sector['points'] as List; final sectorName = sector['name'] as String; debugPrint( 'Centrage sur le secteur: $sectorName (ID: $sectorId) avec ${points.length} points'); if (points.isEmpty) { debugPrint('Aucun point dans ce secteur!'); return; } // Trouver les limites du secteur double minLat = 90.0; double maxLat = -90.0; double minLng = 180.0; double maxLng = -180.0; for (final point in points) { minLat = point.latitude < minLat ? point.latitude : minLat; maxLat = point.latitude > maxLat ? point.latitude : maxLat; minLng = point.longitude < minLng ? point.longitude : minLng; maxLng = point.longitude > maxLng ? point.longitude : maxLng; } // Vérifier si les coordonnées sont valides if (minLat >= maxLat || minLng >= maxLng) { debugPrint('Coordonnées invalides pour le secteur $sectorName'); return; } // Calculer la taille du secteur final latSpan = maxLat - minLat; final lngSpan = maxLng - minLng; // Ajouter un padding minimal aux limites pour s'assurer que le secteur est bien visible final double latPadding, lngPadding; if (latSpan < 0.01 || lngSpan < 0.01) { // Pour les très petits secteurs, utiliser un padding très réduit latPadding = 0.0003; lngPadding = 0.0003; } else if (latSpan < 0.05 || lngSpan < 0.05) { // Pour les petits secteurs, padding réduit latPadding = 0.0005; lngPadding = 0.0005; } else { // Pour les secteurs plus grands, utiliser un pourcentage minimal latPadding = latSpan * 0.03; // 3% au lieu de 10% lngPadding = lngSpan * 0.03; } minLat -= latPadding; maxLat += latPadding; minLng -= lngPadding; maxLng += lngPadding; // Calculer le centre final centerLat = (minLat + maxLat) / 2; final centerLng = (minLng + maxLng) / 2; // Déterminer le zoom approprié en fonction de la taille du secteur double zoom; // Pour les très petits secteurs (comme des quartiers), utiliser un zoom fixe élevé if (latSpan < 0.01 && lngSpan < 0.01) { zoom = 16.0; // Zoom élevé pour les petits quartiers } else if (latSpan < 0.02 && lngSpan < 0.02) { zoom = 15.0; // Zoom élevé pour les petits quartiers } else if (latSpan < 0.05 && lngSpan < 0.05) { zoom = 13.0; // Zoom pour les secteurs de taille moyenne (quelques quartiers) } else if (latSpan < 0.1 && lngSpan < 0.1) { zoom = 12.0; // Zoom pour les grands secteurs (ville) } else { // Pour les secteurs plus grands, calculer le zoom final mapWidth = MediaQuery.of(context).size.width; final mapHeight = MediaQuery.of(context).size.height * 0.7; zoom = _calculateOptimalZoom( minLat, maxLat, minLng, maxLng, mapWidth, mapHeight); } // Centrer la carte sur le secteur avec animation _mapController.move(LatLng(centerLat, centerLng), zoom); // Mettre à jour l'état pour refléter la nouvelle position setState(() { _currentPosition = LatLng(centerLat, centerLng); _currentZoom = zoom; }); // Recharger les passages pour appliquer le filtre par secteur _loadPassages(); } // Calculer le zoom optimal pour afficher une zone géographique dans la fenêtre de la carte double _calculateOptimalZoom(double minLat, double maxLat, double minLng, double maxLng, double mapWidth, double mapHeight) { // Vérifier si les coordonnées sont valides if (minLat >= maxLat || minLng >= maxLng) { debugPrint('Coordonnées invalides pour le calcul du zoom'); return 12.0; // Valeur par défaut raisonnable } // Calculer la taille en degrés final latSpan = maxLat - minLat; final lngSpan = maxLng - minLng; // Ajouter un facteur de sécurité pour éviter les divisions par zéro if (latSpan < 0.0000001 || lngSpan < 0.0000001) { return 15.0; // Zoom élevé pour un point très précis } // Formule simplifiée pour le calcul du zoom double zoom; if (latSpan < 0.005 || lngSpan < 0.005) { // Très petite zone (quartier) zoom = 16.0; } else if (latSpan < 0.01 || lngSpan < 0.01) { // Petite zone (quartier) zoom = 15.0; } else if (latSpan < 0.02 || lngSpan < 0.02) { // Petite zone (plusieurs quartiers) zoom = 14.0; } else if (latSpan < 0.05 || lngSpan < 0.05) { // Zone moyenne (ville) zoom = 13.0; } else if (latSpan < 0.2 || lngSpan < 0.2) { // Grande zone (agglomération) zoom = 11.0; } else if (latSpan < 0.5 || lngSpan < 0.5) { // Très grande zone (département) zoom = 9.0; } else if (latSpan < 2.0 || lngSpan < 2.0) { // Région zoom = 7.0; } else if (latSpan < 5.0 || lngSpan < 5.0) { // Pays zoom = 5.0; } else { // Continent ou plus zoom = 3.0; } return zoom; } // Obtenir la position actuelle de l'utilisateur Future _getUserLocation() async { try { // Afficher un indicateur de chargement ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Recherche de votre position...'), duration: Duration(seconds: 2), ), ); // Obtenir la position actuelle via le service de géolocalisation final position = await LocationService.getCurrentPosition(); if (position != null) { // Mettre à jour la position sur la carte _updateMapPosition(position, zoom: 17); // Sauvegarder la nouvelle position _settingsBox.put('admin_mapLat', position.latitude); _settingsBox.put('admin_mapLng', position.longitude); // Informer l'utilisateur if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Position actualisée'), backgroundColor: Colors.green, duration: Duration(seconds: 1), ), ); } } else { // Informer l'utilisateur en cas d'échec if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Impossible d\'obtenir votre position. Vérifiez vos paramètres de localisation.'), backgroundColor: Colors.red, ), ); } } } catch (e) { // Gérer les erreurs if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: Colors.red, ), ); } } } // Méthode pour mettre à jour la position sur la carte void _updateMapPosition(LatLng position, {double? zoom}) { _mapController.move( position, zoom ?? _mapController.camera.zoom, ); // Mettre à jour les variables d'état setState(() { _currentPosition = position; if (zoom != null) { _currentZoom = zoom; } }); // Sauvegarder les paramètres après mise à jour de la position _saveSettings(); } // Méthode pour construire les marqueurs des passages List _buildMarkers() { if (_passages.isEmpty) { return []; } return _passages.map((passage) { final int passageType = passage['type'] as int; final Color color1 = passage['color'] as Color; // couleur1 du type de passage // Récupérer la couleur2 du type de passage Color color2 = Colors.white; // Couleur par défaut if (AppKeys.typesPassages.containsKey(passageType)) { final colorValue = AppKeys.typesPassages[passageType]!['couleur2'] as int; color2 = Color(colorValue); } return Marker( point: passage['position'] as LatLng, width: 14.0, height: 14.0, child: GestureDetector( onTap: () { _showPassageInfo(passage); }, child: Container( decoration: BoxDecoration( color: color1, shape: BoxShape.circle, border: Border.all( color: color2, width: 1.0, ), ), ), ), ); }).toList(); } // Méthode pour construire les polygones des secteurs List _buildPolygons() { if (_sectors.isEmpty) { return []; } return _sectors.map((sector) { final bool isSelected = _selectedSectorId == sector['id']; final Color sectorColor = sector['color'] as Color; return Polygon( points: sector['points'] as List, color: isSelected ? sectorColor.withOpacity(0.5) : sectorColor.withOpacity(0.3), borderColor: isSelected ? sectorColor : sectorColor.withOpacity(0.8), borderStrokeWidth: isSelected ? 3.0 : 2.0, ); }).toList(); } // Afficher les informations d'un passage lorsqu'on clique dessus void _showPassageInfo(Map passage) { final PassageModel passageModel = passage['model'] as PassageModel; final int type = passageModel.fkType; // Construire l'adresse complète final String adresse = '${passageModel.numero}, ${passageModel.rueBis} ${passageModel.rue}'; // Informations sur l'étage, l'appartement et la résidence (si habitat = 2) String? etageInfo; String? apptInfo; String? residenceInfo; if (passageModel.fkHabitat == 2) { if (passageModel.niveau.isNotEmpty) { etageInfo = 'Etage ${passageModel.niveau}'; } if (passageModel.appt.isNotEmpty) { apptInfo = 'appt. ${passageModel.appt}'; } if (passageModel.residence.isNotEmpty) { residenceInfo = passageModel.residence; } } // Formater la date (uniquement si le type n'est pas 2) String dateInfo = ''; if (type != 2) { dateInfo = 'Date: ${_formatDate(passageModel.passedAt)}'; } // Récupérer le nom du passage (si le type n'est pas 6 - Maison vide) String? nomInfo; if (type != 6 && passageModel.name.isNotEmpty) { nomInfo = passageModel.name; } // Récupérer les informations de règlement si le type est 1 (Effectué) ou 5 (Lot) Widget? reglementInfo; if (type == 1 || type == 5) { final int typeReglementId = passageModel.fkTypeReglement; final String montant = passageModel.montant; // Récupérer les informations du type de règlement if (AppKeys.typesReglements.containsKey(typeReglementId)) { final Map typeReglement = AppKeys.typesReglements[typeReglementId]!; final String titre = typeReglement['titre'] as String; final Color couleur = Color(typeReglement['couleur'] as int); final IconData iconData = typeReglement['icon_data'] as IconData; reglementInfo = Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon(iconData, color: couleur, size: 20), const SizedBox(width: 8), Text('$titre: $montant €', style: TextStyle(color: couleur, fontWeight: FontWeight.bold)), ], ), ); } } // Afficher une bulle d'information showDialog( context: context, builder: (context) => AlertDialog( contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Adresse: $adresse'), if (residenceInfo != null) ...[ const SizedBox(height: 4), Text(residenceInfo) ], if (etageInfo != null) ...[ const SizedBox(height: 4), Text(etageInfo) ], if (apptInfo != null) ...[ const SizedBox(height: 4), Text(apptInfo) ], if (dateInfo.isNotEmpty) ...[ const SizedBox(height: 8), Text(dateInfo) ], if (nomInfo != null) ...[ const SizedBox(height: 8), Text('Nom: $nomInfo') ], if (reglementInfo != null) reglementInfo, ], ), actionsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ // Bouton d'édition IconButton( onPressed: () { Navigator.of(context).pop(); // Logique pour éditer le passage debugPrint('Éditer le passage ${passageModel.id}'); }, icon: const Icon(Icons.edit), color: Colors.blue, tooltip: 'Modifier', ), // Bouton de suppression IconButton( onPressed: () { Navigator.of(context).pop(); // Logique pour supprimer le passage debugPrint('Supprimer le passage ${passageModel.id}'); }, icon: const Icon(Icons.delete), color: Colors.red, tooltip: 'Supprimer', ), ], ), // Bouton de fermeture TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ), ], ), ); } // Formater une date String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } // Widget pour les boutons d'action Widget _buildActionButton({ required IconData icon, required String tooltip, required VoidCallback? onPressed, Color color = Colors.blue, }) { return Container( decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: FloatingActionButton( heroTag: tooltip, // Nécessaire pour éviter les conflits de hero tags onPressed: onPressed, backgroundColor: onPressed != null ? color : Colors.grey, tooltip: tooltip, mini: true, child: Icon(icon), ), ); } @override Widget build(BuildContext context) { return Stack( children: [ // Carte MapBox MapboxMap( initialPosition: _currentPosition, initialZoom: _currentZoom, mapController: _mapController, markers: _buildMarkers(), polygons: _buildPolygons(), showControls: true, onMapEvent: (event) { if (event is MapEventMove) { setState(() { _currentPosition = event.camera.center; _currentZoom = event.camera.zoom; }); _saveSettings(); } }, ), // Bouton Mode édition en haut à droite Positioned( right: 16, top: 16, child: _buildActionButton( icon: Icons.edit, tooltip: 'Mode édition', color: _editMode ? Colors.green : Colors.blue, onPressed: () { setState(() { _editMode = !_editMode; }); }, ), ), // Boutons d'action sous le bouton Mode édition Positioned( right: 16, top: 80, // Positionner sous le bouton Mode édition child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (_editMode) ...[ _buildActionButton( icon: Icons.add, tooltip: 'Ajouter un secteur', onPressed: () { // Action pour ajouter un secteur }, ), const SizedBox(height: 8), _buildActionButton( icon: Icons.edit, tooltip: 'Modifier le secteur sélectionné', onPressed: _selectedSectorId != null ? () { // Action pour modifier le secteur sélectionné } : null, ), const SizedBox(height: 8), _buildActionButton( icon: Icons.delete, tooltip: 'Supprimer le secteur sélectionné', color: Colors.red, onPressed: _selectedSectorId != null ? () { // Action pour supprimer le secteur sélectionné } : null, ), const SizedBox(height: 16), ], ], ), ), // Bouton Ma position en bas à droite Positioned( right: 16, bottom: 16, child: _buildActionButton( icon: Icons.my_location, tooltip: 'Ma position', onPressed: () { _getUserLocation(); }, ), ), // Combobox de sélection de secteurs Positioned( left: 16, top: 16, child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), width: 220, // Largeur fixe pour accommoder les noms longs decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.location_on, size: 18, color: Colors.blue), const SizedBox(width: 8), Expanded( child: DropdownButton( value: _selectedSectorId, hint: const Text('Tous les secteurs'), isExpanded: true, underline: Container(), // Supprimer la ligne sous le dropdown icon: Icon(Icons.arrow_drop_down, color: Colors.blue), items: _sectorItems, onChanged: (int? sectorId) { setState(() { _selectedSectorId = sectorId; }); if (sectorId != null) { _centerMapOnSpecificSector(sectorId); } else { // Si "Tous les secteurs" est sélectionné _centerMapOnSectors(); // Recharger tous les passages sans filtrage par secteur _loadPassages(); } }, ), ), ], ), ), ), ), ], ); } }