feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,11 @@ import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
||||
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
|
||||
import 'package:geosector_app/presentation/widgets/btn_passages.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Page d'historique unifiée utilisant AppScaffold
|
||||
@@ -56,12 +56,16 @@ enum PassageSortType {
|
||||
addressDesc, // Adresse Z-A
|
||||
}
|
||||
|
||||
class _HistoryContentState extends State<HistoryContent> {
|
||||
class _HistoryContentState extends State<HistoryContent> with SingleTickerProviderStateMixin {
|
||||
// Détection du rôle et permissions
|
||||
late final bool isAdmin;
|
||||
late final int currentUserId;
|
||||
late final int currentUserId; // users.id (table centrale)
|
||||
late final int? currentOpeUserId; // ope_users.id (pour comparaisons avec passages)
|
||||
late final bool canDeletePassages; // Permission de suppression pour les users
|
||||
|
||||
// TabController pour les onglets Filtres / Statistiques
|
||||
late TabController _tabController;
|
||||
|
||||
// Filtres principaux (nouveaux)
|
||||
String _selectedTypeFilter = 'Tous les types';
|
||||
String _selectedPaymentFilter = 'Tous les règlements';
|
||||
@@ -89,8 +93,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
|
||||
// Listes pour les filtres
|
||||
List<SectorModel> _sectors = [];
|
||||
List<MembreModel> _membres = [];
|
||||
List<UserModel> _users = []; // Liste des users pour le filtre
|
||||
List<MembreModel> _membres = []; // Liste des membres de l'opération
|
||||
|
||||
// Passages originaux pour l'édition
|
||||
List<PassageModel> _originalPassages = [];
|
||||
@@ -100,14 +103,15 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
bool _isLoading = true;
|
||||
String _errorMessage = '';
|
||||
|
||||
|
||||
// Statistiques pour l'affichage
|
||||
int _totalSectors = 0;
|
||||
int _sharedMembersCount = 0;
|
||||
|
||||
// État de la section graphiques
|
||||
bool _isGraphicsExpanded = true;
|
||||
|
||||
// Hauteur dynamique du TabBarView selon l'onglet actif
|
||||
double _tabBarViewHeight = 280.0; // Hauteur par défaut (Filtres)
|
||||
|
||||
// Onglet précédemment sélectionné (pour détecter les clics sur le même onglet)
|
||||
int _previousTabIndex = 0;
|
||||
|
||||
// Listener pour les changements de secteur depuis map_page
|
||||
late final Box _settingsBox;
|
||||
|
||||
@@ -115,6 +119,9 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialiser le TabController (2 onglets)
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
// Initialiser la box settings et écouter les changements de secteur
|
||||
_initSettingsListener();
|
||||
|
||||
@@ -122,6 +129,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
||||
currentUserId = currentUser?.id ?? 0;
|
||||
currentOpeUserId = currentUser?.opeUserId; // ID dans ope_users pour comparaisons avec passages
|
||||
|
||||
// Vérifier la permission de suppression pour les users
|
||||
bool userCanDelete = false;
|
||||
@@ -158,6 +166,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_startDateController.dispose();
|
||||
_endDateController.dispose();
|
||||
_searchController.dispose();
|
||||
@@ -165,6 +174,68 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Callback pour gérer les clics sur les onglets
|
||||
void _onTabTapped(int index) {
|
||||
setState(() {
|
||||
// Si on clique sur le même onglet alors que l'ExpansionTile est ouvert → le fermer
|
||||
if (index == _previousTabIndex && _isGraphicsExpanded) {
|
||||
_isGraphicsExpanded = false;
|
||||
_saveGraphicsExpandedState();
|
||||
}
|
||||
// Sinon, ouvrir l'ExpansionTile et ajuster la hauteur
|
||||
else {
|
||||
if (!_isGraphicsExpanded) {
|
||||
_isGraphicsExpanded = true;
|
||||
_saveGraphicsExpandedState();
|
||||
}
|
||||
// Onglet 0 = Filtres (hauteur plus petite)
|
||||
// Onglet 1 = Statistiques (hauteur plus grande)
|
||||
_tabBarViewHeight = index == 0 ? 280.0 : 800.0;
|
||||
}
|
||||
|
||||
_previousTabIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
// Callback pour gérer les clics sur les boutons de type de passage
|
||||
void _handleTypeSelected(int? typeId) {
|
||||
setState(() {
|
||||
// Réinitialiser tous les filtres
|
||||
_selectedPaymentFilter = 'Tous les règlements';
|
||||
_selectedPaymentTypeId = null;
|
||||
selectedPaymentTypeId = null;
|
||||
startDate = null;
|
||||
endDate = null;
|
||||
_startDateController.clear();
|
||||
_endDateController.clear();
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
_selectedSectorId = null;
|
||||
selectedSectorId = null;
|
||||
if (isAdmin) {
|
||||
_selectedUserId = null;
|
||||
selectedMemberId = null;
|
||||
}
|
||||
|
||||
// Appliquer le filtre de type
|
||||
if (typeId == null) {
|
||||
// Tous les passages
|
||||
_selectedTypeFilter = 'Tous les types';
|
||||
selectedTypeId = null;
|
||||
} else {
|
||||
// Type spécifique
|
||||
selectedTypeId = typeId;
|
||||
final typeInfo = AppKeys.typesPassages[typeId];
|
||||
if (typeInfo != null) {
|
||||
_selectedTypeFilter = typeInfo['titre'] as String;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Appliquer les filtres
|
||||
_notifyFiltersChanged();
|
||||
}
|
||||
|
||||
// Initialiser le listener pour les changements de secteur
|
||||
Future<void> _initSettingsListener() async {
|
||||
try {
|
||||
@@ -368,14 +439,87 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
debugPrint('HistoryPage: ${filteredPassages.length} passages filtrés sur ${_originalPassages.length}');
|
||||
}
|
||||
|
||||
/// Construire la card de filtres intégrée
|
||||
Widget _buildFiltersCard() {
|
||||
/// Construire la section TabBar + ExpansionTile
|
||||
Widget _buildTabBarSection() {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.transparent,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||
primary: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
key: ValueKey('expansion_tile_$_isGraphicsExpanded'),
|
||||
initiallyExpanded: _isGraphicsExpanded,
|
||||
trailing: const SizedBox.shrink(), // Masquer la flèche d'expansion
|
||||
onExpansionChanged: (expanded) {
|
||||
setState(() {
|
||||
_isGraphicsExpanded = expanded;
|
||||
// Réinitialiser _previousTabIndex quand on ferme manuellement
|
||||
// pour permettre de rouvrir en cliquant sur l'onglet actif
|
||||
if (!expanded) {
|
||||
_previousTabIndex = -1;
|
||||
}
|
||||
});
|
||||
_saveGraphicsExpandedState();
|
||||
},
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppTheme.primaryColor,
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: AppTheme.primaryColor,
|
||||
indicatorWeight: 3,
|
||||
onTap: _onTabTapped,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.filter_list, size: 20),
|
||||
text: 'Filtres',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.analytics_outlined, size: 20),
|
||||
text: 'Statistiques',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: _tabBarViewHeight,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// Onglet 1 : Filtres
|
||||
_buildFiltersContent(),
|
||||
// Onglet 2 : Statistiques
|
||||
_buildGraphicsContent(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construire le contenu des filtres (ancien _buildFiltersCard sans la Card)
|
||||
Widget _buildFiltersContent() {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: Colors.transparent,
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
@@ -534,10 +678,10 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
value: null,
|
||||
child: Text('Membres'),
|
||||
),
|
||||
..._users.map((UserModel user) {
|
||||
..._membres.map((MembreModel membre) {
|
||||
return DropdownMenuItem<int?>(
|
||||
value: user.id,
|
||||
child: Text('${user.firstName ?? ''} ${user.name ?? ''}'),
|
||||
value: membre.opeUserId,
|
||||
child: Text('${membre.firstName ?? ''} ${membre.name ?? ''}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
@@ -910,7 +1054,34 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
// Charger le type de passage sélectionné
|
||||
final typeId = settingsBox.get('history_selectedTypeId');
|
||||
if (typeId != null && typeId is int) {
|
||||
// Réinitialiser TOUS les filtres avant d'appliquer le type
|
||||
setState(() {
|
||||
// Réinitialiser les filtres de type et paiement
|
||||
_selectedPaymentFilter = 'Tous les règlements';
|
||||
_selectedPaymentTypeId = null;
|
||||
selectedPaymentTypeId = null;
|
||||
|
||||
// Réinitialiser les dates
|
||||
startDate = null;
|
||||
endDate = null;
|
||||
_startDateController.clear();
|
||||
_endDateController.clear();
|
||||
|
||||
// Réinitialiser la recherche
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
|
||||
// Réinitialiser le secteur
|
||||
_selectedSectorId = null;
|
||||
selectedSectorId = null;
|
||||
|
||||
// Réinitialiser le membre (admin seulement)
|
||||
if (isAdmin) {
|
||||
_selectedUserId = null;
|
||||
selectedMemberId = null;
|
||||
}
|
||||
|
||||
// Appliquer le type de passage sélectionné
|
||||
selectedTypeId = typeId;
|
||||
final typeInfo = AppKeys.typesPassages[typeId];
|
||||
selectedType = typeInfo != null ? typeInfo['titre'] as String : 'Inconnu';
|
||||
@@ -919,7 +1090,11 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
_selectedTypeFilter = typeInfo['titre'] as String;
|
||||
}
|
||||
});
|
||||
debugPrint('HistoryPage: Type de passage présélectionné: $typeId');
|
||||
|
||||
// Supprimer le typeId de Hive après l'avoir utilisé
|
||||
settingsBox.delete('history_selectedTypeId');
|
||||
|
||||
debugPrint('HistoryPage: Type de passage présélectionné: $typeId (tous les autres filtres réinitialisés)');
|
||||
}
|
||||
|
||||
// Charger le type de règlement sélectionné
|
||||
@@ -977,30 +1152,15 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
_sectors = sectorRepository.getAllSectors()
|
||||
.where((s) => userSectorIds.contains(s.id))
|
||||
.toList();
|
||||
|
||||
// Calculer les statistiques pour l'utilisateur
|
||||
_totalSectors = _sectors.length;
|
||||
|
||||
// Compter les membres partageant les mêmes secteurs
|
||||
final allUserSectors = userRepository.getUserSectors();
|
||||
final sharedMembers = <int>{};
|
||||
for (final userSector in allUserSectors) {
|
||||
if (userSectorIds.contains(userSector.id) && userSector.id != currentUserId) {
|
||||
sharedMembers.add(userSector.id);
|
||||
}
|
||||
}
|
||||
_sharedMembersCount = sharedMembers.length;
|
||||
}
|
||||
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
|
||||
|
||||
// Charger les membres (admin seulement)
|
||||
// Charger les membres de l'opération (admin seulement)
|
||||
if (isAdmin) {
|
||||
_membres = membreRepository.getAllMembres();
|
||||
debugPrint('Nombre de membres récupérés: ${_membres.length}');
|
||||
|
||||
// Convertir les membres en users pour le filtre
|
||||
_users = _convertMembresToUsers();
|
||||
debugPrint('Nombre d\'utilisateurs pour le filtre: ${_users.length}');
|
||||
// Charger directement depuis MembreModel (déjà unique, pas de déduplication nécessaire)
|
||||
final membreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
_membres = membreBox.values.whereType<MembreModel>().toList();
|
||||
debugPrint('Nombre de membres de l\'opération récupérés: ${_membres.length}');
|
||||
}
|
||||
|
||||
// Charger les passages
|
||||
@@ -1018,7 +1178,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
return true;
|
||||
}
|
||||
// Autres types : seulement les passages de l'utilisateur
|
||||
return p.fkUser == currentUserId;
|
||||
return p.fkUser == currentOpeUserId;
|
||||
}).toList();
|
||||
}
|
||||
debugPrint('Nombre de passages récupérés: ${_originalPassages.length}');
|
||||
@@ -1042,46 +1202,6 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir les MembreModel en UserModel pour le filtre (admin seulement)
|
||||
List<UserModel> _convertMembresToUsers() {
|
||||
final users = <UserModel>[];
|
||||
|
||||
for (final membre in _membres) {
|
||||
// Utiliser l'ID du membre pour récupérer l'utilisateur associé
|
||||
final user = userRepository.getUserById(membre.id);
|
||||
if (user != null) {
|
||||
// Si l'utilisateur existe, copier avec le sectName du membre
|
||||
users.add(user.copyWith(
|
||||
sectName: membre.sectName ?? user.sectName,
|
||||
));
|
||||
} else {
|
||||
// Créer un UserModel temporaire si l'utilisateur n'existe pas
|
||||
users.add(UserModel(
|
||||
id: membre.id,
|
||||
username: membre.username ?? 'membre_${membre.id}',
|
||||
name: membre.name,
|
||||
firstName: membre.firstName,
|
||||
email: membre.email,
|
||||
role: membre.role,
|
||||
isActive: membre.isActive,
|
||||
createdAt: membre.createdAt,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
sectName: membre.sectName,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par nom complet
|
||||
users.sort((a, b) {
|
||||
final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim().toLowerCase();
|
||||
final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim().toLowerCase();
|
||||
return nameA.compareTo(nameB);
|
||||
});
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Le contenu sans scaffold (AppScaffold est déjà dans HistoryPage)
|
||||
@@ -1111,129 +1231,47 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
// Titre unique pour tous
|
||||
const pageTitle = 'Historique des passages';
|
||||
|
||||
// Statistiques pour les users
|
||||
final statsText = !isAdmin
|
||||
? '$_totalSectors secteur${_totalSectors > 1 ? 's' : ''} | $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''} en partage'
|
||||
: null;
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
||||
vertical: AppTheme.spacingL,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec titre
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre
|
||||
Text(
|
||||
pageTitle,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (statsText != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
statsText,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 1. Card de filtres intégrée
|
||||
_buildFiltersCard(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. Section graphiques (rétractable)
|
||||
_buildGraphicsSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 3. Liste des passages avec hauteur maximale
|
||||
Card(
|
||||
elevation: 2,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 700,
|
||||
),
|
||||
child: PassagesListWidget(
|
||||
passages: _convertPassagesToMaps(),
|
||||
showActions: true, // Actions disponibles pour tous (avec vérification des droits)
|
||||
showAddButton: true, // Bouton + pour tous
|
||||
onPassageEdit: _handlePassageEditMap, // Tous peuvent éditer (avec vérification)
|
||||
onPassageDelete: canDeletePassages ? _handlePassageDeleteMap : null, // Selon permissions
|
||||
onAddPassage: () async {
|
||||
await _showPassageFormDialog(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
||||
vertical: AppTheme.spacingL,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Construction de la section graphiques rétractable (pour intégration dans PassagesListWidget)
|
||||
Widget _buildGraphicsSection() {
|
||||
// final screenWidth = MediaQuery.of(context).size.width; // Non utilisé actuellement
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.transparent,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||
primary: AppTheme.primaryColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 0. BtnPassages
|
||||
BtnPassages(
|
||||
onTypeSelected: _handleTypeSelected,
|
||||
selectedTypeId: selectedTypeId,
|
||||
),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.analytics_outlined, color: AppTheme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Statistiques graphiques',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// 1. TabBar + ExpansionTile (Filtres / Statistiques) - FIXE EN HAUT
|
||||
_buildTabBarSection(),
|
||||
|
||||
SizedBox(height: _isGraphicsExpanded ? 8 : 16),
|
||||
|
||||
// 2. Liste des passages - EXPANDED pour prendre tout l'espace restant
|
||||
Expanded(
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
child: PassagesListWidget(
|
||||
passages: _convertPassagesToMaps(),
|
||||
showActions: true, // Actions disponibles pour tous (avec vérification des droits)
|
||||
showAddButton: true, // Bouton + pour tous
|
||||
onPassageEdit: _handlePassageEditMap, // Tous peuvent éditer (avec vérification)
|
||||
onPassageDelete: canDeletePassages ? _handlePassageDeleteMap : null, // Selon permissions
|
||||
onAddPassage: () async {
|
||||
await _showPassageFormDialog(context);
|
||||
},
|
||||
filteredPassageType: _selectedTypeFilter != 'Tous les types' ? _selectedTypeFilter : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: !_isGraphicsExpanded ? Text(
|
||||
isAdmin ? "Tous les passages de l'opération" : "Mes passages de l'opération",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
) : null,
|
||||
initiallyExpanded: _isGraphicsExpanded,
|
||||
onExpansionChanged: (expanded) {
|
||||
setState(() {
|
||||
_isGraphicsExpanded = expanded;
|
||||
});
|
||||
_saveGraphicsExpandedState();
|
||||
},
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
childrenPadding: const EdgeInsets.only(top: 0, bottom: 16.0),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildGraphicsContent(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1243,8 +1281,9 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Graphiques en camembert (côte à côte sur desktop)
|
||||
isDesktop
|
||||
? Row(
|
||||
@@ -1266,7 +1305,8 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
|
||||
// Graphique d'activité
|
||||
_buildActivityChart(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1603,7 +1643,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
// - Admin peut tout éditer
|
||||
// - User peut éditer ses propres passages
|
||||
// - Type 2 (À finaliser) : éditable par tous les utilisateurs
|
||||
if (isAdmin || passage.fkUser == currentUserId || passage.fkType == 2) {
|
||||
if (isAdmin || passage.fkUser == currentOpeUserId || passage.fkType == 2) {
|
||||
_handlePassageEdit(passage);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -1624,7 +1664,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
||||
);
|
||||
|
||||
// Vérifier les permissions : admin peut tout supprimer, user seulement ses propres passages si autorisé
|
||||
if (isAdmin || (canDeletePassages && passage.fkUser == currentUserId)) {
|
||||
if (isAdmin || (canDeletePassages && passage.fkUser == currentOpeUserId)) {
|
||||
_handlePassageDelete(passage);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:geosector_app/presentation/widgets/members_board_passages.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
||||
import 'package:geosector_app/presentation/widgets/btn_passages.dart';
|
||||
|
||||
/// Widget de contenu du tableau de bord unifié (sans scaffold)
|
||||
class HomeContent extends StatefulWidget {
|
||||
@@ -22,16 +23,13 @@ class HomeContent extends StatefulWidget {
|
||||
class _HomeContentState extends State<HomeContent> {
|
||||
// Détection du rôle
|
||||
late final bool isAdmin;
|
||||
late final int currentUserId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Déterminer le rôle de l'utilisateur et le mode d'affichage
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
// Déterminer le mode d'affichage
|
||||
isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
||||
currentUserId = currentUser?.id ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -41,14 +39,6 @@ class _HomeContentState extends State<HomeContent> {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
// Récupérer l'opération en cours
|
||||
final currentOperation = userRepository.getCurrentOperation();
|
||||
|
||||
// Titre dynamique avec l'ID et le nom de l'opération
|
||||
final String title = currentOperation != null
|
||||
? 'Opération #${currentOperation.id} ${currentOperation.name}'
|
||||
: 'Opération';
|
||||
|
||||
// Retourner seulement le contenu (sans scaffold)
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -58,14 +48,9 @@ class _HomeContentState extends State<HomeContent> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingM),
|
||||
// Widget BtnPassages
|
||||
const BtnPassages(),
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// LIGNE 1 : Graphiques de répartition (type de passage et mode de paiement)
|
||||
isDesktop
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:geosector_app/core/services/current_amicale_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/presentation/widgets/passage_map_dialog.dart';
|
||||
import 'package:geosector_app/presentation/widgets/grouped_passages_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Page de carte globale pour admin et utilisateurs
|
||||
@@ -29,6 +30,7 @@ class MapPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('🔄 MapPage.build() appelé');
|
||||
// Utiliser le mode d'affichage au lieu du rôle réel
|
||||
final isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
||||
|
||||
@@ -78,6 +80,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
bool _showZoomIndicator = false;
|
||||
Timer? _zoomIndicatorTimer;
|
||||
|
||||
// Timer pour debouncer le setState et la sauvegarde lors du déplacement de carte
|
||||
Timer? _mapMoveDebounceTimer;
|
||||
|
||||
// États
|
||||
MapMode _mapMode = MapMode.view;
|
||||
int? _selectedSectorId;
|
||||
@@ -102,7 +107,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
// États pour le mode édition
|
||||
SectorModel? _selectedSectorForEdit;
|
||||
List<LatLng> _editingPoints = [];
|
||||
Map<int, LatLng> _originalPoints = {}; // Pour annuler les modifications
|
||||
final Map<int, LatLng> _originalPoints = {}; // Pour annuler les modifications
|
||||
int? _hoveredPointIndex; // Index du point principal survolé
|
||||
|
||||
// État pour le mode suppression
|
||||
@@ -115,6 +120,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
// État pour bloquer le drag de la carte pendant le déplacement des points
|
||||
bool _isDraggingPoint = false;
|
||||
|
||||
// État pour bloquer la sauvegarde du zoom lors du centrage sur secteur
|
||||
bool _isCenteringOnSector = false;
|
||||
|
||||
// Comptages des secteurs (calculés uniquement lors de création/modification de secteurs)
|
||||
Map<int, int> _sectorPassageCount = {};
|
||||
Map<int, int> _sectorMemberCount = {};
|
||||
@@ -143,6 +151,14 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
// Écouter les changements du secteur sélectionné
|
||||
_settingsListenable = _settingsBox.listenable(keys: ['selectedSectorId']);
|
||||
_settingsListenable.addListener(_onSectorSelectionChanged);
|
||||
|
||||
// Centrer sur le secteur si déjà sélectionné (navigation depuis home_page)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_selectedSectorId != null && _sectors.any((s) => s['id'] == _selectedSectorId)) {
|
||||
debugPrint('🎯 MapPage: Secteur présélectionné détecté ($_selectedSectorId), centrage...');
|
||||
_centerMapOnSpecificSector(_selectedSectorId!);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,6 +241,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
void dispose() {
|
||||
_settingsListenable.removeListener(_onSectorSelectionChanged);
|
||||
_zoomIndicatorTimer?.cancel();
|
||||
_mapMoveDebounceTimer?.cancel();
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -258,10 +275,14 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
_settingsBox.put('selectedSectorId', _selectedSectorId);
|
||||
}
|
||||
|
||||
// Sauvegarder la position et le zoom actuels
|
||||
// Sauvegarder la position
|
||||
_settingsBox.put('mapLat', _currentPosition.latitude);
|
||||
_settingsBox.put('mapLng', _currentPosition.longitude);
|
||||
_settingsBox.put('mapZoom', _currentZoom);
|
||||
|
||||
// Sauvegarder le zoom SAUF si on est en train de centrer sur un secteur
|
||||
if (!_isCenteringOnSector) {
|
||||
_settingsBox.put('mapZoom', _currentZoom);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les comptages des secteurs (passages et membres)
|
||||
@@ -380,7 +401,8 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Ne faire setState QUE si les données ont vraiment changé
|
||||
if (mounted && !_arePassagesEqual(_passages, newPassages)) {
|
||||
setState(() {
|
||||
_passages.clear();
|
||||
_passages.addAll(newPassages);
|
||||
@@ -391,6 +413,25 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
}
|
||||
}
|
||||
|
||||
// Comparer deux listes de passages pour éviter les setState inutiles
|
||||
bool _arePassagesEqual(List<Map<String, dynamic>> oldPassages, List<Map<String, dynamic>> newPassages) {
|
||||
if (oldPassages.length != newPassages.length) return false;
|
||||
|
||||
// Créer des clés uniques incluant ID + fkType pour détecter les changements de type
|
||||
// (important pour le gradient des immeubles qui dépend du fkType)
|
||||
final oldKeys = oldPassages.map((p) {
|
||||
final model = p['model'] as PassageModel;
|
||||
return '${model.id}_${model.fkType}';
|
||||
}).toSet();
|
||||
|
||||
final newKeys = newPassages.map((p) {
|
||||
final model = p['model'] as PassageModel;
|
||||
return '${model.id}_${model.fkType}';
|
||||
}).toSet();
|
||||
|
||||
return oldKeys.length == newKeys.length && oldKeys.containsAll(newKeys);
|
||||
}
|
||||
|
||||
// Charger les passages depuis la boîte Hive (avec setState)
|
||||
void _loadPassages() {
|
||||
// L'API retourne déjà les passages filtrés selon le rôle (admin ou user)
|
||||
@@ -640,21 +681,30 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
final centerLat = (minLat + maxLat) / 2;
|
||||
final centerLng = (minLng + maxLng) / 2;
|
||||
|
||||
// Lire le zoom actuel de la caméra pour le conserver exactement
|
||||
final currentZoom = _mapController.camera.zoom;
|
||||
// CAPTURER le zoom actuel AVANT toute opération pour le conserver
|
||||
final preservedZoom = _currentZoom;
|
||||
|
||||
// Centrer la carte sur le secteur SANS changer le zoom
|
||||
debugPrint('🔍 MapPage: Centrage sur secteur (zoom conservé: $currentZoom)');
|
||||
_mapController.move(LatLng(centerLat, centerLng), currentZoom);
|
||||
// ACTIVER le flag pour bloquer la sauvegarde du zoom
|
||||
_isCenteringOnSector = true;
|
||||
|
||||
// Mettre à jour uniquement la position (pas le zoom)
|
||||
// Centrer la carte sur le secteur en FORCANT le zoom actuel
|
||||
debugPrint('🔍 MapPage: Centrage sur secteur (zoom FORCÉ à conserver: $preservedZoom)');
|
||||
_mapController.move(LatLng(centerLat, centerLng), preservedZoom);
|
||||
|
||||
// Mettre à jour UNIQUEMENT la position, PAS le zoom
|
||||
setState(() {
|
||||
_currentPosition = LatLng(centerLat, centerLng);
|
||||
// On ne touche PAS à _currentZoom !
|
||||
});
|
||||
|
||||
// Sauvegarder la nouvelle position
|
||||
// Sauvegarder la nouvelle position (le zoom ne sera pas sauvegardé grâce au flag)
|
||||
_saveSettings();
|
||||
|
||||
// DÉSACTIVER le flag après un court délai pour permettre les sauvegardes normales
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_isCenteringOnSector = false;
|
||||
});
|
||||
|
||||
// Recharger les passages pour appliquer le filtre par secteur
|
||||
_loadPassages();
|
||||
}
|
||||
@@ -765,10 +815,10 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
Set<int>? userSectorIds;
|
||||
if (!isAdmin) {
|
||||
final userSectorBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
final currentUserId = CurrentUserService.instance.currentUser?.id;
|
||||
if (currentUserId != null) {
|
||||
final currentOpeUserId = CurrentUserService.instance.opeUserId;
|
||||
if (currentOpeUserId != null) {
|
||||
userSectorIds = userSectorBox.values
|
||||
.where((us) => us.id == currentUserId)
|
||||
.where((us) => us.opeUserId == currentOpeUserId)
|
||||
.map((us) => us.fkSector)
|
||||
.toSet();
|
||||
}
|
||||
@@ -824,6 +874,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
|
||||
// Construire les marqueurs de labels pour les secteurs
|
||||
List<Marker> _buildSectorLabels() {
|
||||
debugPrint('🔄 _buildSectorLabels() appelé - ${_sectors.length} secteurs');
|
||||
// Ne pas afficher les labels en mode dessin ou suppression
|
||||
if (_sectors.isEmpty || _mapMode != MapMode.view) {
|
||||
return [];
|
||||
@@ -859,24 +910,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
fontSize: 14,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, 1),
|
||||
blurRadius: 3,
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
offset: const Offset(0, 0),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -892,24 +928,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, 1),
|
||||
blurRadius: 3,
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
offset: const Offset(0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -923,24 +944,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
fontWeight: FontWeight.w500,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(1, -1),
|
||||
blurRadius: 3,
|
||||
),
|
||||
Shadow(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
offset: const Offset(-1, 1),
|
||||
blurRadius: 3,
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
offset: const Offset(0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -954,20 +960,60 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
}
|
||||
|
||||
// Méthode pour construire les marqueurs des passages
|
||||
/// Groupe les passages par adresse (pour fkHabitat=2)
|
||||
/// Clé: numero+rueBis+rue+ville
|
||||
Map<String, List<Map<String, dynamic>>> _groupPassagesByAddress() {
|
||||
final Map<String, List<Map<String, dynamic>>> grouped = {};
|
||||
|
||||
for (final passage in _passages) {
|
||||
final PassageModel model = passage['model'] as PassageModel;
|
||||
|
||||
// Ne grouper que les passages avec fkHabitat=2
|
||||
if (model.fkHabitat == 2) {
|
||||
final key = '${model.numero}|${model.rueBis}|${model.rue}|${model.ville}';
|
||||
grouped.putIfAbsent(key, () => []);
|
||||
grouped[key]!.add(passage);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
List<Marker> _buildMarkers() {
|
||||
debugPrint('🔄 _buildMarkers() appelé - ${_passages.length} passages');
|
||||
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
|
||||
final List<Marker> markers = [];
|
||||
|
||||
// 1. Grouper les passages fkHabitat=2 par adresse
|
||||
final groupedPassages = _groupPassagesByAddress();
|
||||
final Set<int> groupedPassageIds = {};
|
||||
|
||||
// Collecter les IDs des passages groupés
|
||||
for (final group in groupedPassages.values) {
|
||||
for (final passage in group) {
|
||||
final PassageModel model = passage['model'] as PassageModel;
|
||||
groupedPassageIds.add(model.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Créer les markers pour passages individuels (fkHabitat=1 ou non groupés)
|
||||
for (final passage in _passages) {
|
||||
final PassageModel passageModel = passage['model'] as PassageModel;
|
||||
|
||||
// Ignorer les passages déjà groupés
|
||||
if (groupedPassageIds.contains(passageModel.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int passageType = passage['type'] as int;
|
||||
final Color color1 = passage['color'] as Color;
|
||||
final bool hasNoSector = passageModel.fkSector == null;
|
||||
|
||||
// Récupérer la couleur2 du type de passage
|
||||
Color color2 = Colors.white; // Couleur par défaut
|
||||
Color color2 = Colors.white;
|
||||
if (AppKeys.typesPassages.containsKey(passageType)) {
|
||||
final colorValue =
|
||||
AppKeys.typesPassages[passageType]!['couleur2'] as int;
|
||||
@@ -978,38 +1024,112 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
final Color borderColor = hasNoSector ? Colors.red : color2;
|
||||
final double borderWidth = hasNoSector ? 3.0 : 1.0;
|
||||
|
||||
return Marker(
|
||||
point: passage['position'] as LatLng,
|
||||
width: hasNoSector ? 18.0 : 14.0, // Plus grand si orphelin
|
||||
height: hasNoSector ? 18.0 : 14.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showPassageInfo(passage);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color1,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: borderWidth,
|
||||
markers.add(
|
||||
Marker(
|
||||
point: passage['position'] as LatLng,
|
||||
width: hasNoSector ? 18.0 : 14.0,
|
||||
height: hasNoSector ? 18.0 : 14.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showPassageInfo(passage);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color1,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 3. Créer les markers groupés (carrés avec dégradé blanc→vert selon avancement)
|
||||
for (final entry in groupedPassages.entries) {
|
||||
final passages = entry.value;
|
||||
if (passages.isEmpty) continue;
|
||||
|
||||
// Utiliser la position du premier passage du groupe
|
||||
final position = passages.first['position'] as LatLng;
|
||||
final count = passages.length;
|
||||
final displayCount = count >= 99 ? '99' : count.toString();
|
||||
|
||||
// Calculer le pourcentage de passages réalisés (fkType != 2)
|
||||
final models = passages.map((p) => p['model'] as PassageModel).toList();
|
||||
final realizedCount = models.where((p) => p.fkType != 2).length;
|
||||
final percentage = realizedCount / models.length;
|
||||
|
||||
// Déterminer la couleur de remplissage selon le palier (5 niveaux)
|
||||
Color fillColor;
|
||||
if (percentage == 0) {
|
||||
// 0% : Blanc pur
|
||||
fillColor = Colors.white;
|
||||
} else if (percentage <= 0.25) {
|
||||
// 1-25% : Blanc cassé → Vert très clair
|
||||
fillColor = Color.lerp(Colors.white, const Color(0xFFB3F5E0), (percentage / 0.25))!;
|
||||
} else if (percentage <= 0.50) {
|
||||
// 26-50% : Vert très clair → Vert clair
|
||||
fillColor = Color.lerp(const Color(0xFFB3F5E0), const Color(0xFF66EBBB), ((percentage - 0.25) / 0.25))!;
|
||||
} else if (percentage <= 0.75) {
|
||||
// 51-75% : Vert clair → Vert moyen
|
||||
fillColor = Color.lerp(const Color(0xFF66EBBB), const Color(0xFF33E1A0), ((percentage - 0.50) / 0.25))!;
|
||||
} else if (percentage < 1.0) {
|
||||
// 76-99% : Vert moyen → Vert foncé
|
||||
fillColor = Color.lerp(const Color(0xFF33E1A0), const Color(0xFF00E09D), ((percentage - 0.75) / 0.25))!;
|
||||
} else {
|
||||
// 100% : Vert foncé (couleur "Effectué")
|
||||
fillColor = const Color(0xFF00E09D);
|
||||
}
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
point: position,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showGroupedPassagesDialog(passages.first['model'] as PassageModel);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.blue, // Bordure bleue toujours
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
displayCount,
|
||||
style: TextStyle(
|
||||
color: percentage < 0.5 ? Colors.black87 : Colors.white, // Texte noir sur fond clair, blanc sur fond foncé
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
// Méthode pour construire les polygones des secteurs
|
||||
List<Polygon> _buildPolygons() {
|
||||
debugPrint('🔄 _buildPolygons() appelé - ${_sectors.length} secteurs');
|
||||
if (_sectors.isEmpty) {
|
||||
debugPrint('MapPage: Aucun secteur à afficher');
|
||||
return [];
|
||||
}
|
||||
|
||||
debugPrint('MapPage: Construction de ${_sectors.length} polygones');
|
||||
|
||||
return _sectors.map((sector) {
|
||||
final int sectorId = sector['id'] as int;
|
||||
final bool isSelected = _selectedSectorId == sectorId;
|
||||
@@ -1024,8 +1144,6 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
_mapMode == MapMode.editing && _selectedSectorForEdit?.id == sectorId;
|
||||
final Color sectorColor = sector['color'] as Color;
|
||||
|
||||
debugPrint('MapPage: Secteur ${sector['name']} - Couleur: $sectorColor');
|
||||
|
||||
// Déterminer la couleur et l'opacité selon l'état
|
||||
Color fillColor;
|
||||
Color borderColor;
|
||||
@@ -1033,33 +1151,33 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
|
||||
if (isMarkedForDeletion) {
|
||||
// Secteur marqué pour suppression
|
||||
fillColor = Colors.red.withValues(alpha: 0.5);
|
||||
fillColor = Colors.red.withOpacity(0.5);
|
||||
borderColor = Colors.red;
|
||||
borderWidth = 4.0;
|
||||
} else if (isHovered) {
|
||||
// Secteur survolé en mode suppression
|
||||
fillColor = sectorColor.withValues(alpha: 0.45);
|
||||
borderColor = Colors.red.withValues(alpha: 0.8);
|
||||
fillColor = sectorColor.withOpacity(0.45);
|
||||
borderColor = Colors.red.withOpacity(0.8);
|
||||
borderWidth = 3.0;
|
||||
} else if (isHoveredForEdit) {
|
||||
// Secteur survolé en mode édition
|
||||
fillColor = sectorColor.withValues(alpha: 0.45);
|
||||
fillColor = sectorColor.withOpacity(0.45);
|
||||
borderColor = Colors.green;
|
||||
borderWidth = 4.0;
|
||||
} else if (isSelectedForEdit) {
|
||||
// Secteur sélectionné pour édition
|
||||
fillColor = sectorColor.withValues(alpha: 0.5);
|
||||
fillColor = sectorColor.withOpacity(0.5);
|
||||
borderColor = Colors.orange;
|
||||
borderWidth = 4.0;
|
||||
} else if (isSelected) {
|
||||
// Secteur sélectionné
|
||||
fillColor = sectorColor.withValues(alpha: 0.5);
|
||||
fillColor = sectorColor.withOpacity(0.5);
|
||||
borderColor = sectorColor;
|
||||
borderWidth = 3.0;
|
||||
} else {
|
||||
// Secteur normal
|
||||
fillColor = sectorColor.withValues(alpha: 0.3);
|
||||
borderColor = sectorColor.withValues(alpha: 0.8);
|
||||
fillColor = sectorColor.withOpacity(0.3);
|
||||
borderColor = sectorColor.withOpacity(0.8);
|
||||
borderWidth = 2.0;
|
||||
}
|
||||
|
||||
@@ -1068,7 +1186,6 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
color: fillColor,
|
||||
borderColor: borderColor,
|
||||
borderStrokeWidth: borderWidth,
|
||||
isFilled: true, // IMPORTANT: Active le remplissage coloré
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@@ -1090,13 +1207,31 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
);
|
||||
}
|
||||
|
||||
// Afficher le dialogue des passages groupés (immeuble)
|
||||
void _showGroupedPassagesDialog(PassageModel referencePassage) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => GroupedPassagesDialog(
|
||||
referencePassage: referencePassage,
|
||||
isAdmin: isAdmin,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Démarrer le mode dessin
|
||||
void _startDrawingMode() {
|
||||
if (!canEditSectors) return; // Vérifier les permissions
|
||||
setState(() {
|
||||
_mapMode = MapMode.drawing;
|
||||
_drawingPoints.clear();
|
||||
|
||||
// Sélectionner automatiquement "Aucun passage"
|
||||
_selectedPassageTypeFilter = null;
|
||||
_settingsBox.put('selectedPassageTypeFilter', null);
|
||||
});
|
||||
|
||||
// Recharger les passages avec le nouveau filtre
|
||||
_loadPassages();
|
||||
}
|
||||
|
||||
// Démarrer le mode suppression
|
||||
@@ -1105,7 +1240,14 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
setState(() {
|
||||
_mapMode = MapMode.deleting;
|
||||
_sectorToDeleteId = null;
|
||||
|
||||
// Sélectionner automatiquement "Aucun passage"
|
||||
_selectedPassageTypeFilter = null;
|
||||
_settingsBox.put('selectedPassageTypeFilter', null);
|
||||
});
|
||||
|
||||
// Recharger les passages avec le nouveau filtre
|
||||
_loadPassages();
|
||||
}
|
||||
|
||||
// Démarrer le mode édition
|
||||
@@ -1115,7 +1257,14 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
_mapMode = MapMode.editing;
|
||||
_selectedSectorForEdit = null;
|
||||
_editingPoints.clear();
|
||||
|
||||
// Sélectionner automatiquement "Aucun passage"
|
||||
_selectedPassageTypeFilter = null;
|
||||
_settingsBox.put('selectedPassageTypeFilter', null);
|
||||
});
|
||||
|
||||
// Recharger les passages avec le nouveau filtre
|
||||
_loadPassages();
|
||||
}
|
||||
|
||||
// Construire la carte d'aide pour le mode création
|
||||
@@ -1127,10 +1276,10 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.blue.withValues(alpha: 0.3),
|
||||
color: Colors.blue.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1263,10 +1412,10 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: 360,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.withValues(alpha: 0.3),
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1332,10 +1481,10 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: 340,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
color: Colors.orange.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1384,10 +1533,10 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border:
|
||||
Border.all(color: Colors.orange.withValues(alpha: 0.3)),
|
||||
Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'La modification est verrouillée sur ce secteur.\n'
|
||||
@@ -2772,9 +2921,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -2885,6 +3034,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
// Recharger les secteurs et passages après la suppression
|
||||
_loadSectors();
|
||||
_loadPassages();
|
||||
_updateSectorCounts(); // Rafraîchir le comptage des membres
|
||||
|
||||
// Message de succès simple
|
||||
if (mounted) {
|
||||
@@ -2965,7 +3115,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
builder: (dialogContext) => SectorDialog(
|
||||
existingSector: existingSector,
|
||||
coordinates: finalCoordinates,
|
||||
onSave: (name, color, memberIds) async {
|
||||
onSave: (name, color, memberIds, updatePassages) async {
|
||||
// Le dialog se ferme automatiquement dans _handleSave()
|
||||
// Attendre un peu pour s'assurer que le dialog est fermé
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
@@ -2998,10 +3148,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
if (existingSector == null) {
|
||||
// Création d'un nouveau secteur
|
||||
// Convertir les coordonnées au format attendu par l'API : "lat/lng#lat/lng#..."
|
||||
final sectorString = finalCoordinates
|
||||
final sectorString = '${finalCoordinates
|
||||
.map((coord) => '${coord[0]}/${coord[1]}')
|
||||
.join('#') +
|
||||
'#'; // Ajouter un # final comme dans les exemples
|
||||
.join('#')}#'; // Ajouter un # final comme dans les exemples
|
||||
|
||||
final newSector = SectorModel(
|
||||
id: 0, // L'API assignera l'ID
|
||||
@@ -3059,12 +3208,26 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
|
||||
// Recharger les secteurs et passages
|
||||
_loadSectors();
|
||||
_loadPassages();
|
||||
_updateSectorCounts(); // Rafraîchir le comptage des membres
|
||||
|
||||
// Centrer la carte sur le nouveau secteur
|
||||
// Présélectionner le secteur créé et afficher tous ses passages
|
||||
if (result.containsKey('sector') && result['sector'] != null) {
|
||||
final newSector = result['sector'] as SectorModel;
|
||||
// Attendre un peu que les données soient chargées
|
||||
|
||||
setState(() {
|
||||
// Sélectionner le secteur créé
|
||||
_selectedSectorId = newSector.id;
|
||||
_settingsBox.put('selectedSectorId', newSector.id);
|
||||
|
||||
// Mettre le filtre sur "Tous les passages"
|
||||
_selectedPassageTypeFilter = -1;
|
||||
_settingsBox.put('selectedPassageTypeFilter', -1);
|
||||
});
|
||||
|
||||
// Recharger les passages avec le nouveau filtre
|
||||
_loadPassages();
|
||||
|
||||
// Centrer la carte sur le nouveau secteur
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
_centerMapOnSpecificSector(newSector.id);
|
||||
@@ -3098,10 +3261,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
}
|
||||
} else {
|
||||
// Modification d'un secteur existant
|
||||
final sectorString = finalCoordinates
|
||||
final sectorString = '${finalCoordinates
|
||||
.map((coord) => '${coord[0]}/${coord[1]}')
|
||||
.join('#') +
|
||||
'#';
|
||||
.join('#')}#';
|
||||
|
||||
final updatedSector = existingSector.copyWith(
|
||||
libelle: name,
|
||||
@@ -3109,8 +3271,11 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
sector: sectorString,
|
||||
);
|
||||
|
||||
result = await sectorRepository.updateSector(updatedSector,
|
||||
users: memberIds);
|
||||
result = await sectorRepository.updateSector(
|
||||
updatedSector,
|
||||
users: memberIds,
|
||||
chkAdressesChange: updatePassages ? 1 : 0,
|
||||
);
|
||||
|
||||
if (result['status'] != 'success') {
|
||||
throw Exception(result['message'] ??
|
||||
@@ -3131,8 +3296,29 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
|
||||
// Recharger les secteurs et passages
|
||||
_loadSectors();
|
||||
_updateSectorCounts(); // Rafraîchir le comptage des membres
|
||||
|
||||
// Présélectionner le secteur modifié et afficher tous ses passages
|
||||
setState(() {
|
||||
// Sélectionner le secteur modifié
|
||||
_selectedSectorId = existingSector.id;
|
||||
_settingsBox.put('selectedSectorId', existingSector.id);
|
||||
|
||||
// Mettre le filtre sur "Tous les passages"
|
||||
_selectedPassageTypeFilter = -1;
|
||||
_settingsBox.put('selectedPassageTypeFilter', -1);
|
||||
});
|
||||
|
||||
// Recharger les passages avec le nouveau filtre
|
||||
_loadPassages();
|
||||
|
||||
// Centrer la carte sur le secteur modifié
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
_centerMapOnSpecificSector(existingSector.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (parentContext.mounted) {
|
||||
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
|
||||
}
|
||||
@@ -3206,7 +3392,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -3316,7 +3502,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
Polyline(
|
||||
points: _drawingPoints,
|
||||
strokeWidth: 3.0,
|
||||
color: Colors.blue.withValues(alpha: 0.8),
|
||||
color: Colors.blue.withOpacity(0.8),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3330,7 +3516,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
_editingPoints.first
|
||||
], // Fermer le polygone
|
||||
strokeWidth: 3.0,
|
||||
color: Colors.orange.withValues(alpha: 0.8),
|
||||
color: Colors.orange.withOpacity(0.8),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3450,7 +3636,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: _draggingPointIndex == i ? 6 : 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -3520,8 +3706,8 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: _hoveredMidpointIndex == i
|
||||
? Colors.blue.withValues(alpha: 0.8)
|
||||
: Colors.grey.withValues(alpha: 0.5),
|
||||
? Colors.blue.withOpacity(0.8)
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color:
|
||||
@@ -3666,7 +3852,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
height: 20.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.5),
|
||||
color: Colors.orange.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.orange,
|
||||
@@ -3674,7 +3860,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.orange.withValues(alpha: 0.5),
|
||||
color: Colors.orange.withOpacity(0.5),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
@@ -3825,13 +4011,13 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: isDragging ? 8 : (isHovered ? 6 : 4),
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
if (isHovered && !isDragging)
|
||||
BoxShadow(
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
color: Colors.orange.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
@@ -3889,8 +4075,8 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: _hoveredMidpointIndex == i
|
||||
? Colors.orange.withValues(alpha: 0.8)
|
||||
: Colors.grey.withValues(alpha: 0.5),
|
||||
? Colors.orange.withOpacity(0.8)
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: _hoveredMidpointIndex == i
|
||||
@@ -3980,24 +4166,29 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
onMapEvent: (event) {
|
||||
if (event is MapEventMove) {
|
||||
final displayedZoom = event.camera.zoom;
|
||||
debugPrint('🔍 MapPage: Zoom affiché par la caméra = $displayedZoom (précédent _currentZoom = $_currentZoom)');
|
||||
|
||||
// Afficher l'indicateur de zoom si le niveau a changé
|
||||
if ((displayedZoom - _currentZoom).abs() > 0.01) {
|
||||
_showZoomIndicatorTemporarily();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentPosition = event.camera.center;
|
||||
_currentZoom = displayedZoom;
|
||||
// Mettre à jour les variables sans setState() immédiat
|
||||
_currentPosition = event.camera.center;
|
||||
_currentZoom = displayedZoom;
|
||||
|
||||
// Annuler le timer précédent
|
||||
_mapMoveDebounceTimer?.cancel();
|
||||
|
||||
// Lancer un nouveau timer de 300ms pour debouncer
|
||||
_mapMoveDebounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
if (mounted) {
|
||||
// setState uniquement après 300ms sans mouvement
|
||||
setState(() {
|
||||
// Les variables sont déjà à jour
|
||||
});
|
||||
_saveSettings();
|
||||
}
|
||||
});
|
||||
_saveSettings();
|
||||
// Mettre à jour le survol après un mouvement de carte
|
||||
if (_mapMode == MapMode.deleting && kIsWeb) {
|
||||
// On doit recalculer car la carte a bougé
|
||||
// Note: On ne peut pas obtenir la position de la souris ici,
|
||||
// elle sera mise à jour au prochain mouvement de souris
|
||||
}
|
||||
} else if (event is MapEventTap &&
|
||||
(_mapMode == MapMode.drawing ||
|
||||
_mapMode == MapMode.deleting ||
|
||||
@@ -4091,7 +4282,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
horizontal: 12, vertical: 4),
|
||||
width: 220, // Largeur fixe pour accommoder les noms longs
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
@@ -4152,7 +4343,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
horizontal: 12, vertical: 4),
|
||||
width: 220,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
@@ -4229,7 +4420,7 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.3),
|
||||
color: Colors.blue.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
Reference in New Issue
Block a user