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.
This commit is contained in:
2026-09-10 22:43:33 +02:00
parent ea0e97d930
commit d6d8b299dd
34 changed files with 2467 additions and 174 deletions
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
import 'package:p_tits_pas/models/dossier_list_item.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/services/auth_service.dart';
@@ -9,11 +10,13 @@ import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/utils/date_display_utils.dart';
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
import 'package:p_tits_pas/utils/name_format_utils.dart';
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_am_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_famille_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
@@ -97,8 +100,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
_isUnborn ? 'Date prévisionnelle' : 'Date de naissance';
bool get _canDelete =>
!widget.isCreating &&
(_currentUserRole ?? '').toLowerCase() == 'super_admin';
!widget.isCreating && canDeleteMetier(_currentUserRole);
bool get _busy => _saving || _deleting;
@@ -506,37 +508,99 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!_canDelete || _deleting) return;
final name = _headerTitle();
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Supprimer l\'enfant'),
content: Text(
'Supprimer définitivement la fiche de $name ?\n'
'Cette action est irréversible.',
final enfant = widget.enfant;
if (enfant == null) return;
bool deleteDossierFlag = false;
String? numero;
String familleLabel = '';
bool isLast = false;
String? amLabel;
final linked = _linkedAm;
if (linked != null) {
final label = formatDossierPersonLabel(
nom: linked.user.nom,
prenom: linked.user.prenom,
email: linked.user.email,
);
if (label.isNotEmpty) amLabel = label;
} else {
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.isNotEmpty) {
try {
final parent = await UserService.getParent(parentId);
final num = (parent.user.numeroDossier ?? '').trim();
numero = num;
familleLabel = parent.user.fullName.isNotEmpty
? parent.user.fullName
: parent.user.email;
if (num.isNotEmpty) {
final dossier = await UserService.getDossier(num);
if (dossier.isFamily) {
isLast = dossier.asFamily.enfants.length <= 1;
final names = dossier.asFamily.parents
.map((p) => formatDossierPersonLabel(
nom: p.nom,
prenom: p.prenom,
email: p.email,
))
.where((s) => s.isNotEmpty)
.join(' - ');
if (names.isNotEmpty) familleLabel = names;
}
}
} catch (_) {}
}
if (!mounted) return;
if (isLast && (numero ?? '').isNotEmpty) {
final choice = await showDernierEnfantSuppressionDialog(
context,
enfantName: name,
familleLabel: familleLabel,
numeroDossier: numero!,
amLabel: amLabel,
);
if (choice == null || !mounted) return;
deleteDossierFlag =
choice == DernierEnfantSuppressionChoice.dossierAussi;
} else {
final confirmed = await showSuppressionConfirmDialog(
context,
title: 'Supprimer l\'enfant',
subtitle: (numero ?? '').isEmpty ? null : 'Dossier $numero',
people: [SuppressionPersonLine.enfant(name)],
footnotes: enfantSuppressionFootnotes(
numeroDossier: numero,
familleLabel: familleLabel,
amLabel: amLabel,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
child: const Text('Supprimer'),
),
],
),
);
if (confirmed != true || !mounted) return;
);
if (!confirmed || !mounted) return;
}
setState(() => _deleting = true);
try {
final id = widget.enfant?.id;
if (id == null || id.isEmpty) return;
await UserService.deleteEnfant(id);
await UserService.deleteEnfant(id, deleteDossier: deleteDossierFlag);
if (!mounted) return;
widget.onDeleted?.call();
widget.onSaved?.call();
@@ -548,7 +612,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!mounted) return;
setState(() => _deleting = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
SnackBar(
content: Text(
e is Exception
? e.toString().replaceFirst('Exception: ', '')
: 'Erreur suppression',
),
),
);
}
}