feat(#152/#155): cleanup est_multiple + dashboard sans préfixe Admin (squash develop).
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/.
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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/dashboard/child_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/enfant_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/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) => ChildDetailModal(
|
||||
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 EnfantUserCard.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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user