Compare commits
No commits in common. "5ab8ae3423c92caa3e105ea9139bd2fab8f392e4" and "09386f8aa640e3b77a18b93f120c131a25d75222" have entirely different histories.
5ab8ae3423
...
09386f8aa6
@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
@ -10,7 +11,6 @@ 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,10 +55,6 @@ 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;
|
||||||
|
|
||||||
@ -315,17 +311,6 @@ 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,
|
||||||
@ -361,7 +346,6 @@ 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(
|
||||||
@ -393,7 +377,6 @@ 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 (_) {}
|
||||||
}
|
}
|
||||||
@ -459,75 +442,55 @@ 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 AdminSelectEnfantModal.show(
|
final selected = await showDialog<EnfantAdminModel>(
|
||||||
context,
|
context: context,
|
||||||
excludeIds: _children.map((c) => c.id).toSet(),
|
builder: (ctx) => SimpleDialog(
|
||||||
title: 'Rattacher un enfant',
|
title: const Text('Rattacher un enfant'),
|
||||||
showSansGardeFilter: true,
|
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;
|
||||||
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -34,8 +34,6 @@ 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,
|
||||||
@ -44,8 +42,6 @@ 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(
|
||||||
@ -53,8 +49,6 @@ 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')
|
||||||
@ -74,8 +68,6 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
onCardTap: onCardTap,
|
onCardTap: onCardTap,
|
||||||
margin: margin,
|
|
||||||
contentPadding: contentPadding,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +76,6 @@ 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,
|
||||||
@ -98,8 +88,6 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
onCardTap: onCardTap,
|
onCardTap: onCardTap,
|
||||||
margin: margin,
|
|
||||||
contentPadding: contentPadding,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,8 +100,6 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
subtitleLines: subtitleLines,
|
subtitleLines: subtitleLines,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
onCardTap: onCardTap,
|
onCardTap: onCardTap,
|
||||||
margin: margin,
|
|
||||||
contentPadding: contentPadding,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
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';
|
||||||
@ -6,7 +7,6 @@ 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,11 +321,41 @@ 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 AdminSelectEnfantModal.show(
|
final selected = await showDialog<EnfantAdminModel>(
|
||||||
context,
|
context: context,
|
||||||
excludeIds: _children.map((c) => c.id).toSet(),
|
builder: (ctx) => SimpleDialog(
|
||||||
title: 'Rattacher un enfant',
|
title: const Text('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;
|
||||||
|
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,334 +0,0 @@
|
|||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,6 @@ 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,
|
||||||
@ -31,7 +30,6 @@ class AdminUserCard extends StatefulWidget {
|
|||||||
this.vigilanceTooltip,
|
this.vigilanceTooltip,
|
||||||
this.onCardTap,
|
this.onCardTap,
|
||||||
this.margin,
|
this.margin,
|
||||||
this.contentPadding,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -71,8 +69,7 @@ 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: widget.contentPadding ??
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildAvatar(avatarUrl),
|
_buildAvatar(avatarUrl),
|
||||||
@ -91,10 +88,7 @@ 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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user