feat: Gestion des secteurs et migration v3.0.4+304

- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 6a609fb467
commit 599b9fcda0
662 changed files with 213221 additions and 174243 deletions

309
app/lib/presentation/auth/splash_page.dart Normal file → Executable file
View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/services/hive_service.dart';
import 'package:geosector_app/core/services/location_service.dart';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show kIsWeb;
@@ -9,7 +10,13 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
/// Action à effectuer après l'initialisation (login ou register)
final String? action;
/// Type de login/register (user ou admin) - ignoré pour register
final String? type;
const SplashPage({super.key, this.action, this.type});
@override
State<SplashPage> createState() => _SplashPageState();
@@ -46,6 +53,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
double _progress = 0.0;
bool _showButtons = false;
String _appVersion = '';
bool _showLocationError = false;
String? _locationErrorMessage;
Future<void> _getAppVersion() async {
try {
@@ -100,49 +109,127 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
try {
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
// Étape 1: Initialisation complète de Hive avec HiveService
// Étape 0: Vérification des permissions GPS (obligatoire) - 0 à 10%
if (!kIsWeb) {
if (mounted) {
setState(() {
_statusMessage = "Vérification des autorisations GPS...";
_progress = 0.05;
});
}
await Future.delayed(const Duration(milliseconds: 200));
final hasPermission = await LocationService.checkAndRequestPermission();
final errorMessage = await LocationService.getLocationErrorMessage();
if (!hasPermission) {
// Si les permissions ne sont pas accordées, on arrête tout
debugPrint('❌ Permissions GPS refusées');
if (mounted) {
setState(() {
_showLocationError = true;
_locationErrorMessage = errorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.";
_isInitializing = false;
_progress = 0.0;
});
}
return; // On arrête l'initialisation ici
}
if (mounted) {
setState(() {
_statusMessage = "Autorisations GPS accordées...";
_progress = 0.10;
});
}
}
// Étape 1: Préparation - 10 à 15%
if (mounted) {
setState(() {
_statusMessage = "Initialisation de la base de données...";
_progress = 0.1;
_statusMessage = "Démarrage de l'application...";
_progress = 0.12;
});
}
await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début
if (mounted) {
setState(() {
_statusMessage = "Chargement des composants...";
_progress = 0.15;
});
}
// HiveService fait TOUT le travail lourd (adaptateurs, destruction, recréation)
// Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue)
await HiveService.instance.initializeAndResetHive();
if (mounted) {
setState(() {
_statusMessage = "Vérification des bases de données...";
_progress = 0.7;
_statusMessage = "Configuration du stockage...";
_progress = 0.45;
});
}
await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement
if (mounted) {
setState(() {
_statusMessage = "Préparation des données...";
_progress = 0.60;
});
}
// Étape 2: S'assurer que toutes les Box sont ouvertes
// Étape 3: Ouverture des Box - 60 à 80%
await HiveService.instance.ensureBoxesAreOpen();
if (mounted) {
setState(() {
_statusMessage = "Finalisation...";
_progress = 0.9;
_statusMessage = "Vérification du système...";
_progress = 0.80;
});
}
// Étape 3: Vérification finale
// Étape 4: Vérification finale - 80 à 95%
final allBoxesOpen = HiveService.instance.areAllBoxesOpen();
if (!allBoxesOpen) {
final diagnostic = HiveService.instance.getDiagnostic();
debugPrint('❌ Diagnostic des Box: $diagnostic');
throw Exception('Certaines bases de données ne sont pas accessibles');
throw Exception('Une erreur est survenue lors de l\'initialisation');
}
// Finalisation
if (mounted) {
setState(() {
_statusMessage = "Finalisation du chargement...";
_progress = 0.95;
});
}
await Future.delayed(const Duration(milliseconds: 300)); // Petit délai pour finaliser
// Étape 5: Finalisation - 95 à 100%
if (mounted) {
setState(() {
_statusMessage = "Application prête !";
_progress = 1.0;
_isInitializing = false;
_showButtons = true;
});
// Attendre un court instant pour que l'utilisateur voie "Application prête !"
await Future.delayed(const Duration(milliseconds: 400));
setState(() {
_isInitializing = false;
});
// Redirection automatique si des paramètres sont fournis
if (widget.action != null) {
await _handleAutoRedirect();
} else {
setState(() {
_showButtons = true;
});
}
}
debugPrint('✅ Initialisation complète de l\'application terminée avec succès');
@@ -151,7 +238,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
if (mounted) {
setState(() {
_statusMessage = "Erreur d'initialisation - Redémarrage recommandé";
_statusMessage = "Erreur de chargement - Veuillez redémarrer l'application";
_progress = 1.0;
_isInitializing = false;
_showButtons = true;
@@ -160,6 +247,51 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
}
}
/// Gère la redirection automatique après l'initialisation
Future<void> _handleAutoRedirect() async {
// Petit délai pour voir le message "Application prête !"
await Future.delayed(const Duration(milliseconds: 300));
if (!mounted) return;
final action = widget.action?.toLowerCase();
final type = widget.type?.toLowerCase();
debugPrint('🔄 Redirection automatique: action=$action, type=$type');
// Afficher un message de redirection avant de naviguer
setState(() {
_statusMessage = action == 'login'
? "Redirection vers la connexion..."
: action == 'register'
? "Redirection vers l'inscription..."
: "Redirection...";
});
await Future.delayed(const Duration(milliseconds: 200));
switch (action) {
case 'login':
if (type == 'admin') {
context.go('/login/admin');
} else {
// Par défaut, rediriger vers user si type non spécifié ou invalid
context.go('/login/user');
}
break;
case 'register':
// Pour register, le type n'est pas pris en compte
context.go('/register');
break;
default:
// Si action non reconnue, afficher les boutons normalement
setState(() {
_showButtons = true;
});
break;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -243,26 +375,143 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
const Spacer(flex: 1),
// Indicateur de chargement
if (_isInitializing) ...[
if (_isInitializing && !_showLocationError) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: _progress,
backgroundColor: Colors.grey.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.primary,
child: Column(
children: [
// Barre de progression avec animation
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut,
tween: Tween(begin: 0.0, end: _progress),
builder: (context, value, child) {
return LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey.withOpacity(0.15),
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.primary,
),
minHeight: 12,
);
},
),
),
),
minHeight: 10,
),
const SizedBox(height: 8),
// Pourcentage
Text(
'${(_progress * 100).round()}%',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 16),
Text(
_statusMessage,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
// Message de statut avec animation
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
_statusMessage,
key: ValueKey(_statusMessage),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
// Erreur de localisation
if (_showLocationError) ...[
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Column(
children: [
Icon(
Icons.location_off,
size: 48,
color: Colors.red.shade700,
),
const SizedBox(height: 16),
Text(
'Autorisations GPS requises',
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_locationErrorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.",
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.red.shade700,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Réessayer
ElevatedButton.icon(
onPressed: () {
setState(() {
_showLocationError = false;
_isInitializing = true;
});
_startInitialization();
},
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
const SizedBox(width: 16),
// Bouton Paramètres
OutlinedButton.icon(
onPressed: () async {
if (_locationErrorMessage?.contains('définitivement') ?? false) {
await LocationService.openAppSettings();
} else {
await LocationService.openLocationSettings();
}
},
icon: const Icon(Icons.settings),
label: const Text('Paramètres'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red.shade700,
side: BorderSide(color: Colors.red.shade700),
),
),
],
),
],
),
),
],