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,422 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/dossier_list_item.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/models/pending_family.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||
|
||||
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
||||
class PendingValidationWidget extends StatefulWidget {
|
||||
final VoidCallback? onRefresh;
|
||||
/// Filtre client (n°, nom, email) — onglet Dossiers (#153).
|
||||
final String searchQuery;
|
||||
/// Si true et liste vide : message court (pas de grand vide centré).
|
||||
final bool compactWhenEmpty;
|
||||
/// Numéros des dossiers pending (pour exclure de « Tous les dossiers »).
|
||||
final ValueChanged<Set<String>>? onPendingNumerosChanged;
|
||||
/// Afficher la poubelle (#160) — mêmes règles que dossiers validés.
|
||||
final bool canDelete;
|
||||
|
||||
const PendingValidationWidget({
|
||||
super.key,
|
||||
this.onRefresh,
|
||||
this.searchQuery = '',
|
||||
this.compactWhenEmpty = false,
|
||||
this.onPendingNumerosChanged,
|
||||
this.canDelete = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PendingValidationWidget> createState() =>
|
||||
_PendingValidationWidgetState();
|
||||
}
|
||||
|
||||
class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
List<AppUser> _pendingAM = [];
|
||||
List<PendingFamily> _pendingFamilies = [];
|
||||
/// Noms enrichis via GET /dossiers/:numero (libelle API = noms seuls).
|
||||
final Map<String, String> _familyNamesByNumero = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final am =
|
||||
await UserService.getPendingUsers(role: 'assistante_maternelle');
|
||||
final families = await UserService.getPendingFamilies();
|
||||
final namesByNumero = await _enrichFamilyNames(families);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_pendingAM = am;
|
||||
_pendingFamilies = families;
|
||||
_familyNamesByNumero
|
||||
..clear()
|
||||
..addAll(namesByNumero);
|
||||
_isLoading = false;
|
||||
});
|
||||
_emitPendingNumeros();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e is Exception
|
||||
? e.toString().replaceFirst('Exception: ', '')
|
||||
: 'Erreur inconnue';
|
||||
_isLoading = false;
|
||||
});
|
||||
widget.onPendingNumerosChanged?.call(const {});
|
||||
}
|
||||
}
|
||||
|
||||
/// Complète `NOM Prénom` via le détail dossier (sans changer le back).
|
||||
Future<Map<String, String>> _enrichFamilyNames(
|
||||
List<PendingFamily> families,
|
||||
) async {
|
||||
final out = <String, String>{};
|
||||
await Future.wait(families.map((f) async {
|
||||
final num = (f.numeroDossier ?? '').trim();
|
||||
if (num.isEmpty) return;
|
||||
try {
|
||||
final dossier = await UserService.getDossier(num);
|
||||
if (!dossier.isFamily) return;
|
||||
final labels = <String>[];
|
||||
final seen = <String>{};
|
||||
for (final p in dossier.asFamily.parents) {
|
||||
final id = p.id.trim();
|
||||
if (id.isNotEmpty && !seen.add(id)) continue;
|
||||
final label = formatDossierPersonLabel(
|
||||
nom: p.nom,
|
||||
prenom: p.prenom,
|
||||
email: p.email,
|
||||
);
|
||||
if (label.isNotEmpty) labels.add(label);
|
||||
}
|
||||
if (labels.isNotEmpty) out[num] = labels.join(' - ');
|
||||
} catch (_) {
|
||||
// Repli libellé API ci-dessous.
|
||||
}
|
||||
}));
|
||||
return out;
|
||||
}
|
||||
|
||||
void _emitPendingNumeros() {
|
||||
final nums = <String>{};
|
||||
for (final u in _pendingAM) {
|
||||
final n = (u.numeroDossier ?? '').trim();
|
||||
if (n.isNotEmpty) nums.add(n);
|
||||
}
|
||||
for (final f in _pendingFamilies) {
|
||||
final n = (f.numeroDossier ?? '').trim();
|
||||
if (n.isNotEmpty) nums.add(n);
|
||||
}
|
||||
widget.onPendingNumerosChanged?.call(nums);
|
||||
}
|
||||
|
||||
void _onOpenValidation({String? numeroDossier}) {
|
||||
final num = numeroDossier?.trim();
|
||||
if (num == null || num.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Numéro de dossier manquant.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ValidationDossierModal(
|
||||
numeroDossier: num,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
onSuccess: () {
|
||||
Navigator.of(context).pop();
|
||||
_load();
|
||||
widget.onRefresh?.call();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDeletePending({
|
||||
required String numeroDossier,
|
||||
required String namesLine,
|
||||
required bool isFamille,
|
||||
}) async {
|
||||
final num = numeroDossier.trim();
|
||||
if (num.isEmpty) return;
|
||||
|
||||
var people = <SuppressionPersonLine>[];
|
||||
String? fallbackSummary;
|
||||
try {
|
||||
final dossier = await UserService.getDossier(num);
|
||||
if (dossier.isFamily) {
|
||||
final f = dossier.asFamily;
|
||||
people = suppressionPeopleFromDossier(
|
||||
isFamille: true,
|
||||
parents: f.parents
|
||||
.map((p) => (
|
||||
nom: p.nom ?? '',
|
||||
prenom: p.prenom ?? '',
|
||||
email: p.email,
|
||||
))
|
||||
.toList(),
|
||||
enfants: f.enfants
|
||||
.map((e) => (
|
||||
nom: e.lastName ?? '',
|
||||
prenom: e.firstName ?? '',
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
final am = dossier.asAm.user;
|
||||
people = suppressionPeopleFromDossier(
|
||||
isFamille: false,
|
||||
parents: const [],
|
||||
enfants: const [],
|
||||
amName: formatDossierPersonLabel(
|
||||
nom: am.nom,
|
||||
prenom: am.prenom,
|
||||
email: am.email,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
fallbackSummary = isFamille
|
||||
? 'Tous les parents et enfants rattachés seront supprimés.'
|
||||
: 'Le compte AM sera supprimé ; les enfants accueillis '
|
||||
'seront conservés.';
|
||||
for (final part in namesLine.split(' - ')) {
|
||||
final label = part.trim();
|
||||
if (label.isEmpty) continue;
|
||||
people.add(SuppressionPersonLine(
|
||||
label: label,
|
||||
icon: isFamille
|
||||
? Icons.supervisor_account_outlined
|
||||
: Icons.face,
|
||||
role: isFamille ? 'Parent' : 'AM',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final confirmed = await showDossierSuppressionConfirmDialog(
|
||||
context,
|
||||
numeroDossier: num,
|
||||
isFamille: isFamille,
|
||||
people: people,
|
||||
fallbackSummary: fallbackSummary,
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
try {
|
||||
final result = await UserService.deleteDossier(num);
|
||||
if (!mounted) return;
|
||||
final msg = (result['message'] ?? 'Dossier supprimé.').toString();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
await _load();
|
||||
widget.onRefresh?.call();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e is Exception
|
||||
? e.toString().replaceFirst('Exception: ', '')
|
||||
: 'Erreur suppression',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _matchesQuery(String haystack) {
|
||||
final q = widget.searchQuery.trim().toLowerCase();
|
||||
if (q.isEmpty) return true;
|
||||
return haystack.toLowerCase().contains(q);
|
||||
}
|
||||
|
||||
List<AppUser> get _filteredAM {
|
||||
return _pendingAM.where((u) {
|
||||
final bits = [
|
||||
u.numeroDossier ?? '',
|
||||
u.fullName,
|
||||
u.email,
|
||||
u.nom ?? '',
|
||||
u.prenom ?? '',
|
||||
].join(' ');
|
||||
return _matchesQuery(bits);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<PendingFamily> get _filteredFamilies {
|
||||
return _pendingFamilies.where((f) {
|
||||
final num = (f.numeroDossier ?? '').trim();
|
||||
final enriched = _familyNamesByNumero[num] ?? '';
|
||||
final bits = [
|
||||
f.numeroDossier ?? '',
|
||||
f.libelle,
|
||||
enriched,
|
||||
f.emails.join(' '),
|
||||
].join(' ');
|
||||
return _matchesQuery(bits);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
if (widget.compactWhenEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_error != null && _error!.isNotEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _load,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final pendingAM = _filteredAM;
|
||||
final pendingFamilies = _filteredFamilies;
|
||||
final cards = <Widget>[
|
||||
...pendingAM.map(_buildAMCard),
|
||||
...pendingFamilies.map(_buildFamilyCard),
|
||||
];
|
||||
|
||||
if (cards.isEmpty) {
|
||||
if (widget.compactWhenEmpty) {
|
||||
final searching = widget.searchQuery.trim().isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
searching
|
||||
? 'Aucun dossier en attente ne correspond à la recherche.'
|
||||
: 'Aucun dossier en attente.',
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline,
|
||||
size: 64, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun dossier en attente de validation',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final list = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Dossiers à valider',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...cards,
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.compactWhenEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: list,
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _load();
|
||||
widget.onRefresh?.call();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: list,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _amNamesLine(AppUser user) {
|
||||
return formatDossierPersonLabel(
|
||||
nom: user.nom,
|
||||
prenom: user.prenom,
|
||||
email: user.email,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAMCard(AppUser user) {
|
||||
final names = _amNamesLine(user);
|
||||
final num = user.numeroDossier ?? '';
|
||||
return DossierListCard(
|
||||
numeroDossier: num,
|
||||
namesLine: names,
|
||||
isFamille: false,
|
||||
photoUrl: user.photoUrl,
|
||||
onOpen: () => _onOpenValidation(numeroDossier: user.numeroDossier),
|
||||
onDelete: widget.canDelete
|
||||
? () => _confirmDeletePending(
|
||||
numeroDossier: num,
|
||||
namesLine: names,
|
||||
isFamille: false,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFamilyCard(PendingFamily family) {
|
||||
final num = (family.numeroDossier ?? '').trim();
|
||||
final enriched = num.isNotEmpty ? _familyNamesByNumero[num] : null;
|
||||
final names = (enriched != null && enriched.isNotEmpty)
|
||||
? enriched
|
||||
: formatDossierFamilyNamesLine(family.libelle);
|
||||
return DossierListCard(
|
||||
numeroDossier: family.numeroDossier ?? '',
|
||||
namesLine: names,
|
||||
isFamille: true,
|
||||
onOpen: () => _onOpenValidation(numeroDossier: family.numeroDossier),
|
||||
onDelete: widget.canDelete
|
||||
? () => _confirmDeletePending(
|
||||
numeroDossier: num,
|
||||
namesLine: names,
|
||||
isFamille: true,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user