feat(#132): création enfant staff (onglet Enfants) — front + back.
Squash depuis develop : POST /enfants staff + parent_user_id, photo multipart, modale création + sélection famille, fixes UX/birth_date. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,14 +1,27 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
|
||||
/// Cadre photo identité AM (35×45 mm) — même logique que [ValidationAmWizard].
|
||||
/// Cadre photo identité AM / enfant (35×45 mm) — même logique que [ValidationAmWizard].
|
||||
class AdminAmPhotoFrame extends StatelessWidget {
|
||||
final String? photoUrl;
|
||||
final Uint8List? imageBytes;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onClear;
|
||||
final String emptyLabel;
|
||||
|
||||
static const double idPhotoAspectRatio = 35 / 45;
|
||||
|
||||
const AdminAmPhotoFrame({super.key, this.photoUrl});
|
||||
const AdminAmPhotoFrame({
|
||||
super.key,
|
||||
this.photoUrl,
|
||||
this.imageBytes,
|
||||
this.onTap,
|
||||
this.onClear,
|
||||
this.emptyLabel = 'Aucune photo',
|
||||
});
|
||||
|
||||
/// Largeur colonne photo pour remplir [height] (cadre inclus).
|
||||
static double columnWidthForHeight(double height) {
|
||||
@@ -36,9 +49,12 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
||||
ph = pw / ar;
|
||||
}
|
||||
|
||||
final hasLocal = imageBytes != null && imageBytes!.isNotEmpty;
|
||||
final showClear = onClear != null && hasLocal;
|
||||
|
||||
// Cadre gris = taille photo + padding uniforme ; centré dans la colonne
|
||||
// (évite le vide blanc en bas quand le conteneur parent est plus haut).
|
||||
return Align(
|
||||
Widget frame = Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -60,22 +76,81 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
frame = MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: frame,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!showClear) return frame;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
frame,
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
tooltip: 'Retirer la photo',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.cancel,
|
||||
size: 22,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
onPressed: onClear,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _photoContent(String fullUrl, double pw, double ph) {
|
||||
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||
return Image.memory(
|
||||
imageBytes!,
|
||||
width: pw,
|
||||
height: ph,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
);
|
||||
}
|
||||
if (fullUrl.isEmpty) {
|
||||
return ColoredBox(
|
||||
color: Colors.grey.shade200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person_off_outlined, size: 36, color: Colors.grey.shade400),
|
||||
Icon(
|
||||
onTap != null ? Icons.add_a_photo_outlined : Icons.person_off_outlined,
|
||||
size: 36,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Aucune photo',
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 11),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
emptyLabel,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -108,7 +183,11 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
||||
},
|
||||
errorBuilder: (_, __, ___) => ColoredBox(
|
||||
color: Colors.grey.shade200,
|
||||
child: Icon(Icons.broken_image_outlined, size: 36, color: Colors.grey.shade400),
|
||||
child: Icon(
|
||||
Icons.broken_image_outlined,
|
||||
size: 36,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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/enfant_admin_model.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
@@ -6,26 +8,37 @@ import 'package:p_tits_pas/services/auth_service.dart';
|
||||
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/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/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';
|
||||
|
||||
/// Fiche enfant consultation / édition (ticket #138) — format paysage comme fiche AM.
|
||||
/// Fiche enfant consultation / édition (#138) ou création (#132).
|
||||
class AdminChildDetailModal extends StatefulWidget {
|
||||
final EnfantAdminModel enfant;
|
||||
final EnfantAdminModel? enfant;
|
||||
final VoidCallback? onSaved;
|
||||
final VoidCallback? onDeleted;
|
||||
final bool isCreating;
|
||||
|
||||
const AdminChildDetailModal({
|
||||
super.key,
|
||||
required this.enfant,
|
||||
required EnfantAdminModel this.enfant,
|
||||
this.onSaved,
|
||||
this.onDeleted,
|
||||
});
|
||||
}) : isCreating = false;
|
||||
|
||||
/// Création depuis l'onglet Enfants (#132).
|
||||
const AdminChildDetailModal.create({
|
||||
super.key,
|
||||
this.onSaved,
|
||||
}) : enfant = null,
|
||||
onDeleted = null,
|
||||
isCreating = true;
|
||||
|
||||
@override
|
||||
State<AdminChildDetailModal> createState() => _AdminChildDetailModalState();
|
||||
@@ -37,7 +50,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
late final TextEditingController _birthCtrl;
|
||||
late final TextEditingController _dueCtrl;
|
||||
late String _status;
|
||||
late String _gender;
|
||||
late String? _gender;
|
||||
late bool _consentPhoto;
|
||||
late bool _isMultiple;
|
||||
bool _dirty = false;
|
||||
@@ -49,6 +62,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
/// AM rattachée au chargement (pour sync différé au Sauvegarder).
|
||||
String? _baselineAmUserId;
|
||||
|
||||
/// Famille choisie en mode création (#132).
|
||||
AdminFamilleFoyer? _selectedFamily;
|
||||
|
||||
/// Photo locale (création) — upload multipart `photo`.
|
||||
Uint8List? _photoBytes;
|
||||
String? _photoFilename;
|
||||
|
||||
static const double _modalWidth = 930;
|
||||
static const double _mainRowHeight = 380;
|
||||
static const double _placementHeight = 96;
|
||||
@@ -74,44 +94,102 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
_isUnborn ? 'Date prévisionnelle' : 'Date de naissance';
|
||||
|
||||
bool get _canDelete =>
|
||||
!widget.isCreating &&
|
||||
(_currentUserRole ?? '').toLowerCase() == 'super_admin';
|
||||
|
||||
bool get _busy => _saving || _deleting;
|
||||
|
||||
/// Création : tous les champs sauf photo / consentement ; édition : au moins une modif.
|
||||
bool get _canSubmit {
|
||||
if (_busy) return false;
|
||||
if (widget.isCreating) return _isCreateFormComplete;
|
||||
return _dirty;
|
||||
}
|
||||
|
||||
bool get _isCreateFormComplete {
|
||||
if (_selectedFamily == null) return false;
|
||||
if (formatPersonNameCase(_prenomCtrl.text).isEmpty) return false;
|
||||
if (formatPersonNameCase(_nomCtrl.text).isEmpty) return false;
|
||||
if (_dateToIso(_activeDateCtrl.text) == null) return false;
|
||||
if (_gender == null || !_availableGenders.contains(_gender)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final e = widget.enfant;
|
||||
_prenomCtrl = TextEditingController(text: e.firstName ?? '');
|
||||
_nomCtrl = TextEditingController(text: e.lastName ?? '');
|
||||
_prenomCtrl = TextEditingController(text: e?.firstName ?? '');
|
||||
_nomCtrl = TextEditingController(text: e?.lastName ?? '');
|
||||
_birthCtrl = TextEditingController(
|
||||
text: formatIsoDateFr(e.birthDate, ifEmpty: ''),
|
||||
text: formatIsoDateFrInput(e?.birthDate, ifEmpty: ''),
|
||||
);
|
||||
_dueCtrl = TextEditingController(
|
||||
text: formatIsoDateFr(e.dueDate, ifEmpty: ''),
|
||||
text: formatIsoDateFrInput(e?.dueDate, ifEmpty: ''),
|
||||
);
|
||||
_status = normalizeEnfantStatus(e.status);
|
||||
_status = normalizeEnfantStatus(e?.status);
|
||||
if (!enfantStatusValues.contains(_status)) {
|
||||
_status = 'sans_garde';
|
||||
}
|
||||
_gender = _normalizeGender(e.gender, allowUnknown: _isUnborn);
|
||||
_consentPhoto = e.consentPhoto;
|
||||
_isMultiple = e.isMultiple;
|
||||
for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) {
|
||||
c.addListener(_markDirty);
|
||||
if (widget.isCreating) {
|
||||
_gender = null;
|
||||
} else {
|
||||
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
||||
}
|
||||
_consentPhoto = e?.consentPhoto ?? false;
|
||||
_isMultiple = e?.isMultiple ?? false;
|
||||
for (final c in [_prenomCtrl, _nomCtrl]) {
|
||||
c.addListener(_onNameChanged);
|
||||
}
|
||||
for (final c in [_birthCtrl, _dueCtrl]) {
|
||||
c.addListener(_onDateChanged);
|
||||
}
|
||||
_prenomCtrl.addListener(_onNameChanged);
|
||||
_nomCtrl.addListener(_onNameChanged);
|
||||
_loadCurrentUserRole();
|
||||
_loadLinkedAm();
|
||||
if (widget.isCreating) {
|
||||
_loadingAm = false;
|
||||
_dirty = true;
|
||||
} else {
|
||||
_prenomCtrl.addListener(_markDirty);
|
||||
_nomCtrl.addListener(_markDirty);
|
||||
_loadLinkedAm();
|
||||
}
|
||||
}
|
||||
|
||||
void _onNameChanged() => setState(() {});
|
||||
|
||||
void _onDateChanged() {
|
||||
setState(() => _dirty = true);
|
||||
}
|
||||
|
||||
/// Date prévisionnelle strictement avant aujourd’hui (statut à naître).
|
||||
bool get _dueDateInPast {
|
||||
if (!_isUnborn) return false;
|
||||
final iso = _dateToIso(_dueCtrl.text);
|
||||
if (iso == null) return false;
|
||||
try {
|
||||
final due = DateTime.parse(iso);
|
||||
final now = DateTime.now();
|
||||
final dueDay = DateTime(due.year, due.month, due.day);
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
return dueDay.isBefore(today);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLinkedAm() async {
|
||||
if (widget.isCreating) {
|
||||
setState(() => _loadingAm = false);
|
||||
return;
|
||||
}
|
||||
final enfantId = widget.enfant?.id;
|
||||
if (enfantId == null || enfantId.isEmpty) {
|
||||
setState(() => _loadingAm = false);
|
||||
return;
|
||||
}
|
||||
setState(() => _loadingAm = true);
|
||||
try {
|
||||
final am = await UserService.findAmForEnfant(widget.enfant.id);
|
||||
final am = await UserService.findAmForEnfant(enfantId);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_linkedAm = am;
|
||||
@@ -156,8 +234,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
}
|
||||
|
||||
void _coerceGenderForStatus() {
|
||||
if (!_availableGenders.contains(_gender)) {
|
||||
_gender = 'H';
|
||||
if (_gender != null && !_availableGenders.contains(_gender)) {
|
||||
_gender = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,18 +246,25 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
String? _dateToIso(String text) => parseFrDateToIso(text);
|
||||
|
||||
String _headerTitle() {
|
||||
if (widget.isCreating) {
|
||||
final fn = _prenomCtrl.text.trim();
|
||||
final ln = _nomCtrl.text.trim();
|
||||
if (fn.isEmpty && ln.isEmpty) return 'Nouvel enfant';
|
||||
return '$fn $ln'.trim();
|
||||
}
|
||||
final fn = _prenomCtrl.text.trim();
|
||||
final ln = _nomCtrl.text.trim();
|
||||
if (fn.isEmpty && ln.isEmpty) {
|
||||
final fallback = widget.enfant.fullName.trim();
|
||||
final fallback = widget.enfant?.fullName.trim() ?? '';
|
||||
return fallback.isNotEmpty ? fallback : 'Enfant';
|
||||
}
|
||||
return '$fn $ln'.trim();
|
||||
}
|
||||
|
||||
List<EnfantParentLink> get _parentLinks => widget.enfant.parentLinks
|
||||
.where((l) => l.parentId.trim().isNotEmpty)
|
||||
.toList();
|
||||
List<EnfantParentLink> get _parentLinks =>
|
||||
(widget.enfant?.parentLinks ?? const <EnfantParentLink>[])
|
||||
.where((l) => l.parentId.trim().isNotEmpty)
|
||||
.toList();
|
||||
|
||||
Future<void> _openParent(EnfantParentLink link) async {
|
||||
if (_busy) return;
|
||||
@@ -205,6 +290,17 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
}
|
||||
|
||||
Widget? _parentsSubtitle() {
|
||||
if (widget.isCreating) {
|
||||
final family = _selectedFamily;
|
||||
if (family == null) return null;
|
||||
return Text(
|
||||
family.parentNames.isNotEmpty
|
||||
? 'Famille : ${family.parentNames.join(', ')}'
|
||||
: family.displayTitle,
|
||||
style: const TextStyle(fontSize: 13, color: Colors.black54),
|
||||
);
|
||||
}
|
||||
|
||||
final links = _parentLinks;
|
||||
if (links.isEmpty) return null;
|
||||
|
||||
@@ -252,15 +348,24 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_dirty) return;
|
||||
|
||||
if (widget.isCreating) {
|
||||
await _saveCreate();
|
||||
return;
|
||||
}
|
||||
|
||||
final enfantId = widget.enfant?.id;
|
||||
if (enfantId == null || enfantId.isEmpty) return;
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await _syncAmPlacement();
|
||||
|
||||
await UserService.updateEnfant(
|
||||
enfantId: widget.enfant.id,
|
||||
enfantId: enfantId,
|
||||
body: {
|
||||
'first_name': _prenomCtrl.text.trim(),
|
||||
'last_name': _nomCtrl.text.trim(),
|
||||
'first_name': formatPersonNameCase(_prenomCtrl.text),
|
||||
'last_name': formatPersonNameCase(_nomCtrl.text),
|
||||
'status': _status,
|
||||
'gender': _gender,
|
||||
if (_isUnborn)
|
||||
@@ -291,21 +396,101 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveCreate() async {
|
||||
final family = _selectedFamily;
|
||||
if (family == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Choisissez une famille / un dossier avant d\'enregistrer'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final prenom = formatPersonNameCase(_prenomCtrl.text);
|
||||
final nom = formatPersonNameCase(_nomCtrl.text);
|
||||
if (prenom.isEmpty || nom.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Prénom et nom sont obligatoires')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_isUnborn && _dateToIso(_birthCtrl.text) == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Date de naissance invalide ou manquante')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final hasPhoto = _photoBytes != null && _photoBytes!.isNotEmpty;
|
||||
if (hasPhoto && !_consentPhoto) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Activez le consentement photo pour enregistrer une photo',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await UserService.createEnfant(
|
||||
parentUserId: family.pivotParentUserId,
|
||||
photoBytes: hasPhoto ? _photoBytes : null,
|
||||
photoFilename: _photoFilename,
|
||||
body: {
|
||||
'first_name': prenom,
|
||||
'last_name': nom,
|
||||
'status': _status,
|
||||
'gender': _gender,
|
||||
if (_isUnborn) ...{
|
||||
if (_dateToIso(_dueCtrl.text) != null)
|
||||
'due_date': _dateToIso(_dueCtrl.text),
|
||||
} else ...{
|
||||
if (_dateToIso(_birthCtrl.text) != null)
|
||||
'birth_date': _dateToIso(_birthCtrl.text),
|
||||
},
|
||||
'consent_photo': _consentPhoto,
|
||||
'is_multiple': _isMultiple,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_dirty = false;
|
||||
_saving = false;
|
||||
});
|
||||
widget.onSaved?.call();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enfant créé')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _saving = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applique rattachement / détachement AM (différé jusqu'au Sauvegarder).
|
||||
Future<void> _syncAmPlacement() async {
|
||||
final enfantId = widget.enfant?.id;
|
||||
if (enfantId == null || enfantId.isEmpty) return;
|
||||
|
||||
final baselineId = _baselineAmUserId;
|
||||
final currentId = _linkedAm?.user.id;
|
||||
|
||||
if (baselineId != null && baselineId != currentId) {
|
||||
await UserService.detachEnfantFromAm(
|
||||
amUserId: baselineId,
|
||||
enfantId: widget.enfant.id,
|
||||
enfantId: enfantId,
|
||||
);
|
||||
}
|
||||
if (currentId != null && currentId != baselineId) {
|
||||
await UserService.attachEnfantToAm(
|
||||
amUserId: currentId,
|
||||
enfantId: widget.enfant.id,
|
||||
enfantId: enfantId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -342,7 +527,9 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
|
||||
setState(() => _deleting = true);
|
||||
try {
|
||||
await UserService.deleteEnfant(widget.enfant.id);
|
||||
final id = widget.enfant?.id;
|
||||
if (id == null || id.isEmpty) return;
|
||||
await UserService.deleteEnfant(id);
|
||||
if (!mounted) return;
|
||||
widget.onDeleted?.call();
|
||||
widget.onSaved?.call();
|
||||
@@ -376,10 +563,12 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
|
||||
/// Recharge AM + statut après une modale externe (ex. fiche AM).
|
||||
Future<void> _reloadPlacementFromServer() async {
|
||||
final id = widget.enfant?.id;
|
||||
if (id == null || id.isEmpty) return;
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
UserService.findAmForEnfant(widget.enfant.id),
|
||||
UserService.getEnfant(widget.enfant.id),
|
||||
UserService.findAmForEnfant(id),
|
||||
UserService.getEnfant(id),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
final am = results[0] as AssistanteMaternelleModel?;
|
||||
@@ -476,70 +665,145 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
});
|
||||
}
|
||||
|
||||
InputDecoration _inputDecoration({String? hint}) {
|
||||
return ValidationFieldDecoration.input(hint: hint).copyWith(
|
||||
/// Conserve la date saisie en basculant naissance ↔ prévisionnelle.
|
||||
void _preserveDateOnStatusChange(String from, String to) {
|
||||
final becomingUnborn = to == 'a_naitre' && from != 'a_naitre';
|
||||
final leavingUnborn = from == 'a_naitre' && to != 'a_naitre';
|
||||
if (becomingUnborn) {
|
||||
if (_dueCtrl.text.trim().isEmpty &&
|
||||
_birthCtrl.text.trim().isNotEmpty) {
|
||||
_dueCtrl.text = _birthCtrl.text;
|
||||
}
|
||||
} else if (leavingUnborn) {
|
||||
if (_birthCtrl.text.trim().isEmpty &&
|
||||
_dueCtrl.text.trim().isNotEmpty) {
|
||||
_birthCtrl.text = _dueCtrl.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InputDecoration _inputDecoration({String? hint, bool error = false}) {
|
||||
final base = ValidationFieldDecoration.input(hint: hint).copyWith(
|
||||
isDense: false,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
);
|
||||
if (!error) return base;
|
||||
final border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: Colors.red.shade400),
|
||||
);
|
||||
return base.copyWith(
|
||||
fillColor: Colors.red.shade50,
|
||||
enabledBorder: border,
|
||||
focusedBorder: border,
|
||||
border: border,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dropdownField({
|
||||
Key? key,
|
||||
required String value,
|
||||
required String? value,
|
||||
required List<MapEntry<String, String>> items,
|
||||
required ValueChanged<String> onChanged,
|
||||
String? hint,
|
||||
}) {
|
||||
final safeValue =
|
||||
items.any((e) => e.key == value) ? value : items.first.key;
|
||||
value != null && items.any((e) => e.key == value) ? value : null;
|
||||
return SizedBox(
|
||||
height: _fieldHeight,
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: key,
|
||||
value: safeValue,
|
||||
hint: hint != null
|
||||
? Text(hint, style: TextStyle(color: Colors.grey.shade600))
|
||||
: null,
|
||||
isExpanded: true,
|
||||
decoration: _inputDecoration(),
|
||||
items: items
|
||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: _busy ? null : (v) {
|
||||
if (v != null) onChanged(v);
|
||||
},
|
||||
items: items
|
||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: _busy
|
||||
? null
|
||||
: (v) {
|
||||
if (v != null) onChanged(v);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textField(TextEditingController controller, {String? hint, Key? key}) {
|
||||
Widget _textField(
|
||||
TextEditingController controller, {
|
||||
String? hint,
|
||||
Key? key,
|
||||
TextInputType? keyboardType,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool error = false,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: _fieldHeight,
|
||||
child: TextField(
|
||||
key: key,
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||
decoration: _inputDecoration(hint: hint),
|
||||
style: TextStyle(
|
||||
color: error ? Colors.red.shade800 : Colors.black87,
|
||||
fontSize: 14,
|
||||
),
|
||||
decoration: _inputDecoration(
|
||||
hint: hint,
|
||||
error: error,
|
||||
).copyWith(suffixIcon: suffixIcon),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dateField() {
|
||||
final past = _dueDateInPast;
|
||||
return _textField(
|
||||
_activeDateCtrl,
|
||||
hint: 'jj / mm / aaaa',
|
||||
key: ValueKey(_status),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: const [FrenchDateInputFormatter()],
|
||||
error: past,
|
||||
suffixIcon: past
|
||||
? Tooltip(
|
||||
message:
|
||||
'Date prévisionnelle antérieure à aujourd’hui',
|
||||
child: Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fieldsGrid() {
|
||||
return ValidationEditableSection(
|
||||
rowLayout: _fieldsRowLayout,
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'Prénom',
|
||||
field: _textField(_prenomCtrl),
|
||||
field: _textField(
|
||||
_prenomCtrl,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Nom',
|
||||
field: _textField(_nomCtrl),
|
||||
field: _textField(
|
||||
_nomCtrl,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: _dateFieldLabel,
|
||||
field: _textField(
|
||||
_activeDateCtrl,
|
||||
hint: 'jj/mm/aaaa',
|
||||
key: ValueKey(_status),
|
||||
),
|
||||
field: _dateField(),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Statut',
|
||||
@@ -555,6 +819,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_preserveDateOnStatusChange(_status, v);
|
||||
_status = v;
|
||||
_coerceGenderForStatus();
|
||||
_dirty = true;
|
||||
@@ -570,6 +835,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
field: _dropdownField(
|
||||
key: ValueKey('genre-$_status'),
|
||||
value: _gender,
|
||||
hint: 'Veuillez sélectionner le genre',
|
||||
items: _availableGenders
|
||||
.map((g) => MapEntry(g, _genderLabel(g)))
|
||||
.toList(),
|
||||
@@ -603,6 +869,43 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickPhoto() async {
|
||||
if (_busy || !widget.isCreating) return;
|
||||
try {
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 75,
|
||||
maxWidth: 1200,
|
||||
maxHeight: 1200,
|
||||
);
|
||||
if (picked == null) return;
|
||||
final bytes = await picked.readAsBytes();
|
||||
if (bytes.isEmpty) return;
|
||||
final name = picked.name.trim();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_photoBytes = bytes;
|
||||
_photoFilename = name.isNotEmpty ? name : 'photo.jpg';
|
||||
_consentPhoto = true;
|
||||
_dirty = true;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossible de charger la photo')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _clearPhoto() {
|
||||
if (_busy || !widget.isCreating) return;
|
||||
setState(() {
|
||||
_photoBytes = null;
|
||||
_photoFilename = null;
|
||||
_dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _mainRow() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
@@ -625,7 +928,20 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: AdminAmPhotoFrame(
|
||||
photoUrl: widget.enfant.photoUrl,
|
||||
photoUrl: widget.isCreating
|
||||
? null
|
||||
: widget.enfant?.photoUrl,
|
||||
imageBytes: widget.isCreating ? _photoBytes : null,
|
||||
onTap: widget.isCreating && !_busy ? _pickPhoto : null,
|
||||
onClear: widget.isCreating &&
|
||||
!_busy &&
|
||||
_photoBytes != null &&
|
||||
_photoBytes!.isNotEmpty
|
||||
? _clearPhoto
|
||||
: null,
|
||||
emptyLabel: widget.isCreating
|
||||
? 'Cliquer pour ajouter'
|
||||
: 'Aucune photo',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -893,8 +1209,10 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
);
|
||||
}
|
||||
|
||||
String get _placementTitle =>
|
||||
_isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||
String get _placementTitle {
|
||||
if (widget.isCreating) return 'Sélection du dossier de la famille';
|
||||
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||
}
|
||||
|
||||
Widget _placementBlock() {
|
||||
return Column(
|
||||
@@ -910,11 +1228,161 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_placementSection(),
|
||||
if (widget.isCreating) _familyPlacementSection() else _placementSection(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickFamily() async {
|
||||
if (_busy) return;
|
||||
final selected = await AdminSelectFamilleModal.show(
|
||||
context,
|
||||
title: 'Choisir une famille',
|
||||
);
|
||||
if (selected == null || !mounted) return;
|
||||
setState(() {
|
||||
_selectedFamily = selected;
|
||||
_dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _familyPlacementSection() {
|
||||
final family = _selectedFamily;
|
||||
if (family == null) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _busy ? null : _pickFamily,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
height: _placementHeight,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.family_restroom,
|
||||
size: 30,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Aucune famille sélectionnée',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Cliquer pour choisir un dossier / foyer',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.add_circle_outline, color: Colors.grey.shade500),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _busy ? null : _pickFamily,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
height: _placementHeight,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F5FC),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFD8CCE8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEDE5FA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.family_restroom,
|
||||
size: 30,
|
||||
color: Color(0xFF6B3FA0),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
family.displayTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (family.parentNames.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
family.parentNames.join(', '),
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.black54,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.swap_horiz, color: Colors.grey.shade700),
|
||||
tooltip: 'Changer de famille',
|
||||
onPressed: _busy ? null : _pickFamily,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placementSection() {
|
||||
if (!_showsAmPlacement) return _scolariseCard();
|
||||
|
||||
@@ -966,7 +1434,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
style: ValidationModalTheme.primaryElevatedStyle,
|
||||
onPressed: !_dirty || _busy ? null : _save,
|
||||
onPressed: !_canSubmit || _busy ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
@@ -976,7 +1444,11 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
||||
: Text(
|
||||
widget.isCreating
|
||||
? 'Créer'
|
||||
: (_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/parent_model.dart';
|
||||
import 'package:p_tits_pas/services/user_service.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';
|
||||
|
||||
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132).
|
||||
class AdminFamilleFoyer {
|
||||
/// Parent pivot pour `POST /enfants` (`parent_user_id`).
|
||||
final String pivotParentUserId;
|
||||
final String? numeroDossier;
|
||||
final String displayTitle;
|
||||
final List<String> parentNames;
|
||||
|
||||
const AdminFamilleFoyer({
|
||||
required this.pivotParentUserId,
|
||||
required this.displayTitle,
|
||||
required this.parentNames,
|
||||
this.numeroDossier,
|
||||
});
|
||||
|
||||
String get subtitle {
|
||||
final parts = <String>[];
|
||||
final dossier = (numeroDossier ?? '').trim();
|
||||
if (dossier.isNotEmpty) parts.add('Dossier $dossier');
|
||||
if (parentNames.isNotEmpty) {
|
||||
parts.add('Responsables : ${parentNames.join(', ')}');
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit la liste des foyers uniques à partir de `GET /parents`.
|
||||
List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
||||
final seenDossiers = <String>{};
|
||||
final seenUserIds = <String>{};
|
||||
final foyers = <AdminFamilleFoyer>[];
|
||||
|
||||
for (final p in parents) {
|
||||
final dossier = (p.user.numeroDossier ?? '').trim();
|
||||
if (dossier.isNotEmpty) {
|
||||
if (seenDossiers.contains(dossier)) continue;
|
||||
seenDossiers.add(dossier);
|
||||
} else if (seenUserIds.contains(p.user.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenUserIds.add(p.user.id);
|
||||
final co = p.coParent;
|
||||
if (co != null) seenUserIds.add(co.id);
|
||||
|
||||
final names = <String>[
|
||||
if (p.user.fullName.trim().isNotEmpty) p.user.fullName.trim(),
|
||||
if (co != null && co.fullName.trim().isNotEmpty) co.fullName.trim(),
|
||||
];
|
||||
|
||||
final title = dossier.isNotEmpty
|
||||
? 'Dossier $dossier'
|
||||
: (names.isNotEmpty ? names.first : 'Famille');
|
||||
|
||||
foyers.add(
|
||||
AdminFamilleFoyer(
|
||||
pivotParentUserId: p.user.id,
|
||||
numeroDossier: dossier.isNotEmpty ? dossier : null,
|
||||
displayTitle: title,
|
||||
parentNames: names,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
foyers.sort(
|
||||
(a, b) => a.displayTitle.toLowerCase().compareTo(b.displayTitle.toLowerCase()),
|
||||
);
|
||||
return foyers;
|
||||
}
|
||||
|
||||
/// Sélection d'une famille / dossier pour créer un enfant — ticket #132.
|
||||
class AdminSelectFamilleModal {
|
||||
AdminSelectFamilleModal._();
|
||||
|
||||
static Future<AdminFamilleFoyer?> show(
|
||||
BuildContext context, {
|
||||
String title = 'Choisir une famille',
|
||||
}) {
|
||||
return AdminSelectListModal.show<AdminFamilleFoyer>(
|
||||
context,
|
||||
title: title,
|
||||
searchHint: 'Rechercher par dossier, nom…',
|
||||
emptyMessage: 'Aucune famille disponible',
|
||||
noResultsMessage: 'Aucun résultat pour cette recherche',
|
||||
loadItems: () async {
|
||||
final parents = await UserService.getParents();
|
||||
return buildFamilleFoyers(parents);
|
||||
},
|
||||
matchesQuery: (f, q) {
|
||||
final dossier = (f.numeroDossier ?? '').toLowerCase();
|
||||
final title = f.displayTitle.toLowerCase();
|
||||
final names = f.parentNames.join(' ').toLowerCase();
|
||||
return dossier.contains(q) || title.contains(q) || names.contains(q);
|
||||
},
|
||||
itemBuilder: (context, f, onSelect) {
|
||||
return AdminUserCard(
|
||||
title: f.displayTitle,
|
||||
fallbackIcon: Icons.family_restroom,
|
||||
subtitleLines: [
|
||||
if (f.parentNames.isNotEmpty) f.parentNames.join(', '),
|
||||
if ((f.numeroDossier ?? '').isNotEmpty)
|
||||
'Dossier ${f.numeroDossier}',
|
||||
],
|
||||
onCardTap: onSelect,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
tooltip: 'Choisir',
|
||||
onPressed: onSelect,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/enfant_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
||||
@@ -26,6 +27,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
int _subIndex = 0;
|
||||
int _gestionnaireRefreshTick = 0;
|
||||
int _adminRefreshTick = 0;
|
||||
int _enfantRefreshTick = 0;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final TextEditingController _amCapacityController = TextEditingController();
|
||||
String? _parentStatus;
|
||||
@@ -264,6 +266,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
);
|
||||
case 1:
|
||||
return EnfantManagementWidget(
|
||||
key: ValueKey('enfants-$_enfantRefreshTick'),
|
||||
searchQuery: _searchController.text,
|
||||
statusFilter: _enfantStatus,
|
||||
);
|
||||
@@ -311,6 +314,23 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
|
||||
Future<void> _handleAddPressed() async {
|
||||
final contentIndex = _subIndex - _contentIndexOffset;
|
||||
|
||||
if (contentIndex == 1) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return AdminChildDetailModal.create(
|
||||
onSaved: () {
|
||||
if (!mounted) return;
|
||||
setState(() => _enfantRefreshTick++);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentIndex == 3) {
|
||||
final created = await showDialog<bool>(
|
||||
context: context,
|
||||
|
||||
Reference in New Issue
Block a user