Suppression complète grossesse multiple / est_multiple (BDD, API, front). Rename option C : widgets partagés et panels staff sous widgets/dashboard/, AdminManagementWidget seul restant dans widgets/admin/.
468 lines
14 KiB
Dart
468 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:p_tits_pas/models/dossier_list_item.dart';
|
||
|
||
/// Choix pour le dernier enfant d’un dossier famille (#160).
|
||
enum DernierEnfantSuppressionChoice {
|
||
enfantSeul,
|
||
dossierAussi,
|
||
}
|
||
|
||
/// Ligne d’impact (une personne / une fiche).
|
||
class SuppressionPersonLine {
|
||
final String label;
|
||
final IconData icon;
|
||
final String? role;
|
||
|
||
const SuppressionPersonLine({
|
||
required this.label,
|
||
this.icon = Icons.person_outline,
|
||
this.role,
|
||
});
|
||
|
||
factory SuppressionPersonLine.parent(String label) => SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.supervisor_account_outlined,
|
||
role: 'Parent',
|
||
);
|
||
|
||
factory SuppressionPersonLine.enfant(String label) => SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.child_care_outlined,
|
||
role: 'Enfant',
|
||
);
|
||
|
||
factory SuppressionPersonLine.am(String label) => SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.face,
|
||
role: 'AM',
|
||
);
|
||
|
||
factory SuppressionPersonLine.gestionnaire(String label) =>
|
||
SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.assignment_ind_outlined,
|
||
role: 'Gestionnaire',
|
||
);
|
||
|
||
factory SuppressionPersonLine.administrateur(String label) =>
|
||
SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.manage_accounts_outlined,
|
||
role: 'Admin',
|
||
);
|
||
|
||
factory SuppressionPersonLine.relais(String label) => SuppressionPersonLine(
|
||
label: label,
|
||
icon: Icons.apartment_outlined,
|
||
role: 'Relais',
|
||
);
|
||
}
|
||
|
||
/// Widget unique pour toutes les boîtes de confirmation de suppression (#160).
|
||
class SuppressionConfirmDialog extends StatelessWidget {
|
||
final String title;
|
||
final String? subtitle;
|
||
final String? message;
|
||
final List<SuppressionPersonLine> people;
|
||
final List<String> footnotes;
|
||
final List<Widget> actions;
|
||
|
||
const SuppressionConfirmDialog({
|
||
super.key,
|
||
required this.title,
|
||
this.subtitle,
|
||
this.message,
|
||
this.people = const [],
|
||
this.footnotes = const [],
|
||
required this.actions,
|
||
});
|
||
|
||
/// Variante oui/non standard (Annuler / Supprimer).
|
||
static SuppressionConfirmDialog yesNo({
|
||
required String title,
|
||
String? subtitle,
|
||
String? message,
|
||
List<SuppressionPersonLine> people = const [],
|
||
List<String> footnotes = const [],
|
||
String confirmLabel = 'Supprimer',
|
||
required VoidCallback onCancel,
|
||
required VoidCallback onConfirm,
|
||
}) {
|
||
return SuppressionConfirmDialog(
|
||
title: title,
|
||
subtitle: subtitle,
|
||
message: message,
|
||
people: people,
|
||
footnotes: footnotes,
|
||
actions: [
|
||
TextButton(onPressed: onCancel, child: const Text('Annuler')),
|
||
FilledButton(
|
||
onPressed: onConfirm,
|
||
style: _dangerButtonStyle,
|
||
child: Text(confirmLabel),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
static ButtonStyle get _dangerButtonStyle => FilledButton.styleFrom(
|
||
backgroundColor: Colors.red.shade700,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return AlertDialog(
|
||
backgroundColor: const Color(0xFFF7F2FB),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
titlePadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
|
||
contentPadding: const EdgeInsets.fromLTRB(24, 12, 24, 8),
|
||
actionsPadding: const EdgeInsets.fromLTRB(16, 4, 16, 14),
|
||
title: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade50,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Icon(
|
||
Icons.delete_outline,
|
||
color: Colors.red.shade700,
|
||
size: 22,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: theme.textTheme.titleLarge?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
color: const Color(0xFF2F2F2F),
|
||
fontSize: 20,
|
||
),
|
||
),
|
||
if ((subtitle ?? '').trim().isNotEmpty) ...[
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
subtitle!.trim(),
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: const Color(0xFF6D4EA1),
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
content: SizedBox(
|
||
width: 420,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
if ((message ?? '').trim().isNotEmpty) ...[
|
||
Text(
|
||
message!.trim(),
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: Colors.black87,
|
||
height: 1.35,
|
||
),
|
||
),
|
||
if (people.isNotEmpty || footnotes.isNotEmpty)
|
||
const SizedBox(height: 12),
|
||
],
|
||
if (people.isNotEmpty) ...[
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.9),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE5D8F2)),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
child: Column(
|
||
children: [
|
||
for (var i = 0; i < people.length; i++) ...[
|
||
if (i > 0)
|
||
Divider(
|
||
height: 1,
|
||
indent: 44,
|
||
endIndent: 12,
|
||
color: Colors.grey.shade200,
|
||
),
|
||
_SuppressionPersonRow(line: people[i]),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
if (footnotes.isNotEmpty) const SizedBox(height: 12),
|
||
],
|
||
for (final note in footnotes)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 6),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 2),
|
||
child: Icon(
|
||
Icons.info_outline,
|
||
size: 16,
|
||
color: Colors.orange.shade800,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
note,
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: Colors.black54,
|
||
height: 1.35,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
actions: actions,
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SuppressionPersonRow extends StatelessWidget {
|
||
final SuppressionPersonLine line;
|
||
|
||
const _SuppressionPersonRow({required this.line});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
Icon(line.icon, size: 18, color: const Color(0xFF6D4EA1)),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(
|
||
line.label,
|
||
style: const TextStyle(
|
||
fontWeight: FontWeight.w600,
|
||
fontSize: 14,
|
||
color: Color(0xFF2F2F2F),
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
if ((line.role ?? '').trim().isNotEmpty) ...[
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFEDE5FA),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Text(
|
||
line.role!.trim(),
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFF6D4EA1),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Affiche [SuppressionConfirmDialog] et renvoie `true` si confirmé.
|
||
Future<bool> showSuppressionConfirmDialog(
|
||
BuildContext context, {
|
||
required String title,
|
||
String? subtitle,
|
||
String? message,
|
||
List<SuppressionPersonLine> people = const [],
|
||
List<String> footnotes = const [],
|
||
String confirmLabel = 'Supprimer',
|
||
}) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => SuppressionConfirmDialog.yesNo(
|
||
title: title,
|
||
subtitle: subtitle,
|
||
message: message,
|
||
people: people,
|
||
footnotes: footnotes,
|
||
confirmLabel: confirmLabel,
|
||
onCancel: () => Navigator.of(ctx).pop(false),
|
||
onConfirm: () => Navigator.of(ctx).pop(true),
|
||
),
|
||
);
|
||
return result == true;
|
||
}
|
||
|
||
/// Confirmation delete dossier famille / AM avec liste nominative.
|
||
Future<bool> showDossierSuppressionConfirmDialog(
|
||
BuildContext context, {
|
||
required String numeroDossier,
|
||
required bool isFamille,
|
||
required List<SuppressionPersonLine> people,
|
||
String? fallbackSummary,
|
||
}) {
|
||
final num = numeroDossier.trim();
|
||
if (isFamille) {
|
||
return showSuppressionConfirmDialog(
|
||
context,
|
||
title: 'Supprimer le dossier',
|
||
subtitle: 'Dossier famille $num',
|
||
people: people,
|
||
footnotes: people.isEmpty && (fallbackSummary ?? '').isNotEmpty
|
||
? [fallbackSummary!]
|
||
: const [
|
||
'Tous les comptes et fiches listés seront définitivement '
|
||
'supprimés.',
|
||
'Les placements AM des enfants seront clos.',
|
||
],
|
||
);
|
||
}
|
||
return showSuppressionConfirmDialog(
|
||
context,
|
||
title: 'Supprimer le dossier',
|
||
subtitle: 'Dossier AM $num',
|
||
people: people,
|
||
footnotes: const [
|
||
'Le compte et le dossier AM seront supprimés.',
|
||
'Les enfants accueillis ne seront pas supprimés '
|
||
'(placements clos).',
|
||
],
|
||
);
|
||
}
|
||
|
||
/// Dialog dernier enfant : deux actions métier.
|
||
Future<DernierEnfantSuppressionChoice?> showDernierEnfantSuppressionDialog(
|
||
BuildContext context, {
|
||
required String enfantName,
|
||
required String familleLabel,
|
||
required String numeroDossier,
|
||
String? amLabel,
|
||
}) {
|
||
final am = (amLabel ?? '').trim();
|
||
return showDialog<DernierEnfantSuppressionChoice>(
|
||
context: context,
|
||
builder: (ctx) => SuppressionConfirmDialog(
|
||
title: 'Dernier enfant du dossier',
|
||
subtitle: 'Dossier $numeroDossier'
|
||
'${familleLabel.isEmpty ? '' : ' · $familleLabel'}',
|
||
people: [SuppressionPersonLine.enfant(enfantName)],
|
||
footnotes: [
|
||
'« Enfant seulement » : le dossier reste sans enfant.',
|
||
'« Dossier aussi » : parents et dossier sont également '
|
||
'supprimés.',
|
||
if (am.isNotEmpty) 'Le placement chez $am sera clos.',
|
||
],
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(),
|
||
child: const Text('Annuler'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: () => Navigator.of(ctx).pop(
|
||
DernierEnfantSuppressionChoice.enfantSeul,
|
||
),
|
||
child: const Text('Enfant seulement'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () => Navigator.of(ctx).pop(
|
||
DernierEnfantSuppressionChoice.dossierAussi,
|
||
),
|
||
style: SuppressionConfirmDialog._dangerButtonStyle,
|
||
child: const Text('Dossier aussi'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Notes d’impact pour suppression d’un enfant (AM optionnelle).
|
||
List<String> enfantSuppressionFootnotes({
|
||
required String? numeroDossier,
|
||
required String familleLabel,
|
||
String? amLabel,
|
||
}) {
|
||
final notes = <String>[];
|
||
final num = (numeroDossier ?? '').trim();
|
||
final famille = familleLabel.trim();
|
||
final am = (amLabel ?? '').trim();
|
||
if (num.isEmpty) {
|
||
notes.add('Supprimer définitivement cette fiche enfant.');
|
||
} else {
|
||
notes.add(
|
||
'L’enfant sera retiré du dossier de '
|
||
'${famille.isEmpty ? 'la famille' : famille}.',
|
||
);
|
||
}
|
||
if (am.isNotEmpty) {
|
||
notes.add('Le placement chez $am sera clos.');
|
||
}
|
||
return notes;
|
||
}
|
||
|
||
/// Bouton poubelle compact pour les cartes liste.
|
||
Widget suppressionIconButton({
|
||
required VoidCallback? onPressed,
|
||
String tooltip = 'Supprimer',
|
||
}) {
|
||
return IconButton(
|
||
icon: Icon(Icons.delete_outline, color: Colors.red.shade700),
|
||
tooltip: tooltip,
|
||
onPressed: onPressed,
|
||
);
|
||
}
|
||
|
||
/// Construit les lignes parents / enfants depuis un dossier unifié.
|
||
List<SuppressionPersonLine> suppressionPeopleFromDossier({
|
||
required bool isFamille,
|
||
required List<({String nom, String prenom, String email})> parents,
|
||
required List<({String nom, String prenom})> enfants,
|
||
String? amName,
|
||
}) {
|
||
final lines = <SuppressionPersonLine>[];
|
||
if (isFamille) {
|
||
for (final p in parents) {
|
||
final label = formatDossierPersonLabel(
|
||
nom: p.nom,
|
||
prenom: p.prenom,
|
||
email: p.email,
|
||
);
|
||
if (label.isEmpty) continue;
|
||
lines.add(SuppressionPersonLine.parent(label));
|
||
}
|
||
for (final e in enfants) {
|
||
final label = formatDossierPersonLabel(nom: e.nom, prenom: e.prenom);
|
||
if (label.isEmpty) continue;
|
||
lines.add(SuppressionPersonLine.enfant(label));
|
||
}
|
||
} else if ((amName ?? '').trim().isNotEmpty) {
|
||
lines.add(SuppressionPersonLine.am(amName!.trim()));
|
||
}
|
||
return lines;
|
||
}
|