feat(admin): onglet À valider, dossier unifié et modales de validation

- Onglet « À valider » (AM + familles), pending-families et détail dossier par numéro.
- Wizards validation AM et famille, modale commune, chargement via GET /dossiers/:num.
- UI : cartes enfants, photo AM (cadre uniforme, ratio), NIR affiché formaté (espaces autour du tiret).
- Backend : DTO / routes parents pending ; scripts de test et mise à jour issue Gitea.

Tickets #107, #119.

Made-with: Cursor
This commit is contained in:
2026-03-26 00:15:18 +01:00
parent d832559027
commit 2e02f60601
36 changed files with 3303 additions and 173 deletions
@@ -1,10 +1,12 @@
import 'package:flutter/material.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/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';
import 'package:p_tits_pas/widgets/admin/pending_validation_widget.dart';
class UserManagementPanel extends StatefulWidget {
/// Afficher l'onglet Administrateurs (sinon 3 onglets : Gestionnaires, Parents, AM).
@@ -26,12 +28,41 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
final TextEditingController _searchController = TextEditingController();
final TextEditingController _amCapacityController = TextEditingController();
String? _parentStatus;
bool _hasPending = false;
bool _pendingLoading = true;
@override
void initState() {
super.initState();
_searchController.addListener(_onFilterChanged);
_amCapacityController.addListener(_onFilterChanged);
_loadPending();
}
Future<void> _loadPending() async {
try {
final am = await UserService.getPendingUsers(role: 'assistante_maternelle');
final families = await UserService.getPendingFamilies();
if (!mounted) return;
final hasPending = am.isNotEmpty || families.isNotEmpty;
setState(() {
final hadPending = _hasPending;
_hasPending = hasPending;
_pendingLoading = false;
// Si on passe à "plus de dossiers", recaler l'index (onglet À valider disparaît).
if (hadPending && !hasPending) {
_subIndex = (_subIndex > 0 ? _subIndex - 1 : 0).clamp(0, _tabLabels.length - 1);
} else if (!hadPending && hasPending) {
_subIndex = 0; // Afficher l'onglet À valider
}
});
} catch (_) {
if (!mounted) return;
setState(() {
_hasPending = false;
_pendingLoading = false;
});
}
}
@override
@@ -48,8 +79,17 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
setState(() {});
}
List<String> get _tabLabels {
const base = ['Parents', 'Assistantes maternelles', 'Gestionnaires'];
final withAdmin = [...base, 'Administrateurs'];
final list = widget.showAdministrateursTab ? withAdmin : base;
// Onglet « À valider » visible seulement s'il y a des dossiers en attente (ticket #107).
if (!_pendingLoading && _hasPending) return ['À valider', ...list];
return list;
}
void _onSubTabChange(int index) {
final maxIndex = widget.showAdministrateursTab ? 3 : 2;
final maxIndex = _tabLabels.length - 1;
setState(() {
_subIndex = index.clamp(0, maxIndex);
_searchController.clear();
@@ -58,14 +98,20 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
});
}
/// Index du contenu : -1 = À valider (si visible), 0 = Parents, 1 = AM, 2 = Gestionnaires, 3 = Admin.
int get _contentIndexOffset => (_hasPending && !_pendingLoading) ? 1 : 0;
String _searchHintForTab() {
switch (_subIndex) {
final contentIndex = _subIndex - _contentIndexOffset;
switch (contentIndex) {
case -1:
return 'À valider (pas de recherche)';
case 0:
return 'Rechercher un gestionnaire...';
case 1:
return 'Rechercher un parent...';
case 2:
case 1:
return 'Rechercher une assistante...';
case 2:
return 'Rechercher un gestionnaire...';
case 3:
return 'Rechercher un administrateur...';
default:
@@ -74,7 +120,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
}
Widget? _subBarFilterControl() {
if (_subIndex == 1) {
if (_subIndex == _contentIndexOffset + 0) {
return DropdownButtonHideUnderline(
child: DropdownButton<String?>(
value: _parentStatus,
@@ -122,7 +168,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
);
}
if (_subIndex == 2) {
if (_subIndex == _contentIndexOffset + 1) {
return TextField(
controller: _amCapacityController,
decoration: const InputDecoration(
@@ -139,22 +185,26 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
}
Widget _buildBody() {
switch (_subIndex) {
final contentIndex = _subIndex - _contentIndexOffset;
if (_hasPending && !_pendingLoading && contentIndex == -1) {
return PendingValidationWidget(onRefresh: _loadPending);
}
switch (contentIndex) {
case 0:
return GestionnaireManagementWidget(
key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'),
searchQuery: _searchController.text,
);
case 1:
return ParentManagementWidget(
searchQuery: _searchController.text,
statusFilter: _parentStatus,
);
case 2:
case 1:
return AssistanteMaternelleManagementWidget(
searchQuery: _searchController.text,
capacityMin: int.tryParse(_amCapacityController.text),
);
case 2:
return GestionnaireManagementWidget(
key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'),
searchQuery: _searchController.text,
);
case 3:
return AdminManagementWidget(
key: ValueKey('admins-$_adminRefreshTick'),
@@ -167,7 +217,8 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
@override
Widget build(BuildContext context) {
final subTabCount = widget.showAdministrateursTab ? 4 : 3;
final labels = _tabLabels;
final isAValiderTab = _hasPending && !_pendingLoading && _subIndex == 0;
return Column(
children: [
DashboardUserManagementSubBar(
@@ -176,9 +227,10 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
searchController: _searchController,
searchHint: _searchHintForTab(),
filterControl: _subBarFilterControl(),
onAddPressed: _handleAddPressed,
onAddPressed: isAValiderTab ? null : _handleAddPressed,
addLabel: 'Ajouter',
subTabCount: subTabCount,
subTabCount: labels.length,
tabLabels: labels,
),
Expanded(child: _buildBody()),
],
@@ -186,7 +238,8 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
}
Future<void> _handleAddPressed() async {
if (_subIndex == 0) {
final contentIndex = _subIndex - _contentIndexOffset;
if (contentIndex == 2) {
final created = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -204,7 +257,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
return;
}
if (_subIndex == 3) {
if (contentIndex == 3) {
final created = await showDialog<bool>(
context: context,
barrierDismissible: false,