Files
petitspas/frontend/lib/widgets/admin/enfant_management_widget.dart
T
jmartin d6d8b299dd feat(#159/#160): suppressions métier dashboard — API + UI (squash develop).
Matrice PO #154 : supprimer dossiers famille/AM, parents, enfants et comptes
staff avec règles métier (placements clôturés, sans_enfant, cascade foyer,
droits gestionnaire/admin). Poubelle en liste, dialogues de confirmation,
garde-fou gestionnaire→gestionnaire. Inclut polish hauteur modale fiche AM.
2026-09-10 22:43:33 +02:00

246 lines
7.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/dossier_list_item.dart';
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
class EnfantManagementWidget extends StatefulWidget {
final String searchQuery;
final String? statusFilter;
const EnfantManagementWidget({
super.key,
required this.searchQuery,
this.statusFilter,
});
@override
State<EnfantManagementWidget> createState() => _EnfantManagementWidgetState();
}
class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
bool _isLoading = false;
String? _error;
List<EnfantAdminModel> _enfants = [];
bool _canDelete = false;
@override
void initState() {
super.initState();
_loadRights();
_loadEnfants();
}
Future<void> _loadRights() async {
final user = await AuthService.getCurrentUser();
if (!mounted) return;
setState(() => _canDelete = canDeleteMetier(user?.role));
}
Future<void> _loadEnfants() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final list = await UserService.getEnfants();
if (!mounted) return;
setState(() {
_enfants = list;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _openEnfant(EnfantAdminModel enfant) async {
await showDialog<void>(
context: context,
builder: (ctx) => AdminChildDetailModal(
enfant: enfant,
onSaved: _loadEnfants,
onDeleted: _loadEnfants,
),
);
}
Future<({String? numero, String famille, bool isLast, String? amLabel})>
_resolveContext(
EnfantAdminModel enfant,
) async {
String? amLabel;
try {
final am = await UserService.findAmForEnfant(enfant.id);
if (am != null) {
final label = formatDossierPersonLabel(
nom: am.user.nom,
prenom: am.user.prenom,
email: am.user.email,
);
if (label.isNotEmpty) amLabel = label;
}
} catch (_) {}
final parentId = enfant.parentLinks
.map((l) => l.parentId.trim())
.firstWhere((id) => id.isNotEmpty, orElse: () => '');
if (parentId.isEmpty) {
return (numero: null, famille: '', isLast: true, amLabel: amLabel);
}
try {
final parent = await UserService.getParent(parentId);
final num = (parent.user.numeroDossier ?? '').trim();
final famille = parent.user.fullName.isNotEmpty
? parent.user.fullName
: parent.user.email;
if (num.isEmpty) {
return (
numero: null,
famille: famille,
isLast: true,
amLabel: amLabel,
);
}
final dossier = await UserService.getDossier(num);
if (!dossier.isFamily) {
return (
numero: num,
famille: famille,
isLast: true,
amLabel: amLabel,
);
}
final n = dossier.asFamily.enfants.length;
final names = dossier.asFamily.parents
.map((p) => formatDossierPersonLabel(
nom: p.nom,
prenom: p.prenom,
email: p.email,
))
.where((s) => s.isNotEmpty)
.join(' - ');
return (
numero: num,
famille: names.isNotEmpty ? names : famille,
isLast: n <= 1,
amLabel: amLabel,
);
} catch (_) {
return (numero: null, famille: '', isLast: false, amLabel: amLabel);
}
}
Future<void> _confirmDelete(EnfantAdminModel enfant) async {
final ctx = await _resolveContext(enfant);
if (!mounted) return;
bool deleteDossier = false;
if (ctx.isLast && (ctx.numero ?? '').isNotEmpty) {
final choice = await showDernierEnfantSuppressionDialog(
context,
enfantName: enfant.fullName,
familleLabel: ctx.famille,
numeroDossier: ctx.numero!,
amLabel: ctx.amLabel,
);
if (choice == null || !mounted) return;
deleteDossier =
choice == DernierEnfantSuppressionChoice.dossierAussi;
} else {
final confirmed = await showSuppressionConfirmDialog(
context,
title: 'Supprimer l\'enfant',
subtitle: (ctx.numero ?? '').isEmpty
? null
: 'Dossier ${ctx.numero}',
people: [SuppressionPersonLine.enfant(enfant.fullName)],
footnotes: enfantSuppressionFootnotes(
numeroDossier: ctx.numero,
familleLabel: ctx.famille,
amLabel: ctx.amLabel,
),
);
if (!confirmed || !mounted) return;
}
try {
final result = await UserService.deleteEnfant(
enfant.id,
deleteDossier: deleteDossier,
);
if (!mounted) return;
final msg = (result['message'] ?? 'Enfant supprimé.').toString();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
await _loadEnfants();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e is Exception
? e.toString().replaceFirst('Exception: ', '')
: 'Erreur suppression',
),
),
);
}
}
@override
Widget build(BuildContext context) {
final query = widget.searchQuery.toLowerCase();
final filtered = _enfants.where((e) {
final matchesName = e.fullName.toLowerCase().contains(query);
final matchesStatus = widget.statusFilter == null ||
normalizeEnfantStatus(e.status) ==
normalizeEnfantStatus(widget.statusFilter);
return matchesName && matchesStatus;
}).toList()
..sort((a, b) {
// Orphelins (#157) en tête, puis ordre alphabétique.
final ao = a.hasNoResponsable ? 0 : 1;
final bo = b.hasNoResponsable ? 0 : 1;
if (ao != bo) return ao.compareTo(bo);
return a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase());
});
return UserList(
isLoading: _isLoading,
error: _error,
isEmpty: filtered.isEmpty,
emptyMessage: 'Aucun enfant trouvé.',
itemCount: filtered.length,
itemBuilder: (context, index) {
final enfant = filtered[index];
return AdminEnfantUserCard.fromEnfant(
enfant,
onCardTap: () => _openEnfant(enfant),
actions: [
IconButton(
icon: const Icon(Icons.visibility_outlined),
tooltip: 'Voir / modifier',
onPressed: () => _openEnfant(enfant),
),
if (_canDelete)
suppressionIconButton(
onPressed: () => _confirmDelete(enfant),
),
],
);
},
);
}
}