641 lines
27 KiB
Dart
641 lines
27 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:go_router/src/state.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/core/services/auth_service.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 _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)
|
|
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();
|
|
|
|
// Récupérer le type de connexion depuis les paramètres du widget
|
|
_loginType = widget.loginType ?? 'admin'; // Par défaut admin
|
|
print('DEBUG: LoginType initial depuis widget: $_loginType');
|
|
|
|
// Vérifier explicitement si le type est 'user'
|
|
if (_loginType != null && _loginType!.trim().toLowerCase() == 'user') {
|
|
_loginType = 'user';
|
|
print('DEBUG: LoginType confirmé comme user');
|
|
} else {
|
|
_loginType = 'admin';
|
|
print('DEBUG: LoginType confirmé comme admin');
|
|
}
|
|
|
|
// 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é
|
|
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;
|
|
|
|
// 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();
|
|
} else if (lastUser.email.isNotEmpty) {
|
|
_usernameController.text = lastUser.email;
|
|
_usernameFocusNode.unfocus();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 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: [
|
|
Image.asset(
|
|
'assets/images/geosector-logo-200.png',
|
|
height: 140,
|
|
fit: BoxFit.contain,
|
|
),
|
|
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: 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
|
|
Image.asset(
|
|
'assets/images/geosector-logo-200.png',
|
|
height: 140,
|
|
fit: BoxFit.contain,
|
|
),
|
|
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: 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
|
|
Image.asset(
|
|
'assets/images/geosector-logo-200.png',
|
|
height: 140,
|
|
fit: BoxFit.contain,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
(_loginType != null &&
|
|
_loginType!.trim().toLowerCase() == 'user')
|
|
? 'Connexion Utilisateur'
|
|
: 'Connexion Administrateur',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: (_loginType != null &&
|
|
_loginType!.trim().toLowerCase() == 'user')
|
|
? Colors.green
|
|
: Colors.red,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
// Ajouter un texte de débogage
|
|
Text(
|
|
'Type de connexion détecté: $_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()) {
|
|
// S'assurer que le type est toujours défini
|
|
final loginType = _loginType ?? 'admin';
|
|
final actualType =
|
|
(loginType.trim().toLowerCase() == 'user')
|
|
? 'user'
|
|
: 'admin';
|
|
print('DEBUG: Login avec type: $actualType');
|
|
|
|
final success = await userRepository.login(
|
|
_usernameController.text.trim(),
|
|
_passwordController.text,
|
|
type: actualType,
|
|
);
|
|
|
|
if (success && mounted) {
|
|
if (userRepository.isAdmin()) {
|
|
context.go('/admin');
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
// S'assurer que le type est toujours défini
|
|
final loginType = _loginType ?? 'admin';
|
|
final actualType =
|
|
(loginType.trim().toLowerCase() ==
|
|
'user')
|
|
? 'user'
|
|
: 'admin';
|
|
print(
|
|
'DEBUG: Login bouton avec type: $actualType');
|
|
|
|
// Utiliser le service d'authentification avec l'overlay de chargement
|
|
final authService =
|
|
AuthService(userRepository);
|
|
final success = await authService.login(
|
|
context,
|
|
_usernameController.text.trim(),
|
|
_passwordController.text,
|
|
type: actualType,
|
|
);
|
|
|
|
if (success && mounted) {
|
|
if (userRepository.isAdmin()) {
|
|
context.go('/admin');
|
|
} else {
|
|
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 publique
|
|
TextButton(
|
|
onPressed: () {
|
|
context.go('/public');
|
|
},
|
|
child: Text(
|
|
'Retour au site GEOSECTOR',
|
|
style: TextStyle(
|
|
color: theme.colorScheme.secondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|