fix(#138): différer le rattachement AM de la fiche enfant au Sauvegarder.

Attach/détach AM restent locaux comme sur la fiche AM ; la sync API
n'a lieu qu'à l'enregistrement, avec rechargement du statut au retour
de la fiche AM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-07-17 13:12:46 +02:00
parent 90b185740c
commit 267fe63aec

View File

@ -42,10 +42,11 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
bool _dirty = false; bool _dirty = false;
bool _saving = false; bool _saving = false;
bool _deleting = false; bool _deleting = false;
bool _placementBusy = false;
bool _loadingAm = true; bool _loadingAm = true;
String? _currentUserRole; String? _currentUserRole;
AssistanteMaternelleModel? _linkedAm; AssistanteMaternelleModel? _linkedAm;
/// AM rattachée au chargement (pour sync différé au Sauvegarder).
String? _baselineAmUserId;
static const double _modalWidth = 930; static const double _modalWidth = 930;
static const double _mainRowHeight = 380; static const double _mainRowHeight = 380;
@ -74,7 +75,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
bool get _canDelete => bool get _canDelete =>
(_currentUserRole ?? '').toLowerCase() == 'super_admin'; (_currentUserRole ?? '').toLowerCase() == 'super_admin';
bool get _busy => _saving || _deleting || _placementBusy; bool get _busy => _saving || _deleting;
@override @override
void initState() { void initState() {
@ -113,11 +114,15 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_linkedAm = am; _linkedAm = am;
_baselineAmUserId = am?.user.id;
_loadingAm = false; _loadingAm = false;
}); });
} catch (_) { } catch (_) {
if (!mounted) return; if (!mounted) return;
setState(() => _loadingAm = false); setState(() {
_baselineAmUserId = null;
_loadingAm = false;
});
} }
} }
@ -248,6 +253,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!_dirty) return; if (!_dirty) return;
setState(() => _saving = true); setState(() => _saving = true);
try { try {
await _syncAmPlacement();
await UserService.updateEnfant( await UserService.updateEnfant(
enfantId: widget.enfant.id, enfantId: widget.enfant.id,
body: { body: {
@ -268,6 +275,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
setState(() { setState(() {
_dirty = false; _dirty = false;
_saving = false; _saving = false;
_baselineAmUserId = _linkedAm?.user.id;
}); });
widget.onSaved?.call(); widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -282,6 +290,25 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
} }
} }
/// Applique rattachement / détachement AM (différé jusqu'au Sauvegarder).
Future<void> _syncAmPlacement() async {
final baselineId = _baselineAmUserId;
final currentId = _linkedAm?.user.id;
if (baselineId != null && baselineId != currentId) {
await UserService.detachEnfantFromAm(
amUserId: baselineId,
enfantId: widget.enfant.id,
);
}
if (currentId != null && currentId != baselineId) {
await UserService.attachEnfantToAm(
amUserId: currentId,
enfantId: widget.enfant.id,
);
}
}
Future<void> _delete() async { Future<void> _delete() async {
if (!_canDelete || _deleting) return; if (!_canDelete || _deleting) return;
@ -338,14 +365,39 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
context: context, context: context,
builder: (ctx) => AdminAmEditModal( builder: (ctx) => AdminAmEditModal(
assistante: am, assistante: am,
onSaved: () { onSaved: () async {
_loadLinkedAm(); await _reloadPlacementFromServer();
widget.onSaved?.call(); widget.onSaved?.call();
}, },
), ),
); );
} }
/// Recharge AM + statut après une modale externe (ex. fiche AM).
Future<void> _reloadPlacementFromServer() async {
try {
final results = await Future.wait([
UserService.findAmForEnfant(widget.enfant.id),
UserService.getEnfant(widget.enfant.id),
]);
if (!mounted) return;
final am = results[0] as AssistanteMaternelleModel?;
final enfant = results[1] as EnfantAdminModel;
setState(() {
_linkedAm = am;
_baselineAmUserId = am?.user.id;
_status = normalizeEnfantStatus(enfant.status);
if (!enfantStatusValues.contains(_status)) {
_status = 'sans_garde';
}
_coerceGenderForStatus();
});
} catch (_) {
if (!mounted) return;
await _loadLinkedAm();
}
}
Future<void> _attachAm() async { Future<void> _attachAm() async {
List<AssistanteMaternelleModel> all; List<AssistanteMaternelleModel> all;
try { try {
@ -368,8 +420,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
return; return;
} }
final candidates = all; if (all.isEmpty) {
if (candidates.isEmpty) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucune assistante maternelle disponible')), const SnackBar(content: Text('Aucune assistante maternelle disponible')),
@ -382,7 +433,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
context: context, context: context,
builder: (ctx) => SimpleDialog( builder: (ctx) => SimpleDialog(
title: const Text('Choisir une assistante maternelle'), title: const Text('Choisir une assistante maternelle'),
children: candidates children: all
.map( .map(
(am) => SimpleDialogOption( (am) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, am), onPressed: () => Navigator.pop(ctx, am),
@ -394,41 +445,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
); );
if (selected == null || !mounted) return; if (selected == null || !mounted) return;
setState(() => _placementBusy = true); setState(() {
try { _linkedAm = selected;
// Sans garde : éventuel lien résiduel à clôturer avant rattachement. if (_status == 'sans_garde') {
final previousAm = _linkedAm; _status = 'garde';
if (previousAm != null) {
await UserService.detachEnfantFromAm(
amUserId: previousAm.user.id,
enfantId: widget.enfant.id,
);
} }
await UserService.attachEnfantToAm( _dirty = true;
amUserId: selected.user.id, });
enfantId: widget.enfant.id,
);
if (!mounted) return;
setState(() {
// Le back passe déjà sans_garde garde à l'attach ; on aligne l'UI.
if (_status == 'sans_garde') {
_status = 'garde';
}
});
await _loadLinkedAm();
widget.onSaved?.call();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Assistante maternelle rattachée')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
} finally {
if (mounted) setState(() => _placementBusy = false);
}
} }
Future<void> _detachAm() async { Future<void> _detachAm() async {
@ -440,7 +463,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Détacher l\'assistante'), title: const Text('Détacher l\'assistante'),
content: Text( content: Text(
'Retirer ${am.user.fullName} de la garde de cet enfant ?', 'Retirer ${am.user.fullName} de la garde de cet enfant ?\n'
'(Effectif après Sauvegarder.)',
), ),
actions: [ actions: [
TextButton( TextButton(
@ -457,57 +481,22 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
); );
if (confirmed != true || !mounted) return; if (confirmed != true || !mounted) return;
setState(() => _placementBusy = true); setState(() {
try { _linkedAm = null;
await UserService.detachEnfantFromAm( if (_status != 'a_naitre' && _status != 'scolarise') {
amUserId: am.user.id, _status = 'sans_garde';
enfantId: widget.enfant.id, }
); _dirty = true;
if (!mounted) return; });
setState(() {
// Le back repasse en sans_garde sauf a_naitre / scolarise.
if (_status != 'a_naitre' && _status != 'scolarise') {
_status = 'sans_garde';
}
_linkedAm = null;
});
widget.onSaved?.call();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Assistante maternelle détachée')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
} finally {
if (mounted) setState(() => _placementBusy = false);
}
} }
/// Passe en « Sans garde » : cadre vide + détachement AM si besoin. /// Passe en « Sans garde » : cadre vide local (détachement au Sauvegarder).
Future<void> _applySansGardeStatus() async { void _applySansGardeStatus() {
final am = _linkedAm; if (_linkedAm == null) return;
if (am == null) return; setState(() {
_linkedAm = null;
setState(() => _placementBusy = true); _dirty = true;
try { });
await UserService.detachEnfantFromAm(
amUserId: am.user.id,
enfantId: widget.enfant.id,
);
if (!mounted) return;
setState(() => _linkedAm = null);
widget.onSaved?.call();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
} finally {
if (mounted) setState(() => _placementBusy = false);
}
} }
InputDecoration _inputDecoration({String? hint}) { InputDecoration _inputDecoration({String? hint}) {
@ -952,7 +941,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
Widget _placementSection() { Widget _placementSection() {
if (!_showsAmPlacement) return _scolariseCard(); if (!_showsAmPlacement) return _scolariseCard();
if (_loadingAm || _placementBusy) { if (_loadingAm) {
return SizedBox( return SizedBox(
height: _placementHeight, height: _placementHeight,
width: double.infinity, width: double.infinity,