diff --git a/frontend/lib/services/api/tokenService.dart b/frontend/lib/services/api/tokenService.dart index 9ead7f7..5dc04d4 100644 --- a/frontend/lib/services/api/tokenService.dart +++ b/frontend/lib/services/api/tokenService.dart @@ -1,8 +1,8 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +// import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; class TokenService { - static const _storage = FlutterSecureStorage(); + // static const _storage = FlutterSecureStorage(); static const _tokenKey = 'access_token'; static const String _refreshTokenKey = 'refresh_token'; static const _roleKey = 'user_role'; diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart new file mode 100644 index 0000000..cc08649 --- /dev/null +++ b/frontend/lib/services/user_service.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/services/api/api_config.dart'; +import 'package:p_tits_pas/services/api/tokenService.dart'; +import 'package:http/http.dart' as http; + + +class UserService { + final String baseUrl = ApiConfig.baseUrl; + + //Recuperer tous les utilisateurs + Future>> getAllUsers() async { + try { + final token = await TokenService.getToken(); + if (token == null) { + throw Exception('Token non disponible'); + } + + final response = await http.get( + Uri.parse('$baseUrl${ApiConfig.users}'), + headers: ApiConfig.authHeaders(token), + ); + + if (response.statusCode == 200) { + final List data = jsonDecode(response.body); + return data.cast>(); + } else { + throw Exception('Erreur lors de la récupération des utilisateurs: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Erreur de connexion: $e'); + } + } + + //Récuperer les utilisateurs en fonction du role + Future>> getUsersByRole(String role) async { + try { + final allUsers = await getAllUsers(); + return allUsers.where((user) => + user['role']?.toString().toLowerCase() == role.toLowerCase() + ).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des utilisateurs par rôle: $e'); + } + } + + // Filtrer les utilisateurs par statut + Future>> filterUsersByStatus(String? status) async { + try { + final allUsers = await getAllUsers(); + if (status == null || status.isEmpty) return allUsers; + + return allUsers + .where((user) => + user['status']?.toString().toLowerCase() == status.toLowerCase()) + .toList(); + } catch (e) { + throw Exception('Erreur lors du filtrage: $e'); + } + } + + /// Supprimer un utilisateur + Future deleteUser(String userId) async { + try { + final token = await TokenService.getToken(); + if (token == null) { + throw Exception('Token non disponible'); + } + + final response = await http.delete( + Uri.parse('$baseUrl${ApiConfig.users}/$userId'), + headers: ApiConfig.authHeaders(token), + ); + + return response.statusCode == 200 || response.statusCode == 204; + } catch (e) { + throw Exception('Erreur lors de la suppression: $e'); + } + } + + /// Récupérer les détails d'un utilisateur + Future?> getUserById(String userId) async { + try { + final token = await TokenService.getToken(); + if (token == null) { + throw Exception('Token non disponible'); + } + + final response = await http.get( + Uri.parse('$baseUrl${ApiConfig.users}/$userId'), + headers: ApiConfig.authHeaders(token), + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + return null; + } + } catch (e) { + throw Exception('Erreur lors de la récupération de l\'utilisateur: $e'); + } + } + +} \ No newline at end of file diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index 1bf78a5..f9e0920 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -1,84 +1,218 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/services/user_service.dart'; -class ParentManagementWidget extends StatelessWidget { +class ParentManagementWidget extends StatefulWidget { 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, - }, - ]; + State createState() => _ParentManagementWidgetState(); +} - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ +class _ParentManagementWidgetState extends State { - _buildSearchSection(), + final UserService _userService = UserService(); + final TextEditingController _searchController = TextEditingController(); - const SizedBox(height: 16), + List> _allParents = []; + List> _filteredParents = []; + String? _selectedStatus; + bool _isLoading = true; + String? _error; - 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 - }, - ), - ], - ), - ), - ); - }, + + @override + void initState() { + super.initState(); + _loadParents(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadParents() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final parents = await _userService.getUsersByRole("parent"); + setState(() { + _allParents = parents; + _filteredParents = parents; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _applyFilters() { + setState(() { + _filteredParents = _allParents.where((parent) { + final searchQuery = _searchController.text.toLowerCase(); + final fullName = '${parent['prenom'] ?? ''} ${parent['nom'] ?? ''}'.toLowerCase(); + final email = (parent['email'] ?? '').toLowerCase(); + + bool matchesSearch = searchQuery.isEmpty || + fullName.contains(searchQuery) || + email.contains(searchQuery); + + bool matchesStatus = _selectedStatus == null || + _selectedStatus!.isEmpty || + parent['statut']?.toString() == _selectedStatus; + + return matchesSearch && matchesStatus; + }).toList(); + }); + } + + String _getStatusDisplay(Map parent) { + final status = parent['statut']; + if (status == null) return 'Non défini'; + switch (status.toString().toLowerCase()) { + case 'actif': + return 'Actif'; + case 'en_attente': + return 'En attente'; + case 'inactif': + return 'Inactif'; + case 'supprimé': + return 'Supprimé'; + default: + return status.toString(); + } + } + + Color _getStatusColor(Map parent) { + final status = parent['statut']?.toString().toLowerCase(); + switch (status) { + case 'actif': + return Colors.green; + case 'en_attente': + return Colors.orange; + case 'inactif': + return Colors.grey; + case 'supprimé': + return Colors.red; + default: + return Colors.grey; + } + } + + Future _confirmDelete(Map parent) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text( + 'Êtes-vous sûr de vouloir supprimer le compte de ${parent['firstName']} ${parent['lastName']} ?' ), - ], - ) + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Supprimer'), + ), + ], + ), ); + + if (confirmed == true) { + await _deleteParent(parent); + } + } + + Future _deleteParent(Map parent) async { + try { + final success = await _userService.deleteUser(parent['id']); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${parent['firstName']} ${parent['lastName']} supprimé avec succès'), + backgroundColor: Colors.green, + ), + ); + _loadParents(); // Recharger la liste + } else { + throw Exception('Erreur lors de la suppression'); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + + void _viewParentDetails(Map parent) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('${parent['firstName']} ${parent['lastName']}'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Email: ${parent['email']}'), + Text('Rôle: ${parent['role']}'), + Text('Statut: ${_getStatusDisplay(parent)}'), + Text('ID: ${parent['id']}'), + if (parent['createdAt'] != null) + Text('Créé le: ${DateTime.parse(parent['createdAt']).toLocal().toString().split(' ')[0]}'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ], + ), + ); + } + + + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Gestion des Parents (${_filteredParents.length})', + style: Theme.of(context).textTheme.headlineSmall, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadParents, + tooltip: 'Actualiser', + ), + ], + ), + const SizedBox(height: 16), + _buildSearchSection(), + const SizedBox(height: 16), + Expanded( + child: _buildParentsList(), + ), + ], + )); } Widget _buildSearchSection() { @@ -87,35 +221,184 @@ class ParentManagementWidget extends StatelessWidget { runSpacing: 8, children: [ SizedBox( - width: 220, + width: 250, child: TextField( + controller: _searchController, decoration: const InputDecoration( - labelText: "Nom du parent", + labelText: "Rechercher un parent", + hintText: "Nom ou email", border: OutlineInputBorder(), + prefixIcon: Icon(Icons.search), ), - onChanged: (value) { - // TODO: Ajouter logique de recherche - }, + onChanged: (value) => _applyFilters(), ), ), SizedBox( - width: 220, + width: 200, child: DropdownButtonFormField( + value: _selectedStatus, 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é")), + DropdownMenuItem(value: null, child: Text("Tous")), + DropdownMenuItem(value: "active", child: Text("Actif")), + DropdownMenuItem(value: "pending", child: Text("En attente")), + DropdownMenuItem(value: "inactive", child: Text("Inactif")), + DropdownMenuItem(value: "deleted", child: Text("Supprimé")), ], onChanged: (value) { - // TODO: Ajouter logique de filtrage + setState(() { + _selectedStatus = value; + }); + _applyFilters(); }, ), ), ], ); } + + Widget _buildParentsList() { + if (_isLoading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des parents...'), + ], + ), + ); + } + + if (_error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red[300]), + const SizedBox(height: 16), + Text( + 'Erreur de chargement', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + _error!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadParents, + child: const Text('Réessayer'), + ), + ], + ), + ); + } + + if (_filteredParents.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.people_outline, size: 48, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + _allParents.isEmpty ? 'Aucun parent trouvé' : 'Aucun résultat', + style: Theme.of(context).textTheme.titleLarge, + ), + if (_allParents.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text('Essayez de modifier vos critères de recherche'), + ], + ], + ), + ); + } + + return ListView.builder( + itemCount: _filteredParents.length, + itemBuilder: (context, index) { + final parent = _filteredParents[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getStatusColor(parent).withOpacity(0.2), + child: Text( + '${parent['firstName']?[0] ?? ''}${parent['lastName']?[0] ?? ''}', + style: TextStyle( + color: _getStatusColor(parent), + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text('${parent['firstName'] ?? ''} ${parent['lastName'] ?? ''}'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(parent['email'] ?? ''), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _getStatusColor(parent).withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _getStatusColor(parent)), + ), + child: Text( + _getStatusDisplay(parent), + style: TextStyle( + color: _getStatusColor(parent), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility, color: Colors.blue), + tooltip: "Voir détails", + onPressed: () => _viewParentDetails(parent), + ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.orange), + tooltip: "Modifier", + onPressed: () { + // TODO: Implémenter la modification + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Fonctionnalité de modification à implémenter'), + ), + ); + }, + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + tooltip: "Supprimer", + onPressed: () => _confirmDelete(parent), + ), + ], + ), + ), + ); + }, + ); + } + + }