Amélioration de la splash_page et du login

This commit is contained in:
d6soft
2025-06-04 16:51:40 +02:00
parent 8c9e9a21c4
commit 41f1db1169
228 changed files with 366459 additions and 6183 deletions

View File

@@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:convert';
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
import 'dart:js' as js;
import 'package:go_router/go_router.dart';
import 'package:go_router/src/state.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:http/http.dart' as http;
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/presentation/widgets/custom_button.dart';
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
import 'package:geosector_app/core/services/location_service.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
class LoginPage extends StatefulWidget {
@@ -51,6 +52,7 @@ class _LoginPageState extends State<LoginPage> {
final _passwordController = TextEditingController();
final _usernameFocusNode = FocusNode();
bool _obscurePassword = true;
String _appVersion = '';
// Type de connexion (utilisateur ou administrateur)
late String _loginType;
@@ -63,6 +65,37 @@ class _LoginPageState extends State<LoginPage> {
// État de la connexion Internet
bool _isConnected = false;
Future<void> _getAppVersion() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_appVersion = packageInfo.version;
});
}
} catch (e) {
debugPrint('Erreur lors de la récupération de la version: $e');
// Fallback sur la version du AppInfoService si elle existe
if (mounted) {
setState(() {
_appVersion = AppInfoService.fullVersion
.split(' ')
.last; // Extraire juste le numéro
});
}
}
}
Future<void> _checkConnectivity() async {
await connectivityService.checkConnectivity();
if (mounted) {
setState(() {
_isConnected = connectivityService.isConnected;
});
}
}
@override
void initState() {
super.initState();
@@ -163,6 +196,12 @@ class _LoginPageState extends State<LoginPage> {
}
});
// Récupérer la version de l'application
_getAppVersion();
// Vérification de connectivité au démarrage
_checkConnectivity();
// Pré-remplir le champ username avec l'identifiant du dernier utilisateur connecté
// seulement si le rôle correspond au type de login
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -538,6 +577,14 @@ class _LoginPageState extends State<LoginPage> {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
AppInfoService.fullVersion,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.7),
),
),
const SizedBox(height: 8),
// Ajouter un texte de débogage uniquement en mode développement
if (kDebugMode)
Text(
@@ -558,7 +605,38 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 16),
// Indicateur de connectivité
ConnectivityIndicator(),
const ConnectivityIndicator(),
const SizedBox(height: 16),
if (!kIsWeb && !_isConnected)
Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
theme.colorScheme.error.withOpacity(0.3),
),
),
child: Column(
children: [
Icon(Icons.signal_wifi_off,
color: theme.colorScheme.error, size: 32),
const SizedBox(height: 8),
Text('Connexion Internet requise',
style: theme.textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.error)),
const SizedBox(height: 8),
const Text(
'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
],
),
),
const SizedBox(height: 16),
// Formulaire de connexion
@@ -689,7 +767,7 @@ class _LoginPageState extends State<LoginPage> {
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Naviguer vers la page de récupération de mot de passe
_showForgotPasswordDialog(context);
},
child: Text(
'Mot de passe oublié ?',
@@ -863,10 +941,10 @@ class _LoginPageState extends State<LoginPage> {
onPressed: () {
context.go('/register');
},
child: Text(
child: const Text(
'Inscription Administrateur',
style: TextStyle(
color: theme.colorScheme.tertiary,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
@@ -897,8 +975,255 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
// Badge de version en bas à droite
if (_appVersion.isNotEmpty)
Positioned(
bottom: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.3),
width: 1,
),
),
child: Text(
'v$_appVersion',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.8),
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
// Affiche la boîte de dialogue pour la récupération de mot de passe
void _showForgotPasswordDialog(BuildContext context) {
final emailController = TextEditingController();
final formKey = GlobalKey<FormState>();
bool isLoading = false;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return StatefulBuilder(builder: (context, setState) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.lock_reset, color: Colors.blue),
SizedBox(width: 10),
Text('Récupération de mot de passe'),
],
),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Veuillez entrer votre adresse email pour recevoir un nouveau mot de passe.',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
CustomTextField(
controller: emailController,
label: 'Email',
hintText: 'Entrez votre email',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: isLoading
? null
: () async {
if (formKey.currentState!.validate()) {
setState(() {
isLoading = true;
});
try {
// Vérifier la connexion Internet
await connectivityService.checkConnectivity();
if (!connectivityService.isConnected) {
throw Exception('Aucune connexion Internet');
}
// Construire l'URL de l'API
final baseUrl = Uri.base.origin;
final apiUrl = '$baseUrl/api/lostpassword';
print('Envoi de la requête à: $apiUrl');
print('Email: ${emailController.text.trim()}');
http.Response? response;
try {
// Envoyer la requête à l'API
response = await http.post(
Uri.parse(apiUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'email': emailController.text.trim(),
}),
);
print('Réponse reçue: ${response.statusCode}');
print('Corps de la réponse: ${response.body}');
// Si la réponse est 404, c'est peut-être un problème de route
if (response.statusCode == 404) {
// Essayer avec une URL alternative
final alternativeUrl =
'$baseUrl/api/index.php/lostpassword';
print(
'Tentative avec URL alternative: $alternativeUrl');
final alternativeResponse = await http.post(
Uri.parse(alternativeUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'email': emailController.text.trim(),
}),
);
print(
'Réponse alternative reçue: ${alternativeResponse.statusCode}');
print(
'Corps de la réponse alternative: ${alternativeResponse.body}');
// Si la réponse alternative est un succès, utiliser cette réponse
if (alternativeResponse.statusCode == 200) {
response = alternativeResponse;
}
}
} catch (e) {
print(
'Erreur lors de l\'envoi de la requête: $e');
throw Exception('Erreur de connexion: $e');
}
// Traiter la réponse
if (response != null &&
response.statusCode == 200) {
// Modifier le contenu de la boîte de dialogue pour afficher le message de succès
setState(() {
isLoading = false;
});
// Remplacer le contenu de la boîte de dialogue par un message de succès
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
// Fermer automatiquement la boîte de dialogue après 2 secondes
Future.delayed(Duration(seconds: 2), () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
});
return const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
color: Colors.green,
size: 48,
),
SizedBox(height: 16),
Text(
'Vous recevrez un nouveau mot de passe par email',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
],
),
);
},
);
} else {
// Fermer la boîte de dialogue actuelle
Navigator.of(context).pop();
// Afficher un message d'erreur
final responseData = json.decode(response.body);
throw Exception(responseData['message'] ??
'Erreur lors de la récupération du mot de passe');
}
} catch (e) {
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e
.toString()
.contains('Exception:')
? e.toString().split('Exception: ')[1]
: 'Erreur lors de la récupération du mot de passe'),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
},
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text('Recevoir un nouveau mot de passe'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
],
);
});
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
@@ -15,10 +13,11 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_sector_model.dart';
import 'package:geosector_app/chat/models/conversation_model.dart';
import 'package:geosector_app/chat/models/message_model.dart';
import 'package:geosector_app/core/services/hive_reset_state_service.dart';
import 'package:geosector_app/presentation/widgets/clear_cache_dialog.dart';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@@ -58,12 +57,28 @@ class _SplashPageState extends State<SplashPage>
String _statusMessage = "Initialisation...";
double _progress = 0.0;
bool _showButtons = false;
String _appVersion = '';
final List<String> _initializationSteps = [
"Initialisation des services...",
"Vérification de l'authentification...",
"Chargement des données..."
];
Future<void> _getAppVersion() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_appVersion = packageInfo.version;
});
}
} catch (e) {
debugPrint('Erreur lors de la récupération de la version: $e');
// Fallback sur la version du AppInfoService si elle existe
if (mounted) {
setState(() {
_appVersion = AppInfoService.fullVersion
.split(' ')
.last; // Extraire juste le numéro
});
}
}
}
@override
void initState() {
@@ -89,33 +104,12 @@ class _SplashPageState extends State<SplashPage>
// Démarrer l'animation immédiatement
_animationController.forward();
// Vérifier si Hive a été réinitialisé
_checkHiveReset();
_getAppVersion();
// Simuler le processus d'initialisation
_startInitialization();
}
// Méthode pour vérifier si Hive a été réinitialisé et afficher le dialogue si nécessaire
void _checkHiveReset() {
// Vérifier si Hive a été réinitialisé et si le dialogue n'a pas encore été affiché
if (hiveResetStateService.wasReset && !hiveResetStateService.dialogShown) {
// Attendre que le widget soit complètement construit
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// Afficher le dialogue de nettoyage du cache
ClearCacheDialog.show(
context,
onClose: () {
// Marquer le dialogue comme ayant été affiché
hiveResetStateService.markDialogAsShown();
},
);
}
});
}
}
@override
void dispose() {
_animationController.dispose();
@@ -123,11 +117,11 @@ class _SplashPageState extends State<SplashPage>
}
void _startInitialization() async {
// Étape 1: Initialisation des services
// Étape 1: Initialisation des boîtes Hive (0% à 75%)
if (mounted) {
setState(() {
_statusMessage = _initializationSteps[0];
_progress = 0.2;
_statusMessage = "Initialisation des données...";
_progress = 0.0;
});
}
@@ -135,20 +129,11 @@ class _SplashPageState extends State<SplashPage>
await _initializeAllHiveBoxes();
await Future.delayed(const Duration(milliseconds: 500));
// Étape 2: Vérification de l'authentification
// Étape 2: Initialisation des services (75% à 100%)
if (mounted) {
setState(() {
_statusMessage = _initializationSteps[1];
_progress = 0.4;
});
}
await Future.delayed(const Duration(milliseconds: 500));
// Étape 3: Chargement des données
if (mounted) {
setState(() {
_statusMessage = _initializationSteps[2];
_progress = 1.0; // Directement à 100% après la 3ème étape
_statusMessage = "Préparation de l'application...";
_progress = 0.75;
});
}
await Future.delayed(const Duration(milliseconds: 500));
@@ -157,15 +142,8 @@ class _SplashPageState extends State<SplashPage>
setState(() {
_isInitializing = false;
_showButtons = true;
_progress = 1.0; // S'assurer que la barre est à 100%
});
// Attendre quelques secondes avant de rediriger automatiquement
// si l'utilisateur est déjà connecté
if (userRepository.isLoggedIn) {
Timer(const Duration(seconds: 2), () {
_redirectToAppropriateScreen();
});
}
}
}
@@ -202,8 +180,8 @@ class _SplashPageState extends State<SplashPage>
},
];
// Calculer l'incrément de progression pour chaque boîte
final progressIncrement = 0.2 / boxesToOpen.length;
// Calculer l'incrément de progression pour chaque boîte (0.75 / nombre de boîtes)
final progressIncrement = 0.75 / boxesToOpen.length;
double currentProgress = 0.0;
// Ouvrir chaque boîte si elle n'est pas déjà ouverte
@@ -212,12 +190,11 @@ class _SplashPageState extends State<SplashPage>
final displayName = boxesToOpen[i]['display'] as String;
// Mettre à jour la barre de progression et le message
currentProgress += progressIncrement;
currentProgress = progressIncrement * (i + 1);
if (mounted) {
setState(() {
_statusMessage = displayName;
_progress =
0.2 * (currentProgress / 0.2); // Normaliser entre 0 et 0.2
_progress = currentProgress;
});
}
@@ -266,10 +243,27 @@ class _SplashPageState extends State<SplashPage>
if (mounted) {
setState(() {
_statusMessage = 'Toutes les boîtes sont prêtes';
_progress = 0.2;
_progress = 0.8;
});
await Future.delayed(const Duration(milliseconds: 500));
}
if (mounted) {
setState(() {
_statusMessage = "Préparation de l'application...";
_progress = 0.9;
});
await Future.delayed(const Duration(milliseconds: 500));
}
// Finalisation
if (mounted) {
setState(() {
_isInitializing = false;
_showButtons = true;
_progress = 1.0;
});
}
debugPrint('Toutes les boîtes Hive sont maintenant ouvertes');
} catch (e) {
debugPrint('Erreur lors de l\'initialisation des boîtes Hive: $e');
@@ -283,33 +277,9 @@ class _SplashPageState extends State<SplashPage>
}
}
void _redirectToAppropriateScreen() {
if (!mounted) return;
// Utiliser l'instance globale de userRepository définie dans app.dart
if (userRepository.isLoggedIn) {
debugPrint('SplashPage: Redirection d\'utilisateur connecté');
// Récupérer directement le rôle utilisateur
final roleValue = userRepository.getUserRole();
debugPrint('SplashPage: Rôle utilisateur = $roleValue');
// Redirection simple basée sur le rôle
if (roleValue > 1) {
debugPrint('SplashPage: Redirection vers /admin (rôle $roleValue > 1)');
context.go('/admin');
} else {
debugPrint('SplashPage: Redirection vers /user (rôle $roleValue = 1)');
context.go('/user');
}
}
// Ne redirige plus vers la landing page
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
@@ -424,7 +394,8 @@ class _SplashPageState extends State<SplashPage>
duration: const Duration(milliseconds: 500),
child: ElevatedButton(
onPressed: () {
context.go('/login', extra: {'type': 'user'});
context.go(
'/login/user'); // Utiliser la route spécifique
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
@@ -449,13 +420,14 @@ class _SplashPageState extends State<SplashPage>
),
const SizedBox(height: 16),
// Bouton Connexion Administrateur
// Bouton Connexion Administrateur
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: ElevatedButton(
onPressed: () {
context.go('/login', extra: {'type': 'admin'});
context.go(
'/login/admin'); // Utiliser la route spécifique
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
@@ -479,18 +451,74 @@ class _SplashPageState extends State<SplashPage>
),
),
const SizedBox(height: 16),
const SizedBox(
height: 32), // 2 espaces sous le bouton précédent
// Lien d'inscription
// Bouton d'inscription
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton(
child: ElevatedButton(
onPressed: () {
context.go('/register');
},
child: Text(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 2,
),
child: const Text(
'Pas encore inscrit ?',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 16),
// Lien vers le site web
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton.icon(
onPressed: () {
// Déterminer l'URL du site web en fonction de l'environnement
String webUrl = 'https://geosector.fr';
if (kIsWeb) {
final host = Uri.base.host;
if (host.startsWith('dapp.')) {
webUrl = 'https://dev.geosector.fr';
} else if (host.startsWith('rapp.')) {
webUrl = 'https://rec.geosector.fr';
} else if (host.startsWith('app.')) {
webUrl = 'https://geosector.fr';
}
}
// Ouvrir l'URL dans une nouvelle fenêtre/onglet
launchUrl(
Uri.parse(webUrl),
mode: LaunchMode.externalApplication,
);
},
icon: Icon(
Icons.language,
size: 18,
color: theme.colorScheme.primary,
),
label: Text(
'Site web Geosector',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
@@ -506,6 +534,38 @@ class _SplashPageState extends State<SplashPage>
),
),
),
// Badge de version en bas à droite
if (_appVersion.isNotEmpty)
Positioned(
bottom: 16,
right: 16,
child: AnimatedOpacity(
opacity: _showButtons ? 0.7 : 0.5,
duration: const Duration(milliseconds: 500),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.primary,
width: 1,
),
),
child: Text(
'v$_appVersion',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
);