Compare commits

..

3 Commits

Author SHA1 Message Date
5ab8ae3423 Merge branch 'feature/146-modale-selection-enfant-am' into develop
Modale de sélection d'enfant (#146) pour rattachement AM/parent.
2026-07-17 15:43:10 +02:00
3277f77846 fix(#146): densifier la liste et corriger l'affichage des responsables.
Les cartes compactes montrent plus d'enfants d'un coup ; le nom ne monopolise plus la largeur au détriment des infos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 15:39:59 +02:00
2d80ad0d7e feat(#146): modale de sélection d'enfant pour rattachement AM/parent.
Recherche dynamique, filtre Sans garde côté AM, confirmation de transfert et détachement préalable pour laisser le point de vigilance places sur l'AM d'origine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 15:31:47 +02:00
6 changed files with 504 additions and 71 deletions

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:p_tits_pas/models/assistante_maternelle_model.dart'; import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/models/parent_child_summary.dart'; import 'package:p_tits_pas/models/parent_child_summary.dart';
import 'package:p_tits_pas/utils/am_vigilance.dart'; import 'package:p_tits_pas/utils/am_vigilance.dart';
import 'package:p_tits_pas/utils/date_display_utils.dart'; import 'package:p_tits_pas/utils/date_display_utils.dart';
@ -11,6 +10,7 @@ import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_children_capacity_grid.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_am_children_capacity_grid.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_enfant_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart'; import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
import 'package:p_tits_pas/widgets/common/identity_block.dart'; import 'package:p_tits_pas/widgets/common/identity_block.dart';
@ -55,6 +55,10 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
late List<ParentChildSummary> _children; late List<ParentChildSummary> _children;
late Set<String> _baselineChildIds; late Set<String> _baselineChildIds;
/// Enfants ajoutés localement qui étaient déjà chez une autre AM
/// (enfantId amUserId d'origine). Au save : détacher puis rattacher.
final Map<String, String> _transferFromAmIds = {};
bool _saving = false; bool _saving = false;
bool _dirty = false; bool _dirty = false;
@ -311,6 +315,17 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
); );
} }
for (final id in currentIds.difference(_baselineChildIds)) { for (final id in currentIds.difference(_baselineChildIds)) {
// Transfert : détacher l'ancienne AM sans recalculer ses places
// (le ! de vigilance apparaît côté liste AM).
final previousAmId = _transferFromAmIds[id] ??
(await UserService.findAmForEnfant(id))?.user.id;
if (previousAmId != null &&
previousAmId != widget.assistante.user.id) {
await UserService.detachEnfantFromAm(
amUserId: previousAmId,
enfantId: id,
);
}
await UserService.attachEnfantToAm( await UserService.attachEnfantToAm(
amUserId: widget.assistante.user.id, amUserId: widget.assistante.user.id,
enfantId: id, enfantId: id,
@ -346,6 +361,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
_dirty = false; _dirty = false;
_saving = false; _saving = false;
_baselineChildIds = currentIds; _baselineChildIds = currentIds;
_transferFromAmIds.clear();
}); });
widget.onSaved?.call(); widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -377,6 +393,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
setState(() { setState(() {
_children = kids; _children = kids;
_baselineChildIds = kids.map((c) => c.id).toSet(); _baselineChildIds = kids.map((c) => c.id).toSet();
_transferFromAmIds.clear();
}); });
} catch (_) {} } catch (_) {}
} }
@ -442,55 +459,75 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
setState(() { setState(() {
_children = _children.where((c) => c.id != child.id).toList(); _children = _children.where((c) => c.id != child.id).toList();
_transferFromAmIds.remove(child.id);
_syncPlacesAfterChildrenChange(); _syncPlacesAfterChildrenChange();
_dirty = true; _dirty = true;
}); });
} }
Future<void> _attachChild() async { Future<void> _attachChild() async {
List<EnfantAdminModel> all;
try {
all = await UserService.getEnfants();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
return;
}
final linkedIds = _children.map((c) => c.id).toSet();
final candidates = all.where((e) => !linkedIds.contains(e.id)).toList();
if (candidates.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucun enfant disponible à rattacher')),
);
return;
}
if (!mounted) return; if (!mounted) return;
final selected = await showDialog<EnfantAdminModel>( final selected = await AdminSelectEnfantModal.show(
context: context, context,
builder: (ctx) => SimpleDialog( excludeIds: _children.map((c) => c.id).toSet(),
title: const Text('Rattacher un enfant'), title: 'Rattacher un enfant',
children: candidates showSansGardeFilter: true,
.map(
(e) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, e),
child: Text(e.fullName),
),
)
.toList(),
),
); );
if (selected == null || !mounted) return; if (selected == null || !mounted) return;
AssistanteMaternelleModel? previousAm;
try {
previousAm = await UserService.findAmForEnfant(selected.id);
} catch (_) {
previousAm = null;
}
if (!mounted) return;
final previousAmId = previousAm?.user.id;
final isTransfer = previousAmId != null &&
previousAmId != widget.assistante.user.id;
if (isTransfer) {
final amName = previousAm!.user.fullName.trim().isNotEmpty
? previousAm.user.fullName.trim()
: 'une autre assistante maternelle';
final gardeLabel =
(selected.gender ?? '').trim().toUpperCase() == 'F'
? 'déjà gardée'
: 'déjà gardé';
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Changer d\'affectation'),
content: Text(
'${selected.fullName} est $gardeLabel par $amName.\n\n'
'Confirmer le transfert vers cette assistante ? '
'L\'enfant sera détaché de $amName.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ValidationModalTheme.primaryElevatedStyle,
child: const Text('Confirmer'),
),
],
),
);
if (confirmed != true || !mounted) return;
}
setState(() { setState(() {
_children = [ _children = [
..._children, ..._children,
ParentChildSummary.fromEnfant(selected), ParentChildSummary.fromEnfant(selected),
]; ];
if (isTransfer) {
_transferFromAmIds[selected.id] = previousAmId!;
}
_syncPlacesAfterChildrenChange(); _syncPlacesAfterChildrenChange();
_dirty = true; _dirty = true;
}); });

