feat(#157): alerte enfants sans responsable + rattachement foyer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 19:02:21 +02:00
co-authored by Cursor
parent 7a3d997ff9
commit 2ececa711b
7 changed files with 123 additions and 9 deletions
@@ -65,6 +65,9 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
/// Famille choisie en mode création (#132).
AdminFamilleFoyer? _selectedFamily;
/// Liens parents locaux (édition) — mis à jour après rattachement foyer (#157).
List<EnfantParentLink>? _localParentLinks;
/// Photo locale (création) — upload multipart `photo`.
Uint8List? _photoBytes;
String? _photoFilename;
@@ -262,10 +265,14 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
}
List<EnfantParentLink> get _parentLinks =>
(widget.enfant?.parentLinks ?? const <EnfantParentLink>[])
(_localParentLinks ??
widget.enfant?.parentLinks ??
const <EnfantParentLink>[])
.where((l) => l.parentId.trim().isNotEmpty)
.toList();
bool get _isOrphan => !widget.isCreating && _parentLinks.isEmpty;
Future<void> _openParent(EnfantParentLink link) async {
if (_busy) return;
final id = link.parentId.trim();
@@ -580,6 +587,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!enfantStatusValues.contains(_status)) {
_status = 'sans_garde';
}
_localParentLinks = enfant.parentLinks;
_coerceGenderForStatus();
});
} catch (_) {
@@ -960,7 +968,9 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
padding: const EdgeInsets.only(top: 16),
child: Align(
alignment: Alignment.bottomCenter,
child: _placementBlock(),
child: SingleChildScrollView(
child: _placementBlock(),
),
),
),
),
@@ -1219,6 +1229,24 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isOrphan) ...[
Text(
'Sélection du dossier de la famille',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.red.shade800,
),
),
const SizedBox(height: 4),
Text(
'Aucun responsable rattaché — choisissez un foyer',
style: TextStyle(fontSize: 12, color: Colors.red.shade700),
),
const SizedBox(height: 8),
_familyPlacementSection(),
const SizedBox(height: 16),
],
Text(
_placementTitle,
style: TextStyle(
@@ -1240,10 +1268,45 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
title: 'Choisir une famille',
);
if (selected == null || !mounted) return;
setState(() {
_selectedFamily = selected;
_dirty = true;
});
if (widget.isCreating) {
setState(() {
_selectedFamily = selected;
_dirty = true;
});
return;
}
// Édition orphelin (#157) : rattache immédiatement au foyer.
final enfantId = widget.enfant?.id;
if (enfantId == null || enfantId.isEmpty) return;
setState(() => _saving = true);
try {
for (final parentId in selected.parentUserIds) {
await UserService.attachEnfantToParent(
parentUserId: parentId,
enfantId: enfantId,
);
}
final refreshed = await UserService.getEnfant(enfantId);
if (!mounted) return;
setState(() {
_localParentLinks = refreshed.parentLinks;
_selectedFamily = selected;
_saving = false;
});
widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant rattaché au foyer')),
);
} catch (e) {
if (!mounted) return;
setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
}
Widget _familyPlacementSection() {