fix(#131): polish fiche AM — champs pro, places, vigilance et photo.
Fiche pro entièrement éditable, places déclarées en lecture seule avec alerte rouge, icône vigilance sur la liste AM, et cadrage photo aligné sur le wizard validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
||||
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
||||
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';
|
||||
@@ -41,8 +42,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
late final TextEditingController _villeCtrl;
|
||||
late final TextEditingController _cpCtrl;
|
||||
late final TextEditingController _agrementCtrl;
|
||||
late final TextEditingController _nirCtrl;
|
||||
late final TextEditingController _dateNaissanceCtrl;
|
||||
late final TextEditingController _lieuNaissanceVilleCtrl;
|
||||
late final TextEditingController _lieuNaissancePaysCtrl;
|
||||
late final TextEditingController _dateAgrementCtrl;
|
||||
late final TextEditingController _capaciteCtrl;
|
||||
late final TextEditingController _placesCtrl;
|
||||
|
||||
late String _statut;
|
||||
late bool _disponible;
|
||||
@@ -87,12 +92,22 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
_villeCtrl = TextEditingController(text: u.ville ?? '');
|
||||
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
||||
_agrementCtrl = TextEditingController(text: am.approvalNumber ?? '');
|
||||
_nirCtrl = TextEditingController(text: _formatNirDisplay());
|
||||
_dateNaissanceCtrl = TextEditingController(
|
||||
text: formatIsoDateFr(u.dateNaissance, ifEmpty: ''),
|
||||
);
|
||||
_lieuNaissanceVilleCtrl = TextEditingController(
|
||||
text: u.lieuNaissanceVille ?? '',
|
||||
);
|
||||
_lieuNaissancePaysCtrl = TextEditingController(
|
||||
text: u.lieuNaissancePays ?? '',
|
||||
);
|
||||
_dateAgrementCtrl = TextEditingController(
|
||||
text: formatIsoDateFr(am.agreementDate, ifEmpty: ''),
|
||||
);
|
||||
_capaciteCtrl = TextEditingController(
|
||||
text: am.maxChildren?.toString() ?? '',
|
||||
);
|
||||
_placesCtrl = TextEditingController(
|
||||
text: am.placesAvailable?.toString() ?? '',
|
||||
);
|
||||
_statut = u.statut ?? 'en_attente';
|
||||
_disponible = am.available ?? true;
|
||||
_children = List.of(am.children);
|
||||
@@ -107,11 +122,16 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
_villeCtrl,
|
||||
_cpCtrl,
|
||||
_agrementCtrl,
|
||||
_nirCtrl,
|
||||
_dateNaissanceCtrl,
|
||||
_lieuNaissanceVilleCtrl,
|
||||
_lieuNaissancePaysCtrl,
|
||||
_dateAgrementCtrl,
|
||||
_capaciteCtrl,
|
||||
_placesCtrl,
|
||||
]) {
|
||||
c.addListener(_markDirty);
|
||||
}
|
||||
_capaciteCtrl.addListener(_onCapacityChanged);
|
||||
_nomCtrl.addListener(_onNameFieldChanged);
|
||||
_prenomCtrl.addListener(_onNameFieldChanged);
|
||||
_tabCtrl.addListener(_onTabChanged);
|
||||
@@ -124,6 +144,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
|
||||
void _onNameFieldChanged() => setState(() {});
|
||||
|
||||
void _onCapacityChanged() => setState(() {});
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabCtrl.dispose();
|
||||
@@ -136,8 +158,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
_villeCtrl,
|
||||
_cpCtrl,
|
||||
_agrementCtrl,
|
||||
_nirCtrl,
|
||||
_dateNaissanceCtrl,
|
||||
_lieuNaissanceVilleCtrl,
|
||||
_lieuNaissancePaysCtrl,
|
||||
_dateAgrementCtrl,
|
||||
_capaciteCtrl,
|
||||
_placesCtrl,
|
||||
]) {
|
||||
c.dispose();
|
||||
}
|
||||
@@ -175,11 +201,43 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
|
||||
String _formatNirDisplay() {
|
||||
final raw = widget.assistante.nir?.trim() ?? '';
|
||||
if (raw.isEmpty) return '–';
|
||||
if (raw.isEmpty) return '';
|
||||
final digits = nirToRaw(raw).toUpperCase();
|
||||
return digits.length == 15 ? formatNir(digits) : raw;
|
||||
}
|
||||
|
||||
String? _frDateToIso(String text) => parseFrDateToIso(text);
|
||||
|
||||
int? _capaciteMax() => _parseIntField(_capaciteCtrl);
|
||||
|
||||
int? _computedPlacesAvailable() => amExpectedPlacesAvailable(
|
||||
maxChildren: _capaciteMax(),
|
||||
childrenCount: _children.length,
|
||||
);
|
||||
|
||||
bool _placesInconsistent() => amHasPlacesInconsistency(
|
||||
maxChildren: _capaciteMax(),
|
||||
placesAvailable: widget.assistante.placesAvailable,
|
||||
childrenCount: _children.length,
|
||||
);
|
||||
|
||||
String _placesDisplayValue() {
|
||||
final stored = widget.assistante.placesAvailable;
|
||||
if (stored != null) return stored.toString();
|
||||
return '–';
|
||||
}
|
||||
|
||||
String? _placesInconsistencyMessage() {
|
||||
if (!_placesInconsistent()) return null;
|
||||
final stored = widget.assistante.placesAvailable;
|
||||
final expected = _computedPlacesAvailable();
|
||||
final storedLabel = stored?.toString() ?? 'non renseigné';
|
||||
final expectedLabel = expected?.toString() ?? '–';
|
||||
return 'Incohérence : l\'AM déclare $storedLabel place(s) disponible(s), '
|
||||
'le calcul (capacité ${_capaciteMax() ?? '–'} − ${_children.length} '
|
||||
'enfant(s) rattaché(s)) donne $expectedLabel.';
|
||||
}
|
||||
|
||||
int? _parseIntField(TextEditingController c) {
|
||||
final t = c.text.trim();
|
||||
if (t.isEmpty) return null;
|
||||
@@ -223,10 +281,14 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
'code_postal': _cpCtrl.text.trim(),
|
||||
'statut': _statut,
|
||||
'approval_number': _agrementCtrl.text.trim(),
|
||||
if (_parseIntField(_capaciteCtrl) != null)
|
||||
'max_children': _parseIntField(_capaciteCtrl),
|
||||
if (_parseIntField(_placesCtrl) != null)
|
||||
'places_available': _parseIntField(_placesCtrl),
|
||||
'nir': nirToRaw(_nirCtrl.text),
|
||||
if (_frDateToIso(_dateNaissanceCtrl.text) != null)
|
||||
'date_naissance': _frDateToIso(_dateNaissanceCtrl.text),
|
||||
'lieu_naissance_ville': _lieuNaissanceVilleCtrl.text.trim(),
|
||||
'lieu_naissance_pays': _lieuNaissancePaysCtrl.text.trim(),
|
||||
if (_frDateToIso(_dateAgrementCtrl.text) != null)
|
||||
'agreement_date': _frDateToIso(_dateAgrementCtrl.text),
|
||||
if (_capaciteMax() != null) 'max_children': _capaciteMax(),
|
||||
'available': _disponible,
|
||||
},
|
||||
);
|
||||
@@ -388,8 +450,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
}
|
||||
}
|
||||
|
||||
String _v(String? s) =>
|
||||
(s != null && s.trim().isNotEmpty) ? s.trim() : '–';
|
||||
|
||||
Widget _identityTab() {
|
||||
return SingleChildScrollView(
|
||||
@@ -407,7 +467,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
}
|
||||
|
||||
Widget _proFieldsGrid() {
|
||||
final u = widget.assistante.user;
|
||||
return ValidationFormGrid(
|
||||
title: 'Dossier professionnel',
|
||||
rowLayout: _photoProRowLayout,
|
||||
@@ -415,29 +474,34 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'NIR',
|
||||
field: ValidationReadOnlyField(
|
||||
value: _formatNirDisplay(),
|
||||
field: ValidationEditableField(
|
||||
controller: _nirCtrl,
|
||||
compact: true,
|
||||
hintText: '15 chiffres',
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Date de naissance',
|
||||
field: ValidationReadOnlyField(
|
||||
value: formatIsoDateFr(u.dateNaissance),
|
||||
field: ValidationEditableField(
|
||||
controller: _dateNaissanceCtrl,
|
||||
compact: true,
|
||||
hintText: 'jj/mm/aaaa',
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Ville de naissance',
|
||||
field: ValidationReadOnlyField(
|
||||
value: _v(u.lieuNaissanceVille),
|
||||
field: ValidationEditableField(
|
||||
controller: _lieuNaissanceVilleCtrl,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Pays de naissance',
|
||||
field: ValidationReadOnlyField(
|
||||
value: _v(u.lieuNaissancePays),
|
||||
field: ValidationEditableField(
|
||||
controller: _lieuNaissancePaysCtrl,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
@@ -450,9 +514,10 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Date d\'agrément',
|
||||
field: ValidationReadOnlyField(
|
||||
value: formatIsoDateFr(widget.assistante.agreementDate),
|
||||
field: ValidationEditableField(
|
||||
controller: _dateAgrementCtrl,
|
||||
compact: true,
|
||||
hintText: 'jj/mm/aaaa',
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -513,76 +578,78 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
}
|
||||
|
||||
Widget _childrenCapacityFields() {
|
||||
return ValidationEditableSection(
|
||||
compact: true,
|
||||
rowLayout: const [2],
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'Capacité max (enfants)',
|
||||
field: ValidationEditableField(
|
||||
controller: _capaciteCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
compact: true,
|
||||
),
|
||||
final inconsistent = _placesInconsistent();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ValidationEditableSection(
|
||||
compact: true,
|
||||
rowLayout: const [2],
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'Capacité max (enfants)',
|
||||
field: ValidationEditableField(
|
||||
controller: _capaciteCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Places disponibles',
|
||||
field: ValidationReadOnlyField(
|
||||
value: _placesDisplayValue(),
|
||||
compact: true,
|
||||
error: inconsistent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Places disponibles',
|
||||
field: ValidationEditableField(
|
||||
controller: _placesCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
compact: true,
|
||||
if (inconsistent) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
_placesInconsistencyMessage()!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red.shade700,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _childrenTab() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const headerBlock = 88.0;
|
||||
const capacityBlock = 56.0;
|
||||
const gaps = 20.0;
|
||||
final listHeight = (constraints.maxHeight -
|
||||
headerBlock -
|
||||
capacityBlock -
|
||||
gaps)
|
||||
.clamp(72.0, AdminChildrenAffiliationPanel.defaultViewportHeight);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_childrenCapacityFields(),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Enfants accueillis : ${_children.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
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(
|
||||
height: listHeight,
|
||||
children: _children,
|
||||
scrollController: _childrenScrollCtrl,
|
||||
onOpen: _openChild,
|
||||
onDetach: _detachChild,
|
||||
emptyMessage: 'Aucun enfant rattaché à cette assistante',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_childrenCapacityFields(),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Enfants accueillis : ${_children.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
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',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user