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:
2026-07-17 19:00:12 +02:00
co-authored by Cursor
parent 3c7f4f6e16
commit 04f49cb62f
12 changed files with 1177 additions and 118 deletions
@@ -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 aujourdhui (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 à aujourdhui',
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'),
),
),
],
);