feat: Mise à jour des interfaces mobiles v3.2.3

- Amélioration des interfaces utilisateur sur mobile
- Optimisation de la responsivité des composants Flutter
- Mise à jour des widgets de chat et communication
- Amélioration des formulaires et tableaux
- Ajout de nouveaux composants pour l'administration
- Optimisation des thèmes et styles visuels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-02 20:35:40 +02:00
parent 4153f73ace
commit f7baa7492c
106 changed files with 88501 additions and 88280 deletions

View File

@@ -40,7 +40,8 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
if (operation != null) {
return Text(
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
style: theme.textTheme.headlineMedium?.copyWith(
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
@@ -48,7 +49,8 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
} else {
return Text(
'Tableau de bord',
style: theme.textTheme.headlineMedium?.copyWith(
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
@@ -88,46 +90,45 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
}
// Construction d'une carte combinée pour les règlements (liste + graphique)
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPayments: false,
isDesktop: isDesktop,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: Colors.blue,
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
},
);
}
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPayments: false,
isDesktop: isDesktop,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: Colors.blue,
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
},
);
}
// Construction d'une carte combinée pour les passages (liste + graphique)
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
@@ -136,8 +137,8 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPassages: false,
excludePassageTypes: const [2], // Exclure "À finaliser"
isDesktop: isDesktop,
@@ -160,7 +161,9 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
height: 350,
child: ActivityChart(
useValueListenable: true, // Utiliser le système réactif
excludePassageTypes: const [2], // Exclure les passages "À finaliser"
excludePassageTypes: const [
2
], // Exclure les passages "À finaliser"
daysToShow: 15,
periodType: 'Jour',
height: 350,
@@ -178,27 +181,29 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
// Utilisation directe du widget PassagesListWidget sans Card wrapper
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final recentPassages = _getRecentPassages(passagesBox);
// Debug : afficher le nombre de passages récupérés
debugPrint('UserDashboardHomePage: ${recentPassages.length} passages récents récupérés');
debugPrint(
'UserDashboardHomePage: ${recentPassages.length} passages récents récupérés');
if (recentPassages.isEmpty) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Padding(
child: Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: Text(
'Aucun passage récent',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -208,7 +213,8 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
// Utiliser une hauteur fixe pour le widget dans le dashboard
return SizedBox(
height: 450, // Hauteur légèrement augmentée pour compenser l'absence de Card
height:
450, // Hauteur légèrement augmentée pour compenser l'absence de Card
child: PassagesListWidget(
passages: recentPassages,
showFilters: false,
@@ -217,8 +223,10 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
maxPassages: 20,
// Ne pas appliquer de filtres supplémentaires car les passages
// sont déjà filtrés dans _getRecentPassages
excludePassageTypes: null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId: null, // Pas de filtre, déjà géré dans _getRecentPassages
excludePassageTypes:
null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId:
null, // Pas de filtre, déjà géré dans _getRecentPassages
periodFilter: null, // Pas de filtre de période
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
@@ -245,18 +253,19 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
/// Récupère les passages récents pour la liste
List<Map<String, dynamic>> _getRecentPassages(Box<PassageModel> passagesBox) {
final currentUserId = userRepository.getCurrentUser()?.id;
// Filtrer les passages :
// Filtrer les passages :
// - Avoir une date passedAt
// - Exclure le type 2 ("À finaliser")
// - Appartenir à l'utilisateur courant
final allPassages = passagesBox.values.where((p) {
if (p.passedAt == null) return false;
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
if (currentUserId != null && p.fkUser != currentUserId) return false; // Filtrer par utilisateur
if (currentUserId != null && p.fkUser != currentUserId)
return false; // Filtrer par utilisateur
return true;
}).toList();
// Trier par date décroissante
allPassages.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
@@ -294,7 +303,10 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser,
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
'isOwnedByCurrentUser': passage.fkUser ==
userRepository
.getCurrentUser()
?.id, // Ajout du champ pour le widget
};
}).toList();
}

View File

@@ -190,7 +190,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -217,7 +217,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
Text(
'Vous n\'avez pas encore été affecté à une opération. Veuillez contacter votre administrateur pour obtenir un accès.',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
@@ -240,7 +240,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -267,7 +267,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
Text(
'Vous n\'êtes affecté sur aucun secteur. Contactez votre administrateur pour qu\'il vous en affecte au moins un.',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_map/flutter_map.dart';
@@ -10,7 +11,6 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
@@ -26,36 +26,37 @@ class UserFieldModePage extends StatefulWidget {
State<UserFieldModePage> createState() => _UserFieldModePageState();
}
class _UserFieldModePageState extends State<UserFieldModePage> with TickerProviderStateMixin {
class _UserFieldModePageState extends State<UserFieldModePage>
with TickerProviderStateMixin {
// Controllers
final MapController _mapController = MapController();
final TextEditingController _searchController = TextEditingController();
// Animation controllers pour le clignotement
late AnimationController _gpsBlinkController;
late AnimationController _networkBlinkController;
late Animation<double> _gpsBlinkAnimation;
late Animation<double> _networkBlinkAnimation;
// Position et tracking
Position? _currentPosition;
StreamSubscription<Position>? _positionStreamSubscription;
Timer? _qualityUpdateTimer;
// Qualité des signaux
double _gpsAccuracy = 999;
ConnectivityResult _connectivityResult = ConnectivityResult.none;
bool _isGpsEnabled = false;
// Mode boussole
bool _compassMode = false;
double _heading = 0;
StreamSubscription<MagnetometerEvent>? _magnetometerSubscription;
// Filtrage et recherche
String _searchQuery = '';
List<PassageModel> _nearbyPassages = [];
// État de chargement
bool _isLoading = true;
bool _locationPermissionGranted = false;
@@ -65,7 +66,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
void initState() {
super.initState();
_initializeAnimations();
if (kIsWeb) {
// Sur web, utiliser une position simulée pour éviter le blocage
_initializeWebMode();
@@ -75,19 +76,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_startQualityMonitoring();
}
}
void _initializeWebMode() async {
// Essayer d'obtenir la position réelle depuis le navigateur
try {
setState(() {
_statusMessage = "Demande d'autorisation de géolocalisation...";
});
// Demander la permission et obtenir la position
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
),
);
setState(() {
_currentPosition = position;
_gpsAccuracy = position.accuracy;
@@ -97,38 +100,40 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_locationPermissionGranted = true;
_statusMessage = "";
});
// Charger les passages proches de la position réelle
_updateNearbyPassages();
// Démarrer le suivi de position même sur web
_startLocationTracking();
} catch (e) {
debugPrint('Erreur géolocalisation web: $e');
// Essayer d'utiliser les coordonnées GPS de l'amicale
double fallbackLat = 46.603354; // Centre de la France par défaut
double fallbackLat = 46.603354; // Centre de la France par défaut
double fallbackLng = 1.888334;
String statusMessage = "Position approximative";
try {
final amicale = CurrentAmicaleService.instance.currentAmicale;
if (amicale != null && amicale.gpsLat.isNotEmpty && amicale.gpsLng.isNotEmpty) {
if (amicale != null &&
amicale.gpsLat.isNotEmpty &&
amicale.gpsLng.isNotEmpty) {
final amicaleLat = double.tryParse(amicale.gpsLat);
final amicaleLng = double.tryParse(amicale.gpsLng);
if (amicaleLat != null && amicaleLng != null) {
fallbackLat = amicaleLat;
fallbackLng = amicaleLng;
statusMessage = "Position de l'amicale";
debugPrint('Utilisation des coordonnées de l\'amicale: $fallbackLat, $fallbackLng');
debugPrint(
'Utilisation des coordonnées de l\'amicale: $fallbackLat, $fallbackLng');
}
}
} catch (amicaleError) {
debugPrint('Erreur récupération coordonnées amicale: $amicaleError');
}
// Utiliser la position de fallback (amicale ou centre France)
setState(() {
_currentPosition = Position(
@@ -150,7 +155,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_locationPermissionGranted = false;
_statusMessage = statusMessage;
});
_updateNearbyPassages();
}
}
@@ -207,12 +212,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_isGpsEnabled = true;
_isLoading = false;
});
_updateNearbyPassages();
_updateBlinkAnimations();
// Centrer la carte sur la nouvelle position
if (_mapController.mapEventStream != null && !_compassMode) {
if (!_compassMode) {
_mapController.move(LatLng(position.latitude, position.longitude), 17);
}
}, onError: (error) {
@@ -224,22 +229,24 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
void _startQualityMonitoring() {
// Mise à jour toutes les 5 secondes
_qualityUpdateTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
_qualityUpdateTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
// Vérifier la connexion réseau
final connectivityResults = await Connectivity().checkConnectivity();
setState(() {
// Prendre le premier résultat de la liste
_connectivityResult = connectivityResults.isNotEmpty
? connectivityResults.first
_connectivityResult = connectivityResults.isNotEmpty
? connectivityResults.first
: ConnectivityResult.none;
});
// Vérifier si le GPS est activé
final isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
final isLocationServiceEnabled =
await Geolocator.isLocationServiceEnabled();
setState(() {
_isGpsEnabled = isLocationServiceEnabled;
});
_updateBlinkAnimations();
});
}
@@ -272,9 +279,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
// Calculer les distances et trier
final passagesWithDistance = allPassages.map((passage) {
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
final distance = _calculateDistance(
_currentPosition!.latitude,
_currentPosition!.longitude,
@@ -295,7 +302,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
});
}
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) {
const distance = Distance();
return distance.as(
LengthUnit.Meter,
@@ -315,7 +323,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
setState(() {
_compassMode = !_compassMode;
});
@@ -330,7 +338,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
}
void _startCompass() {
_magnetometerSubscription = magnetometerEvents.listen((MagnetometerEvent event) {
_magnetometerSubscription =
magnetometerEventStream().listen((MagnetometerEvent event) {
setState(() {
// Calculer l'orientation à partir du magnétomètre
_heading = math.atan2(event.y, event.x) * (180 / math.pi);
@@ -346,7 +355,6 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
});
}
void _recenterMap() {
if (_currentPosition != null) {
_mapController.move(
@@ -374,7 +382,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
);
}
// Vérifier si l'amicale autorise la suppression des passages
bool _canDeletePassages() {
try {
@@ -383,17 +391,19 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
return amicale.chkUserDeletePass == true;
}
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de suppression: $e');
debugPrint(
'Erreur lors de la vérification des permissions de suppression: $e');
}
return false;
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(PassageModel passage) {
final TextEditingController confirmController = TextEditingController();
final String streetNumber = passage.numero ?? '';
final String fullAddress = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String streetNumber = passage.numero;
final String fullAddress =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
showDialog(
context: context,
barrierDismissible: false,
@@ -411,12 +421,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
fontSize: AppTheme.r(context, 16),
),
),
const SizedBox(height: 16),
@@ -434,9 +444,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
child: Text(
fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -450,7 +460,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
hintText: streetNumber.isNotEmpty
? 'Ex: $streetNumber'
: 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
@@ -481,8 +493,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
if (streetNumber.isNotEmpty &&
enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
@@ -491,11 +504,11 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passage);
},
@@ -510,20 +523,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
},
);
}
// Supprimer un passage
Future<void> _deletePassage(PassageModel passage) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await passageRepository.deletePassageViaApi(passage.id);
if (success && mounted) {
ApiException.showSuccess(context, 'Passage supprimé avec succès');
// Rafraîchir la liste des passages
_updateNearbyPassages();
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
ApiException.showError(
context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
@@ -546,8 +560,6 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
@@ -558,19 +570,22 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
kIsWeb
? (_locationPermissionGranted
? 'GPS: ${_currentPosition!.latitude.toStringAsFixed(4)}, ${_currentPosition!.longitude.toStringAsFixed(4)}'
: _statusMessage.isNotEmpty ? _statusMessage : 'Position approximative')
: '',
style: const TextStyle(
fontSize: 12,
kIsWeb
? (_locationPermissionGranted
? 'GPS: ${_currentPosition!.latitude.toStringAsFixed(4)}, ${_currentPosition!.longitude.toStringAsFixed(4)}'
: _statusMessage.isNotEmpty
? _statusMessage
: 'Position approximative')
: '',
style: TextStyle(
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
@@ -636,7 +651,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
contentPadding:
const EdgeInsets.symmetric(horizontal: 20),
),
onChanged: (value) {
setState(() {
@@ -648,10 +664,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
// En-tête de la liste
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(Icons.location_on, color: Colors.green[600], size: 20),
Icon(Icons.location_on,
color: Colors.green[600], size: 20),
const SizedBox(width: 8),
Text(
'${_getFilteredPassages().length} passage${_getFilteredPassages().length > 1 ? 's' : ''} à proximité',
@@ -709,7 +727,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -719,7 +737,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
'${_gpsAccuracy.toStringAsFixed(0)}m',
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12)),
),
],
),
@@ -774,7 +795,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -784,7 +805,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
label,
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12)),
),
],
),
@@ -806,7 +830,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
}
final apiService = ApiService.instance;
final mapboxApiKey = AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment());
final mapboxApiKey =
AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment());
return Stack(
children: [
@@ -815,7 +840,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
initialCenter: LatLng(
_currentPosition!.latitude, _currentPosition!.longitude),
initialZoom: 17,
maxZoom: 19,
minZoom: 10,
@@ -827,9 +853,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
children: [
TileLayer(
// Utiliser l'API v4 de Mapbox sur mobile ou OpenStreetMap en fallback
urlTemplate: kIsWeb
? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey'
: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile
urlTemplate: kIsWeb
? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey'
: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile
userAgentPackageName: 'app.geosector.fr',
additionalOptions: const {
'attribution': '© OpenStreetMap contributors',
@@ -840,24 +866,27 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
CircleLayer(
circles: [
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 50,
color: Colors.blue.withOpacity(0.1),
borderColor: Colors.blue.withOpacity(0.3),
color: Colors.blue.withValues(alpha: 0.1),
borderColor: Colors.blue.withValues(alpha: 0.3),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 100,
color: Colors.transparent,
borderColor: Colors.blue.withOpacity(0.2),
borderColor: Colors.blue.withValues(alpha: 0.2),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 250,
color: Colors.transparent,
borderColor: Colors.blue.withOpacity(0.15),
borderColor: Colors.blue.withValues(alpha: 0.15),
borderStrokeWidth: 1,
),
],
@@ -870,7 +899,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
MarkerLayer(
markers: [
Marker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
width: 30,
height: 30,
child: Container(
@@ -880,7 +910,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 5,
),
@@ -941,9 +971,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
'Mode boussole',
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.bold,
),
),
@@ -973,9 +1003,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const borderColor = Color(0xFFF7A278);
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
return Marker(
point: LatLng(lat, lng),
width: 40,
@@ -989,7 +1019,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
border: Border.all(color: borderColor, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -997,11 +1027,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
child: Center(
child: Text(
'${passage.numero ?? ''}${(passage.rueBis != null && passage.rueBis!.isNotEmpty) ? passage.rueBis!.substring(0, 1).toLowerCase() : ''}',
'${passage.numero}${(passage.rueBis.isNotEmpty) ? passage.rueBis.substring(0, 1).toLowerCase() : ''}',
style: TextStyle(
color: fillColor == Colors.white ? Colors.black : Colors.white,
color:
fillColor == Colors.white ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
fontSize: AppTheme.r(context, 12),
),
textAlign: TextAlign.center,
maxLines: 1,
@@ -1016,19 +1047,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
List<Map<String, dynamic>> _getFilteredPassages() {
// Filtrer d'abord par recherche si nécessaire
List<PassageModel> filtered = _searchQuery.isEmpty
? _nearbyPassages
: _nearbyPassages.where((passage) {
final address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim().toLowerCase();
return address.contains(_searchQuery);
}).toList();
List<PassageModel> filtered = _searchQuery.isEmpty
? _nearbyPassages
: _nearbyPassages.where((passage) {
final address = '${passage.numero} ${passage.rueBis} ${passage.rue}'
.trim()
.toLowerCase();
return address.contains(_searchQuery);
}).toList();
// Convertir au format attendu par PassagesListWidget avec distance
return filtered.map((passage) {
// Calculer la distance
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
final distance = _currentPosition != null
? _calculateDistance(
_currentPosition!.latitude,
@@ -1037,10 +1070,11 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
lng,
)
: 0.0;
// Construire l'adresse complète
final String address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String address =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
// Convertir le montant
double amount = 0.0;
try {
@@ -1051,7 +1085,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
} catch (e) {
// Ignorer les erreurs de conversion
}
return {
'id': passage.id,
'address': address.isEmpty ? 'Adresse inconnue' : address,
@@ -1066,7 +1100,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
'fkUser': passage.fkUser,
'distance': distance, // Ajouter la distance pour le tri et l'affichage
'nbPassages': passage.nbPassages, // Pour la couleur de l'indicateur
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
'isOwnedByCurrentUser': passage.fkUser ==
userRepository
.getCurrentUser()
?.id, // Ajout du champ pour le widget
// Garder les données originales pour l'édition
'numero': passage.numero,
'rueBis': passage.rueBis,
@@ -1109,18 +1146,18 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
},
);
},
onPassageDelete: _canDeletePassages()
? (passage) {
// Retrouver le PassageModel original pour la suppression
final passageId = passage['id'] as int;
final originalPassage = _nearbyPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => _nearbyPassages.first,
);
_showDeleteConfirmationDialog(originalPassage);
}
: null,
onPassageDelete: _canDeletePassages()
? (passage) {
// Retrouver le PassageModel original pour la suppression
final passageId = passage['id'] as int;
final originalPassage = _nearbyPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => _nearbyPassages.first,
);
_showDeleteConfirmationDialog(originalPassage);
}
: null,
),
);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
@@ -664,7 +665,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white.withOpacity(0.95),
color: Colors.white.withValues(alpha: 0.95),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -868,56 +869,15 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec bouton de rafraîchissement
// Filtres avec bouton de rafraîchissement
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isLoading
? 'Historique des passages'
: 'Historique des ${_convertedPassages.length} passages${_totalSectors > 0 ? ' ($_totalSectors secteur${_totalSectors > 1 ? 's' : ''})' : ''}',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
if (!_isLoading && _sharedMembersCount > 0)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'Partagés avec $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''}',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontStyle: FontStyle.italic,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadPassages,
tooltip: 'Rafraîchir',
),
],
),
// Filtres (secteur et période)
// Filtres (secteur et période) avec bouton rafraîchir
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: _buildFilters(context),
),
_buildFilters(context),
],
),
),
@@ -940,8 +900,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: theme.textTheme.titleLarge
?.copyWith(color: Colors.red),
style: TextStyle(
fontSize: AppTheme.r(context, 22),
color: Colors.red),
),
const SizedBox(height: 8),
Text(_errorMessage),
@@ -1019,7 +980,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
color: _currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.dateAsc
? 'Tri par date (ancien en premier)'
@@ -1053,7 +1014,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
color: _currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.addressAsc
? 'Tri par adresse (A-Z)'

