feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -1,5 +1,4 @@
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';
@@ -15,8 +14,8 @@ 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';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/grouped_passages_dialog.dart';
import 'package:geosector_app/app.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
class UserFieldModePage extends StatefulWidget {
@@ -45,14 +44,9 @@ class _UserFieldModePageState extends State<UserFieldModePage>
// Qualité des signaux
double _gpsAccuracy = 999;
ConnectivityResult _connectivityResult = ConnectivityResult.none;
List<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 = [];
@@ -62,11 +56,18 @@ class _UserFieldModePageState extends State<UserFieldModePage>
bool _locationPermissionGranted = false;
String _statusMessage = '';
// Listener pour les changements de la box passages
Box<PassageModel>? _passagesBox;
@override
void initState() {
super.initState();
_initializeAnimations();
// Écouter les changements de la Hive box passages pour rafraîchir la carte
_passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
_passagesBox?.listenable().addListener(_onPassagesChanged);
if (kIsWeb) {
// Sur web, utiliser une position simulée pour éviter le blocage
_initializeWebMode();
@@ -77,6 +78,13 @@ class _UserFieldModePageState extends State<UserFieldModePage>
}
}
// Callback appelé quand la box passages change
void _onPassagesChanged() {
if (mounted) {
_updateNearbyPassages();
}
}
void _initializeWebMode() async {
// Essayer d'obtenir la position réelle depuis le navigateur
try {
@@ -86,14 +94,16 @@ class _UserFieldModePageState extends State<UserFieldModePage>
// 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;
_isGpsEnabled = true;
_connectivityResult = ConnectivityResult.wifi;
_connectivityResult = [ConnectivityResult.wifi];
_isLoading = false;
_locationPermissionGranted = true;
_statusMessage = "";
@@ -148,7 +158,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
);
_gpsAccuracy = 100.0;
_isGpsEnabled = false;
_connectivityResult = ConnectivityResult.wifi;
_connectivityResult = [ConnectivityResult.wifi];
_isLoading = false;
_locationPermissionGranted = false;
_statusMessage = statusMessage;
@@ -215,9 +225,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
_updateBlinkAnimations();
// Centrer la carte sur la nouvelle position
if (!_compassMode) {
_mapController.move(LatLng(position.latitude, position.longitude), 17);
}
_mapController.move(LatLng(position.latitude, position.longitude), 17);
}, onError: (error) {
setState(() {
_isGpsEnabled = false;
@@ -256,8 +264,8 @@ class _UserFieldModePageState extends State<UserFieldModePage>
}
// Réseau: clignoter si connexion faible ou absente
if (_connectivityResult == ConnectivityResult.none ||
_connectivityResult == ConnectivityResult.mobile) {
if (_connectivityResult.contains(ConnectivityResult.none) ||
_connectivityResult.contains(ConnectivityResult.mobile)) {
_networkBlinkController.repeat(reverse: true);
} else {
_networkBlinkController.stop();
@@ -288,12 +296,29 @@ class _UserFieldModePageState extends State<UserFieldModePage>
passagesWithDistance.sort((a, b) => a.value.compareTo(b.value));
setState(() {
_nearbyPassages = passagesWithDistance
.where((entry) => entry.value <= 500) // Max 500m
.map((entry) => entry.key)
.toList();
});
final newNearbyPassages = passagesWithDistance
.where((entry) => entry.value <= 500) // Max 500m
.map((entry) => entry.key)
.toList();
// Ne setState que si les passages ont vraiment changé
if (!_arePassagesEqual(_nearbyPassages, newNearbyPassages)) {
setState(() {
_nearbyPassages = newNearbyPassages;
});
}
}
// Comparer deux listes de passages pour éviter les setState inutiles
bool _arePassagesEqual(List<PassageModel> oldPassages, List<PassageModel> newPassages) {
if (oldPassages.length != newPassages.length) return false;
// Créer des clés uniques incluant ID + fkType pour détecter les changements de type
// (important pour le gradient des immeubles qui dépend du fkType)
final oldKeys = oldPassages.map((p) => '${p.id}_${p.fkType}').toSet();
final newKeys = newPassages.map((p) => '${p.id}_${p.fkType}').toSet();
return oldKeys.length == newKeys.length && oldKeys.containsAll(newKeys);
}
double _calculateDistance(
@@ -306,49 +331,6 @@ class _UserFieldModePageState extends State<UserFieldModePage>
);
}
void _toggleCompassMode() {
// Mode boussole désactivé sur web
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le mode boussole nécessite un appareil mobile'),
duration: Duration(seconds: 2),
),
);
return;
}
setState(() {
_compassMode = !_compassMode;
});
if (_compassMode) {
_startCompass();
// Vibration légère pour feedback
HapticFeedback.lightImpact();
} else {
_stopCompass();
}
}
void _startCompass() {
_magnetometerSubscription =
magnetometerEvents.listen((MagnetometerEvent event) {
setState(() {
// Calculer l'orientation à partir du magnétomètre
_heading = math.atan2(event.y, event.x) * (180 / math.pi);
});
});
}
void _stopCompass() {
_magnetometerSubscription?.cancel();
_magnetometerSubscription = null;
setState(() {
_heading = 0;
});
}
void _recenterMap() {
if (_currentPosition != null) {
_mapController.move(
@@ -378,6 +360,17 @@ class _UserFieldModePageState extends State<UserFieldModePage>
);
}
// Afficher la dialog groupée pour les immeubles
void _showGroupedPassagesDialog(PassageModel referencePassage) {
showDialog(
context: context,
builder: (context) => GroupedPassagesDialog(
referencePassage: referencePassage,
isAdmin: false, // Mode terrain = utilisateur simple
),
);
}
// Vérifier si l'amicale autorise la suppression des passages
bool _canDeletePassages() {
try {
@@ -546,10 +539,10 @@ class _UserFieldModePageState extends State<UserFieldModePage>
void dispose() {
_positionStreamSubscription?.cancel();
_qualityUpdateTimer?.cancel();
_magnetometerSubscription?.cancel();
_gpsBlinkController.dispose();
_networkBlinkController.dispose();
_searchController.dispose();
_passagesBox?.listenable().removeListener(_onPassagesChanged);
super.dispose();
}
@@ -568,7 +561,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
@@ -722,7 +715,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -752,7 +745,13 @@ class _UserFieldModePageState extends State<UserFieldModePage>
String label;
String tooltip;
switch (_connectivityResult) {
// Utiliser le premier élément de la liste pour déterminer le type de connexion
final primaryResult = _connectivityResult.firstWhere(
(result) => result != ConnectivityResult.none,
orElse: () => ConnectivityResult.none
);
switch (primaryResult) {
case ConnectivityResult.wifi:
icon = Icons.wifi;
color = Colors.green;
@@ -790,7 +789,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -830,9 +829,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
return Stack(
children: [
Transform.rotate(
angle: _compassMode ? _heading * (math.pi / 180) : 0,
child: FlutterMap(
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: LatLng(
@@ -851,41 +848,11 @@ class _UserFieldModePageState extends State<UserFieldModePage>
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',
userAgentPackageName: 'app3.geosector.fr',
additionalOptions: const {
'attribution': '© OpenStreetMap contributors',
},
),
// Cercles de distance en mode boussole
if (_compassMode)
CircleLayer(
circles: [
CircleMarker(
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 50,
color: Colors.blue.withValues(alpha: 0.1),
borderColor: Colors.blue.withValues(alpha: 0.3),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 100,
color: Colors.transparent,
borderColor: Colors.blue.withValues(alpha: 0.2),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 250,
color: Colors.transparent,
borderColor: Colors.blue.withValues(alpha: 0.15),
borderStrokeWidth: 1,
),
],
),
// Markers des passages
MarkerLayer(
markers: _buildPassageMarkers(),
@@ -905,7 +872,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.3),
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 5,
),
@@ -921,7 +888,6 @@ class _UserFieldModePageState extends State<UserFieldModePage>
],
),
],
),
),
// Bouton recentrage (bas gauche)
Positioned(
@@ -934,48 +900,6 @@ class _UserFieldModePageState extends State<UserFieldModePage>
child: const Icon(Icons.my_location),
),
),
// Bouton boussole (bas droite)
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton.small(
backgroundColor: _compassMode ? Colors.green : Colors.white,
foregroundColor: _compassMode ? Colors.white : Colors.grey[700],
onPressed: _toggleCompassMode,
child: Transform.rotate(
angle: _compassMode ? _heading * (math.pi / 180) : 0,
child: const Icon(Icons.explore),
),
),
),
// Indicateur de mode boussole
if (_compassMode)
Positioned(
top: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.explore, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
'Mode boussole',
style: TextStyle(
color: Colors.white,
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
);
}
@@ -993,7 +917,34 @@ class _UserFieldModePageState extends State<UserFieldModePage>
List<Marker> _buildPassageMarkers() {
if (_currentPosition == null) return [];
return _nearbyPassages.map((passage) {
final List<Marker> markers = [];
// 1. Séparer les passages immeubles (fkHabitat=2) des autres
final buildingPassages = <String, List<Map<String, dynamic>>>{};
final individualPassages = <PassageModel>[];
for (final passage in _nearbyPassages) {
if (passage.fkHabitat == 2) {
// Créer une clé unique basée sur l'adresse complète
final addressKey = '${passage.numero}|${passage.rueBis}|${passage.rue}|${passage.ville}';
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
buildingPassages.putIfAbsent(addressKey, () => []);
buildingPassages[addressKey]!.add({
'model': passage,
'position': LatLng(lat, lng),
'id': passage.id,
});
} else {
individualPassages.add(passage);
}
}
// 2. Créer les markers individuels (fkHabitat != 2) - Cercles
for (final passage in individualPassages) {
// Déterminer la couleur selon le type de passage
Color fillColor = Colors.grey; // Couleur par défaut
@@ -1022,45 +973,121 @@ class _UserFieldModePageState extends State<UserFieldModePage>
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
return Marker(
point: LatLng(lat, lng),
width: 40,
height: 40,
child: GestureDetector(
onTap: () => _openPassageForm(passage),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: fillColor,
border: Border.all(color: borderColor, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 4,
offset: const Offset(0, 2),
markers.add(
Marker(
point: LatLng(lat, lng),
width: 40,
height: 40,
child: GestureDetector(
onTap: () => _openPassageForm(passage),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: fillColor,
border: Border.all(color: borderColor, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Text(
'${passage.numero}${(passage.rueBis.isNotEmpty) ? passage.rueBis.substring(0, 1).toLowerCase() : ''}',
style: TextStyle(
// Texte noir sur fond clair, blanc sur fond foncé
color: fillColor.computeLuminance() > 0.5
? Colors.black
: Colors.white,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
child: Center(
child: Text(
'${passage.numero}${(passage.rueBis.isNotEmpty) ? passage.rueBis.substring(0, 1).toLowerCase() : ''}',
style: TextStyle(
// Texte noir sur fond clair, blanc sur fond foncé
color: fillColor.computeLuminance() > 0.5
? Colors.black
: Colors.white,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}).toList();
}
// 3. Créer les markers groupés (carrés avec dégradé blanc→vert selon avancement)
for (final entry in buildingPassages.entries) {
final passages = entry.value;
if (passages.isEmpty) continue;
// Utiliser la position du premier passage du groupe
final position = passages.first['position'] as LatLng;
final count = passages.length;
final displayCount = count >= 99 ? '99' : count.toString();
// Calculer le pourcentage de passages réalisés (fkType != 2)
final models = passages.map((p) => p['model'] as PassageModel).toList();
final realizedCount = models.where((p) => p.fkType != 2).length;
final percentage = realizedCount / models.length;
// Déterminer la couleur de remplissage selon le palier (5 niveaux)
Color fillColor;
if (percentage == 0) {
// 0% : Blanc pur
fillColor = Colors.white;
} else if (percentage <= 0.25) {
// 1-25% : Blanc cassé → Vert très clair
fillColor = Color.lerp(Colors.white, const Color(0xFFB3F5E0), (percentage / 0.25))!;
} else if (percentage <= 0.50) {
// 26-50% : Vert très clair → Vert clair
fillColor = Color.lerp(const Color(0xFFB3F5E0), const Color(0xFF66EBBB), ((percentage - 0.25) / 0.25))!;
} else if (percentage <= 0.75) {
// 51-75% : Vert clair → Vert moyen
fillColor = Color.lerp(const Color(0xFF66EBBB), const Color(0xFF33E1A0), ((percentage - 0.50) / 0.25))!;
} else if (percentage < 1.0) {
// 76-99% : Vert moyen → Vert foncé
fillColor = Color.lerp(const Color(0xFF33E1A0), const Color(0xFF00E09D), ((percentage - 0.75) / 0.25))!;
} else {
// 100% : Vert foncé (couleur "Effectué")
fillColor = const Color(0xFF00E09D);
}
markers.add(
Marker(
point: position,
width: 24.0,
height: 24.0,
child: GestureDetector(
onTap: () {
_showGroupedPassagesDialog(passages.first['model'] as PassageModel);
},
child: Container(
decoration: BoxDecoration(
color: fillColor,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.blue, // Bordure bleue toujours
width: 2,
),
),
child: Center(
child: Text(
displayCount,
style: TextStyle(
color: percentage < 0.5 ? Colors.black87 : Colors.white, // Texte noir sur fond clair, blanc sur fond foncé
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
}
return markers;
}
List<Map<String, dynamic>> _getFilteredPassages() {
@@ -1121,7 +1148,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
'isOwnedByCurrentUser': passage.fkUser ==
userRepository
.getCurrentUser()
?.id, // Ajout du champ pour le widget
?.opeUserId, // Comparer avec ope_users.id
// Garder les données originales pour l'édition
'numero': passage.numero,
'rueBis': passage.rueBis,