View File

@ -34,6 +34,8 @@ class AdminEnfantUserCard extends StatelessWidget {
final List<String> subtitleLines; final List<String> subtitleLines;
final List<Widget> actions; final List<Widget> actions;
final VoidCallback? onCardTap; final VoidCallback? onCardTap;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? contentPadding;
const AdminEnfantUserCard({ const AdminEnfantUserCard({
super.key, super.key,
@ -42,6 +44,8 @@ class AdminEnfantUserCard extends StatelessWidget {
required this.subtitleLines, required this.subtitleLines,
this.actions = const [], this.actions = const [],
this.onCardTap, this.onCardTap,
this.margin,
this.contentPadding,
}); });
factory AdminEnfantUserCard.fromEnfant( factory AdminEnfantUserCard.fromEnfant(
@ -49,6 +53,8 @@ class AdminEnfantUserCard extends StatelessWidget {
List<String> extraSubtitleLines = const [], List<String> extraSubtitleLines = const [],
List<Widget> actions = const [], List<Widget> actions = const [],
VoidCallback? onCardTap, VoidCallback? onCardTap,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? contentPadding,
}) { }) {
final parents = enfant.parentLinks final parents = enfant.parentLinks
.map((l) => l.parentName ?? 'Parent') .map((l) => l.parentName ?? 'Parent')
@ -68,6 +74,8 @@ class AdminEnfantUserCard extends StatelessWidget {
), ),
actions: actions, actions: actions,
onCardTap: onCardTap, onCardTap: onCardTap,
margin: margin,
contentPadding: contentPadding,
); );
} }
@ -76,6 +84,8 @@ class AdminEnfantUserCard extends StatelessWidget {
List<String> extraSubtitleLines = const [], List<String> extraSubtitleLines = const [],
List<Widget> actions = const [], List<Widget> actions = const [],
VoidCallback? onCardTap, VoidCallback? onCardTap,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? contentPadding,
}) { }) {
return AdminEnfantUserCard( return AdminEnfantUserCard(
title: child.fullName, title: child.fullName,
@ -88,6 +98,8 @@ class AdminEnfantUserCard extends StatelessWidget {
), ),
actions: actions, actions: actions,
onCardTap: onCardTap, onCardTap: onCardTap,
margin: margin,
contentPadding: contentPadding,
); );
} }
@ -100,6 +112,8 @@ class AdminEnfantUserCard extends StatelessWidget {
subtitleLines: subtitleLines, subtitleLines: subtitleLines,
actions: actions, actions: actions,
onCardTap: onCardTap, onCardTap: onCardTap,
margin: margin,
contentPadding: contentPadding,
); );
} }
} }

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/utils/phone_utils.dart'; import 'package:p_tits_pas/utils/phone_utils.dart';
import 'package:p_tits_pas/models/parent_child_summary.dart'; import 'package:p_tits_pas/models/parent_child_summary.dart';
import 'package:p_tits_pas/models/parent_model.dart'; import 'package:p_tits_pas/models/parent_model.dart';
@ -7,6 +6,7 @@ import 'package:p_tits_pas/models/user.dart';
import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_children_affiliation_panel.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_children_affiliation_panel.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_enfant_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
import 'package:p_tits_pas/widgets/common/identity_block.dart'; import 'package:p_tits_pas/widgets/common/identity_block.dart';
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart'; import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
@ -321,41 +321,11 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
} }
Future<void> _attachChild() async { Future<void> _attachChild() async {
List<EnfantAdminModel> all;
try {
all = await UserService.getEnfants();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
return;
}
final linkedIds = _children.map((c) => c.id).toSet();
final candidates = all.where((e) => !linkedIds.contains(e.id)).toList();
if (candidates.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucun enfant disponible à rattacher')),
);
return;
}
if (!mounted) return; if (!mounted) return;
final selected = await showDialog<EnfantAdminModel>( final selected = await AdminSelectEnfantModal.show(
context: context, context,
builder: (ctx) => SimpleDialog( excludeIds: _children.map((c) => c.id).toSet(),
title: const Text('Rattacher un enfant'), title: 'Rattacher un enfant',
children: candidates
.map(
(e) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, e),
child: Text(e.fullName),
),
)
.toList(),
),
); );
if (selected == null || !mounted) return; if (selected == null || !mounted) return;

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
/// Sélection d'un enfant à rattacher (fiche AM / fiche parent) — ticket #146.
/// S'appuie sur [AdminSelectListModal] (shell partagé avec #147).
class AdminSelectEnfantModal {
AdminSelectEnfantModal._();
static Future<EnfantAdminModel?> show(
BuildContext context, {
Set<String> excludeIds = const {},
String title = 'Rattacher un enfant',
/// Affiché uniquement depuis la fiche AM : filtre les enfants déjà en garde.
bool showSansGardeFilter = false,
}) {
return AdminSelectListModal.show<EnfantAdminModel>(
context,
title: title,
searchHint: 'Rechercher par nom ou prénom…',
emptyMessage: 'Aucun enfant disponible à rattacher',
noResultsMessage: showSansGardeFilter
? 'Aucun enfant sans garde pour cette recherche'
: 'Aucun résultat pour cette recherche',
toggleFilter: showSansGardeFilter
? AdminSelectToggleFilter<EnfantAdminModel>(
label: 'Sans garde',
initialValue: true,
whenEnabled: (e) =>
normalizeEnfantStatus(e.status) == 'sans_garde',
)
: null,
loadItems: () async {
final list = await UserService.getEnfants();
return list
.where((e) => !excludeIds.contains(e.id))
.toList()
..sort(
(a, b) =>
a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase()),
);
},
matchesQuery: (e, q) {
final name = e.fullName.toLowerCase();
final fn = (e.firstName ?? '').toLowerCase();
final ln = (e.lastName ?? '').toLowerCase();
return name.contains(q) || fn.contains(q) || ln.contains(q);
},
itemBuilder: (context, e, onSelect) {
return AdminEnfantUserCard.fromEnfant(
e,
onCardTap: onSelect,
margin: const EdgeInsets.only(bottom: 4),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
actions: [
IconButton(
icon: const Icon(Icons.add_link),
tooltip: 'Rattacher',
onPressed: onSelect,
),
],
);
},
);
}
}

