feat(#131): grille enfants AM, statuts garde et polish champs admin.

Aligne le front sur les statuts enfant back (garde/sans_garde), refond l’onglet Enfants accueillis en grille 2×2 avec correction des places, et unifie les champs validation éditables/lecture seule.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 23:01:41 +02:00
co-authored by Cursor
parent 53721ffbb3
commit 66c7f22280
14 changed files with 557 additions and 131 deletions
@@ -8,9 +8,9 @@ import 'package:p_tits_pas/utils/date_display_utils.dart';
import 'package:p_tits_pas/utils/nir_utils.dart';
import 'package:p_tits_pas/utils/phone_utils.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_children_capacity_grid.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_children_affiliation_panel.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
import 'package:p_tits_pas/widgets/common/identity_block.dart';
@@ -51,8 +51,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
late String _statut;
late bool _disponible;
late int? _placesAvailable;
late List<ParentChildSummary> _children;
late final ScrollController _childrenScrollCtrl;
bool _saving = false;
bool _dirty = false;
@@ -64,12 +64,24 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
static const double _proTabHeight = 300;
static const List<int> _photoProRowLayout = [2, 2, 2];
/// Hauteur onglet enfants : champs + titre + grille 2×2 (+ alerte places si besoin).
double _childrenTabHeight() {
const capacityFields = 72.0;
const titleSection = 40.0;
const inconsistencyExtra = 46.0;
var h = capacityFields +
titleSection +
AdminAmChildrenCapacityGrid.fixedHeight;
if (_placesInconsistent()) h += inconsistencyExtra;
return h + 4;
}
double _tabViewHeight(int index) {
switch (index) {
case 1:
return _proTabHeight;
case 2:
return 280;
return _childrenTabHeight();
default:
return 292;
}
@@ -110,8 +122,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
);
_statut = u.statut ?? 'en_attente';
_disponible = am.available ?? true;
_placesAvailable = am.placesAvailable;
_children = List.of(am.children);
_childrenScrollCtrl = ScrollController();
for (final c in [
_nomCtrl,
@@ -167,7 +179,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
]) {
c.dispose();
}
_childrenScrollCtrl.dispose();
super.dispose();
}
@@ -217,19 +228,27 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
bool _placesInconsistent() => amHasPlacesInconsistency(
maxChildren: _capaciteMax(),
placesAvailable: widget.assistante.placesAvailable,
placesAvailable: _placesAvailable,
childrenCount: _children.length,
);
String _placesDisplayValue() {
final stored = widget.assistante.placesAvailable;
if (stored != null) return stored.toString();
if (_placesAvailable != null) return _placesAvailable.toString();
return '';
}
void _applyPlacesCorrection() {
final expected = _computedPlacesAvailable();
if (expected == null) return;
setState(() {
_placesAvailable = expected;
_dirty = true;
});
}
String? _placesInconsistencyMessage() {
if (!_placesInconsistent()) return null;
final stored = widget.assistante.placesAvailable;
final stored = _placesAvailable;
final expected = _computedPlacesAvailable();
final storedLabel = stored?.toString() ?? 'non renseigné';
final expectedLabel = expected?.toString() ?? '';
@@ -289,6 +308,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
if (_frDateToIso(_dateAgrementCtrl.text) != null)
'agreement_date': _frDateToIso(_dateAgrementCtrl.text),
if (_capaciteMax() != null) 'max_children': _capaciteMax(),
if (_placesAvailable != widget.assistante.placesAvailable)
'places_available': _placesAvailable,
'available': _disponible,
},
);
@@ -363,9 +384,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
),
style: ValidationModalTheme.primaryElevatedStyle,
child: const Text('Détacher'),
),
],
@@ -470,13 +489,11 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
return ValidationFormGrid(
title: 'Dossier professionnel',
rowLayout: _photoProRowLayout,
compact: true,
fields: [
ValidationLabeledField(
label: 'NIR',
field: ValidationEditableField(
controller: _nirCtrl,
compact: true,
hintText: '15 chiffres',
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
@@ -487,7 +504,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
label: 'Date de naissance',
field: ValidationEditableField(
controller: _dateNaissanceCtrl,
compact: true,
hintText: 'jj/mm/aaaa',
),
),
@@ -495,28 +511,24 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
label: 'Ville de naissance',
field: ValidationEditableField(
controller: _lieuNaissanceVilleCtrl,
compact: true,
),
),
ValidationLabeledField(
label: 'Pays de naissance',
field: ValidationEditableField(
controller: _lieuNaissancePaysCtrl,
compact: true,
),
),
ValidationLabeledField(
label: 'N° Agrément',
field: ValidationEditableField(
controller: _agrementCtrl,
compact: true,
),
),
ValidationLabeledField(
label: 'Date d\'agrément',
field: ValidationEditableField(
controller: _dateAgrementCtrl,
compact: true,
hintText: 'jj/mm/aaaa',
),
),
@@ -583,7 +595,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ValidationEditableSection(
compact: true,
rowLayout: const [2],
fields: [
ValidationLabeledField(
@@ -592,14 +603,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
controller: _capaciteCtrl,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
compact: true,
),
),
ValidationLabeledField(
label: 'Places disponibles',
field: ValidationReadOnlyField(
value: _placesDisplayValue(),
compact: true,
error: inconsistent,
),
),
@@ -607,13 +616,37 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
),
if (inconsistent) ...[
const SizedBox(height: 6),
Text(
_placesInconsistencyMessage()!,
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
height: 1.3,
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 6,
runSpacing: 4,
children: [
Text(
_placesInconsistencyMessage()!,
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
height: 1.3,
),
),
OutlinedButton(
onPressed: _saving ? null : _applyPlacesCorrection,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red.shade800,
disabledForegroundColor: Colors.red.shade300,
side: BorderSide(color: Colors.red.shade400),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
minimumSize: const Size(0, 28),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
child: const Text('Mettre à jour'),
),
],
),
],
],
@@ -621,6 +654,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
}
Widget _childrenTab() {
final capacity = (_capaciteMax() ?? 4).clamp(1, 4);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -634,20 +668,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
'Enfants rattachés à cette assistante (accueil / garde).',
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
),
const SizedBox(height: 8),
Expanded(
child: AdminChildrenAffiliationPanel(
children: _children,
scrollController: _childrenScrollCtrl,
onOpen: _openChild,
onDetach: _detachChild,
emptyMessage: 'Aucun enfant rattaché à cette assistante',
),
AdminAmChildrenCapacityGrid(
children: _children,
capacity: capacity,
onOpen: _openChild,
onDetach: _detachChild,
),
],
);