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 _saving = false;
bool _deleting = false;
bool _placementBusy = false;
bool _loadingAm = true;
String? _currentUserRole;
AssistanteMaternelleModel? _linkedAm;
/// AM rattachée au chargement (pour sync différé au Sauvegarder).
String? _baselineAmUserId;
static const double _modalWidth = 930;
static const double _mainRowHeight = 380;
@ -74,7 +75,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
bool get _canDelete =>
(_currentUserRole ?? '').toLowerCase() == 'super_admin';
bool get _busy => _saving || _deleting || _placementBusy;
bool get _busy => _saving || _deleting;
@override
void initState() {
@ -113,11 +114,15 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!mounted) return;
setState(() {
_linkedAm = am;
_baselineAmUserId = am?.user.id;
_loadingAm = false;
});
} catch (_) {
if (!mounted) return;
setState(() => _loadingAm = false);
setState(() {
_baselineAmUserId = null;
_loadingAm = false;
});
}
}
@ -248,6 +253,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!_dirty) return;
setState(() => _saving = true);
try {
await _syncAmPlacement();
await UserService.updateEnfant(
enfantId: widget.enfant.id,
body: {
@ -268,6 +275,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
setState(() {
_dirty = false;
_saving = false;
_baselineAmUserId = _linkedAm?.user.id;
});
widget.onSaved?.call();
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 {
if (!_canDelete || _deleting) return;
@ -338,14 +365,39 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
context: context,
builder: (ctx) => AdminAmEditModal(
assistante: am,
onSaved: () {
_loadLinkedAm();
onSaved: () async {
await _reloadPlacementFromServer();
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 {
List<AssistanteMaternelleModel> all;
try {
@ -368,8 +420,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
return;
}
final candidates = all;
if (candidates.isEmpty) {
if (all.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucune assistante maternelle disponible')),
@ -382,7 +433,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
context: context,
builder: (ctx) => SimpleDialog(
title: const Text('Choisir une assistante maternelle'),
children: candidates
children: all
.map(
(am) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, am),
@ -394,41 +445,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
);
if (selected == null || !mounted) return;
setState(() => _placementBusy = true);
try {
// Sans garde : éventuel lien résiduel à clôturer avant rattachement.
final previousAm = _linkedAm;
if (previousAm != null) {
await UserService.detachEnfantFromAm(
amUserId: previousAm.user.id,
enfantId: widget.enfant.id,
);
}
await UserService.attachEnfantToAm(
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.
_linkedAm = selected;
if (_status == 'sans_garde') {
_status = 'garde';
}
_dirty = true;
});
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 {
@ -440,7 +463,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
builder: (ctx) => AlertDialog(
title: const Text('Détacher l\'assistante'),
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: [
TextButton(
@ -457,57 +481,22 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
);
if (confirmed != true || !mounted) return;
setState(() => _placementBusy = true);
try {
await UserService.detachEnfantFromAm(
amUserId: am.user.id,
enfantId: widget.enfant.id,
);
if (!mounted) return;
setState(() {
// Le back repasse en sans_garde sauf a_naitre / scolarise.
_linkedAm = null;
if (_status != 'a_naitre' && _status != 'scolarise') {
_status = 'sans_garde';
}
_linkedAm = null;
_dirty = true;
});
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.
Future<void> _applySansGardeStatus() async {
final am = _linkedAm;
if (am == null) return;
setState(() => _placementBusy = 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);
}
/// Passe en « Sans garde » : cadre vide local (détachement au Sauvegarder).
void _applySansGardeStatus() {
if (_linkedAm == null) return;
setState(() {
_linkedAm = null;
_dirty = true;
});
}
InputDecoration _inputDecoration({String? hint}) {
@ -952,7 +941,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
Widget _placementSection() {
if (!_showsAmPlacement) return _scolariseCard();
if (_loadingAm || _placementBusy) {
if (_loadingAm) {
return SizedBox(
height: _placementHeight,
width: double.infinity,