ptitspas-ynov/frontend/lib/screens/auth/login_screen.dart

474 lines
17 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:p_tits_pas/services/api/tokenService.dart';
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_app_text_field.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final AuthService _authService = AuthService();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? 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;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
Future<void> _handleLogin() async {
if (_formKey.currentState?.validate() ?? false) {
setState(() {
_isLoading = true;
});
try {
final response = await _authService.login(
_emailController.text.trim(),
_passwordController.text,
);
print('Login response: ${response}');
if (!mounted) return;
// Navigation selon le rôle
final role = await TokenService.getRole();
print('User role: $role');
if (role != null) {
switch (role.toLowerCase()) {
case 'parent':
Navigator.pushReplacementNamed(context, '/parent-dashboard');
break;
case 'assistante_maternelle':
Navigator.pushReplacementNamed(
context, '/assistante_maternelle_dashboard');
break;
case 'super_admin' || 'administrateur':
Navigator.pushReplacementNamed(context, '/admin_dashboard');
break;
case 'gestionnaire':
Navigator.pushReplacementNamed(
context, '/gestionnaire_dashboard');
break;
default:
_showErrorSnackBar('Rôle utilisateur non reconnu: $role');
return;
}
} else {
_showErrorSnackBar('Rôle utilisateur non trouvé');
}
} catch (e) {
print('Login error: $e');
if (!mounted) return;
String errorMessage = e.toString();
String errorString = e.toString();
if (errorString.contains('Failed to login:')) {
// Extraire le message d'erreur réel
errorMessage =
errorString.replaceFirst('Exception: Failed to login: ', '');
}
_showErrorSnackBar(errorMessage);
} finally {
if (mounted) {
setState(() {
_isLoading = false; // AJOUT : Fin du chargement
});
}
}
}
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4), // Plus long pour lire l'erreur
),
);
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(
builder: (context, constraints) {
// Version desktop (web)
if (kIsWeb) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return FutureBuilder(
future: _getImageDimensions(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final imageDimensions = snapshot.data!;
final imageHeight = h;
final imageWidth = imageHeight *
(imageDimensions.width / imageDimensions.height);
final remainingWidth = w - imageWidth;
final leftMargin = remainingWidth / 4;
return Stack(
children: [
// Fond en papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Image principale
Positioned(
left: leftMargin,
top: 0,
height: imageHeight,
width: imageWidth,
child: Image.asset(
'assets/images/river_logo_desktop.png',
fit: BoxFit.contain,
),
),
// Formulaire dans le cadran en bas à droite
Positioned(
right: 0,
bottom: 0,
width: w * 0.6, // 60% de la largeur de l'écran
height: h * 0.5, // 50% de la hauteur de l'écran
child: Padding(
padding: EdgeInsets.all(w * 0.02), // 2% de padding
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Champs côte à côte
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: CustomAppTextField(
controller: _emailController,
labelText: 'Email',
hintText: 'Votre adresse email',
validator: _validateEmail,
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 53,
fieldWidth: double.infinity,
enabled: !_isLoading,
),
),
const SizedBox(width: 20),
Expanded(
child: CustomAppTextField(
controller: _passwordController,
labelText: 'Mot de passe',
hintText: 'Votre mot de passe',
obscureText: true,
validator: _validatePassword,
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 53,
fieldWidth: double.infinity,
enabled: !_isLoading,
),
),
],
),
const SizedBox(height: 20),
// Bouton centré
Center(
child: _isLoading
? const SizedBox(
width: 300,
height: 40,
child: Center(
child: CircularProgressIndicator(),
),
)
: ImageButton(
bg: 'assets/images/btn_green.png',
width: 300,
height: 40,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: _handleLogin,
),
),
const SizedBox(height: 10),
// Lien mot de passe oublié
Center(
child: TextButton(
onPressed: () {
// TODO: Implémenter la logique de récupération de mot de passe
},
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
fontSize: 14,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 10),
// Lien de création de compte
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(
context, '/register-choice');
},
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(
height: 20), // Réduit l'espacement en bas
],
),
),
),
),
// Pied de page
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_FooterLink(
text: 'Contact support',
onTap: () async {
final Uri emailLaunchUri = Uri(
scheme: 'mailto',
path: 'support@supernounou.local',
);
if (await canLaunchUrl(emailLaunchUri)) {
await launchUrl(emailLaunchUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Impossible d\'ouvrir le client mail',
style: GoogleFonts.merienda(),
),
),
);
}
},
),
_FooterLink(
text: 'Signaler un bug',
onTap: () {
_showBugReportDialog(context);
},
),
_FooterLink(
text: 'Mentions légales',
onTap: () {
Navigator.pushNamed(context, '/legal');
},
),
_FooterLink(
text: 'Politique de confidentialité',
onTap: () {
Navigator.pushNamed(context, '/privacy');
},
),
],
),
),
),
],
);
},
);
}
// Version mobile (à implémenter)
return const Center(
child: Text('Version mobile à implémenter'),
);
},
),
);
}
void _showBugReportDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Signaler un bug',
style: GoogleFonts.merienda(),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Décrivez le problème rencontré...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: GoogleFonts.merienda(),
),
),
TextButton(
onPressed: () async {
if (controller.text.trim().isEmpty) {
_showErrorSnackBar('Veuillez décrire le problème');
return;
}
try {
await BugReportService.sendReport(controller.text);
if (context.mounted) {
Navigator.pop(context);
_showSuccessSnackBar('Rapport envoyé avec succès');
}
} catch (e) {
if (context.mounted) {
_showErrorSnackBar('Erreur lors de l\'envoi du rapport');
}
}
},
child: Text(
'Envoyer',
style: GoogleFonts.merienda(),
),
),
],
),
);
}
Future<ImageDimensions> _getImageDimensions() async {
final image = Image.asset('assets/images/river_logo_desktop.png');
final completer = Completer<ImageDimensions>();
image.image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((info, _) {
completer.complete(ImageDimensions(
width: info.image.width.toDouble(),
height: info.image.height.toDouble(),
));
}),
);
return completer.future;
}
}
class ImageDimensions {
final double width;
final double height;
ImageDimensions({required this.width, required this.height});
}
// ───────────────────────────────────────────────────────────────
// Lien du pied de page
// ───────────────────────────────────────────────────────────────
class _FooterLink extends StatelessWidget {
final String text;
final VoidCallback onTap;
const _FooterLink({
required this.text,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: 14,
color: Colors.black87,
decoration: TextDecoration.underline,
),
),
),
);
}
}