View File

@ -0,0 +1,334 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
/// Filtre optionnel (switch) sur la même ligne que la barre de recherche.
class AdminSelectToggleFilter<T> {
final String label;
final bool initialValue;
/// Si le switch est activé, ne garde que les éléments pour lesquels
/// [whenEnabled] renvoie `true`.
final bool Function(T item) whenEnabled;
const AdminSelectToggleFilter({
required this.label,
required this.whenEnabled,
this.initialValue = true,
});
}
/// Shell générique « rechercher + liste + sélection » pour les modales admin.
/// Utilisé par la sélection d'enfant (#146) et prévu pour la sélection d'AM (#147).
class AdminSelectListModal<T> extends StatefulWidget {
final String title;
final String searchHint;
final Future<List<T>> Function() loadItems;
final bool Function(T item, String query) matchesQuery;
final Widget Function(
BuildContext context,
T item,
VoidCallback onSelect,
) itemBuilder;
final String emptyMessage;
final String noResultsMessage;
final double modalWidth;
/// Hauteur d'une carte (pour dimensionner la liste à ≥ [minVisibleCards]).
final double cardExtent;
/// Nombre minimum de cartes visibles dans la zone scrollable.
final int minVisibleCards;
/// Switch optionnel à droite du champ de recherche (ex. « Sans garde »).
final AdminSelectToggleFilter<T>? toggleFilter;
const AdminSelectListModal({
super.key,
required this.title,
required this.loadItems,
required this.matchesQuery,
required this.itemBuilder,
this.searchHint = 'Rechercher…',
this.emptyMessage = 'Aucun élément disponible',
this.noResultsMessage = 'Aucun résultat pour cette recherche',
this.modalWidth = 930,
this.cardExtent = 52,
this.minVisibleCards = 8,
this.toggleFilter,
});
static Future<T?> show<T>(
BuildContext context, {
required String title,
required Future<List<T>> Function() loadItems,
required bool Function(T item, String query) matchesQuery,
required Widget Function(
BuildContext context,
T item,
VoidCallback onSelect,
) itemBuilder,
String searchHint = 'Rechercher…',
String emptyMessage = 'Aucun élément disponible',
String noResultsMessage = 'Aucun résultat pour cette recherche',
double modalWidth = 930,
double cardExtent = 52,
int minVisibleCards = 8,
AdminSelectToggleFilter<T>? toggleFilter,
}) {
return showDialog<T>(
context: context,
builder: (ctx) => AdminSelectListModal<T>(
title: title,
loadItems: loadItems,
matchesQuery: matchesQuery,
itemBuilder: itemBuilder,
searchHint: searchHint,
emptyMessage: emptyMessage,
noResultsMessage: noResultsMessage,
modalWidth: modalWidth,
cardExtent: cardExtent,
minVisibleCards: minVisibleCards,
toggleFilter: toggleFilter,
),
);
}
@override
State<AdminSelectListModal<T>> createState() =>
_AdminSelectListModalState<T>();
}
class _AdminSelectListModalState<T> extends State<AdminSelectListModal<T>> {
final _searchCtrl = TextEditingController();
List<T> _all = [];
bool _loading = true;
String? _error;
late bool _toggleOn;
double get _listHeight =>
widget.cardExtent * widget.minVisibleCards;
@override
void initState() {
super.initState();
_toggleOn = widget.toggleFilter?.initialValue ?? false;
_searchCtrl.addListener(() => setState(() {}));
_load();
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
final list = await widget.loadItems();
if (!mounted) return;
setState(() {
_all = list;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_error = e.toString().replaceFirst('Exception: ', '');
});
}
}
List<T> get _filtered {
var list = _all;
final toggle = widget.toggleFilter;
if (toggle != null && _toggleOn) {
list = list.where(toggle.whenEnabled).toList();
}
final q = _searchCtrl.text.trim().toLowerCase();
if (q.isEmpty) return list;
return list.where((item) => widget.matchesQuery(item, q)).toList();
}
@override
Widget build(BuildContext context) {
final toggle = widget.toggleFilter;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.modalWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 4, 0),
child: Row(
children: [
Expanded(
child: Text(
widget.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
),
IconButton(
icon: const Icon(Icons.close),
tooltip: 'Fermer',
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
isDense: true,
hintText: widget.searchHint,
prefixIcon: const Icon(Icons.search, size: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
),
),
if (toggle != null) ...[
const SizedBox(width: 12),
Text(
toggle.label,
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 4),
Switch(
value: _toggleOn,
activeColor:
ValidationModalTheme.primaryActionBackground,
onChanged: (v) => setState(() => _toggleOn = v),
),
],
],
),
const SizedBox(height: 12),
SizedBox(
height: _listHeight,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildBody(),
),
),
),
const SizedBox(height: 12),
Row(
children: [
const Spacer(),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildBody() {
if (_loading) {
return const Center(
child: CircularProgressIndicator(
color: ValidationModalTheme.primaryActionBackground,
),
);
}
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 40, color: Colors.red.shade400),
const SizedBox(height: 12),
Text(
_error!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade700),
),
const SizedBox(height: 12),
TextButton.icon(
onPressed: _load,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
),
);
}
final items = _filtered;
if (_all.isEmpty) {
return Center(
child: Text(
widget.emptyMessage,
style: const TextStyle(fontSize: 14, color: Colors.black54),
),
);
}
if (items.isEmpty) {
return Center(
child: Text(
widget.noResultsMessage,
style: const TextStyle(fontSize: 14, color: Colors.black54),
),
);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
itemExtent: widget.cardExtent,
itemCount: items.length,
itemBuilder: (context, i) {
final item = items[i];
return widget.itemBuilder(
context,
item,
() => Navigator.of(context).pop(item),
);
},
);
}
}

