From e2e38076aadd2103aeb3f2bb91de1d551b84bfd2 Mon Sep 17 00:00:00 2001 From: Hanim Date: Fri, 12 Sep 2025 15:24:52 +0200 Subject: [PATCH] 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 + }, + ), + ), + ], + ); + } +}