Compare commits
No commits in common. "f9477d3fbe39e008075350a79d496ea274f94cfd" and "04b910295cfa94576ba2523cfe684721b9103aad" have entirely different histories.
f9477d3fbe
...
04b910295c
@ -256,23 +256,3 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
|
|||||||
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
|
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
|
||||||
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
|
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
|
||||||
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.
|
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.
|
||||||
|
|
||||||
## 8. Évolution future - Gouvernance intra-RPE
|
|
||||||
|
|
||||||
### 8.1 Niveaux d'accès et rôles différenciés dans un même Relais
|
|
||||||
|
|
||||||
#### 8.1.1 Situation actuelle
|
|
||||||
- Le périmètre actuel prévoit un rattachement simple entre gestionnaire et relais.
|
|
||||||
- Le rôle "gestionnaire" est traité de manière uniforme dans l'outil.
|
|
||||||
|
|
||||||
#### 8.1.2 Évolution à prévoir
|
|
||||||
- Introduire un modèle de rôles internes au relais (par exemple : responsable/coordinatrice, animatrice/référente, administratif).
|
|
||||||
- Permettre des niveaux d'autorité différents selon les actions (pilotage, validation, consultation, administration locale).
|
|
||||||
- Définir des permissions fines par fonctionnalité (lecture, création, modification, suppression, validation).
|
|
||||||
- Prévoir une gestion multi-utilisateurs par relais avec traçabilité des décisions.
|
|
||||||
|
|
||||||
#### 8.1.3 Impact attendu
|
|
||||||
- Évolution du modèle de données vers un RBAC intra-RPE.
|
|
||||||
- Adaptation des écrans d'administration pour gérer les rôles locaux.
|
|
||||||
- Renforcement des contrôles d'accès backend et des règles métier.
|
|
||||||
- Clarification des workflows décisionnels dans l'application.
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
class RelaisModel {
|
|
||||||
final String id;
|
|
||||||
final String nom;
|
|
||||||
final String adresse;
|
|
||||||
final Map<String, dynamic>? horairesOuverture;
|
|
||||||
final String? ligneFixe;
|
|
||||||
final bool actif;
|
|
||||||
final String? notes;
|
|
||||||
|
|
||||||
const RelaisModel({
|
|
||||||
required this.id,
|
|
||||||
required this.nom,
|
|
||||||
required this.adresse,
|
|
||||||
this.horairesOuverture,
|
|
||||||
this.ligneFixe,
|
|
||||||
required this.actif,
|
|
||||||
this.notes,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory RelaisModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return RelaisModel(
|
|
||||||
id: (json['id'] ?? '').toString(),
|
|
||||||
nom: (json['nom'] ?? '').toString(),
|
|
||||||
adresse: (json['adresse'] ?? '').toString(),
|
|
||||||
horairesOuverture: json['horaires_ouverture'] is Map<String, dynamic>
|
|
||||||
? json['horaires_ouverture'] as Map<String, dynamic>
|
|
||||||
: null,
|
|
||||||
ligneFixe: json['ligne_fixe'] as String?,
|
|
||||||
actif: json['actif'] as bool? ?? true,
|
|
||||||
notes: json['notes'] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,8 +13,6 @@ class AppUser {
|
|||||||
final String? adresse;
|
final String? adresse;
|
||||||
final String? ville;
|
final String? ville;
|
||||||
final String? codePostal;
|
final String? codePostal;
|
||||||
final String? relaisId;
|
|
||||||
final String? relaisNom;
|
|
||||||
|
|
||||||
AppUser({
|
AppUser({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -31,15 +29,9 @@ class AppUser {
|
|||||||
this.adresse,
|
this.adresse,
|
||||||
this.ville,
|
this.ville,
|
||||||
this.codePostal,
|
this.codePostal,
|
||||||
this.relaisId,
|
|
||||||
this.relaisNom,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AppUser.fromJson(Map<String, dynamic> json) {
|
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||||
final relaisJson = json['relais'];
|
|
||||||
final relaisMap =
|
|
||||||
relaisJson is Map<String, dynamic> ? relaisJson : <String, dynamic>{};
|
|
||||||
|
|
||||||
return AppUser(
|
return AppUser(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
email: json['email'] as String,
|
email: json['email'] as String,
|
||||||
@ -64,9 +56,6 @@ class AppUser {
|
|||||||
adresse: json['adresse'] as String?,
|
adresse: json['adresse'] as String?,
|
||||||
ville: json['ville'] as String?,
|
ville: json['ville'] as String?,
|
||||||
codePostal: json['code_postal'] as String?,
|
codePostal: json['code_postal'] as String?,
|
||||||
relaisId: (json['relaisId'] ?? json['relais_id'] ?? relaisMap['id'])
|
|
||||||
?.toString(),
|
|
||||||
relaisNom: relaisMap['nom']?.toString(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,8 +75,6 @@ class AppUser {
|
|||||||
'adresse': adresse,
|
'adresse': adresse,
|
||||||
'ville': ville,
|
'ville': ville,
|
||||||
'code_postal': codePostal,
|
'code_postal': codePostal,
|
||||||
'relais_id': relaisId,
|
|
||||||
'relais_nom': relaisNom,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
import 'package:p_tits_pas/services/configuration_service.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/admin/admin_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
|
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/user_management_panel.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||||
|
|
||||||
@ -15,7 +18,7 @@ class AdminDashboardScreen extends StatefulWidget {
|
|||||||
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||||
bool? _setupCompleted;
|
bool? _setupCompleted;
|
||||||
int mainTabIndex = 0;
|
int mainTabIndex = 0;
|
||||||
int settingsSubIndex = 0;
|
int subIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -23,11 +26,6 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
_loadSetupStatus();
|
_loadSetupStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadSetupStatus() async {
|
Future<void> _loadSetupStatus() async {
|
||||||
try {
|
try {
|
||||||
final completed = await ConfigurationService.getSetupStatus();
|
final completed = await ConfigurationService.getSetupStatus();
|
||||||
@ -37,14 +35,12 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
if (!completed) mainTabIndex = 1;
|
if (!completed) mainTabIndex = 1;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) setState(() {
|
||||||
setState(() {
|
|
||||||
_setupCompleted = false;
|
_setupCompleted = false;
|
||||||
mainTabIndex = 1;
|
mainTabIndex = 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void onMainTabChange(int index) {
|
void onMainTabChange(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -52,9 +48,9 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSettingsSubTabChange(int index) {
|
void onSubTabChange(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
settingsSubIndex = index;
|
subIndex = index;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +80,9 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (mainTabIndex == 0)
|
if (mainTabIndex == 0)
|
||||||
const SizedBox.shrink()
|
DashboardUserManagementSubBar(
|
||||||
else
|
selectedSubIndex: subIndex,
|
||||||
DashboardSettingsSubBar(
|
onSubTabChange: onSubTabChange,
|
||||||
selectedSubIndex: settingsSubIndex,
|
|
||||||
onSubTabChange: onSettingsSubTabChange,
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _getBody(),
|
child: _getBody(),
|
||||||
@ -101,11 +95,19 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
|
|
||||||
Widget _getBody() {
|
Widget _getBody() {
|
||||||
if (mainTabIndex == 1) {
|
if (mainTabIndex == 1) {
|
||||||
return ParametresPanel(
|
return ParametresPanel(redirectToLoginAfterSave: !_setupCompleted!);
|
||||||
redirectToLoginAfterSave: !_setupCompleted!,
|
}
|
||||||
selectedSettingsTabIndex: settingsSubIndex,
|
switch (subIndex) {
|
||||||
);
|
case 0:
|
||||||
|
return const GestionnaireManagementWidget();
|
||||||
|
case 1:
|
||||||
|
return const ParentManagementWidget();
|
||||||
|
case 2:
|
||||||
|
return const AssistanteMaternelleManagementWidget();
|
||||||
|
case 3:
|
||||||
|
return const AdminManagementWidget();
|
||||||
|
default:
|
||||||
|
return const Center(child: Text('Page non trouvée'));
|
||||||
}
|
}
|
||||||
return const AdminUserManagementPanel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,451 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/relais_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
|
||||||
import 'package:p_tits_pas/services/relais_service.dart';
|
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
|
||||||
|
|
||||||
class GestionnaireCreateDialog extends StatefulWidget {
|
class GestionnairesCreate extends StatelessWidget {
|
||||||
final AppUser? initialUser;
|
const GestionnairesCreate({super.key});
|
||||||
|
|
||||||
const GestionnaireCreateDialog({
|
|
||||||
super.key,
|
|
||||||
this.initialUser,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<GestionnaireCreateDialog> createState() =>
|
|
||||||
_GestionnaireCreateDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GestionnaireCreateDialogState extends State<GestionnaireCreateDialog> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
final _nomController = TextEditingController();
|
|
||||||
final _prenomController = TextEditingController();
|
|
||||||
final _emailController = TextEditingController();
|
|
||||||
final _passwordController = TextEditingController();
|
|
||||||
final _telephoneController = TextEditingController();
|
|
||||||
|
|
||||||
bool _isSubmitting = false;
|
|
||||||
bool _obscurePassword = true;
|
|
||||||
bool _isLoadingRelais = true;
|
|
||||||
List<RelaisModel> _relais = [];
|
|
||||||
String? _selectedRelaisId;
|
|
||||||
bool get _isEditMode => widget.initialUser != null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final user = widget.initialUser;
|
|
||||||
if (user != null) {
|
|
||||||
_nomController.text = user.nom ?? '';
|
|
||||||
_prenomController.text = user.prenom ?? '';
|
|
||||||
_emailController.text = user.email;
|
|
||||||
_telephoneController.text = user.telephone ?? '';
|
|
||||||
// En édition, on ne préremplit jamais le mot de passe.
|
|
||||||
_passwordController.clear();
|
|
||||||
final initialRelaisId = user.relaisId?.trim();
|
|
||||||
_selectedRelaisId =
|
|
||||||
(initialRelaisId == null || initialRelaisId.isEmpty)
|
|
||||||
? null
|
|
||||||
: initialRelaisId;
|
|
||||||
}
|
|
||||||
_loadRelais();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nomController.dispose();
|
|
||||||
_prenomController.dispose();
|
|
||||||
_emailController.dispose();
|
|
||||||
_passwordController.dispose();
|
|
||||||
_telephoneController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadRelais() async {
|
|
||||||
try {
|
|
||||||
final list = await RelaisService.getRelais();
|
|
||||||
if (!mounted) return;
|
|
||||||
final uniqueById = <String, RelaisModel>{};
|
|
||||||
for (final relais in list) {
|
|
||||||
uniqueById[relais.id] = relais;
|
|
||||||
}
|
|
||||||
|
|
||||||
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
|
||||||
if (_selectedRelaisId != null &&
|
|
||||||
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
|
||||||
final selected = uniqueById[_selectedRelaisId!];
|
|
||||||
if (selected != null) {
|
|
||||||
filtered.add(selected);
|
|
||||||
} else {
|
|
||||||
_selectedRelaisId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_relais = filtered;
|
|
||||||
_isLoadingRelais = false;
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_selectedRelaisId = null;
|
|
||||||
_relais = [];
|
|
||||||
_isLoadingRelais = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _required(String? value, String field) {
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
|
||||||
return '$field est requis';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validateEmail(String? value) {
|
|
||||||
final base = _required(value, 'Email');
|
|
||||||
if (base != null) return base;
|
|
||||||
final email = value!.trim();
|
|
||||||
final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
|
|
||||||
if (!ok) return 'Format email invalide';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validatePassword(String? value) {
|
|
||||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final base = _required(value, 'Mot de passe');
|
|
||||||
if (base != null) return base;
|
|
||||||
if (value!.trim().length < 6) return 'Minimum 6 caractères';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submit() async {
|
|
||||||
if (_isSubmitting) return;
|
|
||||||
if (!_formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_isEditMode) {
|
|
||||||
await UserService.updateGestionnaire(
|
|
||||||
gestionnaireId: widget.initialUser!.id,
|
|
||||||
nom: _nomController.text.trim(),
|
|
||||||
prenom: _prenomController.text.trim(),
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
telephone: _telephoneController.text.trim(),
|
|
||||||
relaisId: _selectedRelaisId,
|
|
||||||
password: _passwordController.text.trim().isEmpty
|
|
||||||
? null
|
|
||||||
: _passwordController.text,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await UserService.createGestionnaire(
|
|
||||||
nom: _nomController.text.trim(),
|
|
||||||
prenom: _prenomController.text.trim(),
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
password: _passwordController.text,
|
|
||||||
telephone: _telephoneController.text.trim(),
|
|
||||||
relaisId: _selectedRelaisId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
_isEditMode
|
|
||||||
? 'Gestionnaire modifié avec succès.'
|
|
||||||
: 'Gestionnaire créé avec succès.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
e.toString().replaceFirst('Exception: ', ''),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _delete() async {
|
|
||||||
if (!_isEditMode || _isSubmitting) return;
|
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Confirmer la suppression'),
|
|
||||||
content: Text(
|
|
||||||
'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(ctx).pop(false),
|
|
||||||
child: const Text('Annuler'),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.of(ctx).pop(true),
|
|
||||||
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
|
||||||
child: const Text('Supprimer'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed != true) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await UserService.deleteUser(widget.initialUser!.id);
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Gestionnaire supprimé.')),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return Scaffold(
|
||||||
title: Row(
|
appBar: AppBar(
|
||||||
children: [
|
title: const Text('Créer un gestionnaire'),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_isEditMode
|
|
||||||
? 'Modifier un gestionnaire'
|
|
||||||
: 'Créer un gestionnaire',
|
|
||||||
),
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('Formulaire de création de gestionnaire'),
|
||||||
),
|
),
|
||||||
if (_isEditMode)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
tooltip: 'Fermer',
|
|
||||||
onPressed: _isSubmitting
|
|
||||||
? null
|
|
||||||
: () => Navigator.of(context).pop(false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: SizedBox(
|
|
||||||
width: 620,
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildNomField()),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: _buildPrenomField()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildEmailField(),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildPasswordField()),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: _buildTelephoneField()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildRelaisField(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
if (_isEditMode) ...[
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: _isSubmitting ? null : _delete,
|
|
||||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
|
||||||
child: const Text('Supprimer'),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _isSubmitting ? null : _submit,
|
|
||||||
icon: _isSubmitting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.edit),
|
|
||||||
label: Text(_isSubmitting ? 'Modification...' : 'Modifier'),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed:
|
|
||||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Annuler'),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _isSubmitting ? null : _submit,
|
|
||||||
icon: _isSubmitting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.person_add_alt_1),
|
|
||||||
label: Text(_isSubmitting ? 'Création...' : 'Créer'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNomField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _nomController,
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Nom',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (v) => _required(v, 'Nom'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPrenomField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _prenomController,
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Prénom',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (v) => _required(v, 'Prénom'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmailField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _emailController,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Email',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: _validateEmail,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPasswordField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _passwordController,
|
|
||||||
obscureText: _obscurePassword,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
autofillHints: _isEditMode
|
|
||||||
? const <String>[]
|
|
||||||
: const [AutofillHints.newPassword],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: _isEditMode
|
|
||||||
? 'Nouveau mot de passe'
|
|
||||||
: 'Mot de passe',
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_obscurePassword = !_obscurePassword;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: _validatePassword,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTelephoneField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _telephoneController,
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Téléphone',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (v) => _required(v, 'Téléphone'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRelaisField() {
|
|
||||||
final selectedValue = _selectedRelaisId != null &&
|
|
||||||
_relais.any((relais) => relais.id == _selectedRelaisId)
|
|
||||||
? _selectedRelaisId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
DropdownButtonFormField<String?>(
|
|
||||||
isExpanded: true,
|
|
||||||
value: selectedValue,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Relais principal',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String?>(
|
|
||||||
value: null,
|
|
||||||
child: Text('Aucun relais'),
|
|
||||||
),
|
|
||||||
..._relais.map(
|
|
||||||
(relais) => DropdownMenuItem<String?>(
|
|
||||||
value: relais.id,
|
|
||||||
child: Text(relais.nom),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: _isLoadingRelais
|
|
||||||
? null
|
|
||||||
: (value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedRelaisId = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_isLoadingRelais) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const LinearProgressIndicator(minHeight: 2),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -18,13 +18,11 @@ class ApiConfig {
|
|||||||
static const String gestionnaires = '/gestionnaires';
|
static const String gestionnaires = '/gestionnaires';
|
||||||
static const String parents = '/parents';
|
static const String parents = '/parents';
|
||||||
static const String assistantesMaternelles = '/assistantes-maternelles';
|
static const String assistantesMaternelles = '/assistantes-maternelles';
|
||||||
static const String relais = '/relais';
|
|
||||||
|
|
||||||
// Configuration (admin)
|
// Configuration (admin)
|
||||||
static const String configuration = '/configuration';
|
static const String configuration = '/configuration';
|
||||||
static const String configurationSetupStatus = '/configuration/setup/status';
|
static const String configurationSetupStatus = '/configuration/setup/status';
|
||||||
static const String configurationSetupComplete =
|
static const String configurationSetupComplete = '/configuration/setup/complete';
|
||||||
'/configuration/setup/complete';
|
|
||||||
static const String configurationTestSmtp = '/configuration/test-smtp';
|
static const String configurationTestSmtp = '/configuration/test-smtp';
|
||||||
static const String configurationBulk = '/configuration/bulk';
|
static const String configurationBulk = '/configuration/bulk';
|
||||||
|
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:p_tits_pas/models/relais_model.dart';
|
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
|
||||||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
|
||||||
|
|
||||||
class RelaisService {
|
|
||||||
static Future<Map<String, String>> _headers() async {
|
|
||||||
final token = await TokenService.getToken();
|
|
||||||
return token != null
|
|
||||||
? ApiConfig.authHeaders(token)
|
|
||||||
: Map<String, String>.from(ApiConfig.headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _extractError(String body, String fallback) {
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(body);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final message = decoded['message'];
|
|
||||||
if (message is String && message.trim().isNotEmpty) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<RelaisModel>> getRelais() async {
|
|
||||||
final response = await http.get(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.relais}'),
|
|
||||||
headers: await _headers(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception(
|
|
||||||
_extractError(response.body, 'Erreur chargement relais'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<dynamic> data = jsonDecode(response.body);
|
|
||||||
return data
|
|
||||||
.whereType<Map<String, dynamic>>()
|
|
||||||
.map(RelaisModel.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<RelaisModel> createRelais(Map<String, dynamic> payload) async {
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.relais}'),
|
|
||||||
headers: await _headers(),
|
|
||||||
body: jsonEncode(payload),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 201 && response.statusCode != 200) {
|
|
||||||
throw Exception(
|
|
||||||
_extractError(response.body, 'Erreur création relais'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RelaisModel.fromJson(
|
|
||||||
jsonDecode(response.body) as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<RelaisModel> updateRelais(
|
|
||||||
String id,
|
|
||||||
Map<String, dynamic> payload,
|
|
||||||
) async {
|
|
||||||
final response = await http.patch(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.relais}/$id'),
|
|
||||||
headers: await _headers(),
|
|
||||||
body: jsonEncode(payload),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception(
|
|
||||||
_extractError(response.body, 'Erreur mise à jour relais'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RelaisModel.fromJson(
|
|
||||||
jsonDecode(response.body) as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> deleteRelais(String id) async {
|
|
||||||
final response = await http.delete(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.relais}/$id'),
|
|
||||||
headers: await _headers(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
||||||
throw Exception(
|
|
||||||
_extractError(response.body, 'Erreur suppression relais'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -29,52 +29,13 @@ class UserService {
|
|||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||||
throw Exception(
|
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement gestionnaires');
|
||||||
_toStr(err?['message']) ?? 'Erreur chargement gestionnaires');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<dynamic> data = jsonDecode(response.body);
|
final List<dynamic> data = jsonDecode(response.body);
|
||||||
return data.map((e) => AppUser.fromJson(e)).toList();
|
return data.map((e) => AppUser.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AppUser> createGestionnaire({
|
|
||||||
required String nom,
|
|
||||||
required String prenom,
|
|
||||||
required String email,
|
|
||||||
required String password,
|
|
||||||
required String telephone,
|
|
||||||
String? relaisId,
|
|
||||||
}) async {
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
|
||||||
headers: await _headers(),
|
|
||||||
body: jsonEncode(<String, dynamic>{
|
|
||||||
'nom': nom,
|
|
||||||
'prenom': prenom,
|
|
||||||
'email': email,
|
|
||||||
'password': password,
|
|
||||||
'telephone': telephone,
|
|
||||||
'cguAccepted': true,
|
|
||||||
'relaisId': relaisId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
|
||||||
final decoded = jsonDecode(response.body);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final message = decoded['message'];
|
|
||||||
if (message is List && message.isNotEmpty) {
|
|
||||||
throw Exception(message.join(' - '));
|
|
||||||
}
|
|
||||||
throw Exception(_toStr(message) ?? 'Erreur création gestionnaire');
|
|
||||||
}
|
|
||||||
throw Exception('Erreur création gestionnaire');
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
|
||||||
return AppUser.fromJson(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer la liste des parents
|
// Récupérer la liste des parents
|
||||||
static Future<List<ParentModel>> getParents() async {
|
static Future<List<ParentModel>> getParents() async {
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
@ -92,8 +53,7 @@ class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer la liste des assistantes maternelles
|
// Récupérer la liste des assistantes maternelles
|
||||||
static Future<List<AssistanteMaternelleModel>>
|
static Future<List<AssistanteMaternelleModel>> getAssistantesMaternelles() async {
|
||||||
getAssistantesMaternelles() async {
|
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}'),
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
@ -127,89 +87,8 @@ class UserService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// On garde un fallback vide pour ne pas bloquer l'UI admin.
|
print('Erreur chargement admins: $e');
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> updateGestionnaireRelais({
|
|
||||||
required String gestionnaireId,
|
|
||||||
required String? relaisId,
|
|
||||||
}) async {
|
|
||||||
final response = await http.patch(
|
|
||||||
Uri.parse(
|
|
||||||
'${ApiConfig.baseUrl}${ApiConfig.gestionnaires}/$gestionnaireId'),
|
|
||||||
headers: await _headers(),
|
|
||||||
body: jsonEncode(<String, dynamic>{'relaisId': relaisId}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
||||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
|
||||||
throw Exception(
|
|
||||||
_toStr(err?['message']) ?? 'Erreur rattachement relais au gestionnaire',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<AppUser> updateGestionnaire({
|
|
||||||
required String gestionnaireId,
|
|
||||||
required String nom,
|
|
||||||
required String prenom,
|
|
||||||
required String email,
|
|
||||||
required String telephone,
|
|
||||||
required String? relaisId,
|
|
||||||
String? password,
|
|
||||||
}) async {
|
|
||||||
final body = <String, dynamic>{
|
|
||||||
'nom': nom,
|
|
||||||
'prenom': prenom,
|
|
||||||
'email': email,
|
|
||||||
'telephone': telephone,
|
|
||||||
'relaisId': relaisId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (password != null && password.trim().isNotEmpty) {
|
|
||||||
body['password'] = password.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await http.patch(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}/$gestionnaireId'),
|
|
||||||
headers: await _headers(),
|
|
||||||
body: jsonEncode(body),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
final decoded = jsonDecode(response.body);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final message = decoded['message'];
|
|
||||||
if (message is List && message.isNotEmpty) {
|
|
||||||
throw Exception(message.join(' - '));
|
|
||||||
}
|
|
||||||
throw Exception(_toStr(message) ?? 'Erreur modification gestionnaire');
|
|
||||||
}
|
|
||||||
throw Exception('Erreur modification gestionnaire');
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
|
||||||
return AppUser.fromJson(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> deleteUser(String userId) async {
|
|
||||||
final response = await http.delete(
|
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId'),
|
|
||||||
headers: await _headers(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
||||||
final decoded = jsonDecode(response.body);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final message = decoded['message'];
|
|
||||||
if (message is List && message.isNotEmpty) {
|
|
||||||
throw Exception(message.join(' - '));
|
|
||||||
}
|
|
||||||
throw Exception(_toStr(message) ?? 'Erreur suppression utilisateur');
|
|
||||||
}
|
|
||||||
throw Exception('Erreur suppression utilisateur');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
|
||||||
|
|
||||||
class AdminManagementWidget extends StatefulWidget {
|
class AdminManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
const AdminManagementWidget({super.key});
|
||||||
|
|
||||||
const AdminManagementWidget({
|
|
||||||
super.key,
|
|
||||||
required this.searchQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminManagementWidget> createState() => _AdminManagementWidgetState();
|
State<AdminManagementWidget> createState() => _AdminManagementWidgetState();
|
||||||
@ -20,15 +13,21 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
List<AppUser> _admins = [];
|
List<AppUser> _admins = [];
|
||||||
|
List<AppUser> _filteredAdmins = [];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadAdmins();
|
_loadAdmins();
|
||||||
|
_searchController.addListener(_onSearchChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() => super.dispose();
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadAdmins() async {
|
Future<void> _loadAdmins() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -40,6 +39,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_admins = list;
|
_admins = list;
|
||||||
|
_filteredAdmins = list;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -51,41 +51,91 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _onSearchChanged() {
|
||||||
Widget build(BuildContext context) {
|
final query = _searchController.text.toLowerCase();
|
||||||
final query = widget.searchQuery.toLowerCase();
|
setState(() {
|
||||||
final filteredAdmins = _admins.where((u) {
|
_filteredAdmins = _admins.where((u) {
|
||||||
final name = u.fullName.toLowerCase();
|
final name = u.fullName.toLowerCase();
|
||||||
final email = u.email.toLowerCase();
|
final email = u.email.toLowerCase();
|
||||||
return name.contains(query) || email.contains(query);
|
return name.contains(query) || email.contains(query);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return UserList(
|
@override
|
||||||
isLoading: _isLoading,
|
Widget build(BuildContext context) {
|
||||||
error: _error,
|
return Padding(
|
||||||
isEmpty: filteredAdmins.isEmpty,
|
padding: const EdgeInsets.all(16),
|
||||||
emptyMessage: 'Aucun administrateur trouvé.',
|
child: Column(
|
||||||
itemCount: filteredAdmins.length,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
itemBuilder: (context, index) {
|
children: [
|
||||||
final user = filteredAdmins[index];
|
Row(
|
||||||
return AdminUserCard(
|
children: [
|
||||||
title: user.fullName,
|
Expanded(
|
||||||
subtitleLines: [
|
child: TextField(
|
||||||
user.email,
|
controller: _searchController,
|
||||||
'Rôle : ${user.role}',
|
decoration: const InputDecoration(
|
||||||
],
|
hintText: "Rechercher un administrateur...",
|
||||||
avatarUrl: user.photoUrl,
|
prefixIcon: Icon(Icons.search),
|
||||||
actions: [
|
border: OutlineInputBorder(),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.edit),
|
),
|
||||||
tooltip: 'Modifier',
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Modifier admin
|
// TODO: Créer admin
|
||||||
},
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text("Créer un admin"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_isLoading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_error != null)
|
||||||
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
|
else if (_filteredAdmins.isEmpty)
|
||||||
|
const Center(child: Text("Aucun administrateur trouvé."))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredAdmins.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = _filteredAdmins[index];
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: Text(user.fullName.isNotEmpty
|
||||||
|
? user.fullName[0].toUpperCase()
|
||||||
|
: 'A'),
|
||||||
|
),
|
||||||
|
title: Text(user.fullName.isNotEmpty
|
||||||
|
? user.fullName
|
||||||
|
: 'Sans nom'),
|
||||||
|
subtitle: Text(user.email),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
|
||||||
|
|
||||||
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
const AssistanteMaternelleManagementWidget({super.key});
|
||||||
final int? capacityMin;
|
|
||||||
|
|
||||||
const AssistanteMaternelleManagementWidget({
|
|
||||||
super.key,
|
|
||||||
required this.searchQuery,
|
|
||||||
this.capacityMin,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AssistanteMaternelleManagementWidget> createState() =>
|
State<AssistanteMaternelleManagementWidget> createState() =>
|
||||||
@ -25,15 +15,25 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
List<AssistanteMaternelleModel> _assistantes = [];
|
List<AssistanteMaternelleModel> _assistantes = [];
|
||||||
|
List<AssistanteMaternelleModel> _filteredAssistantes = [];
|
||||||
|
|
||||||
|
final TextEditingController _zoneController = TextEditingController();
|
||||||
|
final TextEditingController _capacityController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadAssistantes();
|
_loadAssistantes();
|
||||||
|
_zoneController.addListener(_filter);
|
||||||
|
_capacityController.addListener(_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() => super.dispose();
|
void dispose() {
|
||||||
|
_zoneController.dispose();
|
||||||
|
_capacityController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadAssistantes() async {
|
Future<void> _loadAssistantes() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -45,6 +45,7 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_assistantes = list;
|
_assistantes = list;
|
||||||
|
_filter();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -56,100 +57,117 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _filter() {
|
||||||
|
final zoneQuery = _zoneController.text.toLowerCase();
|
||||||
|
final capacityQuery = int.tryParse(_capacityController.text);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_filteredAssistantes = _assistantes.where((am) {
|
||||||
|
final matchesZone = zoneQuery.isEmpty ||
|
||||||
|
(am.residenceCity?.toLowerCase().contains(zoneQuery) ?? false);
|
||||||
|
final matchesCapacity = capacityQuery == null ||
|
||||||
|
(am.maxChildren != null && am.maxChildren! >= capacityQuery);
|
||||||
|
return matchesZone && matchesCapacity;
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final query = widget.searchQuery.toLowerCase();
|
return Padding(
|
||||||
final filteredAssistantes = _assistantes.where((am) {
|
padding: const EdgeInsets.all(16),
|
||||||
final matchesName = am.user.fullName.toLowerCase().contains(query) ||
|
child: Column(
|
||||||
am.user.email.toLowerCase().contains(query) ||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
(am.residenceCity?.toLowerCase().contains(query) ?? false);
|
children: [
|
||||||
final matchesCapacity = widget.capacityMin == null ||
|
// 🔎 Zone de filtre
|
||||||
(am.maxChildren != null && am.maxChildren! >= widget.capacityMin!);
|
_buildFilterSection(),
|
||||||
return matchesName && matchesCapacity;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return UserList(
|
const SizedBox(height: 16),
|
||||||
isLoading: _isLoading,
|
|
||||||
error: _error,
|
// 📋 Liste des assistantes
|
||||||
isEmpty: filteredAssistantes.isEmpty,
|
if (_isLoading)
|
||||||
emptyMessage: 'Aucune assistante maternelle trouvée.',
|
const Center(child: CircularProgressIndicator())
|
||||||
itemCount: filteredAssistantes.length,
|
else if (_error != null)
|
||||||
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
|
else if (_filteredAssistantes.isEmpty)
|
||||||
|
const Center(child: Text("Aucune assistante maternelle trouvée."))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredAssistantes.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final assistante = filteredAssistantes[index];
|
final assistante = _filteredAssistantes[index];
|
||||||
return AdminUserCard(
|
return Card(
|
||||||
title: assistante.user.fullName,
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
avatarUrl: assistante.user.photoUrl,
|
child: ListTile(
|
||||||
fallbackIcon: Icons.face,
|
leading: CircleAvatar(
|
||||||
subtitleLines: [
|
backgroundImage: assistante.user.photoUrl != null
|
||||||
assistante.user.email,
|
? NetworkImage(assistante.user.photoUrl!)
|
||||||
'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}',
|
: null,
|
||||||
],
|
child: assistante.user.photoUrl == null
|
||||||
actions: [
|
? const Icon(Icons.face)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Text(assistante.user.fullName.isNotEmpty
|
||||||
|
? assistante.user.fullName
|
||||||
|
: 'Sans nom'),
|
||||||
|
subtitle: Text(
|
||||||
|
"N° Agrément : ${assistante.approvalNumber ?? 'N/A'}\nZone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}"),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
tooltip: 'Modifier',
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_openAssistanteDetails(assistante);
|
// TODO: Ajouter modification
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Ajouter suppression
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openAssistanteDetails(AssistanteMaternelleModel assistante) {
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AdminDetailModal(
|
|
||||||
title: assistante.user.fullName.isEmpty
|
|
||||||
? 'Assistante maternelle'
|
|
||||||
: assistante.user.fullName,
|
|
||||||
subtitle: assistante.user.email,
|
|
||||||
fields: [
|
|
||||||
AdminDetailField(label: 'ID', value: _v(assistante.user.id)),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Numero agrement',
|
|
||||||
value: _v(assistante.approvalNumber),
|
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
|
||||||
label: 'Ville residence',
|
|
||||||
value: _v(assistante.residenceCity),
|
|
||||||
),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Capacite max',
|
|
||||||
value: assistante.maxChildren?.toString() ?? '-',
|
|
||||||
),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Places disponibles',
|
|
||||||
value: assistante.placesAvailable?.toString() ?? '-',
|
|
||||||
),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Telephone',
|
|
||||||
value: _v(assistante.user.telephone),
|
|
||||||
),
|
|
||||||
AdminDetailField(label: 'Adresse', value: _v(assistante.user.adresse)),
|
|
||||||
AdminDetailField(label: 'Ville', value: _v(assistante.user.ville)),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Code postal',
|
|
||||||
value: _v(assistante.user.codePostal),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onEdit: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Action Modifier a implementer')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDelete: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Action Supprimer a implementer')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _v(String? value) => (value == null || value.isEmpty) ? '-' : value;
|
Widget _buildFilterSection() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: TextField(
|
||||||
|
controller: _zoneController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Zone géographique",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: TextField(
|
||||||
|
controller: _capacityController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Capacité minimum",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AdminDetailField {
|
|
||||||
final String label;
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
const AdminDetailField({
|
|
||||||
required this.label,
|
|
||||||
required this.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdminDetailModal extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String? subtitle;
|
|
||||||
final List<AdminDetailField> fields;
|
|
||||||
final VoidCallback onEdit;
|
|
||||||
final VoidCallback onDelete;
|
|
||||||
|
|
||||||
const AdminDetailModal({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
this.subtitle,
|
|
||||||
required this.fields,
|
|
||||||
required this.onEdit,
|
|
||||||
required this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 620),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(18),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (subtitle != null && subtitle!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
subtitle!,
|
|
||||||
style: const TextStyle(color: Colors.black54),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: 'Fermer',
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(height: 1),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: fields
|
|
||||||
.map(
|
|
||||||
(field) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 180,
|
|
||||||
child: Text(
|
|
||||||
field.label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
field.value,
|
|
||||||
style: const TextStyle(color: Colors.black87),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: onDelete,
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
label: const Text('Supprimer'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: Colors.red.shade700,
|
|
||||||
side: BorderSide(color: Colors.red.shade300),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: onEdit,
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
label: const Text('Modifier'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AdminListState extends StatelessWidget {
|
|
||||||
final bool isLoading;
|
|
||||||
final String? error;
|
|
||||||
final bool isEmpty;
|
|
||||||
final String emptyMessage;
|
|
||||||
final Widget list;
|
|
||||||
|
|
||||||
const AdminListState({
|
|
||||||
super.key,
|
|
||||||
required this.isLoading,
|
|
||||||
required this.error,
|
|
||||||
required this.isEmpty,
|
|
||||||
required this.emptyMessage,
|
|
||||||
required this.list,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (isLoading) {
|
|
||||||
return const Expanded(
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
return Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'Erreur: $error',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
return Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(emptyMessage),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Expanded(child: list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AdminUserCard extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final List<String> subtitleLines;
|
|
||||||
final String? avatarUrl;
|
|
||||||
final IconData fallbackIcon;
|
|
||||||
final List<Widget> actions;
|
|
||||||
|
|
||||||
const AdminUserCard({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.subtitleLines,
|
|
||||||
this.avatarUrl,
|
|
||||||
this.fallbackIcon = Icons.person,
|
|
||||||
this.actions = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AdminUserCard> createState() => _AdminUserCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AdminUserCardState extends State<AdminUserCard> {
|
|
||||||
bool _isHovered = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final infoLine =
|
|
||||||
widget.subtitleLines.where((e) => e.trim().isNotEmpty).join(' ');
|
|
||||||
final actionsWidth =
|
|
||||||
widget.actions.isNotEmpty ? widget.actions.length * 30.0 : 0.0;
|
|
||||||
|
|
||||||
return MouseRegion(
|
|
||||||
onEnter: (_) => setState(() => _isHovered = true),
|
|
||||||
onExit: (_) => setState(() => _isHovered = false),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {},
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
hoverColor: const Color(0x149CC5C0),
|
|
||||||
child: Card(
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
side: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
backgroundColor: const Color(0xFFEDE5FA),
|
|
||||||
backgroundImage: widget.avatarUrl != null
|
|
||||||
? NetworkImage(widget.avatarUrl!)
|
|
||||||
: null,
|
|
||||||
child: widget.avatarUrl == null
|
|
||||||
? Icon(
|
|
||||||
widget.fallbackIcon,
|
|
||||||
size: 16,
|
|
||||||
color: const Color(0xFF6B3FA0),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.loose,
|
|
||||||
child: Text(
|
|
||||||
widget.title.isNotEmpty ? widget.title : 'Sans nom',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
infoLine,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black54,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.actions.isNotEmpty)
|
|
||||||
SizedBox(
|
|
||||||
width: actionsWidth,
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
duration: const Duration(milliseconds: 120),
|
|
||||||
opacity: _isHovered ? 1 : 0,
|
|
||||||
child: IgnorePointer(
|
|
||||||
ignoring: !_isHovered,
|
|
||||||
child: IconTheme(
|
|
||||||
data: const IconThemeData(size: 17),
|
|
||||||
child: IconButtonTheme(
|
|
||||||
data: IconButtonThemeData(
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
minimumSize: const Size(28, 28),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: widget.actions,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart';
|
|
||||||
|
|
||||||
class UserList extends StatelessWidget {
|
|
||||||
final bool isLoading;
|
|
||||||
final String? error;
|
|
||||||
final bool isEmpty;
|
|
||||||
final String emptyMessage;
|
|
||||||
final int itemCount;
|
|
||||||
final Widget Function(BuildContext context, int index) itemBuilder;
|
|
||||||
final EdgeInsetsGeometry padding;
|
|
||||||
|
|
||||||
const UserList({
|
|
||||||
super.key,
|
|
||||||
required this.isLoading,
|
|
||||||
required this.error,
|
|
||||||
required this.isEmpty,
|
|
||||||
required this.emptyMessage,
|
|
||||||
required this.itemCount,
|
|
||||||
required this.itemBuilder,
|
|
||||||
this.padding = const EdgeInsets.all(16),
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
AdminListState(
|
|
||||||
isLoading: isLoading,
|
|
||||||
error: error,
|
|
||||||
isEmpty: isEmpty,
|
|
||||||
emptyMessage: emptyMessage,
|
|
||||||
list: ListView.builder(
|
|
||||||
itemCount: itemCount,
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,8 +3,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
|
|
||||||
/// Barre du dashboard admin : onglets Gestion des utilisateurs | Paramètres + déconnexion.
|
/// Barre du dashboard admin : onglets Gestion des utilisateurs | Paramètres + déconnexion.
|
||||||
class DashboardAppBarAdmin extends StatelessWidget
|
class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget {
|
||||||
implements PreferredSizeWidget {
|
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
final ValueChanged<int> onTabChange;
|
final ValueChanged<int> onTabChange;
|
||||||
final bool setupCompleted;
|
final bool setupCompleted;
|
||||||
@ -37,8 +36,7 @@ class DashboardAppBarAdmin extends StatelessWidget
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildNavItem(context, 'Gestion des utilisateurs', 0,
|
_buildNavItem(context, 'Gestion des utilisateurs', 0, enabled: setupCompleted),
|
||||||
enabled: setupCompleted),
|
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
_buildNavItem(context, 'Paramètres', 1, enabled: true),
|
_buildNavItem(context, 'Paramètres', 1, enabled: true),
|
||||||
],
|
],
|
||||||
@ -80,8 +78,7 @@ class DashboardAppBarAdmin extends StatelessWidget
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNavItem(BuildContext context, String title, int index,
|
Widget _buildNavItem(BuildContext context, String title, int index, {bool enabled = true}) {
|
||||||
{bool enabled = true}) {
|
|
||||||
final bool isActive = index == selectedIndex;
|
final bool isActive = index == selectedIndex;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: enabled ? () => onTabChange(index) : null,
|
onTap: enabled ? () => onTabChange(index) : null,
|
||||||
@ -136,124 +133,11 @@ class DashboardAppBarAdmin extends StatelessWidget
|
|||||||
class DashboardUserManagementSubBar extends StatelessWidget {
|
class DashboardUserManagementSubBar extends StatelessWidget {
|
||||||
final int selectedSubIndex;
|
final int selectedSubIndex;
|
||||||
final ValueChanged<int> onSubTabChange;
|
final ValueChanged<int> onSubTabChange;
|
||||||
final TextEditingController searchController;
|
|
||||||
final String searchHint;
|
|
||||||
final Widget? filterControl;
|
|
||||||
final VoidCallback? onAddPressed;
|
|
||||||
final String addLabel;
|
|
||||||
|
|
||||||
const DashboardUserManagementSubBar({
|
const DashboardUserManagementSubBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.selectedSubIndex,
|
required this.selectedSubIndex,
|
||||||
required this.onSubTabChange,
|
required this.onSubTabChange,
|
||||||
required this.searchController,
|
|
||||||
required this.searchHint,
|
|
||||||
this.filterControl,
|
|
||||||
this.onAddPressed,
|
|
||||||
this.addLabel = '+ Ajouter',
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: 56,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildSubNavItem(context, 'Gestionnaires', 0),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildSubNavItem(context, 'Parents', 1),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildSubNavItem(context, 'Assistantes maternelles', 2),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildSubNavItem(context, 'Administrateurs', 3),
|
|
||||||
const SizedBox(width: 36),
|
|
||||||
_pillField(
|
|
||||||
width: 320,
|
|
||||||
child: TextField(
|
|
||||||
controller: searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: searchHint,
|
|
||||||
prefixIcon: const Icon(Icons.search, size: 18),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (filterControl != null) ...[
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_pillField(width: 150, child: filterControl!),
|
|
||||||
],
|
|
||||||
const Spacer(),
|
|
||||||
_buildAddButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _pillField({required double width, required Widget child}) {
|
|
||||||
return Container(
|
|
||||||
width: width,
|
|
||||||
height: 34,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
border: Border.all(color: Colors.black26),
|
|
||||||
),
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAddButton() {
|
|
||||||
return ElevatedButton.icon(
|
|
||||||
onPressed: onAddPressed,
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: Text(addLabel),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubNavItem(BuildContext context, String title, int index) {
|
|
||||||
final bool isActive = index == selectedSubIndex;
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => onSubTabChange(index),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: isActive ? null : Border.all(color: Colors.black26),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: isActive ? Colors.white : Colors.black87,
|
|
||||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sous-barre Paramètres : Paramètres généraux | Paramètres territoriaux.
|
|
||||||
class DashboardSettingsSubBar extends StatelessWidget {
|
|
||||||
final int selectedSubIndex;
|
|
||||||
final ValueChanged<int> onSubTabChange;
|
|
||||||
|
|
||||||
const DashboardSettingsSubBar({
|
|
||||||
Key? key,
|
|
||||||
required this.selectedSubIndex,
|
|
||||||
required this.onSubTabChange,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -269,9 +153,13 @@ class DashboardSettingsSubBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildSubNavItem(context, 'Paramètres généraux', 0),
|
_buildSubNavItem(context, 'Gestionnaires', 0),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_buildSubNavItem(context, 'Paramètres territoriaux', 1),
|
_buildSubNavItem(context, 'Parents', 1),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_buildSubNavItem(context, 'Assistantes maternelles', 2),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_buildSubNavItem(context, 'Administrateurs', 3),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
75
frontend/lib/widgets/admin/gestionnaire_card.dart
Normal file
75
frontend/lib/widgets/admin/gestionnaire_card.dart
Normal file
@ -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<String>(
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
|
||||||
|
|
||||||
class GestionnaireManagementWidget extends StatefulWidget {
|
class GestionnaireManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
const GestionnaireManagementWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
const GestionnaireManagementWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.searchQuery,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GestionnaireManagementWidget> createState() =>
|
State<GestionnaireManagementWidget> createState() =>
|
||||||
@ -23,15 +16,21 @@ class _GestionnaireManagementWidgetState
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
List<AppUser> _gestionnaires = [];
|
List<AppUser> _gestionnaires = [];
|
||||||
|
List<AppUser> _filteredGestionnaires = [];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadGestionnaires();
|
_loadGestionnaires();
|
||||||
|
_searchController.addListener(_onSearchChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() => super.dispose();
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadGestionnaires() async {
|
Future<void> _loadGestionnaires() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -39,10 +38,11 @@ class _GestionnaireManagementWidgetState
|
|||||||
_error = null;
|
_error = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final gestionnaires = await UserService.getGestionnaires();
|
final list = await UserService.getGestionnaires();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_gestionnaires = gestionnaires;
|
_gestionnaires = list;
|
||||||
|
_filteredGestionnaires = list;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -54,55 +54,71 @@ class _GestionnaireManagementWidgetState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openGestionnaireEditDialog(AppUser user) async {
|
void _onSearchChanged() {
|
||||||
final changed = await showDialog<bool>(
|
final query = _searchController.text.toLowerCase();
|
||||||
context: context,
|
setState(() {
|
||||||
barrierDismissible: false,
|
_filteredGestionnaires = _gestionnaires.where((u) {
|
||||||
builder: (dialogContext) {
|
|
||||||
return GestionnaireCreateDialog(initialUser: user);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (changed == true) {
|
|
||||||
await _loadGestionnaires();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final query = widget.searchQuery.toLowerCase();
|
|
||||||
final filteredGestionnaires = _gestionnaires.where((u) {
|
|
||||||
final name = u.fullName.toLowerCase();
|
final name = u.fullName.toLowerCase();
|
||||||
final email = u.email.toLowerCase();
|
final email = u.email.toLowerCase();
|
||||||
return name.contains(query) || email.contains(query);
|
return name.contains(query) || email.contains(query);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return UserList(
|
@override
|
||||||
isLoading: _isLoading,
|
Widget build(BuildContext context) {
|
||||||
error: _error,
|
return Padding(
|
||||||
isEmpty: filteredGestionnaires.isEmpty,
|
padding: const EdgeInsets.all(16),
|
||||||
emptyMessage: 'Aucun gestionnaire trouvé.',
|
child: Column(
|
||||||
itemCount: filteredGestionnaires.length,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
itemBuilder: (context, index) {
|
children: [
|
||||||
final user = filteredGestionnaires[index];
|
// 🔹 Barre du haut avec bouton
|
||||||
return AdminUserCard(
|
Row(
|
||||||
title: user.fullName,
|
children: [
|
||||||
avatarUrl: user.photoUrl,
|
Expanded(
|
||||||
subtitleLines: [
|
child: TextField(
|
||||||
user.email,
|
controller: _searchController,
|
||||||
'Statut : ${user.statut ?? 'Inconnu'}',
|
decoration: const InputDecoration(
|
||||||
'Relais : ${user.relaisNom ?? 'Non rattaché'}',
|
hintText: "Rechercher un gestionnaire...",
|
||||||
],
|
prefixIcon: Icon(Icons.search),
|
||||||
actions: [
|
border: OutlineInputBorder(),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.edit),
|
),
|
||||||
tooltip: 'Modifier',
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_openGestionnaireEditDialog(user);
|
// TODO: 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
|
||||||
|
if (_isLoading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_error != null)
|
||||||
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
|
else if (_filteredGestionnaires.isEmpty)
|
||||||
|
const Center(child: Text("Aucun gestionnaire trouvé."))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredGestionnaires.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = _filteredGestionnaires[index];
|
||||||
|
return GestionnaireCard(
|
||||||
|
name: user.fullName.isNotEmpty ? user.fullName : "Sans nom",
|
||||||
|
email: user.email,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/relais_management_panel.dart';
|
|
||||||
|
|
||||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||||
class ParametresPanel extends StatefulWidget {
|
class ParametresPanel extends StatefulWidget {
|
||||||
/// Si true, après sauvegarde on redirige vers le login (première config). Sinon on reste sur la page.
|
/// Si true, après sauvegarde on redirige vers le login (première config). Sinon on reste sur la page.
|
||||||
final bool redirectToLoginAfterSave;
|
final bool redirectToLoginAfterSave;
|
||||||
final int selectedSettingsTabIndex;
|
|
||||||
|
|
||||||
const ParametresPanel({
|
const ParametresPanel({super.key, this.redirectToLoginAfterSave = false});
|
||||||
super.key,
|
|
||||||
this.redirectToLoginAfterSave = false,
|
|
||||||
this.selectedSettingsTabIndex = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ParametresPanel> createState() => _ParametresPanelState();
|
State<ParametresPanel> createState() => _ParametresPanelState();
|
||||||
@ -39,18 +33,10 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
|
|
||||||
void _createControllers() {
|
void _createControllers() {
|
||||||
final keys = [
|
final keys = [
|
||||||
'smtp_host',
|
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_password',
|
||||||
'smtp_port',
|
'email_from_name', 'email_from_address',
|
||||||
'smtp_user',
|
'app_name', 'app_url', 'app_logo_url',
|
||||||
'smtp_password',
|
'password_reset_token_expiry_days', 'jwt_expiry_hours', 'max_upload_size_mb',
|
||||||
'email_from_name',
|
|
||||||
'email_from_address',
|
|
||||||
'app_name',
|
|
||||||
'app_url',
|
|
||||||
'app_logo_url',
|
|
||||||
'password_reset_token_expiry_days',
|
|
||||||
'jwt_expiry_hours',
|
|
||||||
'max_upload_size_mb',
|
|
||||||
];
|
];
|
||||||
for (final k in keys) {
|
for (final k in keys) {
|
||||||
_controllers[k] = TextEditingController();
|
_controllers[k] = TextEditingController();
|
||||||
@ -107,29 +93,18 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
payload['smtp_auth_required'] = _smtpAuthRequired;
|
payload['smtp_auth_required'] = _smtpAuthRequired;
|
||||||
payload['smtp_user'] = _controllers['smtp_user']!.text.trim();
|
payload['smtp_user'] = _controllers['smtp_user']!.text.trim();
|
||||||
final pwd = _controllers['smtp_password']!.text.trim();
|
final pwd = _controllers['smtp_password']!.text.trim();
|
||||||
if (pwd.isNotEmpty && pwd != '***********') {
|
if (pwd.isNotEmpty && pwd != '***********') payload['smtp_password'] = pwd;
|
||||||
payload['smtp_password'] = pwd;
|
|
||||||
}
|
|
||||||
payload['email_from_name'] = _controllers['email_from_name']!.text.trim();
|
payload['email_from_name'] = _controllers['email_from_name']!.text.trim();
|
||||||
payload['email_from_address'] =
|
payload['email_from_address'] = _controllers['email_from_address']!.text.trim();
|
||||||
_controllers['email_from_address']!.text.trim();
|
|
||||||
payload['app_name'] = _controllers['app_name']!.text.trim();
|
payload['app_name'] = _controllers['app_name']!.text.trim();
|
||||||
payload['app_url'] = _controllers['app_url']!.text.trim();
|
payload['app_url'] = _controllers['app_url']!.text.trim();
|
||||||
payload['app_logo_url'] = _controllers['app_logo_url']!.text.trim();
|
payload['app_logo_url'] = _controllers['app_logo_url']!.text.trim();
|
||||||
final tokenDays = int.tryParse(
|
final tokenDays = int.tryParse(_controllers['password_reset_token_expiry_days']!.text.trim());
|
||||||
_controllers['password_reset_token_expiry_days']!.text.trim());
|
if (tokenDays != null) payload['password_reset_token_expiry_days'] = tokenDays;
|
||||||
if (tokenDays != null) {
|
final jwtHours = int.tryParse(_controllers['jwt_expiry_hours']!.text.trim());
|
||||||
payload['password_reset_token_expiry_days'] = tokenDays;
|
if (jwtHours != null) payload['jwt_expiry_hours'] = jwtHours;
|
||||||
}
|
|
||||||
final jwtHours =
|
|
||||||
int.tryParse(_controllers['jwt_expiry_hours']!.text.trim());
|
|
||||||
if (jwtHours != null) {
|
|
||||||
payload['jwt_expiry_hours'] = jwtHours;
|
|
||||||
}
|
|
||||||
final maxMb = int.tryParse(_controllers['max_upload_size_mb']!.text.trim());
|
final maxMb = int.tryParse(_controllers['max_upload_size_mb']!.text.trim());
|
||||||
if (maxMb != null) {
|
if (maxMb != null) payload['max_upload_size_mb'] = maxMb;
|
||||||
payload['max_upload_size_mb'] = maxMb;
|
|
||||||
}
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,10 +191,6 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.selectedSettingsTabIndex == 1) {
|
|
||||||
return const RelaisManagementPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@ -243,8 +214,7 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isSuccess = _message != null &&
|
final isSuccess = _message != null &&
|
||||||
(_message!.startsWith('Configuration') ||
|
(_message!.startsWith('Configuration') || _message!.startsWith('Connexion'));
|
||||||
_message!.startsWith('Connexion'));
|
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -267,18 +237,9 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildField(
|
_buildField('smtp_host', 'Serveur SMTP', hint: 'mail.example.com'),
|
||||||
'smtp_host',
|
|
||||||
'Serveur SMTP',
|
|
||||||
hint: 'mail.example.com',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('smtp_port', 'Port SMTP', keyboard: TextInputType.number, hint: '25, 465, 587'),
|
||||||
'smtp_port',
|
|
||||||
'Port SMTP',
|
|
||||||
keyboard: TextInputType.number,
|
|
||||||
hint: '25, 465, 587',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 14),
|
padding: const EdgeInsets.only(bottom: 14),
|
||||||
@ -286,17 +247,14 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: _smtpSecure,
|
value: _smtpSecure,
|
||||||
onChanged: (v) =>
|
onChanged: (v) => setState(() => _smtpSecure = v ?? false),
|
||||||
setState(() => _smtpSecure = v ?? false),
|
|
||||||
activeColor: const Color(0xFF9CC5C0),
|
activeColor: const Color(0xFF9CC5C0),
|
||||||
),
|
),
|
||||||
const Text('SSL/TLS (secure)'),
|
const Text('SSL/TLS (secure)'),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: _smtpAuthRequired,
|
value: _smtpAuthRequired,
|
||||||
onChanged: (v) => setState(
|
onChanged: (v) => setState(() => _smtpAuthRequired = v ?? false),
|
||||||
() => _smtpAuthRequired = v ?? false,
|
|
||||||
),
|
|
||||||
activeColor: const Color(0xFF9CC5C0),
|
activeColor: const Color(0xFF9CC5C0),
|
||||||
),
|
),
|
||||||
const Text('Authentification requise'),
|
const Text('Authentification requise'),
|
||||||
@ -305,19 +263,11 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
),
|
),
|
||||||
_buildField('smtp_user', 'Utilisateur SMTP'),
|
_buildField('smtp_user', 'Utilisateur SMTP'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('smtp_password', 'Mot de passe SMTP', obscure: true),
|
||||||
'smtp_password',
|
|
||||||
'Mot de passe SMTP',
|
|
||||||
obscure: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField('email_from_name', 'Nom expéditeur'),
|
_buildField('email_from_name', 'Nom expéditeur'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('email_from_address', 'Email expéditeur', hint: 'no-reply@example.com'),
|
||||||
'email_from_address',
|
|
||||||
'Email expéditeur',
|
|
||||||
hint: 'no-reply@example.com',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@ -327,13 +277,8 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
label: const Text('Tester la connexion SMTP'),
|
label: const Text('Tester la connexion SMTP'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF2D6A4F),
|
foregroundColor: const Color(0xFF2D6A4F),
|
||||||
side: const BorderSide(
|
side: const BorderSide(color: Color(0xFF9CC5C0)),
|
||||||
color: Color(0xFF9CC5C0),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -350,17 +295,9 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
children: [
|
children: [
|
||||||
_buildField('app_name', 'Nom de l\'application'),
|
_buildField('app_name', 'Nom de l\'application'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('app_url', 'URL de l\'application', hint: 'https://app.example.com'),
|
||||||
'app_url',
|
|
||||||
'URL de l\'application',
|
|
||||||
hint: 'https://app.example.com',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('app_logo_url', 'URL du logo', hint: '/assets/logo.png'),
|
||||||
'app_logo_url',
|
|
||||||
'URL du logo',
|
|
||||||
hint: '/assets/logo.png',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -372,23 +309,11 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildField(
|
_buildField('password_reset_token_expiry_days', 'Validité token MDP (jours)', keyboard: TextInputType.number),
|
||||||
'password_reset_token_expiry_days',
|
|
||||||
'Validité token MDP (jours)',
|
|
||||||
keyboard: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('jwt_expiry_hours', 'Validité session JWT (heures)', keyboard: TextInputType.number),
|
||||||
'jwt_expiry_hours',
|
|
||||||
'Validité session JWT (heures)',
|
|
||||||
keyboard: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildField(
|
_buildField('max_upload_size_mb', 'Taille max upload (MB)', keyboard: TextInputType.number),
|
||||||
'max_upload_size_mb',
|
|
||||||
'Taille max upload (MB)',
|
|
||||||
keyboard: TextInputType.number,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -402,14 +327,7 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
child: _isSaving
|
child: _isSaving
|
||||||
? const SizedBox(
|
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Sauvegarder la configuration'),
|
: const Text('Sauvegarder la configuration'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -421,8 +339,7 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionCard(BuildContext context,
|
Widget _buildSectionCard(BuildContext context, {required IconData icon, required String title, required Widget child}) {
|
||||||
{required IconData icon, required String title, required Widget child}) {
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
@ -452,8 +369,7 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildField(String key, String label,
|
Widget _buildField(String key, String label, {bool obscure = false, TextInputType? keyboard, String? hint}) {
|
||||||
{bool obscure = false, TextInputType? keyboard, String? hint}) {
|
|
||||||
final c = _controllers[key];
|
final c = _controllers[key];
|
||||||
if (c == null) return const SizedBox.shrink();
|
if (c == null) return const SizedBox.shrink();
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
@ -465,8 +381,7 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
|||||||
labelText: label,
|
labelText: label,
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/parent_model.dart';
|
import 'package:p_tits_pas/models/parent_model.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
|
||||||
|
|
||||||
class ParentManagementWidget extends StatefulWidget {
|
class ParentManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
const ParentManagementWidget({super.key});
|
||||||
final String? statusFilter;
|
|
||||||
|
|
||||||
const ParentManagementWidget({
|
|
||||||
super.key,
|
|
||||||
required this.searchQuery,
|
|
||||||
this.statusFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
|
State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
|
||||||
@ -23,15 +13,23 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
List<ParentModel> _parents = [];
|
List<ParentModel> _parents = [];
|
||||||
|
List<ParentModel> _filteredParents = [];
|
||||||
|
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
String? _selectedStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadParents();
|
_loadParents();
|
||||||
|
_searchController.addListener(_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() => super.dispose();
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadParents() async {
|
Future<void> _loadParents() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -43,6 +41,7 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_parents = list;
|
_parents = list;
|
||||||
|
_filter(); // Apply initial filter (if any)
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -54,101 +53,139 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _filter() {
|
||||||
Widget build(BuildContext context) {
|
final query = _searchController.text.toLowerCase();
|
||||||
final query = widget.searchQuery.toLowerCase();
|
setState(() {
|
||||||
final filteredParents = _parents.where((p) {
|
_filteredParents = _parents.where((p) {
|
||||||
final matchesName = p.user.fullName.toLowerCase().contains(query) ||
|
final matchesName = p.user.fullName.toLowerCase().contains(query) ||
|
||||||
p.user.email.toLowerCase().contains(query);
|
p.user.email.toLowerCase().contains(query);
|
||||||
final matchesStatus =
|
final matchesStatus = _selectedStatus == null ||
|
||||||
widget.statusFilter == null || p.user.statut == widget.statusFilter;
|
_selectedStatus == 'Tous' ||
|
||||||
|
(p.user.statut?.toLowerCase() == _selectedStatus?.toLowerCase());
|
||||||
|
|
||||||
|
// Mapping simple pour le statut affiché vs backend
|
||||||
|
// Backend: en_attente, actif, suspendu
|
||||||
|
// Dropdown: En attente, Actif, Suspendu
|
||||||
|
|
||||||
return matchesName && matchesStatus;
|
return matchesName && matchesStatus;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return UserList(
|
@override
|
||||||
isLoading: _isLoading,
|
Widget build(BuildContext context) {
|
||||||
error: _error,
|
return Padding(
|
||||||
isEmpty: filteredParents.isEmpty,
|
padding: const EdgeInsets.all(16),
|
||||||
emptyMessage: 'Aucun parent trouvé.',
|
child: Column(
|
||||||
itemCount: filteredParents.length,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSearchSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_isLoading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_error != null)
|
||||||
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
|
else if (_filteredParents.isEmpty)
|
||||||
|
const Center(child: Text("Aucun parent trouvé."))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredParents.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final parent = filteredParents[index];
|
final parent = _filteredParents[index];
|
||||||
return AdminUserCard(
|
return Card(
|
||||||
title: parent.user.fullName,
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
avatarUrl: parent.user.photoUrl,
|
child: ListTile(
|
||||||
subtitleLines: [
|
leading: CircleAvatar(
|
||||||
parent.user.email,
|
backgroundImage: parent.user.photoUrl != null
|
||||||
'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${parent.childrenCount}',
|
? NetworkImage(parent.user.photoUrl!)
|
||||||
],
|
: null,
|
||||||
actions: [
|
child: parent.user.photoUrl == null
|
||||||
|
? const Icon(Icons.person)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Text(parent.user.fullName.isNotEmpty
|
||||||
|
? parent.user.fullName
|
||||||
|
: 'Sans nom'),
|
||||||
|
subtitle: Text(
|
||||||
|
"${parent.user.email}\nStatut : ${parent.user.statut ?? 'Inconnu'} | Enfants : ${parent.childrenCount}",
|
||||||
|
),
|
||||||
|
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(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
tooltip: 'Modifier',
|
tooltip: "Modifier",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_openParentDetails(parent);
|
// TODO: Modifier parent
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
tooltip: "Supprimer",
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Supprimer compte
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _displayStatus(String? status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'actif':
|
|
||||||
return 'Actif';
|
|
||||||
case 'en_attente':
|
|
||||||
return 'En attente';
|
|
||||||
case 'suspendu':
|
|
||||||
return 'Suspendu';
|
|
||||||
default:
|
|
||||||
return 'Inconnu';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openParentDetails(ParentModel parent) {
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AdminDetailModal(
|
|
||||||
title: parent.user.fullName.isEmpty ? 'Parent' : parent.user.fullName,
|
|
||||||
subtitle: parent.user.email,
|
|
||||||
fields: [
|
|
||||||
AdminDetailField(label: 'ID', value: _v(parent.user.id)),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Statut',
|
|
||||||
value: _displayStatus(parent.user.statut),
|
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
|
||||||
label: 'Telephone',
|
|
||||||
value: _v(parent.user.telephone),
|
|
||||||
),
|
|
||||||
AdminDetailField(label: 'Adresse', value: _v(parent.user.adresse)),
|
|
||||||
AdminDetailField(label: 'Ville', value: _v(parent.user.ville)),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Code postal',
|
|
||||||
value: _v(parent.user.codePostal),
|
|
||||||
),
|
|
||||||
AdminDetailField(
|
|
||||||
label: 'Nombre d\'enfants',
|
|
||||||
value: parent.childrenCount.toString(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onEdit: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Action Modifier a implementer')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDelete: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Action Supprimer a implementer')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _v(String? value) => (value == null || value.isEmpty) ? '-' : value;
|
Widget _buildSearchSection() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 220,
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Nom du parent",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 220,
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Statut",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
value: _selectedStatus,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: null, child: Text("Tous")),
|
||||||
|
DropdownMenuItem(value: "actif", child: Text("Actif")),
|
||||||
|
DropdownMenuItem(value: "en_attente", child: Text("En attente")),
|
||||||
|
DropdownMenuItem(value: "suspendu", child: Text("Suspendu")),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStatus = value;
|
||||||
|
_filter();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,206 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
|
||||||
|
|
||||||
class AdminUserManagementPanel extends StatefulWidget {
|
|
||||||
const AdminUserManagementPanel({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AdminUserManagementPanel> createState() =>
|
|
||||||
_AdminUserManagementPanelState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AdminUserManagementPanelState extends State<AdminUserManagementPanel> {
|
|
||||||
int _subIndex = 0;
|
|
||||||
int _gestionnaireRefreshTick = 0;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
final TextEditingController _amCapacityController = TextEditingController();
|
|
||||||
String? _parentStatus;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_searchController.addListener(_onFilterChanged);
|
|
||||||
_amCapacityController.addListener(_onFilterChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_searchController.removeListener(_onFilterChanged);
|
|
||||||
_amCapacityController.removeListener(_onFilterChanged);
|
|
||||||
_searchController.dispose();
|
|
||||||
_amCapacityController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onFilterChanged() {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSubTabChange(int index) {
|
|
||||||
setState(() {
|
|
||||||
_subIndex = index;
|
|
||||||
_searchController.clear();
|
|
||||||
_parentStatus = null;
|
|
||||||
_amCapacityController.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String _searchHintForTab() {
|
|
||||||
switch (_subIndex) {
|
|
||||||
case 0:
|
|
||||||
return 'Rechercher un gestionnaire...';
|
|
||||||
case 1:
|
|
||||||
return 'Rechercher un parent...';
|
|
||||||
case 2:
|
|
||||||
return 'Rechercher une assistante...';
|
|
||||||
case 3:
|
|
||||||
return 'Rechercher un administrateur...';
|
|
||||||
default:
|
|
||||||
return 'Rechercher...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? _subBarFilterControl() {
|
|
||||||
if (_subIndex == 1) {
|
|
||||||
return DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String?>(
|
|
||||||
value: _parentStatus,
|
|
||||||
isExpanded: true,
|
|
||||||
hint: const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10),
|
|
||||||
child: Text('Statut', style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: null,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10),
|
|
||||||
child: Text('Tous', style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: 'actif',
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10),
|
|
||||||
child: Text('Actif', style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: 'en_attente',
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10),
|
|
||||||
child: Text('En attente', style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: 'suspendu',
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10),
|
|
||||||
child: Text('Suspendu', style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_parentStatus = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_subIndex == 2) {
|
|
||||||
return TextField(
|
|
||||||
controller: _amCapacityController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'Capacité min',
|
|
||||||
hintStyle: TextStyle(fontSize: 12),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBody() {
|
|
||||||
switch (_subIndex) {
|
|
||||||
case 0:
|
|
||||||
return GestionnaireManagementWidget(
|
|
||||||
key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'),
|
|
||||||
searchQuery: _searchController.text,
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
return ParentManagementWidget(
|
|
||||||
searchQuery: _searchController.text,
|
|
||||||
statusFilter: _parentStatus,
|
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return AssistanteMaternelleManagementWidget(
|
|
||||||
searchQuery: _searchController.text,
|
|
||||||
capacityMin: int.tryParse(_amCapacityController.text),
|
|
||||||
);
|
|
||||||
case 3:
|
|
||||||
return AdminManagementWidget(
|
|
||||||
searchQuery: _searchController.text,
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return const Center(child: Text('Page non trouvée'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
DashboardUserManagementSubBar(
|
|
||||||
selectedSubIndex: _subIndex,
|
|
||||||
onSubTabChange: _onSubTabChange,
|
|
||||||
searchController: _searchController,
|
|
||||||
searchHint: _searchHintForTab(),
|
|
||||||
filterControl: _subBarFilterControl(),
|
|
||||||
onAddPressed: _handleAddPressed,
|
|
||||||
addLabel: 'Ajouter',
|
|
||||||
),
|
|
||||||
Expanded(child: _buildBody()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleAddPressed() async {
|
|
||||||
if (_subIndex != 0) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'La création est disponible uniquement pour les gestionnaires.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final created = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (dialogContext) {
|
|
||||||
return const GestionnaireCreateDialog();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
if (created == true) {
|
|
||||||
setState(() {
|
|
||||||
_gestionnaireRefreshTick++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user