Restructuration majeure du projet: migration de flutt vers app, ajout de l'API et mise à jour du site web

This commit is contained in:
d6soft
2025-05-16 09:19:03 +02:00
parent b5aafc424b
commit 5c2620de30
391 changed files with 19780 additions and 7233 deletions

View File

@@ -0,0 +1,904 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
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: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:geosector_app/app.dart'; // Pour accéder aux instances globales
class LoginPage extends StatefulWidget {
final String? loginType;
const LoginPage({super.key, this.loginType});
@override
State<LoginPage> createState() => _LoginPageState();
}
// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _usernameFocusNode = FocusNode();
bool _obscurePassword = true;
// Type de connexion (utilisateur ou administrateur)
late String _loginType;
// État des permissions de géolocalisation
bool _checkingPermission = true;
bool _hasLocationPermission = false;
String? _locationErrorMessage;
// État de la connexion Internet
bool _isConnected = false;
@override
void initState() {
super.initState();
// Vérification du type de connexion
if (widget.loginType == null) {
// Si aucun type n'est spécifié, naviguer vers la splash page
print(
'LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
WidgetsBinding.instance.addPostFrameCallback((_) {
GoRouter.of(context).go('/');
});
_loginType = '';
} else {
_loginType = widget.loginType!;
print('LoginPage: Type de connexion utilisé: $_loginType');
}
// En mode web, essayer de détecter le paramètre dans l'URL directement
// UNIQUEMENT si le loginType n'est pas déjà 'user'
if (kIsWeb && _loginType != 'user') {
try {
final uri = Uri.parse(Uri.base.toString());
// 1. Vérifier d'abord si nous avons déjà le paramètre 'type=user'
final typeParam = uri.queryParameters['type'];
if (typeParam != null && typeParam.trim().toLowerCase() == 'user') {
setState(() {
_loginType = 'user';
});
}
// 2. Sinon, vérifier le fragment d'URL (hash)
else if (uri.fragment.trim().toLowerCase() == 'user') {
setState(() {
_loginType = 'user';
});
}
// 3. Enfin, si toujours pas de type 'user', vérifier le sessionStorage
if (_loginType != 'user') {
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
// Utiliser une approche plus robuste pour accéder au sessionStorage
// Éviter d'utiliser hasProperty qui peut causer des erreurs
final result = js.context.callMethod('eval', [
'''
(function() {
try {
if (window.sessionStorage) {
var value = sessionStorage.getItem('loginType');
return value;
}
return null;
} catch (e) {
console.error('Error accessing sessionStorage:', e);
return null;
}
})()
'''
]);
if (result != null &&
result is String &&
result.toLowerCase() == 'user') {
setState(() {
_loginType = 'user';
print(
'LoginPage: Type détecté depuis sessionStorage: $_loginType');
});
}
} catch (e) {
print('LoginPage: Erreur lors de l\'accès au sessionStorage: $e');
}
});
}
} catch (e) {
print('Erreur lors de la récupération des paramètres d\'URL: $e');
}
}
// Vérifier les permissions de géolocalisation au démarrage seulement sur mobile
if (!kIsWeb) {
_checkLocationPermission();
} else {
// En version web, on considère que les permissions sont accordées
setState(() {
_checkingPermission = false;
_hasLocationPermission = true;
});
}
// Initialiser l'état de la connexion
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isConnected = connectivityService.isConnected;
});
}
});
// 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((_) {
final users = userRepository.getAllUsers();
if (users.isNotEmpty) {
// Trouver l'utilisateur le plus récent (celui avec la date de dernière connexion la plus récente)
users.sort((a, b) => (b.lastSyncedAt).compareTo(a.lastSyncedAt));
final lastUser = users.first;
// Convertir le rôle en int si nécessaire
int roleValue;
if (lastUser.role is String) {
roleValue = int.tryParse(lastUser.role as String) ?? 0;
} else {
roleValue = lastUser.role as int;
}
// Vérifier si le rôle correspond au type de login
bool roleMatches = false;
if (_loginType == 'user' && roleValue == 1) {
roleMatches = true;
debugPrint('Rôle utilisateur (1) correspond au type de login (user)');
} else if (_loginType == 'admin' && roleValue > 1) {
roleMatches = true;
debugPrint(
'Rôle administrateur (${roleValue}) correspond au type de login (admin)');
}
// Pré-remplir le champ username seulement si le rôle correspond
if (roleMatches) {
// Utiliser le username s'il existe, sinon utiliser l'email comme fallback
if (lastUser.username != null && lastUser.username!.isNotEmpty) {
_usernameController.text = lastUser.username!;
// Déplacer le focus sur le champ mot de passe puisque le username est déjà rempli
_usernameFocusNode.unfocus();
debugPrint('Champ username pré-rempli avec: ${lastUser.username}');
} else if (lastUser.email.isNotEmpty) {
_usernameController.text = lastUser.email;
_usernameFocusNode.unfocus();
debugPrint(
'Champ username pré-rempli avec email: ${lastUser.email}');
}
} else {
debugPrint(
'Le rôle (${roleValue}) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
}
}
});
}
/// Vérifie les permissions de géolocalisation
Future<void> _checkLocationPermission() async {
// Ne pas vérifier les permissions en version web
if (kIsWeb) {
setState(() {
_hasLocationPermission = true;
_checkingPermission = false;
});
return;
}
setState(() {
_checkingPermission = true;
});
// Vérifier si les services de localisation sont activés et si l'application a la permission
final hasPermission = await LocationService.checkAndRequestPermission();
final errorMessage = await LocationService.getLocationErrorMessage();
setState(() {
_hasLocationPermission = hasPermission;
_locationErrorMessage = errorMessage;
_checkingPermission = false;
});
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
_usernameFocusNode.dispose();
super.dispose();
}
/// Construit l'écran de chargement pendant la vérification des permissions
Widget _buildLoadingScreen(ThemeData theme) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo simlifié
Image.asset(
'assets/images/logo-geosector-1024.png',
height: 160,
),
const SizedBox(height: 32),
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(
'Vérification des permissions...',
style: theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
/// Construit l'écran de demande de permission de géolocalisation
Widget _buildLocationPermissionScreen(ThemeData theme) {
return Scaffold(
body: Stack(
children: [
// Fond dégradé avec petits points blancs
AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: _loginType == 'user'
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
elevation: 8,
shadowColor: _loginType == 'user'
? Colors.green.withOpacity(0.5)
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo simplifié
Image.asset(
'assets/images/logo-geosector-1024.png',
height: 160,
),
const SizedBox(height: 24),
Text(
'Accès à la localisation requis',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Message d'erreur
Container(
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.location_disabled,
color: theme.colorScheme.error,
size: 48,
),
const SizedBox(height: 16),
Text(
_locationErrorMessage ??
'L\'accès à la localisation est nécessaire pour utiliser cette application.',
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'Cette application utilise votre position pour enregistrer les passages et assurer le suivi des secteurs géographiques. Sans cette permission, l\'application ne peut pas fonctionner correctement.',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
// Instructions pour activer la localisation
Text(
'Comment activer la localisation :',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
_buildInstructionStep(theme, 1,
'Ouvrez les paramètres de votre appareil'),
_buildInstructionStep(theme, 2,
'Accédez aux paramètres de confidentialité ou de localisation'),
_buildInstructionStep(theme, 3,
'Recherchez GEOSECTOR dans la liste des applications'),
_buildInstructionStep(theme, 4,
'Activez l\'accès à la localisation pour cette application'),
const SizedBox(height: 32),
// Boutons d'action
CustomButton(
onPressed: () async {
// Ouvrir les paramètres de l'application
await LocationService.openAppSettings();
},
text: 'Ouvrir les paramètres de l\'application',
icon: Icons.settings,
),
const SizedBox(height: 16),
CustomButton(
onPressed: () async {
// Ouvrir les paramètres de localisation
await LocationService.openLocationSettings();
},
text: 'Ouvrir les paramètres de localisation',
icon: Icons.location_on,
backgroundColor: theme.colorScheme.secondary,
),
const SizedBox(height: 16),
CustomButton(
onPressed: () {
// Vérifier à nouveau les permissions
_checkLocationPermission();
},
text: 'Vérifier à nouveau',
icon: Icons.refresh,
backgroundColor: theme.colorScheme.tertiary,
),
],
),
),
),
),
),
),
),
],
),
);
}
/// Construit une étape d'instruction pour activer la localisation
Widget _buildInstructionStep(
ThemeData theme, int stepNumber, String instruction) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$stepNumber',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
instruction,
style: theme.textTheme.bodyMedium,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
print('DEBUG BUILD: Reconstruction de LoginPage avec type: $_loginType');
// Utiliser l'instance globale de userRepository
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
// Afficher l'écran de permission de géolocalisation si l'utilisateur n'a pas accordé la permission (sauf en version web)
if (!kIsWeb && _checkingPermission) {
return _buildLoadingScreen(theme);
} else if (!kIsWeb && !_hasLocationPermission) {
return _buildLocationPermissionScreen(theme);
}
return Scaffold(
body: Stack(
children: [
// Fond dégradé avec petits points blancs
AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: _loginType == 'user'
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
elevation: 8,
shadowColor: _loginType == 'user'
? Colors.green.withOpacity(0.5)
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo simplifié avec chemin direct
Image.asset(
'assets/images/logo-geosector-1024.png',
height: 140,
),
const SizedBox(height: 24),
Text(
_loginType == 'user'
? 'Connexion Utilisateur'
: 'Connexion Administrateur',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _loginType == 'user'
? Colors.green
: Colors.red,
),
textAlign: TextAlign.center,
),
// Ajouter un texte de débogage uniquement en mode développement
if (kDebugMode)
Text(
'Type de connexion: $_loginType',
style:
TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Bienvenue sur GEOSECTOR',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onBackground
.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Indicateur de connectivité
ConnectivityIndicator(),
const SizedBox(height: 16),
// Formulaire de connexion
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CustomTextField(
controller: _usernameController,
label: 'Identifiant',
hintText: 'Entrez votre identifiant',
prefixIcon: Icons.person_outline,
keyboardType: TextInputType.text,
autofocus: true,
focusNode: _usernameFocusNode,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre identifiant';
}
return null;
},
),
const SizedBox(height: 16),
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Entrez votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
return null;
},
onFieldSubmitted: (_) async {
if (!userRepository.isLoading &&
_formKey.currentState!.validate()) {
// Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) {
print(
'Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/');
return;
}
print(
'Login: Tentative avec type: $_loginType');
final success =
await userRepository.login(
_usernameController.text.trim(),
_passwordController.text,
type: _loginType,
);
if (success && mounted) {
// Récupérer directement le rôle de l'utilisateur
final user =
userRepository.getCurrentUser();
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
return;
}
// Convertir le rôle en int si nécessaire
int roleValue;
if (user.role is String) {
roleValue = int.tryParse(
user.role as String) ??
1;
} else {
roleValue = user.role as int;
}
debugPrint(
'Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
context.go('/admin');
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
context.go('/user');
}
} else if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red,
),
);
}
}
},
),
const SizedBox(height: 8),
// Mot de passe oublié
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Naviguer vers la page de récupération de mot de passe
},
child: Text(
'Mot de passe oublié ?',
style: TextStyle(
color: theme.colorScheme.primary,
),
),
),
),
const SizedBox(height: 24),
// Bouton de connexion
CustomButton(
onPressed: (userRepository.isLoading ||
!_isConnected)
? null
: () async {
if (_formKey.currentState!
.validate()) {
// Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web)
if (!kIsWeb) {
await _checkLocationPermission();
// Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer
if (!_hasLocationPermission) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'L\'accès à la localisation est nécessaire pour utiliser cette application.'),
backgroundColor: Colors.red,
),
);
return;
}
}
// Vérifier la connexion Internet
await connectivityService
.checkConnectivity();
if (!connectivityService
.isConnected) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
backgroundColor:
theme.colorScheme.error,
duration: const Duration(
seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
),
);
}
},
),
),
);
return;
}
// Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) {
print(
'Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/');
return;
}
print(
'Login: Tentative avec type: $_loginType');
// Utiliser directement userRepository avec l'overlay de chargement
final success = await userRepository
.loginWithUI(
context,
_usernameController.text.trim(),
_passwordController.text,
type: _loginType,
);
if (success && mounted) {
debugPrint(
'Connexion réussie, tentative de redirection...');
// Récupérer directement le rôle de l'utilisateur
final user = userRepository
.getCurrentUser();
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
return;
}
// Convertir le rôle en int si nécessaire
int roleValue;
if (user.role is String) {
roleValue = int.tryParse(
user.role as String) ??
1;
} else {
roleValue = user.role as int;
}
debugPrint(
'Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
context.go('/admin');
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
context.go('/user');
}
} else if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red,
),
);
}
}
},
text: _isConnected
? 'Se connecter'
: 'Connexion Internet requise',
isLoading: userRepository.isLoading,
),
const SizedBox(height: 24),
// Inscription administrateur uniquement
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Pas encore de compte ?',
style: theme.textTheme.bodyMedium,
),
TextButton(
onPressed: () {
context.go('/register');
},
child: Text(
'Inscription Administrateur',
style: TextStyle(
color: theme.colorScheme.tertiary,
fontWeight: FontWeight.bold,
),
),
),
],
),
// Lien vers la page d'accueil
TextButton(
onPressed: () {
context.go('/');
},
child: Text(
'Retour à l\'accueil',
style: TextStyle(
color: theme.colorScheme.secondary,
),
),
),
],
),
),
],
),
),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,316 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:geosector_app/core/repositories/user_repository.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/connectivity_service.dart';
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _amicaleNameController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityNameController = TextEditingController();
final _emailController = TextEditingController();
// État de la connexion Internet
bool _isConnected = false;
@override
void initState() {
super.initState();
// Initialiser l'état de la connexion
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// Utiliser l'instance globale de connectivityService définie dans app.dart
setState(() {
_isConnected = connectivityService.isConnected;
});
}
});
}
@override
void dispose() {
_nameController.dispose();
_amicaleNameController.dispose();
_postalCodeController.dispose();
_cityNameController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Utiliser l'instance globale de userRepository définie dans app.dart
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo et titre
SvgPicture.asset(
'assets/images/icon-geosector.svg',
height: 140,
fit: BoxFit.contain,
),
const SizedBox(height: 16),
Text(
'Inscription Administrateur',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Enregistrez votre amicale sur GeoSector',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onBackground.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Indicateur de connectivité
ConnectivityIndicator(
onConnectivityChanged: (isConnected) {
if (mounted && _isConnected != isConnected) {
setState(() {
_isConnected = isConnected;
});
}
},
),
const SizedBox(height: 16),
// Formulaire d'inscription
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CustomTextField(
controller: _nameController,
label: 'Nom complet',
hintText: 'Entrez votre nom complet',
prefixIcon: Icons.person_outline,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom complet';
}
return null;
},
),
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;
},
),
const SizedBox(height: 16),
CustomTextField(
controller: _amicaleNameController,
label: 'Nom de l\'amicale',
hintText: 'Entrez le nom de votre amicale',
prefixIcon: Icons.local_fire_department,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer le nom de votre amicale';
}
return null;
},
),
const SizedBox(height: 16),
CustomTextField(
controller: _postalCodeController,
label: 'Code postal de l\'amicale',
hintText: 'Entrez le code postal de votre amicale',
prefixIcon: Icons.location_on_outlined,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre code postal';
}
if (!RegExp(r'^[0-9]{5}$').hasMatch(value)) {
return 'Le code postal doit contenir 5 chiffres';
}
return null;
},
),
const SizedBox(height: 16),
CustomTextField(
controller: _cityNameController,
label: 'Commune de l\'amicale',
hintText:
'Entrez le nom de la commune de votre amicale',
prefixIcon: Icons.location_city_outlined,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer le nom de la commune de votre amicale';
}
return null;
},
),
const SizedBox(height: 16),
const SizedBox(height: 32),
// Bouton d'inscription
CustomButton(
onPressed: (userRepository.isLoading || !_isConnected)
? null
: () async {
if (_formKey.currentState!.validate()) {
// Vérifier la connexion Internet avant de soumettre
// Utiliser l'instance globale de connectivityService définie dans app.dart
await connectivityService
.checkConnectivity();
if (!connectivityService.isConnected) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
backgroundColor:
theme.colorScheme.error,
duration:
const Duration(seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
),
);
}
},
),
),
);
}
return;
}
final success =
await userRepository.register(
_emailController.text.trim(),
'', // Mot de passe vide, sera généré par le serveur
_nameController.text.trim(),
_amicaleNameController.text.trim(),
_postalCodeController.text,
_cityNameController.text.trim(),
);
if (success && mounted) {
context.go('/user');
} else if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Échec de l\'inscription. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
}
}
},
text: _isConnected
? 'Enregistrer mon amicale'
: 'Connexion Internet requise',
isLoading: userRepository.isLoading,
),
const SizedBox(height: 24),
// Déjà un compte
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Déjà un compte ?',
style: theme.textTheme.bodyMedium,
),
TextButton(
onPressed: () {
context.go('/login');
},
child: Text(
'Se connecter',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
// Lien vers la page d'accueil
TextButton(
onPressed: () {
context.go('/');
},
child: Text(
'Revenir à l\'accueil',
style: TextStyle(
color: theme.colorScheme.secondary,
),
),
),
],
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,513 @@
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/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/client_model.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
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;
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<SplashPage> createState() => _SplashPageState();
}
// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class _SplashPageState extends State<SplashPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
bool _isInitializing = true;
String _statusMessage = "Initialisation...";
double _progress = 0.0;
bool _showButtons = false;
final List<String> _initializationSteps = [
"Initialisation des services...",
"Vérification de l'authentification...",
"Chargement des données..."
];
@override
void initState() {
super.initState();
// Animation controller sur 5 secondes (augmenté de 3 à 5 secondes)
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
);
// Animation de 4x la taille à 1x la taille (augmenté de 3x à 4x)
_scaleAnimation = Tween<double>(
begin: 4.0, // Commencer à 4x la taille
end: 1.0, // Terminer à la taille normale
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack, // Curve pour un effet de rebond
),
);
// Démarrer l'animation immédiatement
_animationController.forward();
// Vérifier si Hive a été réinitialisé
_checkHiveReset();
// 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();
super.dispose();
}
void _startInitialization() async {
// Étape 1: Initialisation des services
if (mounted) {
setState(() {
_statusMessage = _initializationSteps[0];
_progress = 0.2;
});
}
// Initialiser toutes les boîtes Hive
await _initializeAllHiveBoxes();
await Future.delayed(const Duration(milliseconds: 500));
// Étape 2: Vérification de l'authentification
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
});
}
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() {
_isInitializing = false;
_showButtons = true;
});
// Attendre quelques secondes avant de rediriger automatiquement
// si l'utilisateur est déjà connecté
if (userRepository.isLoggedIn) {
Timer(const Duration(seconds: 2), () {
_redirectToAppropriateScreen();
});
}
}
}
// Méthode pour initialiser toutes les boîtes Hive
Future<void> _initializeAllHiveBoxes() async {
try {
debugPrint('Initialisation de toutes les boîtes Hive...');
// Structure pour les boîtes à ouvrir avec leurs noms d'affichage
final boxesToOpen = [
{'name': AppKeys.usersBoxName, 'display': 'Préparation utilisateurs'},
{'name': AppKeys.amicaleBoxName, 'display': 'Préparation amicale'},
{'name': AppKeys.clientsBoxName, 'display': 'Préparation clients'},
{'name': AppKeys.regionsBoxName, 'display': 'Préparation régions'},
{
'name': AppKeys.operationsBoxName,
'display': 'Préparation opérations'
},
{'name': AppKeys.sectorsBoxName, 'display': 'Préparation secteurs'},
{'name': AppKeys.passagesBoxName, 'display': 'Préparation passages'},
{'name': AppKeys.membresBoxName, 'display': 'Préparation membres'},
{
'name': AppKeys.userSectorBoxName,
'display': 'Préparation secteurs utilisateurs'
},
{'name': AppKeys.settingsBoxName, 'display': 'Préparation paramètres'},
{
'name': AppKeys.chatConversationsBoxName,
'display': 'Préparation conversations'
},
{
'name': AppKeys.chatMessagesBoxName,
'display': 'Préparation messages'
},
];
// Calculer l'incrément de progression pour chaque boîte
final progressIncrement = 0.2 / boxesToOpen.length;
double currentProgress = 0.0;
// Ouvrir chaque boîte si elle n'est pas déjà ouverte
for (int i = 0; i < boxesToOpen.length; i++) {
final boxName = boxesToOpen[i]['name'] as String;
final displayName = boxesToOpen[i]['display'] as String;
// Mettre à jour la barre de progression et le message
currentProgress += progressIncrement;
if (mounted) {
setState(() {
_statusMessage = displayName;
_progress =
0.2 * (currentProgress / 0.2); // Normaliser entre 0 et 0.2
});
}
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName ($displayName)...');
// Ouvrir la boîte avec le type approprié
if (boxName == AppKeys.usersBoxName) {
await Hive.openBox<UserModel>(boxName);
} else if (boxName == AppKeys.amicaleBoxName) {
await Hive.openBox<AmicaleModel>(boxName);
} else if (boxName == AppKeys.clientsBoxName) {
await Hive.openBox<ClientModel>(boxName);
} else if (boxName == AppKeys.regionsBoxName) {
// Ouvrir la boîte des régions sans type spécifique pour l'instant
// car RegionModelAdapter n'est pas encore enregistré
await Hive.openBox(boxName);
} else if (boxName == AppKeys.operationsBoxName) {
await Hive.openBox<OperationModel>(boxName);
} else if (boxName == AppKeys.sectorsBoxName) {
await Hive.openBox<SectorModel>(boxName);
} else if (boxName == AppKeys.passagesBoxName) {
await Hive.openBox<PassageModel>(boxName);
} else if (boxName == AppKeys.membresBoxName) {
await Hive.openBox<MembreModel>(boxName);
} else if (boxName == AppKeys.userSectorBoxName) {
await Hive.openBox<UserSectorModel>(boxName);
} else if (boxName == AppKeys.chatConversationsBoxName) {
await Hive.openBox<ConversationModel>(boxName);
} else if (boxName == AppKeys.chatMessagesBoxName) {
await Hive.openBox<MessageModel>(boxName);
} else {
await Hive.openBox(boxName);
}
debugPrint('Boîte $boxName ouverte avec succès');
} else {
debugPrint('Boîte $boxName déjà ouverte');
}
// Ajouter une temporisation entre chaque ouverture
await Future.delayed(const Duration(milliseconds: 500));
}
// Mettre à jour la barre de progression à 0.2 (20%) à la fin
if (mounted) {
setState(() {
_statusMessage = 'Toutes les boîtes sont prêtes';
_progress = 0.2;
});
}
debugPrint('Toutes les boîtes Hive sont maintenant ouvertes');
} catch (e) {
debugPrint('Erreur lors de l\'initialisation des boîtes Hive: $e');
// En cas d'erreur, mettre à jour le message
if (mounted) {
setState(() {
_statusMessage = 'Erreur lors de l\'initialisation des données';
});
}
}
}
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(
children: [
// Fond dégradé avec petits points blancs
AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.blue.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
// Contenu principal
SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(flex: 2),
// Logo avec animation de réduction
AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: Image.asset(
'assets/images/logo-geosector-1024.png',
height: 180, // Augmenté de 140 à 180
),
),
const SizedBox(height: 24),
// Titre avec animation fade-in
AnimatedOpacity(
opacity: _isInitializing ? 0.9 : 1.0,
duration: const Duration(milliseconds: 500),
child: Text(
'Geosector',
style: theme.textTheme.headlineLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 16),
// Sous-titre avec nouveau slogan
AnimatedOpacity(
opacity: _isInitializing ? 0.8 : 1.0,
duration: const Duration(milliseconds: 500),
child: Text(
'Une application puissante et intuitive de gestion de vos distributions de calendriers',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color:
theme.colorScheme.onBackground.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
),
const Spacer(flex: 1),
// Indicateur de chargement
if (_isInitializing) ...[
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,
),
minHeight: 10, // Augmenté de 6 à 10
),
),
),
const SizedBox(height: 16),
Text(
_statusMessage,
style: theme.textTheme.bodyMedium?.copyWith(
color:
theme.colorScheme.onBackground.withOpacity(0.7),
),
),
],
// Boutons après l'initialisation
if (_showButtons) ...[
// Bouton Connexion Utilisateur
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: ElevatedButton(
onPressed: () {
context.go('/login', extra: {'type': 'user'});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 2,
),
child: const Text(
'Connexion Utilisateur',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 16),
// Bouton Connexion Administrateur
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: ElevatedButton(
onPressed: () {
context.go('/login', extra: {'type': 'admin'});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 2,
),
child: const Text(
'Connexion Administrateur',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 16),
// Lien d'inscription
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton(
onPressed: () {
context.go('/register');
},
child: Text(
'Pas encore inscrit ?',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
),
],
const Spacer(flex: 1),
],
),
),
),
),
],
),
);
}
}