Compare commits
84
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67336c64fe | ||
|
|
97afbbcf9a | ||
|
|
e235b30140 | ||
|
|
7f23356273 | ||
|
|
c8c9cfbc4d | ||
|
|
530e896b66 | ||
|
|
0029c5ab86 | ||
|
|
2ce9e9215f | ||
|
|
3fdd913367 | ||
|
|
6708f73b06 | ||
|
|
f596f062a6 | ||
|
|
86701731e3 | ||
|
|
b4abb7d6de | ||
|
|
cb5c1a5518 | ||
|
|
30ca99fb65 | ||
|
|
8ee2ca8ea6 | ||
|
|
e3552667bc | ||
|
|
4ae334b247 | ||
|
|
471a62ddb7 | ||
|
|
59afeb0a8d | ||
|
|
d2172eafdb | ||
|
|
d247867fa0 | ||
|
|
93912d1374 | ||
|
|
925b6d5cd4 | ||
|
|
b0dddd6695 | ||
|
|
ef7512dc1e | ||
|
|
2ececa711b | ||
|
|
7a3d997ff9 | ||
|
|
6b89d7b405 | ||
|
|
f7ac628445 | ||
|
|
26e9738ec7 | ||
|
|
0ec3410457 | ||
|
|
c3acf1970d | ||
|
|
6fe2b89a61 | ||
|
|
d6a9b3fd66 | ||
|
|
134b9781c8 | ||
|
|
b936d27445 | ||
|
|
18c1d1eba7 | ||
|
|
f74b14c203 | ||
|
|
f194b5f9e8 | ||
|
|
5ab8ae3423 | ||
|
|
3277f77846 | ||
|
|
2d80ad0d7e | ||
|
|
09386f8aa6 | ||
|
|
faa50f637c | ||
|
|
267fe63aec | ||
|
|
90b185740c | ||
|
|
b903dbf60b | ||
|
|
291ea26b34 | ||
|
|
59919834b9 | ||
|
|
cf6acbae7c | ||
|
|
7951c38d4d | ||
|
|
77d952a6f7 | ||
|
|
b4b546044d | ||
|
|
6788351070 | ||
|
|
7b69f27ca5 | ||
|
|
8ed68797aa | ||
|
|
9ad371a342 | ||
|
|
18718670e9 | ||
|
|
fbf22f2540 | ||
|
|
43a2cd213b | ||
|
|
c865d11dc3 | ||
|
|
d03a8e6c8b | ||
|
|
66c7f22280 | ||
|
|
53721ffbb3 | ||
|
|
52e40d0001 | ||
|
|
4985726bc6 | ||
|
|
479a32b4bf | ||
|
|
2fd97ddecb | ||
|
|
ce474797c4 | ||
|
|
ebf794e1ac | ||
|
|
d55f240f56 | ||
|
|
966f4a9c9c | ||
|
|
8494341b56 | ||
|
|
1dddc67933 | ||
|
|
df776d8200 | ||
|
|
c26ed00374 | ||
|
|
9d54d9b19b | ||
|
|
c438009286 | ||
|
|
9b7231f1da | ||
|
|
f300505225 | ||
|
|
25c10c885a | ||
|
|
d70577b1c3 | ||
|
|
671da71752 |
@@ -13,6 +13,10 @@ class DossierListItem {
|
||||
final String? statut;
|
||||
/// Photo profil (AM) — affichée à la place de l’icône si présente.
|
||||
final String? photoUrl;
|
||||
/// Dossier famille sans enfant lié (#159 / #160).
|
||||
final bool sansEnfant;
|
||||
/// Nombre d’enfants du foyer (famille uniquement ; null pour AM).
|
||||
final int? enfantsCount;
|
||||
|
||||
const DossierListItem({
|
||||
required this.type,
|
||||
@@ -21,8 +25,32 @@ class DossierListItem {
|
||||
this.emails = const [],
|
||||
this.statut,
|
||||
this.photoUrl,
|
||||
this.sansEnfant = false,
|
||||
this.enfantsCount,
|
||||
});
|
||||
|
||||
DossierListItem copyWith({
|
||||
DossierListType? type,
|
||||
String? numeroDossier,
|
||||
String? libelle,
|
||||
List<String>? emails,
|
||||
String? statut,
|
||||
String? photoUrl,
|
||||
bool? sansEnfant,
|
||||
int? enfantsCount,
|
||||
}) {
|
||||
return DossierListItem(
|
||||
type: type ?? this.type,
|
||||
numeroDossier: numeroDossier ?? this.numeroDossier,
|
||||
libelle: libelle ?? this.libelle,
|
||||
emails: emails ?? this.emails,
|
||||
statut: statut ?? this.statut,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
sansEnfant: sansEnfant ?? this.sansEnfant,
|
||||
enfantsCount: enfantsCount ?? this.enfantsCount,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isFamille => type == DossierListType.famille;
|
||||
bool get isAm => type == DossierListType.assistanteMaternelle;
|
||||
|
||||
@@ -98,6 +126,19 @@ class DossierListItem {
|
||||
}
|
||||
}
|
||||
|
||||
final childIds = <String>{};
|
||||
var maxCountFallback = 0;
|
||||
for (final p in entry.value) {
|
||||
for (final c in p.children) {
|
||||
final id = c.id.trim();
|
||||
if (id.isNotEmpty) childIds.add(id);
|
||||
}
|
||||
final n = p.children.isNotEmpty ? p.children.length : p.childrenCount;
|
||||
if (n > maxCountFallback) maxCountFallback = n;
|
||||
}
|
||||
final enfantsCount =
|
||||
childIds.isNotEmpty ? childIds.length : maxCountFallback;
|
||||
|
||||
items.add(
|
||||
DossierListItem(
|
||||
type: DossierListType.famille,
|
||||
@@ -105,6 +146,8 @@ class DossierListItem {
|
||||
libelle: names.isNotEmpty ? names.join(' - ') : 'Famille',
|
||||
emails: emails,
|
||||
statut: _preferStatut(statuts),
|
||||
enfantsCount: enfantsCount,
|
||||
sansEnfant: enfantsCount == 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||
|
||||
@@ -151,30 +152,19 @@ class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
||||
Future<void> _delete() async {
|
||||
if (!_isEditMode || _isSubmitting) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text(
|
||||
'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
final name = widget.initialUser!.fullName.isEmpty
|
||||
? widget.initialUser!.email
|
||||
: widget.initialUser!.fullName;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer l\'administrateur',
|
||||
people: [SuppressionPersonLine.administrateur(name)],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/relais_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/common/suppression_confirm_dialog.dart';
|
||||
|
||||
class AdminUserFormDialog extends StatefulWidget {
|
||||
final AppUser? initialUser;
|
||||
@@ -43,6 +45,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
List<RelaisModel> _relais = [];
|
||||
String? _selectedRelaisId;
|
||||
String? _currentUserId;
|
||||
String? _currentUserRole;
|
||||
bool get _isEditMode => widget.initialUser != null;
|
||||
bool get _isSuperAdminTarget =>
|
||||
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
||||
@@ -50,7 +53,12 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
_isEditMode &&
|
||||
_currentUserId != null &&
|
||||
widget.initialUser!.id == _currentUserId;
|
||||
bool get _canDeleteTarget => !_isSuperAdminTarget && !_isSelfTarget;
|
||||
/// Gestionnaire : pas de delete staff. Admin+ seulement (#154 / #160).
|
||||
bool get _canDeleteTarget {
|
||||
if (!_isEditMode || widget.readOnly) return false;
|
||||
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
||||
return canDeleteGestionnaire(_currentUserRole);
|
||||
}
|
||||
bool get _isLockedAdminIdentity =>
|
||||
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
||||
String get _targetRoleKey {
|
||||
@@ -125,6 +133,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
if (cached != null) {
|
||||
setState(() {
|
||||
_currentUserId = cached.id;
|
||||
_currentUserRole = cached.role;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -132,6 +141,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
if (!mounted || refreshed == null) return;
|
||||
setState(() {
|
||||
_currentUserId = refreshed.id;
|
||||
_currentUserRole = refreshed.role;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -378,30 +388,25 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
if (!_canDeleteTarget) return;
|
||||
if (!_isEditMode || _isSubmitting) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text(
|
||||
'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
final name = widget.initialUser!.fullName.isEmpty
|
||||
? widget.initialUser!.email
|
||||
: widget.initialUser!.fullName;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: widget.adminMode
|
||||
? 'Supprimer l\'administrateur'
|
||||
: 'Supprimer le gestionnaire',
|
||||
people: [
|
||||
widget.adminMode
|
||||
? SuppressionPersonLine.administrateur(name)
|
||||
: SuppressionPersonLine.gestionnaire(name),
|
||||
],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
|
||||
@@ -646,14 +646,93 @@ class UserService {
|
||||
return enrichEnfantParentNames(enfant);
|
||||
}
|
||||
|
||||
static Future<void> deleteEnfant(String enfantId) async {
|
||||
/// DELETE /enfants/:id?deleteDossier= — ticket #159 / #160.
|
||||
static Future<Map<String, dynamic>> deleteEnfant(
|
||||
String enfantId, {
|
||||
bool deleteDossier = false,
|
||||
}) async {
|
||||
final uri = Uri.parse(
|
||||
'${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId',
|
||||
).replace(
|
||||
queryParameters: {
|
||||
'deleteDossier': deleteDossier ? 'true' : 'false',
|
||||
},
|
||||
);
|
||||
final response = await http.delete(uri, headers: await _headers());
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception(
|
||||
_extractErrorMessage(response.body, 'Erreur suppression enfant'),
|
||||
);
|
||||
}
|
||||
return _parseSuppressionBody(response.body);
|
||||
}
|
||||
|
||||
/// DELETE /dossiers/:numeroDossier — ticket #159 / #160.
|
||||
static Future<Map<String, dynamic>> deleteDossier(
|
||||
String numeroDossier,
|
||||
) async {
|
||||
final num = numeroDossier.trim();
|
||||
if (num.isEmpty) {
|
||||
throw Exception('Numéro de dossier manquant.');
|
||||
}
|
||||
final encoded = Uri.encodeComponent(num);
|
||||
final response = await http.delete(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.dossiers}/$encoded'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception(_extractErrorMessage(response.body, 'Erreur suppression enfant'));
|
||||
throw Exception(
|
||||
_extractErrorMessage(response.body, 'Erreur suppression dossier'),
|
||||
);
|
||||
}
|
||||
return _parseSuppressionBody(response.body);
|
||||
}
|
||||
|
||||
/// Liste unifiée GET /dossiers — flags `sans_enfant` (#159).
|
||||
static Future<List<Map<String, dynamic>>> listDossiers({String? q}) async {
|
||||
final uri = Uri.parse('${ApiConfig.baseUrl}${ApiConfig.dossiers}')
|
||||
.replace(
|
||||
queryParameters: (q != null && q.trim().isNotEmpty)
|
||||
? {'q': q.trim()}
|
||||
: null,
|
||||
);
|
||||
final response = await http.get(uri, headers: await _headers());
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
_extractErrorMessage(response.body, 'Erreur chargement dossiers'),
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! List) return const [];
|
||||
return decoded
|
||||
.whereType<Map>()
|
||||
.map((e) => Map<String, dynamic>.from(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Map `numero_dossier` → `sans_enfant` (familles uniquement).
|
||||
static Future<Map<String, bool>> getSansEnfantByNumero() async {
|
||||
final rows = await listDossiers();
|
||||
final out = <String, bool>{};
|
||||
for (final row in rows) {
|
||||
final num = (row['numero_dossier'] ?? '').toString().trim();
|
||||
if (num.isEmpty) continue;
|
||||
final type = (row['type'] ?? '').toString().toLowerCase();
|
||||
if (type != 'famille') continue;
|
||||
out[num] = row['sans_enfant'] == true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _parseSuppressionBody(String body) {
|
||||
if (body.trim().isEmpty) return <String, dynamic>{};
|
||||
try {
|
||||
final decoded = jsonDecode(body);
|
||||
if (decoded is Map) {
|
||||
return Map<String, dynamic>.from(decoded);
|
||||
}
|
||||
} catch (_) {}
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
/// AM dont la liste d'enfants actifs contient [enfantId] (API actuelle).
|
||||
@@ -1224,22 +1303,18 @@ class UserService {
|
||||
return AppUser.fromJson(data);
|
||||
}
|
||||
|
||||
static Future<void> deleteUser(String userId) async {
|
||||
/// DELETE /users/:id — cascades métier #159 / #160.
|
||||
static Future<Map<String, dynamic>> deleteUser(String userId) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final message = decoded['message'];
|
||||
if (message is List && message.isNotEmpty) {
|
||||
throw Exception(message.join(' - '));
|
||||
}
|
||||
throw Exception(_toStr(message) ?? 'Erreur suppression utilisateur');
|
||||
}
|
||||
throw Exception('Erreur suppression utilisateur');
|
||||
throw Exception(
|
||||
_extractErrorMessage(response.body, 'Erreur suppression utilisateur'),
|
||||
);
|
||||
}
|
||||
return _parseSuppressionBody(response.body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/// Droits d’affichage poubelle — tickets #154 / #160.
|
||||
bool canDeleteMetier(String? role) {
|
||||
final r = (role ?? '').trim().toLowerCase();
|
||||
return r == 'gestionnaire' ||
|
||||
r == 'administrateur' ||
|
||||
r == 'super_admin';
|
||||
}
|
||||
|
||||
bool canDeleteGestionnaire(String? role) {
|
||||
final r = (role ?? '').trim().toLowerCase();
|
||||
return r == 'administrateur' || r == 'super_admin';
|
||||
}
|
||||
|
||||
bool canDeleteAdministrateur({
|
||||
required String? currentRole,
|
||||
required String? currentUserId,
|
||||
required String targetUserId,
|
||||
required String targetRole,
|
||||
required int adminCount,
|
||||
}) {
|
||||
final me = (currentRole ?? '').trim().toLowerCase();
|
||||
if (me != 'administrateur' && me != 'super_admin') return false;
|
||||
if (targetUserId.trim().isEmpty) return false;
|
||||
if (currentUserId != null &&
|
||||
currentUserId.trim() == targetUserId.trim()) {
|
||||
return false;
|
||||
}
|
||||
final target = targetRole.trim().toLowerCase();
|
||||
if (target == 'super_admin') return false;
|
||||
if (adminCount <= 1 && me != 'super_admin') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
String? adminDeleteBlockedReason({
|
||||
required String? currentRole,
|
||||
required String? currentUserId,
|
||||
required String targetUserId,
|
||||
required String targetRole,
|
||||
required int adminCount,
|
||||
}) {
|
||||
if (currentUserId != null &&
|
||||
currentUserId.trim() == targetUserId.trim()) {
|
||||
return 'Vous ne pouvez pas supprimer votre propre compte.';
|
||||
}
|
||||
if (targetRole.trim().toLowerCase() == 'super_admin') {
|
||||
return 'Le super administrateur ne peut pas être supprimé.';
|
||||
}
|
||||
if (adminCount <= 1 &&
|
||||
(currentRole ?? '').trim().toLowerCase() != 'super_admin') {
|
||||
return 'Seul un super administrateur peut supprimer le dernier administrateur.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.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/common/admin_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
|
||||
class AdminManagementWidget extends StatefulWidget {
|
||||
@@ -24,6 +26,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
String? _error;
|
||||
List<AppUser> _admins = [];
|
||||
String? _currentUserRole;
|
||||
String? _currentUserId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -62,6 +65,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
if (cached != null) {
|
||||
setState(() {
|
||||
_currentUserRole = (cached.role).toLowerCase();
|
||||
_currentUserId = cached.id;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -69,6 +73,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
if (!mounted || refreshed == null) return;
|
||||
setState(() {
|
||||
_currentUserRole = (refreshed.role).toLowerCase();
|
||||
_currentUserId = refreshed.id;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,6 +84,16 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
return _currentUserRole == 'super_admin';
|
||||
}
|
||||
|
||||
bool _canDeleteAdmin(AppUser target) {
|
||||
return canDeleteAdministrateur(
|
||||
currentRole: _currentUserRole,
|
||||
currentUserId: _currentUserId,
|
||||
targetUserId: target.id,
|
||||
targetRole: target.role,
|
||||
adminCount: _admins.length,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openAdminEditDialog(AppUser user) async {
|
||||
final canEdit = _canEditAdmin(user);
|
||||
final changed = await showDialog<bool>(
|
||||
@@ -98,6 +113,56 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(AppUser user) async {
|
||||
final blocked = adminDeleteBlockedReason(
|
||||
currentRole: _currentUserRole,
|
||||
currentUserId: _currentUserId,
|
||||
targetUserId: user.id,
|
||||
targetRole: user.role,
|
||||
adminCount: _admins.length,
|
||||
);
|
||||
if (blocked != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(blocked)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final name = user.fullName.isNotEmpty ? user.fullName : user.email;
|
||||
final isLast = _admins.length <= 1;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer l\'administrateur',
|
||||
people: [SuppressionPersonLine.administrateur(name)],
|
||||
footnotes: [
|
||||
if (isLast)
|
||||
'Attention : c’est le dernier administrateur.',
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
try {
|
||||
final result = await UserService.deleteUser(user.id);
|
||||
if (!mounted) return;
|
||||
final msg =
|
||||
(result['message'] ?? 'Administrateur supprimé.').toString();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
await _loadAdmins();
|
||||
} 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();
|
||||
@@ -117,6 +182,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
final user = filteredAdmins[index];
|
||||
final isSuperAdmin = _isSuperAdmin(user);
|
||||
final canEdit = _canEditAdmin(user);
|
||||
final canDelete = _canDeleteAdmin(user);
|
||||
return AdminUserCard(
|
||||
title: user.fullName,
|
||||
fallbackIcon: isSuperAdmin
|
||||
@@ -148,6 +214,8 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
_openAdminEditDialog(user);
|
||||
},
|
||||
),
|
||||
if (canDelete)
|
||||
suppressionIconButton(onPressed: () => _confirmDelete(user)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.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/utils/am_vigilance.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/common/admin_am_edit_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
|
||||
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
||||
@@ -26,16 +30,24 @@ class _AssistanteMaternelleManagementWidgetState
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<AssistanteMaternelleModel> _assistantes = [];
|
||||
bool _canDelete = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRights();
|
||||
_loadAssistantes();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() => super.dispose();
|
||||
|
||||
Future<void> _loadRights() async {
|
||||
final user = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
setState(() => _canDelete = canDeleteMetier(user?.role));
|
||||
}
|
||||
|
||||
Future<void> _loadAssistantes() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@@ -57,6 +69,46 @@ class _AssistanteMaternelleManagementWidgetState
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(AssistanteMaternelleModel am) async {
|
||||
final num = (am.user.numeroDossier ?? '').trim();
|
||||
final name = formatDossierPersonLabel(
|
||||
nom: am.user.nom,
|
||||
prenom: am.user.prenom,
|
||||
email: am.user.email,
|
||||
);
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer l\'assistante maternelle',
|
||||
subtitle: num.isEmpty ? null : 'Dossier $num',
|
||||
people: [SuppressionPersonLine.am(name)],
|
||||
footnotes: const [
|
||||
'Le compte et le dossier AM seront supprimés.',
|
||||
'Les enfants accueillis ne seront pas supprimés '
|
||||
'(placements clos).',
|
||||
],
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
try {
|
||||
final result = await UserService.deleteUser(am.user.id);
|
||||
if (!mounted) return;
|
||||
final msg = (result['message'] ?? 'AM supprimée.').toString();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
await _loadAssistantes();
|
||||
} 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();
|
||||
@@ -96,6 +148,10 @@ class _AssistanteMaternelleManagementWidgetState
|
||||
_openAssistanteDetails(assistante);
|
||||
},
|
||||
),
|
||||
if (_canDelete)
|
||||
suppressionIconButton(
|
||||
onPressed: () => _confirmDelete(assistante),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -69,29 +69,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
static const double _proTabHeight = 300;
|
||||
static const List<int> _photoProRowLayout = [2, 2, 2];
|
||||
|
||||
/// Hauteur onglet enfants : champs + titre + grille 2×2 (+ alerte places si besoin).
|
||||
double _childrenTabHeight() {
|
||||
const capacityFields = 72.0;
|
||||
const titleSection = 40.0;
|
||||
const inconsistencyExtra = 46.0;
|
||||
var h = capacityFields +
|
||||
titleSection +
|
||||
AdminAmChildrenCapacityGrid.fixedHeight;
|
||||
if (_placesInconsistent()) h += inconsistencyExtra;
|
||||
return h + 4;
|
||||
}
|
||||
|
||||
double _tabViewHeight(int index) {
|
||||
switch (index) {
|
||||
case 1:
|
||||
return _proTabHeight;
|
||||
case 2:
|
||||
return _childrenTabHeight();
|
||||
default:
|
||||
return 292;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -541,17 +518,15 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
|
||||
|
||||
Widget _identityTab() {
|
||||
return SingleChildScrollView(
|
||||
child: IdentityBlock.editable(
|
||||
title: 'Identité et coordonnées',
|
||||
nomController: _nomCtrl,
|
||||
prenomController: _prenomCtrl,
|
||||
telephoneController: _telCtrl,
|
||||
emailController: _emailCtrl,
|
||||
adresseController: _adresseCtrl,
|
||||
codePostalController: _cpCtrl,
|
||||
villeController: _villeCtrl,
|
||||
),
|
||||
return IdentityBlock.editable(
|
||||
title: 'Identité et coordonnées',
|
||||
nomController: _nomCtrl,
|
||||
prenomController: _prenomCtrl,
|
||||
telephoneController: _telCtrl,
|
||||
emailController: _emailCtrl,
|
||||
adresseController: _adresseCtrl,
|
||||
codePostalController: _cpCtrl,
|
||||
villeController: _villeCtrl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -610,7 +585,9 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
return LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
final maxRowW = c.maxWidth;
|
||||
final maxRowH = c.maxHeight;
|
||||
// Hauteur bornée : l’onglet pro a une hauteur fixe (photo ID).
|
||||
final maxRowH =
|
||||
c.maxHeight.isFinite ? c.maxHeight : _proTabHeight;
|
||||
final bodyH = maxRowH;
|
||||
final idealPhotoW = bodyH * AdminAmPhotoFrame.idPhotoAspectRatio + 16;
|
||||
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
||||
@@ -659,6 +636,18 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
);
|
||||
}
|
||||
|
||||
/// Corps de l’onglet actif — hauteur naturelle (pas de TabBarView).
|
||||
Widget _buildActiveTabBody() {
|
||||
switch (_tabCtrl.index) {
|
||||
case 1:
|
||||
return SizedBox(height: _proTabHeight, child: _proTab());
|
||||
case 2:
|
||||
return _childrenTab();
|
||||
default:
|
||||
return _identityTab();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _childrenCapacityFields() {
|
||||
final inconsistent = _placesInconsistent();
|
||||
return Column(
|
||||
@@ -867,15 +856,13 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||
child: SizedBox(
|
||||
height: _tabViewHeight(_tabCtrl.index),
|
||||
child: TabBarView(
|
||||
controller: _tabCtrl,
|
||||
children: [
|
||||
_identityTab(),
|
||||
_proTab(),
|
||||
_childrenTab(),
|
||||
],
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: Alignment.topCenter,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<int>(_tabCtrl.index),
|
||||
child: _buildActiveTabBody(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
|
||||
/// Carte dossier unifiée (#153) — fond neutre, accent couleur sur l’icône.
|
||||
class DossierListCard extends StatelessWidget {
|
||||
@@ -9,6 +10,12 @@ class DossierListCard extends StatelessWidget {
|
||||
final VoidCallback onOpen;
|
||||
/// Photo AM (si absente → icône fallback).
|
||||
final String? photoUrl;
|
||||
final VoidCallback? onDelete;
|
||||
/// Warning vigilance (ex. dossier sans enfant #160).
|
||||
final String? vigilanceTooltip;
|
||||
/// Nombre d’enfants (famille) — affiché à côté des noms.
|
||||
final int? enfantsCount;
|
||||
final bool sansEnfant;
|
||||
|
||||
/// Lavande — Famille / Parents.
|
||||
static const Color familleAccent = Color(0xFFB289C9);
|
||||
@@ -23,6 +30,10 @@ class DossierListCard extends StatelessWidget {
|
||||
required this.isFamille,
|
||||
required this.onOpen,
|
||||
this.photoUrl,
|
||||
this.onDelete,
|
||||
this.vigilanceTooltip,
|
||||
this.enfantsCount,
|
||||
this.sansEnfant = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -31,23 +42,41 @@ class DossierListCard extends StatelessWidget {
|
||||
final num = numeroDossier.trim().isEmpty ? '–' : numeroDossier.trim();
|
||||
final names = namesLine.trim();
|
||||
final avatar = (photoUrl ?? '').trim();
|
||||
final emptyKids = isFamille && (sansEnfant || enfantsCount == 0);
|
||||
final count = enfantsCount;
|
||||
final countLabel = (!isFamille || count == null)
|
||||
? null
|
||||
: (count <= 1 ? '$count enfant' : '$count enfants');
|
||||
|
||||
final subtitle = <String>[
|
||||
if (names.isNotEmpty) names,
|
||||
if (emptyKids) 'Sans enfant',
|
||||
if (!emptyKids && countLabel != null) countLabel,
|
||||
];
|
||||
|
||||
return AdminUserCard(
|
||||
title: num,
|
||||
subtitleLines: names.isEmpty ? const [] : [names],
|
||||
subtitleLines: subtitle,
|
||||
avatarUrl: !isFamille && avatar.isNotEmpty ? avatar : null,
|
||||
fallbackIcon:
|
||||
isFamille ? Icons.family_restroom_outlined : Icons.face,
|
||||
// N° = titre neutre (comme Parents / AM) ; accent = icône seule.
|
||||
avatarIconColor: accent,
|
||||
infoColor: Colors.black87,
|
||||
infoColor: emptyKids ? Colors.red.shade700 : Colors.black87,
|
||||
onCardTap: onOpen,
|
||||
vigilanceTooltip: emptyKids
|
||||
? (vigilanceTooltip ??
|
||||
'Aucun enfant rattaché à ce dossier famille')
|
||||
: vigilanceTooltip,
|
||||
borderColor: emptyKids ? Colors.red.shade300 : null,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Ouvrir',
|
||||
icon: Icon(Icons.open_in_new, size: 20, color: accent),
|
||||
onPressed: onOpen,
|
||||
),
|
||||
if (onDelete != null)
|
||||
suppressionIconButton(onPressed: onDelete),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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.
|
||||
@@ -27,13 +30,21 @@ class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
||||
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;
|
||||
@@ -42,12 +53,29 @@ class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
||||
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);
|
||||
@@ -95,6 +123,95 @@ class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -115,6 +232,7 @@ class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
||||
key: ValueKey('pending-$_pendingRefreshTick'),
|
||||
searchQuery: query,
|
||||
compactWhenEmpty: true,
|
||||
canDelete: _canDelete,
|
||||
onPendingNumerosChanged: (nums) {
|
||||
if (!mounted) return;
|
||||
setState(() => _pendingNumeros = nums);
|
||||
@@ -189,7 +307,15 @@ class _DossiersManagementWidgetState extends State<DossiersManagementWidget> {
|
||||
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,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
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/admin/common/admin_child_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
|
||||
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
|
||||
@@ -25,13 +29,21 @@ 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;
|
||||
@@ -59,10 +71,133 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
|
||||
builder: (ctx) => AdminChildDetailModal(
|
||||
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();
|
||||
@@ -98,6 +233,10 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
|
||||
tooltip: 'Voir / modifier',
|
||||
onPressed: () => _openEnfant(enfant),
|
||||
),
|
||||
if (_canDelete)
|
||||
suppressionIconButton(
|
||||
onPressed: () => _confirmDelete(enfant),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.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/common/admin_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
|
||||
class GestionnaireManagementWidget extends StatefulWidget {
|
||||
@@ -23,16 +26,28 @@ class _GestionnaireManagementWidgetState
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<AppUser> _gestionnaires = [];
|
||||
bool _canDelete = false;
|
||||
String? _currentUserId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRights();
|
||||
_loadGestionnaires();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() => super.dispose();
|
||||
|
||||
Future<void> _loadRights() async {
|
||||
final user = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_canDelete = canDeleteGestionnaire(user?.role);
|
||||
_currentUserId = user?.id;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadGestionnaires() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@@ -67,6 +82,46 @@ class _GestionnaireManagementWidgetState
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(AppUser user) async {
|
||||
if (_currentUserId != null && _currentUserId == user.id) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vous ne pouvez pas supprimer votre propre compte.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final name = user.fullName.isNotEmpty ? user.fullName : user.email;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer le gestionnaire',
|
||||
people: [SuppressionPersonLine.gestionnaire(name)],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
try {
|
||||
final result = await UserService.deleteUser(user.id);
|
||||
if (!mounted) return;
|
||||
final msg = (result['message'] ?? 'Gestionnaire supprimé.').toString();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
await _loadGestionnaires();
|
||||
} 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();
|
||||
@@ -84,6 +139,8 @@ class _GestionnaireManagementWidgetState
|
||||
itemCount: filteredGestionnaires.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = filteredGestionnaires[index];
|
||||
final isSelf =
|
||||
_currentUserId != null && _currentUserId == user.id;
|
||||
return AdminUserCard(
|
||||
title: user.fullName,
|
||||
fallbackIcon: Icons.assignment_ind_outlined,
|
||||
@@ -102,6 +159,8 @@ class _GestionnaireManagementWidgetState
|
||||
_openGestionnaireEditDialog(user);
|
||||
},
|
||||
),
|
||||
if (_canDelete && !isSelf)
|
||||
suppressionIconButton(onPressed: () => _confirmDelete(user)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/dossier_list_item.dart';
|
||||
import 'package:p_tits_pas/models/parent_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/admin/common/admin_parent_edit_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
|
||||
class ParentManagementWidget extends StatefulWidget {
|
||||
@@ -23,16 +27,24 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<ParentModel> _parents = [];
|
||||
bool _canDelete = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRights();
|
||||
_loadParents();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() => super.dispose();
|
||||
|
||||
Future<void> _loadRights() async {
|
||||
final user = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
setState(() => _canDelete = canDeleteMetier(user?.role));
|
||||
}
|
||||
|
||||
Future<void> _loadParents() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@@ -54,6 +66,67 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isLastParent(ParentModel parent) {
|
||||
final co = parent.coParent?.id.trim();
|
||||
if (co != null && co.isNotEmpty) return false;
|
||||
final num = (parent.user.numeroDossier ?? '').trim();
|
||||
if (num.isEmpty) return true;
|
||||
return !_parents.any((other) {
|
||||
if (other.user.id == parent.user.id) return false;
|
||||
return (other.user.numeroDossier ?? '').trim() == num;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(ParentModel parent) async {
|
||||
final num = (parent.user.numeroDossier ?? '').trim();
|
||||
final name = formatDossierPersonLabel(
|
||||
nom: parent.user.nom,
|
||||
prenom: parent.user.prenom,
|
||||
email: parent.user.email,
|
||||
);
|
||||
final last = _isLastParent(parent);
|
||||
final enfants = ParentModel.foyerChildrenCount(parent, _parents);
|
||||
final footnotes = last
|
||||
? <String>[
|
||||
if (num.isNotEmpty) 'Dernier parent du dossier $num.',
|
||||
if (num.isEmpty) 'Dernier parent du dossier.',
|
||||
'Les $enfants enfant(s) rattaché(s) seront aussi supprimés.',
|
||||
]
|
||||
: <String>[
|
||||
if (num.isNotEmpty)
|
||||
'Ce parent sera retiré du dossier $num.',
|
||||
'Les enfants restent avec le co-parent.',
|
||||
];
|
||||
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer le parent',
|
||||
subtitle: num.isEmpty ? null : 'Dossier $num',
|
||||
people: [SuppressionPersonLine.parent(name)],
|
||||
footnotes: footnotes,
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
try {
|
||||
final result = await UserService.deleteUser(parent.user.id);
|
||||
if (!mounted) return;
|
||||
final msg = (result['message'] ?? 'Parent supprimé.').toString();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
await _loadParents();
|
||||
} 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();
|
||||
@@ -90,6 +163,8 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
||||
_openParentDetails(parent);
|
||||
},
|
||||
),
|
||||
if (_canDelete)
|
||||
suppressionIconButton(onPressed: () => _confirmDelete(parent)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/admin/dossier_list_card.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
||||
|
||||
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
||||
@@ -15,6 +16,8 @@ class PendingValidationWidget extends StatefulWidget {
|
||||
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,
|
||||
@@ -22,6 +25,7 @@ class PendingValidationWidget extends StatefulWidget {
|
||||
this.searchQuery = '',
|
||||
this.compactWhenEmpty = false,
|
||||
this.onPendingNumerosChanged,
|
||||
this.canDelete = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -141,6 +145,97 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -286,12 +381,21 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
}
|
||||
|
||||
Widget _buildAMCard(AppUser user) {
|
||||
final names = _amNamesLine(user);
|
||||
final num = user.numeroDossier ?? '';
|
||||
return DossierListCard(
|
||||
numeroDossier: user.numeroDossier ?? '',
|
||||
namesLine: _amNamesLine(user),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,6 +410,13 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
namesLine: names,
|
||||
isFamille: true,
|
||||
onOpen: () => _onOpenValidation(numeroDossier: family.numeroDossier),
|
||||
onDelete: widget.canDelete
|
||||
? () => _confirmDeletePending(
|
||||
numeroDossier: num,
|
||||
namesLine: names,
|
||||
isFamille: true,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:p_tits_pas/models/relais_model.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/services/relais_service.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
|
||||
class RelaisManagementPanel extends StatefulWidget {
|
||||
const RelaisManagementPanel({super.key});
|
||||
@@ -56,28 +57,16 @@ class _RelaisManagementPanelState extends State<RelaisManagementPanel> {
|
||||
try {
|
||||
if (result.action == _RelaisDialogAction.delete) {
|
||||
if (relais == null) return;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Supprimer le relais'),
|
||||
content: Text('Confirmer la suppression de "${relais.nom}" ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: 'Supprimer le relais',
|
||||
people: [SuppressionPersonLine.relais(relais.nom)],
|
||||
footnotes: const [
|
||||
'Cette action est irréversible.',
|
||||
],
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
if (!confirmed) return;
|
||||
await RelaisService.deleteRelais(relais.id);
|
||||
} else if (relais == null) {
|
||||
await RelaisService.createRelais(result.payload!);
|
||||
|
||||
Reference in New Issue
Block a user