Compare commits
3 Commits
bbbff60a7a
...
1e85819fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e85819fea | ||
|
|
4392567509 | ||
|
|
c98c4d51d0 |
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
@ -43,3 +43,9 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
|
# Fichiers générés automatiquement par Flutter pour l'enregistrement des plugins
|
||||||
|
**/GeneratedPluginRegistrant.java
|
||||||
|
**/generated_plugin_registrant.*
|
||||||
|
**/generated_plugins.cmake
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package io.flutter.plugins;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import io.flutter.Log;
|
|
||||||
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated file. Do not edit.
|
|
||||||
* This file is generated by the Flutter tool based on the
|
|
||||||
* plugins that support the Android platform.
|
|
||||||
*/
|
|
||||||
@Keep
|
|
||||||
public final class GeneratedPluginRegistrant {
|
|
||||||
private static final String TAG = "GeneratedPluginRegistrant";
|
|
||||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
|
||||||
} 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 io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,106 +1,278 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/base_user_management.dart';
|
||||||
|
|
||||||
class AssistanteMaternelleManagementWidget extends StatelessWidget {
|
class AssistanteMaternelleManagementWidget extends StatelessWidget {
|
||||||
const AssistanteMaternelleManagementWidget({super.key});
|
const AssistanteMaternelleManagementWidget({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final assistantes = [
|
return BaseUserManagementWidget(
|
||||||
{
|
config: UserDisplayConfig(
|
||||||
"nom": "Marie Dupont",
|
title: 'Assistantes Maternelles',
|
||||||
"numeroAgrement": "AG123456",
|
role: 'assistante_maternelle',
|
||||||
"zone": "Paris 14",
|
defaultIcon: Icons.face,
|
||||||
"capacite": 3,
|
filterFields: [
|
||||||
},
|
FilterField(
|
||||||
{
|
label: 'Rechercher',
|
||||||
"nom": "Claire Martin",
|
hint: 'Nom ou email',
|
||||||
"numeroAgrement": "AG654321",
|
type: FilterType.text,
|
||||||
"zone": "Lyon 7",
|
filter: (user, query) {
|
||||||
"capacite": 2,
|
final fullName = '${user['prenom'] ?? ''} ${user['nom'] ?? ''}'.toLowerCase();
|
||||||
},
|
final email = (user['email'] ?? '').toLowerCase();
|
||||||
];
|
return fullName.contains(query.toLowerCase()) ||
|
||||||
|
email.contains(query.toLowerCase());
|
||||||
return Padding(
|
},
|
||||||
padding: const EdgeInsets.all(16),
|
),
|
||||||
child: Column(
|
FilterField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
label: 'Zone géographique',
|
||||||
children: [
|
hint: 'Ville ou département',
|
||||||
// 🔎 Zone de filtre
|
type: FilterType.text,
|
||||||
_buildFilterSection(),
|
filter: (user, query) {
|
||||||
|
final zone = (user['zone'] ?? user['ville'] ?? user['code_postal'] ?? '').toLowerCase();
|
||||||
const SizedBox(height: 16),
|
return zone.contains(query.toLowerCase());
|
||||||
|
},
|
||||||
// 📋 Liste des assistantes
|
),
|
||||||
ListView.builder(
|
FilterField(
|
||||||
shrinkWrap: true,
|
label: 'Capacité minimum',
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
hint: 'Nombre d\'enfants',
|
||||||
itemCount: assistantes.length,
|
type: FilterType.number,
|
||||||
itemBuilder: (context, index) {
|
filter: (user, query) {
|
||||||
final assistante = assistantes[index];
|
final capacite = int.tryParse(user['capacite']?.toString() ?? '0') ?? 0;
|
||||||
return Card(
|
final minCapacite = int.tryParse(query) ?? 0;
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
return capacite >= minCapacite;
|
||||||
child: ListTile(
|
},
|
||||||
leading: const Icon(Icons.face),
|
),
|
||||||
title: Text(assistante['nom'].toString()),
|
FilterField(
|
||||||
subtitle: Text(
|
label: 'Statut',
|
||||||
"N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"),
|
hint: 'Tous',
|
||||||
trailing: Row(
|
type: FilterType.dropdown,
|
||||||
mainAxisSize: MainAxisSize.min,
|
options: ['actif', 'en attente', 'inactif'],
|
||||||
children: [
|
filter: (user, status) {
|
||||||
IconButton(
|
if (status.isEmpty) return true;
|
||||||
icon: const Icon(Icons.edit),
|
return user['statut']?.toString().toLowerCase() == status.toLowerCase();
|
||||||
onPressed: () {
|
},
|
||||||
// TODO: Ajouter modification
|
),
|
||||||
},
|
],
|
||||||
),
|
actions: [
|
||||||
IconButton(
|
UserAction(
|
||||||
icon: const Icon(Icons.delete),
|
icon: Icons.edit,
|
||||||
onPressed: () {
|
color: Colors.orange,
|
||||||
// TODO: Ajouter suppression
|
tooltip: 'Modifier',
|
||||||
},
|
onPressed: _editAssistante,
|
||||||
),
|
),
|
||||||
],
|
UserAction(
|
||||||
),
|
icon: Icons.delete,
|
||||||
),
|
color: Colors.red,
|
||||||
);
|
tooltip: 'Supprimer',
|
||||||
},
|
onPressed: _deleteAssistante,
|
||||||
),
|
),
|
||||||
],
|
UserAction(
|
||||||
),
|
icon: Icons.location_on,
|
||||||
|
color: Colors.green,
|
||||||
|
tooltip: 'Voir zone',
|
||||||
|
onPressed: _showZone,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
getSubtitle: (user) {
|
||||||
|
final email = user['email'] ?? '';
|
||||||
|
final numeroAgrement = user['numeroAgrement'] ?? user['agrement'] ?? 'N/A';
|
||||||
|
final zone = user['code_postal'] ?? user['ville'] ?? 'Non spécifiée';
|
||||||
|
final capacite = user['capacite'] ?? user['capaciteAccueil'] ?? 'N/A';
|
||||||
|
return '$email\nN° Agrément: $numeroAgrement\nZone: $zone | Capacité: $capacite';
|
||||||
|
},
|
||||||
|
getDisplayName: (user) => '${user['prenom'] ?? ''} ${user['nom'] ?? ''}',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterSection() {
|
static Future<void> _editAssistante(BuildContext context, Map<String, dynamic> assistante) async {
|
||||||
return Wrap(
|
// TODO: Implémenter l'édition
|
||||||
spacing: 16,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
runSpacing: 8,
|
const SnackBar(
|
||||||
children: [
|
content: Text('Fonctionnalité de modification à implémenter'),
|
||||||
SizedBox(
|
backgroundColor: Colors.orange,
|
||||||
width: 200,
|
),
|
||||||
child: TextField(
|
);
|
||||||
decoration: const InputDecoration(
|
}
|
||||||
labelText: "Zone géographique",
|
|
||||||
border: OutlineInputBorder(),
|
static Future<void> _deleteAssistante(BuildContext context, Map<String, dynamic> assistante) async {
|
||||||
),
|
final confirmed = await showDialog<bool>(
|
||||||
onChanged: (value) {
|
context: context,
|
||||||
// TODO: Ajouter logique de filtrage par zone
|
builder: (context) => AlertDialog(
|
||||||
},
|
title: const Text('Confirmer la suppression'),
|
||||||
),
|
content: Text(
|
||||||
|
'Êtes-vous sûr de vouloir supprimer le compte de ${assistante['firstName']} ${assistante['lastName']} ?\n\n'
|
||||||
|
'Cette action supprimera également tous les contrats et données associés.'
|
||||||
),
|
),
|
||||||
SizedBox(
|
actions: [
|
||||||
width: 200,
|
TextButton(
|
||||||
child: TextField(
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
decoration: const InputDecoration(
|
child: const Text('Annuler'),
|
||||||
labelText: "Capacité minimum",
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
onChanged: (value) {
|
|
||||||
// TODO: Ajouter logique de filtrage par capacité
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
final userService = UserService();
|
||||||
|
final success = await userService.deleteUser(assistante['id']);
|
||||||
|
|
||||||
|
if (success && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${assistante['firstName']} ${assistante['lastName']} supprimé avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Le widget se rechargera automatiquement via le système de state
|
||||||
|
} else {
|
||||||
|
throw Exception('Erreur lors de la suppression');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showZone(BuildContext context, Map<String, dynamic> assistante) async {
|
||||||
|
final zone = assistante['zone'] ?? assistante['ville'] ?? 'Non spécifiée';
|
||||||
|
final adresse = assistante['adresse'] ?? assistante['address'] ?? '';
|
||||||
|
final codePostal = assistante['codePostal'] ?? assistante['zipCode'] ?? '';
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Zone d\'intervention - ${assistante['firstName']} ${assistante['lastName']}'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (zone.isNotEmpty) Text('Zone: $zone', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (adresse.isNotEmpty) Text('Adresse: $adresse'),
|
||||||
|
if (codePostal.isNotEmpty) Text('Code postal: $codePostal'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Capacité d\'accueil: ${assistante['capacite'] ?? assistante['capaciteAccueil'] ?? 'N/A'} enfants'),
|
||||||
|
Text('N° Agrément: ${assistante['numeroAgrement'] ?? assistante['agrement'] ?? 'N/A'}'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Fermer'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Ouvrir dans Maps
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Intégration Maps à implémenter'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Voir sur la carte'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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é
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
424
frontend/lib/widgets/admin/base_user_management.dart
Normal file
424
frontend/lib/widgets/admin/base_user_management.dart
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
|
||||||
|
/// Configuration pour personnaliser l'affichage des utilisateurs
|
||||||
|
class UserDisplayConfig {
|
||||||
|
final String title;
|
||||||
|
final String role;
|
||||||
|
final IconData defaultIcon;
|
||||||
|
final List<FilterField> filterFields;
|
||||||
|
final List<UserAction> actions;
|
||||||
|
final String Function(Map<String, dynamic>) getSubtitle;
|
||||||
|
final String Function(Map<String, dynamic>) getDisplayName;
|
||||||
|
|
||||||
|
const UserDisplayConfig({
|
||||||
|
required this.title,
|
||||||
|
required this.role,
|
||||||
|
required this.defaultIcon,
|
||||||
|
required this.filterFields,
|
||||||
|
required this.actions,
|
||||||
|
required this.getSubtitle,
|
||||||
|
required this.getDisplayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration d'un champ de filtre
|
||||||
|
class FilterField {
|
||||||
|
final String label;
|
||||||
|
final String hint;
|
||||||
|
final FilterType type;
|
||||||
|
final List<String>? options;
|
||||||
|
final bool Function(Map<String, dynamic>, String) filter;
|
||||||
|
|
||||||
|
const FilterField({
|
||||||
|
required this.label,
|
||||||
|
required this.hint,
|
||||||
|
required this.type,
|
||||||
|
required this.filter,
|
||||||
|
this.options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FilterType { text, dropdown, number }
|
||||||
|
|
||||||
|
/// Configuration d'une action sur un utilisateur
|
||||||
|
class UserAction {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String tooltip;
|
||||||
|
final Future<void> Function(BuildContext, Map<String, dynamic>) onPressed;
|
||||||
|
|
||||||
|
const UserAction({
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.tooltip,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget de gestion d'utilisateurs réutilisable
|
||||||
|
class BaseUserManagementWidget extends StatefulWidget {
|
||||||
|
final UserDisplayConfig config;
|
||||||
|
|
||||||
|
const BaseUserManagementWidget({
|
||||||
|
super.key,
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BaseUserManagementWidget> createState() =>
|
||||||
|
_BaseUserManagementWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BaseUserManagementWidgetState extends State<BaseUserManagementWidget> {
|
||||||
|
final UserService _userService = UserService();
|
||||||
|
final Map<String, TextEditingController> _filterControllers = {};
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _allUsers = [];
|
||||||
|
List<Map<String, dynamic>> _filteredUsers = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeFilters();
|
||||||
|
_loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeFilters() {
|
||||||
|
for (final field in widget.config.filterFields) {
|
||||||
|
_filterControllers[field.label] = TextEditingController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (final controller in _filterControllers.values) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUsers() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final users = await _userService.getUsersByRole(widget.config.role);
|
||||||
|
setState(() {
|
||||||
|
_allUsers = users;
|
||||||
|
_filteredUsers = users;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFilters() {
|
||||||
|
setState(() {
|
||||||
|
_filteredUsers = _allUsers.where((user) {
|
||||||
|
return widget.config.filterFields.every((field) {
|
||||||
|
final controller = _filterControllers[field.label];
|
||||||
|
if (controller == null || controller.text.isEmpty) return true;
|
||||||
|
return field.filter(user, controller.text);
|
||||||
|
});
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getStatusDisplay(Map<String, dynamic> user) {
|
||||||
|
final status = user['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 'deleted':
|
||||||
|
return 'Supprimé';
|
||||||
|
default:
|
||||||
|
return status.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getStatusColor(Map<String, dynamic> user) {
|
||||||
|
final status = user['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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUserDetails(Map<String, dynamic> user) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(widget.config.getDisplayName(user)),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Email: ${user['email']}'),
|
||||||
|
Text('Rôle: ${user['role']}'),
|
||||||
|
Text('Statut: ${_getStatusDisplay(user)}'),
|
||||||
|
Text('ID: ${user['id']}'),
|
||||||
|
if (user['createdAt'] != null)
|
||||||
|
Text(
|
||||||
|
'Créé le: ${DateTime.parse(user['createdAt']).toLocal().toString().split(' ')[0]}'),
|
||||||
|
// Affichage des champs spécifiques selon le type d'utilisateur
|
||||||
|
...user.entries
|
||||||
|
.where((e) => ![
|
||||||
|
'id',
|
||||||
|
'email',
|
||||||
|
'role',
|
||||||
|
'status',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'firstName',
|
||||||
|
'lastName'
|
||||||
|
].contains(e.key))
|
||||||
|
.map((e) => Text('${e.key}: ${e.value}'))
|
||||||
|
.toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Fermer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${widget.config.title} (${_filteredUsers.length})',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: _loadUsers,
|
||||||
|
tooltip: 'Actualiser',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildFilterSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildUsersList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterSection() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: widget.config.filterFields.map((field) {
|
||||||
|
final controller = _filterControllers[field.label]!;
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case FilterType.text:
|
||||||
|
case FilterType.number:
|
||||||
|
return SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: field.type == FilterType.number
|
||||||
|
? TextInputType.number
|
||||||
|
: TextInputType.text,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: field.label,
|
||||||
|
hintText: field.hint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
),
|
||||||
|
onChanged: (value) => _applyFilters(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case FilterType.dropdown:
|
||||||
|
return SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: field.label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem(value: '', child: Text("Tous")),
|
||||||
|
...?field.options?.map((option) =>
|
||||||
|
DropdownMenuItem(value: option, child: Text(option))),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.text = value ?? '';
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUsersList() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Chargement...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: _loadUsers,
|
||||||
|
child: const Text('Réessayer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filteredUsers.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.people_outline, size: 48, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_allUsers.isEmpty ? 'Aucun utilisateur trouvé' : 'Aucun résultat',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
if (_allUsers.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text('Essayez de modifier vos critères de recherche'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: _filteredUsers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = _filteredUsers[index];
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _getStatusColor(user).withOpacity(0.2),
|
||||||
|
child: Icon(
|
||||||
|
widget.config.defaultIcon,
|
||||||
|
color: _getStatusColor(user),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(widget.config.getDisplayName(user)),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.config.getSubtitle(user)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStatusColor(user).withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: _getStatusColor(user)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getStatusDisplay(user),
|
||||||
|
style: TextStyle(
|
||||||
|
color: _getStatusColor(user),
|
||||||
|
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: () => _showUserDetails(user),
|
||||||
|
),
|
||||||
|
...widget.config.actions
|
||||||
|
.map(
|
||||||
|
(action) => IconButton(
|
||||||
|
icon: Icon(action.icon, color: action.color),
|
||||||
|
tooltip: action.tooltip,
|
||||||
|
onPressed: () => action.onPressed(context, user),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
#define GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
||||||
#include <flutter/plugin_registry.h>
|
|
||||||
|
|
||||||
// Registers Flutter plugins.
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
|
||||||
|
|
||||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
#
|
|
||||||
# Generated file, do not edit.
|
|
||||||
#
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
|
||||||
file_selector_windows
|
|
||||||
url_launcher_windows
|
|
||||||
)
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
|
||||||
)
|
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
|
||||||
|
|
||||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
|
||||||
endforeach(plugin)
|
|
||||||
|
|
||||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
|
||||||
endforeach(ffi_plugin)
|
|
||||||
Loading…
x
Reference in New Issue
Block a user