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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user