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.
330 lines
11 KiB
Dart
330 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:p_tits_pas/models/dossier_list_item.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/dossier_list_card.dart';
|
|
import 'package:p_tits_pas/widgets/admin/pending_validation_widget.dart';
|
|
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
|
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
|
|
|
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
|
class DossiersManagementWidget extends StatefulWidget {
|
|
final String searchQuery;
|
|
final VoidCallback? onRefresh;
|
|
|
|
const DossiersManagementWidget({
|
|
super.key,
|
|
this.searchQuery = '',
|
|
this.onRefresh,
|
|
});
|
|
|
|
@override
|
|
State<DossiersManagementWidget> createState() =>
|
|
_DossiersManagementWidgetState();
|
|
}
|
|
|
|
class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
|
bool _loading = true;
|
|
String? _error;
|
|
List<DossierListItem> _all = [];
|
|
Set<String> _pendingNumeros = {};
|
|
int _pendingRefreshTick = 0;
|
|
bool _canDelete = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadRights();
|
|
_loadAll();
|
|
}
|
|
|
|
Future<void> _loadRights() async {
|
|
final user = await AuthService.getCurrentUser();
|
|
if (!mounted) return;
|
|
setState(() => _canDelete = canDeleteMetier(user?.role));
|
|
}
|
|
|
|
Future<void> _loadAll() async {
|
|
setState(() {
|
|
_loading = true;
|
|
_error = null;
|
|
});
|
|
try {
|
|
final parents = await UserService.getParents();
|
|
final ams = await UserService.getAssistantesMaternelles();
|
|
Map<String, bool> sansEnfant = {};
|
|
try {
|
|
sansEnfant = await UserService.getSansEnfantByNumero();
|
|
} catch (_) {
|
|
// Flag optionnel : ne bloque pas la liste.
|
|
}
|
|
if (!mounted) return;
|
|
final items = <DossierListItem>[
|
|
...DossierListItem.fromParents(parents),
|
|
...DossierListItem.fromAssistantes(ams),
|
|
].map((item) {
|
|
if (!item.isFamille) return item;
|
|
final apiFlag = sansEnfant[item.numeroDossier] == true;
|
|
final localEmpty = (item.enfantsCount ?? 0) == 0;
|
|
return item.copyWith(
|
|
sansEnfant: apiFlag || localEmpty || item.sansEnfant,
|
|
);
|
|
}).toList();
|
|
items.sort((a, b) {
|
|
// Dossiers sans enfant en tête (#160), comme orphelins #157.
|
|
final ae = a.sansEnfant ? 0 : 1;
|
|
final be = b.sansEnfant ? 0 : 1;
|
|
if (ae != be) return ae.compareTo(be);
|
|
final byNum = a.numeroDossier.compareTo(b.numeroDossier);
|
|
if (byNum != 0) return byNum;
|
|
return a.typeLabel.compareTo(b.typeLabel);
|
|
});
|
|
setState(() {
|
|
_all = items;
|
|
_loading = false;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e is Exception
|
|
? e.toString().replaceFirst('Exception: ', '')
|
|
: 'Erreur inconnue';
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshEverything() async {
|
|
setState(() => _pendingRefreshTick++);
|
|
await _loadAll();
|
|
widget.onRefresh?.call();
|
|
}
|
|
|
|
void _openDossier(String numeroDossier) {
|
|
final num = numeroDossier.trim();
|
|
if (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,
|
|
openAsEdit: true,
|
|
onClose: () => Navigator.of(context).pop(),
|
|
onSuccess: () {
|
|
Navigator.of(context).pop();
|
|
_refreshEverything();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _confirmDeleteDossier(DossierListItem item) async {
|
|
final num = item.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 = item.isFamille
|
|
? 'Tous les parents et enfants rattachés seront supprimés.'
|
|
: 'Le compte AM sera supprimé ; les enfants accueillis '
|
|
'seront conservés.';
|
|
if (item.namesLine.trim().isNotEmpty) {
|
|
for (final part in item.namesLine.split(' - ')) {
|
|
final label = part.trim();
|
|
if (label.isEmpty) continue;
|
|
people.add(SuppressionPersonLine(
|
|
label: label,
|
|
icon: item.isFamille
|
|
? Icons.supervisor_account_outlined
|
|
: Icons.face,
|
|
role: item.isFamille ? 'Parent' : 'AM',
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mounted) return;
|
|
final confirmed = await showDossierSuppressionConfirmDialog(
|
|
context,
|
|
numeroDossier: num,
|
|
isFamille: item.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 _refreshEverything();
|
|
} 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;
|
|
// Pending uniquement en haut — exclus de « Tous les dossiers ».
|
|
final filtered = _all
|
|
.where((d) => !_pendingNumeros.contains(d.numeroDossier))
|
|
.where((d) => (d.statut ?? '').toLowerCase() != 'en_attente')
|
|
.where((d) => d.matchesQuery(query))
|
|
.toList(growable: false);
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: _refreshEverything,
|
|
child: CustomScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: PendingValidationWidget(
|
|
key: ValueKey('pending-$_pendingRefreshTick'),
|
|
searchQuery: query,
|
|
compactWhenEmpty: true,
|
|
canDelete: _canDelete,
|
|
onPendingNumerosChanged: (nums) {
|
|
if (!mounted) return;
|
|
setState(() => _pendingNumeros = nums);
|
|
},
|
|
onRefresh: () {
|
|
_loadAll();
|
|
widget.onRefresh?.call();
|
|
},
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
child: Text(
|
|
'Tous les dossiers',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (_loading)
|
|
const SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: Center(child: CircularProgressIndicator()),
|
|
)
|
|
else if (_error != null && _error!.isNotEmpty)
|
|
SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: 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: _loadAll,
|
|
child: const Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
else if (filtered.isEmpty)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 32),
|
|
child: Text(
|
|
query.trim().isEmpty
|
|
? 'Aucun dossier pour le moment.\n'
|
|
'Pour créer un dossier → onglet Parents (+ Parents) '
|
|
'ou Assistantes maternelles (+ Asmat).'
|
|
: 'Aucun dossier ne correspond à la recherche.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.grey.shade600, height: 1.4),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
final item = filtered[index];
|
|
return DossierListCard(
|
|
numeroDossier: item.numeroDossier,
|
|
namesLine: item.namesLine,
|
|
isFamille: item.isFamille,
|
|
photoUrl: item.photoUrl,
|
|
sansEnfant: item.sansEnfant,
|
|
enfantsCount: item.enfantsCount,
|
|
vigilanceTooltip: item.sansEnfant
|
|
? 'Aucun enfant rattaché à ce dossier famille'
|
|
: null,
|
|
onOpen: () => _openDossier(item.numeroDossier),
|
|
onDelete: _canDelete
|
|
? () => _confirmDeleteDossier(item)
|
|
: null,
|
|
);
|
|
},
|
|
childCount: filtered.length,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|