feat(#131): fiches parent/AM éditable, placement AM↔enfant, statuts garde/sans_garde

Squash merge develop → master.

- Fiche parent éditable (co-parent, PATCH fiche, GET /parents)
- Fiche AM 3 onglets (PATCH fiche, rattacher/détacher enfants)
- Table enfants_assistantes_maternelles + enum garde/sans_garde
- Migration SQL + BDD.sql canonique
- Correctifs recette : @Get() parents, DTO fiche AM, fix NIR

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 22:49:57 +02:00
co-authored by Cursor
parent b99745e0fe
commit 003fe6b762
69 changed files with 6290 additions and 417 deletions
@@ -4,6 +4,7 @@ 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/enfant_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/pending_validation_widget.dart';
@@ -28,6 +29,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
final TextEditingController _searchController = TextEditingController();
final TextEditingController _amCapacityController = TextEditingController();
String? _parentStatus;
String? _enfantStatus;
bool _hasPending = false;
bool _pendingLoading = true;
@@ -80,7 +82,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
}
List<String> get _tabLabels {
const base = ['Parents', 'Assistantes maternelles', 'Gestionnaires'];
const base = ['Parents', 'Enfants', '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).
@@ -94,11 +96,12 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
_subIndex = index.clamp(0, maxIndex);
_searchController.clear();
_parentStatus = null;
_enfantStatus = null;
_amCapacityController.clear();
});
}
/// Index du contenu : -1 = À valider (si visible), 0 = Parents, 1 = AM, 2 = Gestionnaires, 3 = Admin.
/// Index du contenu : -1 = À valider, 0 = Parents, 1 = Enfants, 2 = AM, 3 = Gestionnaires, 4 = Admin.
int get _contentIndexOffset => (_hasPending && !_pendingLoading) ? 1 : 0;
String _searchHintForTab() {
@@ -109,10 +112,12 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
case 0:
return 'Rechercher un parent...';
case 1:
return 'Rechercher une assistante...';
return 'Rechercher un enfant...';
case 2:
return 'Rechercher un gestionnaire...';
return 'Rechercher une assistante...';
case 3:
return 'Rechercher un gestionnaire...';
case 4:
return 'Rechercher un administrateur...';
default:
return 'Rechercher...';
@@ -176,6 +181,61 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
}
if (_subIndex == _contentIndexOffset + 1) {
return DropdownButtonHideUnderline(
child: DropdownButton<String?>(
value: _enfantStatus,
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: 'a_naitre',
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('À naître', style: TextStyle(fontSize: 12)),
),
),
DropdownMenuItem<String?>(
value: 'sans_garde',
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('Sans garde', style: TextStyle(fontSize: 12)),
),
),
DropdownMenuItem<String?>(
value: 'garde',
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('En garde', style: TextStyle(fontSize: 12)),
),
),
DropdownMenuItem<String?>(
value: 'scolarise',
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('Scolarisé', style: TextStyle(fontSize: 12)),
),
),
],
onChanged: (value) {
setState(() {
_enfantStatus = value;
});
},
),
);
}
if (_subIndex == _contentIndexOffset + 2) {
return TextField(
controller: _amCapacityController,
decoration: const InputDecoration(
@@ -203,16 +263,21 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
statusFilter: _parentStatus,
);
case 1:
return EnfantManagementWidget(
searchQuery: _searchController.text,
statusFilter: _enfantStatus,
);
case 2:
return AssistanteMaternelleManagementWidget(
searchQuery: _searchController.text,
capacityMin: int.tryParse(_amCapacityController.text),
);
case 2:
case 3:
return GestionnaireManagementWidget(
key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'),
searchQuery: _searchController.text,
);
case 3:
case 4:
return AdminManagementWidget(
key: ValueKey('admins-$_adminRefreshTick'),
searchQuery: _searchController.text,
@@ -246,7 +311,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
Future<void> _handleAddPressed() async {
final contentIndex = _subIndex - _contentIndexOffset;
if (contentIndex == 2) {
if (contentIndex == 3) {
final created = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -264,7 +329,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
return;
}
if (contentIndex == 3) {
if (contentIndex == 4) {
final created = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -289,7 +354,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'La création est disponible pour les gestionnaires et administrateurs.',
'La création parent / enfant / AM sera disponible avec le ticket #129.',
),
),
);