From 9fd6cb7b76ca96b893484fcf4e017c8350ec675c Mon Sep 17 00:00:00 2001 From: Hanim Date: Wed, 3 Sep 2025 11:03:53 +0200 Subject: [PATCH 1/2] feat: Implement authentication service and login handling with role-based navigation --- .../plugins/GeneratedPluginRegistrant.java | 5 ++ .../creation/gestionnaires_create.dart | 17 +++++ frontend/lib/screens/auth/login_screen.dart | 48 ++++++++++-- frontend/lib/services/api/api_config.dart | 21 ++++++ frontend/lib/services/auth_service.dart | 75 ++++++++++++++++++- .../services/login_navigation_service.dart | 20 +++++ frontend/pubspec.lock | 60 ++++++++++++++- frontend/pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart create mode 100644 frontend/lib/services/api/api_config.dart create mode 100644 frontend/lib/services/login_navigation_service.dart diff --git a/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 2ecdf29..86acc23 100644 --- a/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -20,6 +20,11 @@ public final class GeneratedPluginRegistrant { } catch (Exception e) { Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); } + try { + flutterEngine.getPlugins().add(new com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_secure_storage, com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin()); } catch (Exception e) { diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart new file mode 100644 index 0000000..de00a86 --- /dev/null +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class GestionnairesCreate extends StatelessWidget { + const GestionnairesCreate({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Créer un gestionnaire'), + ), + body: const Center( + child: Text('Formulaire de création de gestionnaire'), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index f6c8a8f..17dbc69 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -2,6 +2,7 @@ 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/auth_service.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:p_tits_pas/services/bug_report_service.dart'; import 'package:go_router/go_router.dart'; @@ -19,6 +20,7 @@ class _LoginPageState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); + final AuthService _authService = AuthService(); @override void dispose() { @@ -47,6 +49,46 @@ class _LoginPageState extends State { return null; } + Future _handleLogin() async { + if (_formKey.currentState?.validate() ?? false) { + try { + final response = await _authService.login( + _emailController.text, + _passwordController.text, + ); + + if (!mounted) return; + + // Navigation selon le rôle + switch (response.role.toLowerCase()) { + case 'parent': + Navigator.pushReplacementNamed(context, '/parent-dashboard'); + break; + case 'assistante_maternelle': + Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard'); + break; + case 'admin': + Navigator.pushReplacementNamed(context, '/admin_dashboard'); + break; + case 'gestionnaire': + Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard'); + break; + default: + Navigator.pushReplacementNamed(context, '/home'); + } + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Échec de la connexion. Vérifiez vos identifiants.'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -144,11 +186,7 @@ class _LoginPageState extends State { height: 40, text: 'Se connecter', textColor: const Color(0xFF2D6A4F), - onPressed: () { - if (_formKey.currentState?.validate() ?? false) { - // TODO: Implémenter la logique de connexion - } - }, + onPressed: _handleLogin, ), ), const SizedBox(height: 10), diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart new file mode 100644 index 0000000..f6be021 --- /dev/null +++ b/frontend/lib/services/api/api_config.dart @@ -0,0 +1,21 @@ +class ApiConfig { + // static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1'; + static const String baseUrl = 'http://localhost:3000/api/v1'; + + // Auth endpoints + static const String login = '/auth/login'; + static const String register = '/auth/register'; + static const String refreshToken = '/auth/refresh'; + + // Users endpoints + static const String users = '/users'; + static const String userProfile = '/users/profile'; + static const String userChildren = '/users/children'; + + // Dashboard endpoints + static const String dashboard = '/dashboard'; + static const String events = '/events'; + static const String contracts = '/contracts'; + static const String conversations = '/conversations'; + static const String notifications = '/notifications'; +} \ No newline at end of file diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index 5234c8e..bbfb1ec 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -1,9 +1,78 @@ import 'dart:convert'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:p_tits_pas/services/api/api_config.dart'; import '../models/user.dart'; +import 'package:http/http.dart' as http; + + +class AuthResponse { + final String acessToken; + final String role; + + AuthResponse({required this.acessToken, required this.role}); + + factory AuthResponse.fromJson(Map json) { + return AuthResponse( + acessToken: json['acessToken'], + role: json['role'], + ); + } +} class AuthService { - static const String _usersKey = 'users'; + ApiConfig apiConfig = ApiConfig(); + String baseUrl = ApiConfig.baseUrl; + final storage = const FlutterSecureStorage(); + + //login + Future login(String email, String password) async { + final response = await http.post( + Uri.parse('$baseUrl${ApiConfig.login}'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'email': email, 'password': password}), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final authResponse = AuthResponse.fromJson(data); + + await storage.write(key: 'access_token', value: authResponse.acessToken); + await storage.write(key: 'role', value: authResponse.role); + return authResponse; + } else { + throw Exception('Failed to login'); + } + } + + //register + Future register({ + required String email, + required String password, + required String firstName, + required String lastName, + required String role, + }) async { + final response = await http.post( + Uri.parse('$baseUrl${ApiConfig.register}'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'email': email, + 'password': password, + 'firstName': firstName, + 'lastName': lastName, + 'role': role, + }), + ); + + if (response.statusCode == 201) { + final data = jsonDecode(response.body); + return AppUser.fromJson(data['user']); + } else { + throw Exception('Failed to register'); + } + } + + /*static const String _usersKey = 'users'; static const String _parentsKey = 'parents'; static const String _childrenKey = 'children'; @@ -38,5 +107,5 @@ class AuthService { // Méthode pour récupérer l'utilisateur connecté (mode démonstration) static Future getCurrentUser() async { return null; // Aucun utilisateur en mode démonstration - } + }*/ } \ No newline at end of file diff --git a/frontend/lib/services/login_navigation_service.dart b/frontend/lib/services/login_navigation_service.dart new file mode 100644 index 0000000..6b6ca24 --- /dev/null +++ b/frontend/lib/services/login_navigation_service.dart @@ -0,0 +1,20 @@ +import 'package:flutter/cupertino.dart'; + +class NavigationService { + static void handleLoginSuccess(BuildContext context, String role) { + switch (role) { + case 'admin': + Navigator.pushReplacementNamed(context, '/admin_dashboard'); + break; + case 'gestionnaire': + Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard'); + break; + case 'parent': + Navigator.pushReplacementNamed(context, '/parent-dashboard'); + break; + case 'assistante_maternelle': + Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard'); + break; + } + } +} \ No newline at end of file diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 9d0bbd5..81bd6a6 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -139,6 +139,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -169,10 +217,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" http_parser: dependency: transitive description: @@ -626,6 +674,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" xdg_directories: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 1a73bb5..d0533dc 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -18,7 +18,8 @@ dependencies: image_picker: ^1.0.7 js: ^0.6.7 url_launcher: ^6.2.4 - http: ^1.2.0 + http: ^1.5.0 + flutter_secure_storage: ^9.2.4 dev_dependencies: flutter_test: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 043a96f..602b168 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index a95e267..b918cf8 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + flutter_secure_storage_windows url_launcher_windows ) -- 2.47.2 From e2e38076aadd2103aeb3f2bb91de1d551b84bfd2 Mon Sep 17 00:00:00 2001 From: Hanim Date: Fri, 12 Sep 2025 15:24:52 +0200 Subject: [PATCH 2/2] Add routes navigation login and admin dashboard --- frontend/lib/navigation/app_router.dart | 5 + .../admin_dashboardScreen.dart | 66 ++++++++ frontend/lib/screens/auth/login_screen.dart | 148 +++++++++++------- frontend/lib/services/api/api_config.dart | 15 +- frontend/lib/services/api/tokenService.dart | 72 +++++++++ frontend/lib/services/auth_service.dart | 104 ++++++++---- ...sistante_maternelle_management_widget.dart | 106 +++++++++++++ .../lib/widgets/admin/dashboard_admin.dart | 145 +++++++++++++++++ .../lib/widgets/admin/gestionnaire_card.dart | 75 +++++++++ .../admin/gestionnaire_management_widget.dart | 54 +++++++ .../admin/parent_managmant_widget.dart | 121 ++++++++++++++ 11 files changed, 821 insertions(+), 90 deletions(-) create mode 100644 frontend/lib/screens/administrateurs/admin_dashboardScreen.dart create mode 100644 frontend/lib/services/api/tokenService.dart create mode 100644 frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart create mode 100644 frontend/lib/widgets/admin/dashboard_admin.dart create mode 100644 frontend/lib/widgets/admin/gestionnaire_card.dart create mode 100644 frontend/lib/widgets/admin/gestionnaire_management_widget.dart create mode 100644 frontend/lib/widgets/admin/parent_managmant_widget.dart diff --git a/frontend/lib/navigation/app_router.dart b/frontend/lib/navigation/app_router.dart index a5a9632..4499f0d 100644 --- a/frontend/lib/navigation/app_router.dart +++ b/frontend/lib/navigation/app_router.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/am_user_registration_data.dart'; +import 'package:p_tits_pas/screens/administrateurs/admin_dashboardScreen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart'; @@ -33,6 +34,7 @@ class AppRouter { static const String amRegisterStep3 = '/am-register/step3'; static const String amRegisterStep4 = '/am-register/step4'; static const String parentDashboard = '/parent-dashboard'; + static const String admin_dashboard = '/admin_dashboard'; static const String findNanny = '/find-nanny'; static Route generateRoute(RouteSettings settings) { @@ -128,6 +130,9 @@ class AppRouter { case parentDashboard: screen = const ParentDashboardScreen(); break; + case admin_dashboard: + screen = const AdminDashboardScreen(); + break; case findNanny: screen = const FindNannyScreen(); break; diff --git a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart new file mode 100644 index 0000000..d10062e --- /dev/null +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; +import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; +import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; +import 'package:p_tits_pas/widgets/app_footer.dart'; +import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; + +class AdminDashboardScreen extends StatefulWidget { + const AdminDashboardScreen({super.key}); + + @override + _AdminDashboardScreenState createState() => _AdminDashboardScreenState(); +} + +class _AdminDashboardScreenState extends State { + int selectedIndex = 0; + + void onTabChange(int index) { + setState(() { + selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60.0), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey.shade300), + ), + ), + child: DashboardAppBarAdmin( + selectedIndex: selectedIndex, + onTabChange: onTabChange, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: _getBody(), + ), + const AppFooter(), + ], + ), + ); + } + + Widget _getBody() { + switch (selectedIndex) { + case 0: + return const GestionnaireManagementWidget(); + case 1: + return const ParentManagementWidget(); + case 2: + return const AssistanteMaternelleManagementWidget(); + case 3: + return const Center(child: Text("👨‍💼 Administrateurs")); + default: + return const Center(child: Text("Page non trouvée")); + } + } +} diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index 17dbc69..6527fd9 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -2,10 +2,10 @@ 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 'package:go_router/go_router.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_app_text_field.dart'; @@ -21,6 +21,7 @@ class _LoginPageState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final AuthService _authService = AuthService(); + bool _isLoading = false; @override void dispose() { @@ -51,44 +52,87 @@ class _LoginPageState extends State { Future _handleLogin() async { if (_formKey.currentState?.validate() ?? false) { + setState(() { + _isLoading = true; + }); + try { final response = await _authService.login( - _emailController.text, + _emailController.text.trim(), _passwordController.text, ); + print('Login response: ${response}'); if (!mounted) return; // Navigation selon le rôle - switch (response.role.toLowerCase()) { - case 'parent': - Navigator.pushReplacementNamed(context, '/parent-dashboard'); - break; - case 'assistante_maternelle': - Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard'); - break; - case 'admin': - Navigator.pushReplacementNamed(context, '/admin_dashboard'); - break; - case 'gestionnaire': - Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard'); - break; - default: - Navigator.pushReplacementNamed(context, '/home'); + 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: ', ''); + } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Échec de la connexion. Vérifiez vos identifiants.'), - backgroundColor: Colors.red, - ), - ); + _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( @@ -109,7 +153,8 @@ class _LoginPageState extends State { final imageDimensions = snapshot.data!; final imageHeight = h; - final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height); + final imageWidth = imageHeight * + (imageDimensions.width / imageDimensions.height); final remainingWidth = w - imageWidth; final leftMargin = remainingWidth / 4; @@ -138,10 +183,10 @@ class _LoginPageState extends State { 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 + 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 + padding: EdgeInsets.all(w * 0.02), // 2% de padding child: Form( key: _formKey, child: Column( @@ -160,6 +205,7 @@ class _LoginPageState extends State { style: CustomAppTextFieldStyle.lavande, fieldHeight: 53, fieldWidth: double.infinity, + enabled: !_isLoading, ), ), const SizedBox(width: 20), @@ -173,6 +219,7 @@ class _LoginPageState extends State { style: CustomAppTextFieldStyle.jaune, fieldHeight: 53, fieldWidth: double.infinity, + enabled: !_isLoading, ), ), ], @@ -180,7 +227,15 @@ class _LoginPageState extends State { const SizedBox(height: 20), // Bouton centré Center( - child: ImageButton( + child: _isLoading + ? const SizedBox( + width: 300, + height: 40, + child: Center( + child: CircularProgressIndicator(), + ), + ) + : ImageButton( bg: 'assets/images/btn_green.png', width: 300, height: 40, @@ -211,7 +266,8 @@ class _LoginPageState extends State { Center( child: TextButton( onPressed: () { - Navigator.pushNamed(context, '/register-choice'); + Navigator.pushNamed( + context, '/register-choice'); }, child: Text( 'Créer un compte', @@ -223,7 +279,8 @@ class _LoginPageState extends State { ), ), ), - const SizedBox(height: 20), // Réduit l'espacement en bas + const SizedBox( + height: 20), // Réduit l'espacement en bas ], ), ), @@ -290,7 +347,7 @@ class _LoginPageState extends State { }, ); } - + // Version mobile (à implémenter) return const Center( child: Text('Version mobile à implémenter'), @@ -336,14 +393,7 @@ class _LoginPageState extends State { TextButton( onPressed: () async { if (controller.text.trim().isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Veuillez décrire le problème', - style: GoogleFonts.merienda(), - ), - ), - ); + _showErrorSnackBar('Veuillez décrire le problème'); return; } @@ -351,25 +401,11 @@ class _LoginPageState extends State { await BugReportService.sendReport(controller.text); if (context.mounted) { Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Rapport envoyé avec succès', - style: GoogleFonts.merienda(), - ), - ), - ); + _showSuccessSnackBar('Rapport envoyé avec succès'); } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Erreur lors de l\'envoi du rapport', - style: GoogleFonts.merienda(), - ), - ), - ); + _showErrorSnackBar('Erreur lors de l\'envoi du rapport'); } } }, @@ -434,4 +470,4 @@ class _FooterLink extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart index f6be021..700673e 100644 --- a/frontend/lib/services/api/api_config.dart +++ b/frontend/lib/services/api/api_config.dart @@ -1,6 +1,6 @@ class ApiConfig { - // static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1'; - static const String baseUrl = 'http://localhost:3000/api/v1'; + // static const String baseUrl = 'http://localhost:3000/api/v1/'; + static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1'; // Auth endpoints static const String login = '/auth/login'; @@ -18,4 +18,15 @@ class ApiConfig { static const String contracts = '/contracts'; static const String conversations = '/conversations'; static const String notifications = '/notifications'; + + // Headers + static Map get headers => { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + static Map authHeaders(String token) => { + ...headers, + 'Authorization': 'Bearer $token', + }; } \ No newline at end of file diff --git a/frontend/lib/services/api/tokenService.dart b/frontend/lib/services/api/tokenService.dart new file mode 100644 index 0000000..9ead7f7 --- /dev/null +++ b/frontend/lib/services/api/tokenService.dart @@ -0,0 +1,72 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class TokenService { + static const _storage = FlutterSecureStorage(); + static const _tokenKey = 'access_token'; + static const String _refreshTokenKey = 'refresh_token'; + static const _roleKey = 'user_role'; + + // Stockage du token + static Future saveToken(String token) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, token); + } + + // Stockage du refresh token + static Future saveRefreshToken(String refreshToken) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_refreshTokenKey, refreshToken); + } + + // Stockage du rôle + static Future saveRole(String role) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_roleKey, role); + } + + // Récupération du token + static Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_tokenKey); + } + + // Récupération du refresh token + static Future getRefreshToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_refreshTokenKey); + } + + // Récupération du rôle + static Future getRole() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_roleKey); + } + + // Suppression du token + static Future deleteToken() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + } + + // Suppression du refresh token + static Future deleteRefreshToken() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_refreshTokenKey); + } + + + // Suppression du rôle + static Future deleteRole() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_roleKey); + } + + // Nettoyage complet + static Future clearAll() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + await prefs.remove(_refreshTokenKey); + await prefs.remove(_roleKey); + } +} \ No newline at end of file diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index bbfb1ec..f2b87a6 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -1,46 +1,86 @@ import 'dart:convert'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:p_tits_pas/services/api/api_config.dart'; +import 'package:p_tits_pas/services/api/tokenService.dart'; import '../models/user.dart'; import 'package:http/http.dart' as http; -class AuthResponse { - final String acessToken; - final String role; - - AuthResponse({required this.acessToken, required this.role}); - - factory AuthResponse.fromJson(Map json) { - return AuthResponse( - acessToken: json['acessToken'], - role: json['role'], - ); - } -} - class AuthService { - ApiConfig apiConfig = ApiConfig(); - String baseUrl = ApiConfig.baseUrl; - final storage = const FlutterSecureStorage(); + final String baseUrl = ApiConfig.baseUrl; //login - Future login(String email, String password) async { - final response = await http.post( - Uri.parse('$baseUrl${ApiConfig.login}'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'email': email, 'password': password}), - ); + Future> login(String email, String password) async { + try { + final response = await http.post( + Uri.parse('$baseUrl${ApiConfig.login}'), + headers: ApiConfig.headers, + body: jsonEncode({ + 'email': email, + 'password': password + }), + ); + if (response.statusCode == 201) { + final data = jsonDecode(response.body); - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final authResponse = AuthResponse.fromJson(data); + await TokenService.saveToken(data['access_token']); + await TokenService.saveRefreshToken(data['refresh_token']); + final role = _extractRoleFromToken(data['access_token']); + await TokenService.saveRole(role); - await storage.write(key: 'access_token', value: authResponse.acessToken); - await storage.write(key: 'role', value: authResponse.role); - return authResponse; - } else { - throw Exception('Failed to login'); + return data; + } else { + throw Exception('Failed to login: ${response.body}'); + } + } catch (e) { + throw Exception('Failed to login: $e'); + } + } + + String _extractRoleFromToken(String token) { + try { + final parts = token.split('.'); + if (parts.length != 3) return ''; + + final payload = parts[1]; + final normalizedPayload = base64Url.normalize(payload); + final decoded = utf8.decode(base64Url.decode(normalizedPayload)); + final Map payloadMap = jsonDecode(decoded); + + return payloadMap['role'] ?? ''; + } catch (e) { + print('Error extracting role from token: $e'); + return ''; + } + } + + Future logout() async { + await TokenService.clearAll(); + } + + Future isAuthenticated() async { + final token = await TokenService.getToken(); + if (token == null) return false; + + return !_isTokenExpired(token); + } + + bool _isTokenExpired(String token) { + try { + final parts = token.split('.'); + if (parts.length != 3) return true; + + final payload = parts[1]; + final normalizedPayload = base64Url.normalize(payload); + final decoded = utf8.decode(base64Url.decode(normalizedPayload)); + final Map payloadMap = jsonDecode(decoded); + + final exp = payloadMap['exp']; + if (exp == null) return true; + + final expirationDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000); + return DateTime.now().isAfter(expirationDate); + } catch (e) { + return true; } } diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart new file mode 100644 index 0000000..220f946 --- /dev/null +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +class AssistanteMaternelleManagementWidget extends StatelessWidget { + const AssistanteMaternelleManagementWidget({super.key}); + + @override + Widget build(BuildContext context) { + final assistantes = [ + { + "nom": "Marie Dupont", + "numeroAgrement": "AG123456", + "zone": "Paris 14", + "capacite": 3, + }, + { + "nom": "Claire Martin", + "numeroAgrement": "AG654321", + "zone": "Lyon 7", + "capacite": 2, + }, + ]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🔎 Zone de filtre + _buildFilterSection(), + + const SizedBox(height: 16), + + // 📋 Liste des assistantes + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: assistantes.length, + itemBuilder: (context, index) { + final assistante = assistantes[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: const Icon(Icons.face), + title: Text(assistante['nom'].toString()), + subtitle: Text( + "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // TODO: Ajouter modification + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + // TODO: Ajouter suppression + }, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildFilterSection() { + return Wrap( + spacing: 16, + runSpacing: 8, + children: [ + SizedBox( + width: 200, + child: TextField( + decoration: const InputDecoration( + labelText: "Zone géographique", + border: OutlineInputBorder(), + ), + onChanged: (value) { + // TODO: Ajouter logique de filtrage par zone + }, + ), + ), + SizedBox( + width: 200, + child: TextField( + decoration: const InputDecoration( + labelText: "Capacité minimum", + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) { + // TODO: Ajouter logique de filtrage par capacité + }, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/widgets/admin/dashboard_admin.dart b/frontend/lib/widgets/admin/dashboard_admin.dart new file mode 100644 index 0000000..6f63760 --- /dev/null +++ b/frontend/lib/widgets/admin/dashboard_admin.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; + +class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget { + final int selectedIndex; + final ValueChanged onTabChange; + + const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10); + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 768; + return AppBar( + elevation: 0, + automaticallyImplyLeading: false, + title: Row( + children: [ + SizedBox(width: MediaQuery.of(context).size.width * 0.19), + const Text( + "P'tit Pas", + style: TextStyle( + color: Color(0xFF9CC5C0), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + + // Navigation principale + _buildNavItem(context, 'Gestionnaires', 0), + const SizedBox(width: 24), + _buildNavItem(context, 'Parents', 1), + const SizedBox(width: 24), + _buildNavItem(context, 'Assistantes maternelles', 2), + const SizedBox(width: 24), + _buildNavItem(context, 'Administrateurs', 3), + ], + ), + actions: isMobile + ? [_buildMobileMenu(context)] + : [ + // Nom de l'utilisateur + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: Text( + 'Admin', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + // Bouton déconnexion + Padding( + padding: const EdgeInsets.only(right: 16), + child: TextButton( + onPressed: () => _handleLogout(context), + style: TextButton.styleFrom( + backgroundColor: const Color(0xFF9CC5C0), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text('Se déconnecter'), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + ], + ); + } + + Widget _buildNavItem(BuildContext context, String title, int index) { + final bool isActive = index == selectedIndex; + return InkWell( + onTap: () => onTabChange(index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: isActive ? null : Border.all(color: Colors.black26), + ), + child: Text( + title, + style: TextStyle( + color: isActive ? Colors.white : Colors.black, + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + fontSize: 14, + ), + ), + ), + ); +} + + + Widget _buildMobileMenu(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.menu, color: Colors.white), + onSelected: (value) { + if (value == 4) { + _handleLogout(context); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 0, child: Text("Gestionnaires")), + const PopupMenuItem(value: 1, child: Text("Parents")), + const PopupMenuItem(value: 2, child: Text("Assistantes maternelles")), + const PopupMenuItem(value: 3, child: Text("Administrateurs")), + const PopupMenuDivider(), + const PopupMenuItem(value: 4, child: Text("Se déconnecter")), + ], + ); + } + + void _handleLogout(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Déconnexion'), + content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + // TODO: Implémenter la logique de déconnexion + }, + child: const Text('Déconnecter'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/admin/gestionnaire_card.dart b/frontend/lib/widgets/admin/gestionnaire_card.dart new file mode 100644 index 0000000..5d80255 --- /dev/null +++ b/frontend/lib/widgets/admin/gestionnaire_card.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +class GestionnaireCard extends StatelessWidget { + final String name; + final String email; + + const GestionnaireCard({ + Key? key, + required this.name, + required this.email, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🔹 Infos principales + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), + Text(email, style: const TextStyle(color: Colors.grey)), + ], + ), + const SizedBox(height: 12), + + // 🔹 Attribution à des RPE (dropdown fictif ici) + Row( + children: [ + const Text("RPE attribué : "), + const SizedBox(width: 8), + DropdownButton( + value: "RPE 1", + items: const [ + DropdownMenuItem(value: "RPE 1", child: Text("RPE 1")), + DropdownMenuItem(value: "RPE 2", child: Text("RPE 2")), + DropdownMenuItem(value: "RPE 3", child: Text("RPE 3")), + ], + onChanged: (value) {}, + ), + ], + ), + const SizedBox(height: 12), + + // 🔹 Boutons d'action + Row( + children: [ + TextButton.icon( + onPressed: () { + // Réinitialisation mot de passe + }, + icon: const Icon(Icons.lock_reset), + label: const Text("Réinitialiser MDP"), + ), + const SizedBox(width: 12), + TextButton.icon( + onPressed: () { + // Suppression du compte + }, + icon: const Icon(Icons.delete, color: Colors.red), + label: const Text("Supprimer", style: TextStyle(color: Colors.red)), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart new file mode 100644 index 0000000..3f5d6c2 --- /dev/null +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart'; + +class GestionnaireManagementWidget extends StatelessWidget { + const GestionnaireManagementWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 🔹 Barre du haut avec bouton + Row( + children: [ + const Expanded( + child: TextField( + decoration: InputDecoration( + hintText: "Rechercher un gestionnaire...", + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 16), + ElevatedButton.icon( + onPressed: () { + // Rediriger vers la page de création + }, + icon: const Icon(Icons.add), + label: const Text("Créer un gestionnaire"), + ), + ], + ), + const SizedBox(height: 24), + + // 🔹 Liste des gestionnaires + Expanded( + child: ListView.builder( + itemCount: 5, // À remplacer par liste dynamique + itemBuilder: (context, index) { + return GestionnaireCard( + name: "Dupont $index", + email: "dupont$index@mail.com", + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart new file mode 100644 index 0000000..1bf78a5 --- /dev/null +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; + +class ParentManagementWidget extends StatelessWidget { + const ParentManagementWidget({super.key}); + + @override + Widget build(BuildContext context) { + // 🔁 Simulation de données parents + final parents = [ + { + "nom": "Jean Dupuis", + "email": "jean.dupuis@email.com", + "statut": "Actif", + "enfants": 2, + }, + { + "nom": "Lucie Morel", + "email": "lucie.morel@email.com", + "statut": "En attente", + "enfants": 1, + }, + ]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + _buildSearchSection(), + + const SizedBox(height: 16), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: parents.length, + itemBuilder: (context, index) { + final parent = parents[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: const Icon(Icons.person_outline), + title: Text(parent['nom'].toString()), + subtitle: Text( + "${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}", + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: "Voir dossier", + onPressed: () { + // TODO: Voir le statut du dossier + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: "Modifier", + onPressed: () { + // TODO: Modifier parent + }, + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: "Supprimer", + onPressed: () { + // TODO: Supprimer compte + }, + ), + ], + ), + ), + ); + }, + ), + ], + ) + ); + } + + Widget _buildSearchSection() { + return Wrap( + spacing: 16, + runSpacing: 8, + children: [ + SizedBox( + width: 220, + child: TextField( + decoration: const InputDecoration( + labelText: "Nom du parent", + border: OutlineInputBorder(), + ), + onChanged: (value) { + // TODO: Ajouter logique de recherche + }, + ), + ), + SizedBox( + width: 220, + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: "Statut", + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: "Actif", child: Text("Actif")), + DropdownMenuItem(value: "En attente", child: Text("En attente")), + DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")), + ], + onChanged: (value) { + // TODO: Ajouter logique de filtrage + }, + ), + ), + ], + ); + } +} -- 2.47.2