View File

@ -15,6 +15,7 @@ class AdminUserCard extends StatefulWidget {
final String? vigilanceTooltip; final String? vigilanceTooltip;
final VoidCallback? onCardTap; final VoidCallback? onCardTap;
final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? contentPadding;
const AdminUserCard({ const AdminUserCard({
super.key, super.key,
@ -30,6 +31,7 @@ class AdminUserCard extends StatefulWidget {
this.vigilanceTooltip, this.vigilanceTooltip,
this.onCardTap, this.onCardTap,
this.margin, this.margin,
this.contentPadding,
}); });
@override @override
@ -69,7 +71,8 @@ class _AdminUserCardState extends State<AdminUserCard> {
side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300), side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), padding: widget.contentPadding ??
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
child: Row( child: Row(
children: [ children: [
_buildAvatar(avatarUrl), _buildAvatar(avatarUrl),
@ -88,7 +91,10 @@ class _AdminUserCardState extends State<AdminUserCard> {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
// flex: 0 largeur du nom ; le reste va aux infos
// (évite le partage 50/50 qui tronque « Responsables »).
Flexible( Flexible(
flex: 0,
fit: FlexFit.loose, fit: FlexFit.loose,
child: Text( child: Text(
widget.title.isNotEmpty ? widget.title : 'Sans nom', widget.title.isNotEmpty ? widget.title : 'Sans nom',