Modales de rattachement partagées, filtre Libre/Sans garde, recalcul des places à l'attach, désactivation si capacité pleine, clic sur case libre. Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
7.0 KiB
Dart
219 lines
7.0 KiB
Dart
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||
import 'package:p_tits_pas/services/user_service.dart';
|
||
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
||
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
|
||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
||
|
||
List<String> _amSelectSubtitleLines(AssistanteMaternelleModel am) {
|
||
final lines = <String>[];
|
||
final zone = (am.residenceCity ?? '').trim();
|
||
if (zone.isNotEmpty) lines.add('Zone : $zone');
|
||
final agrement = (am.approvalNumber ?? '').trim();
|
||
if (agrement.isNotEmpty) lines.add('Agrément : $agrement');
|
||
final max = am.maxChildren;
|
||
final free = amExpectedPlacesAvailable(
|
||
maxChildren: max,
|
||
childrenCount: am.children.length,
|
||
) ??
|
||
am.placesAvailable;
|
||
if (free != null || max != null) {
|
||
lines.add('Places libres : ${free ?? '–'} / capa. ${max ?? '–'}');
|
||
}
|
||
lines.add('${am.children.length} enfant(s)');
|
||
return lines;
|
||
}
|
||
|
||
/// Sélection d'une AM à rattacher (fiche enfant) — ticket #147.
|
||
/// S'appuie sur [AdminSelectListModal] (shell partagé avec #146).
|
||
class AdminSelectAmModal {
|
||
AdminSelectAmModal._();
|
||
|
||
static Future<AssistanteMaternelleModel?> show(
|
||
BuildContext context, {
|
||
Set<String> excludeIds = const {},
|
||
String title = 'Choisir une assistante maternelle',
|
||
}) {
|
||
return AdminSelectListModal.show<AssistanteMaternelleModel>(
|
||
context,
|
||
title: title,
|
||
searchHint: 'Rechercher par nom, prénom ou zone…',
|
||
emptyMessage: 'Aucune assistante maternelle disponible',
|
||
noResultsMessage: 'Aucune AM avec place libre pour cette recherche',
|
||
toggleFilter: const AdminSelectToggleFilter<AssistanteMaternelleModel>(
|
||
label: 'Libre',
|
||
initialValue: true,
|
||
whenEnabled: amHasFreePlace,
|
||
),
|
||
loadItems: () async {
|
||
final list = await UserService.getAssistantesMaternelles();
|
||
return list
|
||
.where((am) => !excludeIds.contains(am.user.id))
|
||
.toList()
|
||
..sort(
|
||
(a, b) => a.user.fullName
|
||
.toLowerCase()
|
||
.compareTo(b.user.fullName.toLowerCase()),
|
||
);
|
||
},
|
||
matchesQuery: (am, q) {
|
||
final u = am.user;
|
||
final name = u.fullName.toLowerCase();
|
||
final fn = (u.prenom ?? '').toLowerCase();
|
||
final ln = (u.nom ?? '').toLowerCase();
|
||
final zone = (am.residenceCity ?? '').toLowerCase();
|
||
final agrement = (am.approvalNumber ?? '').toLowerCase();
|
||
return name.contains(q) ||
|
||
fn.contains(q) ||
|
||
ln.contains(q) ||
|
||
zone.contains(q) ||
|
||
agrement.contains(q);
|
||
},
|
||
resolveSelect: (ctx, am, reload) =>
|
||
_resolveAmSelection(ctx, am, reload),
|
||
itemBuilder: (context, am, onSelect) {
|
||
final full = !amHasFreePlace(am);
|
||
return AdminUserCard(
|
||
title: am.user.fullName,
|
||
avatarUrl: am.user.photoUrl,
|
||
fallbackIcon: Icons.face,
|
||
subtitleLines: _amSelectSubtitleLines(am),
|
||
vigilanceTooltip: amPlacesVigilanceMessage(am),
|
||
onCardTap: onSelect,
|
||
margin: const EdgeInsets.only(bottom: 4),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 10,
|
||
vertical: 5,
|
||
),
|
||
backgroundColor: full ? const Color(0xFFFFEBEE) : null,
|
||
borderColor: full ? Colors.red.shade200 : null,
|
||
actions: [
|
||
IconButton(
|
||
icon: const Icon(Icons.add_link),
|
||
tooltip: 'Rattacher',
|
||
onPressed: onSelect,
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
static Future<AssistanteMaternelleModel?> _resolveAmSelection(
|
||
BuildContext context,
|
||
AssistanteMaternelleModel am,
|
||
Future<void> Function() reloadList,
|
||
) async {
|
||
if (amHasFreePlace(am)) return am;
|
||
|
||
final selected = await showDialog<AssistanteMaternelleModel>(
|
||
context: context,
|
||
builder: (ctx) => _AmNoPlaceWarningDialog(initialAm: am),
|
||
);
|
||
await reloadList();
|
||
return selected;
|
||
}
|
||
}
|
||
|
||
/// Avertissement AM saturée + lien vers la fiche pour ajuster les places.
|
||
class _AmNoPlaceWarningDialog extends StatefulWidget {
|
||
final AssistanteMaternelleModel initialAm;
|
||
|
||
const _AmNoPlaceWarningDialog({required this.initialAm});
|
||
|
||
@override
|
||
State<_AmNoPlaceWarningDialog> createState() =>
|
||
_AmNoPlaceWarningDialogState();
|
||
}
|
||
|
||
class _AmNoPlaceWarningDialogState extends State<_AmNoPlaceWarningDialog> {
|
||
late AssistanteMaternelleModel _am;
|
||
TapGestureRecognizer? _linkRecognizer;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_am = widget.initialAm;
|
||
_linkRecognizer = TapGestureRecognizer()..onTap = _openAmFiche;
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_linkRecognizer?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _openAmFiche() async {
|
||
if (!mounted) return;
|
||
await showDialog<void>(
|
||
context: context,
|
||
builder: (ctx) => AdminAmEditModal(
|
||
assistante: _am,
|
||
onSaved: () async {
|
||
try {
|
||
final fresh =
|
||
await UserService.getAssistanteMaternelle(_am.user.id);
|
||
if (mounted) setState(() => _am = fresh);
|
||
} catch (_) {}
|
||
},
|
||
),
|
||
);
|
||
if (!mounted) return;
|
||
|
||
try {
|
||
final fresh = await UserService.getAssistanteMaternelle(_am.user.id);
|
||
if (!mounted) return;
|
||
setState(() => _am = fresh);
|
||
if (amHasFreePlace(fresh)) {
|
||
Navigator.of(context).pop(fresh);
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final name = _am.user.fullName.trim().isNotEmpty
|
||
? _am.user.fullName.trim()
|
||
: 'Cette assistante maternelle';
|
||
const linkColor = ValidationModalTheme.primaryActionBackground;
|
||
|
||
return AlertDialog(
|
||
title: const Text('Plus de place disponible'),
|
||
content: Text.rich(
|
||
TextSpan(
|
||
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
|
||
children: [
|
||
TextSpan(
|
||
text: '$name n\'a plus de place libre pour accueillir '
|
||
'un enfant supplémentaire.\n\n',
|
||
),
|
||
const TextSpan(text: 'Vous pouvez '),
|
||
TextSpan(
|
||
text: 'ouvrir sa fiche',
|
||
style: TextStyle(
|
||
color: linkColor,
|
||
decoration: TextDecoration.underline,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
recognizer: _linkRecognizer,
|
||
),
|
||
const TextSpan(
|
||
text: ' pour modifier la capacité ou les places, '
|
||
'puis la sélectionner si une place se libère.',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Fermer'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|