View File

@@ -37,9 +37,6 @@ class _UserMapPageState extends State<UserMapPage> {
final List<Map<String, dynamic>> _sectors = [];
final List<Map<String, dynamic>> _passages = [];
// État du plein écran
bool _isFullScreen = false;
// Items pour la combobox de secteurs
List<DropdownMenuItem<int?>> _sectorItems = [];
@@ -567,32 +564,12 @@ class _UserMapPageState extends State<UserMapPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête - affiché uniquement si pas en plein écran
if (!_isFullScreen)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Carte des passages',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Filtres - affichés uniquement si pas en plein écran
if (!_isFullScreen) _buildFilters(theme, isDesktop),
// Carte
Expanded(
child: Stack(
@@ -606,7 +583,7 @@ class _UserMapPageState extends State<UserMapPage> {
useOpenStreetMap: !kIsWeb,
markers: _buildPassageMarkers(),
polygons: _buildSectorPolygons(),
showControls: true,
showControls: false, // Désactiver les contrôles par défaut pour éviter la duplication
onMapEvent: (event) {
if (event is MapEventMove) {
// Mettre à jour la position et le zoom actuels
@@ -632,7 +609,7 @@ class _UserMapPageState extends State<UserMapPage> {
width:
220, // Largeur fixe pour accommoder les noms longs
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
color: Colors.white.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@@ -673,31 +650,148 @@ class _UserMapPageState extends State<UserMapPage> {
),
),
// Bouton de plein écran (les autres contrôles sont gérés par MapboxMap)
// Contrôles de zoom et localisation en bas à droite
Positioned(
bottom: 16.0,
right: 16.0,
child: _buildMapButton(
icon: _isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
onPressed: () {
setState(() {
_isFullScreen = !_isFullScreen;
});
},
child: Column(
children: [
// Bouton zoom +
_buildMapButton(
icon: Icons.add,
onPressed: () {
final newZoom = _currentZoom + 1;
_mapController.move(_currentPosition, newZoom);
setState(() {
_currentZoom = newZoom;
});
_saveSettings();
},
),
const SizedBox(height: 8),
// Bouton zoom -
_buildMapButton(
icon: Icons.remove,
onPressed: () {
final newZoom = _currentZoom - 1;
_mapController.move(_currentPosition, newZoom);
setState(() {
_currentZoom = newZoom;
});
_saveSettings();
},
),
const SizedBox(height: 8),
// Bouton de localisation
_buildMapButton(
icon: Icons.my_location,
onPressed: () {
_getUserLocation();
},
),
],
),
),
// Bouton de localisation personnalisé (pour utiliser notre propre logique)
// Filtres de type de passage en bas à gauche
Positioned(
bottom: 80.0, // Positionné au-dessus du bouton plein écran
right: 16.0,
child: _buildMapButton(
icon: Icons.my_location,
onPressed: () {
_getUserLocation();
},
bottom: 16.0,
left: 16.0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Filtre Effectués (type 1)
_buildFilterDot(
color: Color(AppKeys.typesPassages[1]?['couleur2'] as int),
selected: _showEffectues,
onTap: () {
setState(() {
_showEffectues = !_showEffectues;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre À finaliser (type 2)
_buildFilterDot(
color: Color(AppKeys.typesPassages[2]?['couleur2'] as int),
selected: _showAFinaliser,
onTap: () {
setState(() {
_showAFinaliser = !_showAFinaliser;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Refusés (type 3)
_buildFilterDot(
color: Color(AppKeys.typesPassages[3]?['couleur2'] as int),
selected: _showRefuses,
onTap: () {
setState(() {
_showRefuses = !_showRefuses;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Dons (type 4)
_buildFilterDot(
color: Color(AppKeys.typesPassages[4]?['couleur2'] as int),
selected: _showDons,
onTap: () {
setState(() {
_showDons = !_showDons;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Lots (type 5)
_buildFilterDot(
color: Color(AppKeys.typesPassages[5]?['couleur2'] as int),
selected: _showLots,
onTap: () {
setState(() {
_showLots = !_showLots;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Maisons vides (type 6)
_buildFilterDot(
color: Color(AppKeys.typesPassages[6]?['couleur2'] as int),
selected: _showMaisonsVides,
onTap: () {
setState(() {
_showMaisonsVides = !_showMaisonsVides;
_loadPassages();
_saveSettings();
});
},
),
],
),
),
),
],
@@ -709,145 +803,26 @@ class _UserMapPageState extends State<UserMapPage> {
);
}
// Construire les filtres pour les passages
Widget _buildFilters(ThemeData theme, bool isDesktop) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Filtre pour les passages effectués
_buildFilterChip(
label: AppKeys.typesPassages[1]?['titres'] as String? ??
'Effectués',
selected: _showEffectues,
color: Color(AppKeys.typesPassages[1]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showEffectues = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les passages à finaliser
_buildFilterChip(
label: AppKeys.typesPassages[2]?['titres'] as String? ??
'À finaliser',
selected: _showAFinaliser,
color: Color(AppKeys.typesPassages[2]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showAFinaliser = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les passages refusés
_buildFilterChip(
label:
AppKeys.typesPassages[3]?['titres'] as String? ?? 'Refusés',
selected: _showRefuses,
color: Color(AppKeys.typesPassages[3]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showRefuses = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les dons
_buildFilterChip(
label: AppKeys.typesPassages[4]?['titres'] as String? ?? 'Dons',
selected: _showDons,
color: Color(AppKeys.typesPassages[4]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showDons = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les lots
_buildFilterChip(
label: AppKeys.typesPassages[5]?['titres'] as String? ?? 'Lots',
selected: _showLots,
color: Color(AppKeys.typesPassages[5]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showLots = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les maisons vides
_buildFilterChip(
label: AppKeys.typesPassages[6]?['titres'] as String? ??
'Maisons vides',
selected: _showMaisonsVides,
color: Color(AppKeys.typesPassages[6]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showMaisonsVides = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
],
),
],
),
);
}
// Construire un chip de filtre
Widget _buildFilterChip({
required String label,
required bool selected,
// Construire une pastille de filtre pour la carte
Widget _buildFilterDot({
required Color color,
required Function(bool) onSelected,
required bool selected,
required VoidCallback onTap,
}) {
// Utiliser la couleur vive pour les boutons sélectionnés et une version plus terne pour les désélectionnés
final Color avatarColor = selected ? color : color.withOpacity(0.4);
final Color chipColor =
selected ? color.withOpacity(0.2) : Colors.grey.withOpacity(0.1);
return FilterChip(
label: Text(
label,
style: TextStyle(
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
color: selected ? Colors.black : Colors.black54,
return GestureDetector(
onTap: onTap,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: selected ? color : color.withValues(alpha: 0.3),
shape: BoxShape.circle,
border: Border.all(
color: selected ? Colors.white : Colors.white.withValues(alpha: 0.5),
width: 1.5,
),
),
),
selected: selected,
showCheckmark: false,
avatar: CircleAvatar(
backgroundColor: avatarColor,
radius: 10.0,
),
backgroundColor: Colors.white,
selectedColor: chipColor,
side: BorderSide(
color: selected ? color : Colors.grey.withOpacity(0.3),
width: selected ? 1.5 : 1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
onSelected: onSelected,
);
}
@@ -864,7 +839,7 @@ class _UserMapPageState extends State<UserMapPage> {
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 3),
),
@@ -918,8 +893,8 @@ class _UserMapPageState extends State<UserMapPage> {
return _sectors.map((sector) {
return Polygon(
points: sector['points'] as List<LatLng>,
color: (sector['color'] as Color).withOpacity(0.3),
borderColor: (sector['color'] as Color).withOpacity(1.0),
color: (sector['color'] as Color).withValues(alpha: 0.3),
borderColor: (sector['color'] as Color).withValues(alpha: 1.0),
borderStrokeWidth: 2.0,
);
}).toList();

View File

@@ -31,15 +31,6 @@ class _UserStatisticsPageState extends State<UserStatisticsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistiques',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
// Filtres
_buildFilters(theme, isDesktop),