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'; 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 (#138) ou création (#132). class AdminChildDetailModal extends StatefulWidget { final EnfantAdminModel? enfant; final VoidCallback? onSaved; final VoidCallback? onDeleted; final bool isCreating; const AdminChildDetailModal({ super.key, 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 createState() => _AdminChildDetailModalState(); } class _AdminChildDetailModalState extends State { late final TextEditingController _prenomCtrl; late final TextEditingController _nomCtrl; late final TextEditingController _birthCtrl; late final TextEditingController _dueCtrl; late String _status; late String? _gender; late bool _consentPhoto; late bool _isMultiple; bool _dirty = false; bool _saving = false; bool _deleting = false; bool _loadingAm = true; String? _currentUserRole; AssistanteMaternelleModel? _linkedAm; /// 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; static const double _fieldHeight = 44; static const double _photoProGap = 24; static const double _proColumnMinWidth = 260; static const double _photoColumnMinWidth = 160; static const double _photoColumnMaxWidth = 280; static const List _fieldsRowLayout = [2, 2, 1]; bool get _isUnborn => _status == 'a_naitre'; bool get _isScolarise => _status == 'scolarise'; bool get _isSansGarde => _status == 'sans_garde'; bool get _showsAmPlacement => !_isScolarise; List get _availableGenders => _isUnborn ? const ['H', 'F', 'Autre'] : const ['H', 'F']; TextEditingController get _activeDateCtrl => _isUnborn ? _dueCtrl : _birthCtrl; String get _dateFieldLabel => _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 ?? ''); _birthCtrl = TextEditingController( text: formatIsoDateFrInput(e?.birthDate, ifEmpty: ''), ); _dueCtrl = TextEditingController( text: formatIsoDateFrInput(e?.dueDate, ifEmpty: ''), ); _status = normalizeEnfantStatus(e?.status); if (!enfantStatusValues.contains(_status)) { _status = 'sans_garde'; } 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); } _loadCurrentUserRole(); 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 _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(enfantId); if (!mounted) return; setState(() { _linkedAm = am; _baselineAmUserId = am?.user.id; _loadingAm = false; }); } catch (_) { if (!mounted) return; setState(() { _baselineAmUserId = null; _loadingAm = false; }); } } Future _loadCurrentUserRole() async { final cached = await AuthService.getCurrentUser(); if (!mounted) return; if (cached != null) { setState(() => _currentUserRole = cached.role); return; } final refreshed = await AuthService.refreshCurrentUser(); if (!mounted || refreshed == null) return; setState(() => _currentUserRole = refreshed.role); } @override void dispose() { for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) { c.dispose(); } super.dispose(); } static String _normalizeGender(String? raw, {required bool allowUnknown}) { final g = (raw ?? '').trim(); if (g == 'M') return 'H'; if (g == 'F' || g == 'H') return g; if (g == 'Autre' && allowUnknown) return 'Autre'; return 'H'; } void _coerceGenderForStatus() { if (_gender != null && !_availableGenders.contains(_gender)) { _gender = null; } } void _markDirty() { if (!_dirty) setState(() => _dirty = true); } 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() ?? ''; return fallback.isNotEmpty ? fallback : 'Enfant'; } return '$fn $ln'.trim(); } List get _parentLinks => (widget.enfant?.parentLinks ?? const []) .where((l) => l.parentId.trim().isNotEmpty) .toList(); Future _openParent(EnfantParentLink link) async { if (_busy) return; final id = link.parentId.trim(); if (id.isEmpty) return; try { final parent = await UserService.getParent(id); if (!mounted) return; await showDialog( context: context, builder: (ctx) => AdminParentEditModal( parent: parent, onSaved: () => widget.onSaved?.call(), ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } 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; return Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ const Text( 'Responsables : ', style: TextStyle(fontSize: 13, color: Colors.black54), ), for (var i = 0; i < links.length; i++) ...[ if (i > 0) const Text( ', ', style: TextStyle(fontSize: 13, color: Colors.black54), ), _parentNameLink(links[i]), ], ], ); } Widget _parentNameLink(EnfantParentLink link) { final name = (link.parentName ?? '').trim(); final label = name.isNotEmpty ? name : 'Parent rattaché'; return InkWell( onTap: _busy ? null : () => _openParent(link), borderRadius: BorderRadius.circular(4), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), child: Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: ValidationModalTheme.primaryActionBackground, decoration: TextDecoration.underline, decorationColor: ValidationModalTheme.primaryActionBackground .withValues(alpha: 0.5), ), ), ), ); } Future _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: enfantId, body: { 'first_name': formatPersonNameCase(_prenomCtrl.text), 'last_name': formatPersonNameCase(_nomCtrl.text), '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; _baselineAmUserId = _linkedAm?.user.id; }); widget.onSaved?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fiche enfant enregistrée')), ); } catch (e) { if (!mounted) return; setState(() => _saving = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Future _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 _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: enfantId, ); } if (currentId != null && currentId != baselineId) { await UserService.attachEnfantToAm( amUserId: currentId, enfantId: enfantId, ); } } Future _delete() async { if (!_canDelete || _deleting) return; final name = _headerTitle(); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Supprimer l\'enfant'), content: Text( 'Supprimer définitivement la fiche de $name ?\n' 'Cette action est irréversible.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade700, foregroundColor: Colors.white, ), child: const Text('Supprimer'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _deleting = true); try { final id = widget.enfant?.id; if (id == null || id.isEmpty) return; await UserService.deleteEnfant(id); if (!mounted) return; widget.onDeleted?.call(); widget.onSaved?.call(); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fiche enfant supprimée')), ); } catch (e) { if (!mounted) return; setState(() => _deleting = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Future _openLinkedAm() async { final am = _linkedAm; if (am == null) return; await showDialog( context: context, builder: (ctx) => AdminAmEditModal( assistante: am, onSaved: () async { await _reloadPlacementFromServer(); widget.onSaved?.call(); }, ), ); } /// Recharge AM + statut après une modale externe (ex. fiche AM). Future _reloadPlacementFromServer() async { final id = widget.enfant?.id; if (id == null || id.isEmpty) return; try { final results = await Future.wait([ UserService.findAmForEnfant(id), UserService.getEnfant(id), ]); if (!mounted) return; final am = results[0] as AssistanteMaternelleModel?; final enfant = results[1] as EnfantAdminModel; setState(() { _linkedAm = am; _baselineAmUserId = am?.user.id; _status = normalizeEnfantStatus(enfant.status); if (!enfantStatusValues.contains(_status)) { _status = 'sans_garde'; } _coerceGenderForStatus(); }); } catch (_) { if (!mounted) return; await _loadLinkedAm(); } } Future _attachAm() async { if (_linkedAm != null && !_isSansGarde) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Détachez l\'assistante actuelle avant d\'en choisir une autre', ), ), ); return; } if (!mounted) return; final selected = await AdminSelectAmModal.show( context, excludeIds: { if (_linkedAm != null) _linkedAm!.user.id, }, title: 'Choisir une assistante maternelle', ); if (selected == null || !mounted) return; setState(() { _linkedAm = selected; if (_status == 'sans_garde') { _status = 'garde'; } _dirty = true; }); } Future _detachAm() async { final am = _linkedAm; if (am == null) return; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Détacher l\'assistante'), content: Text( 'Retirer ${am.user.fullName} de la garde de cet enfant ?\n' '(Effectif après Sauvegarder.)', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ValidationModalTheme.primaryElevatedStyle, child: const Text('Détacher'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() { _linkedAm = null; if (_status != 'a_naitre' && _status != 'scolarise') { _status = 'sans_garde'; } _dirty = true; }); } /// Passe en « Sans garde » : cadre vide local (détachement au Sauvegarder). void _applySansGardeStatus() { if (_linkedAm == null) return; setState(() { _linkedAm = null; _dirty = true; }); } /// 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 List> items, required ValueChanged onChanged, String? hint, }) { final safeValue = value != null && items.any((e) => e.key == value) ? value : null; return SizedBox( height: _fieldHeight, child: DropdownButtonFormField( 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); }, ), ); } Widget _textField( TextEditingController controller, { String? hint, Key? key, TextInputType? keyboardType, List? inputFormatters, bool error = false, Widget? suffixIcon, }) { return SizedBox( height: _fieldHeight, child: TextField( key: key, controller: controller, keyboardType: keyboardType, inputFormatters: inputFormatters, textAlignVertical: TextAlignVertical.center, 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, inputFormatters: const [PersonNameInputFormatter()], ), ), ValidationLabeledField( label: 'Nom', field: _textField( _nomCtrl, inputFormatters: const [PersonNameInputFormatter()], ), ), ValidationLabeledField( label: _dateFieldLabel, field: _dateField(), ), ValidationLabeledField( label: 'Statut', field: _dropdownField( value: _status, items: enfantStatusValues .map( (s) => MapEntry( s, enfantStatusLabel(s, gender: _gender), ), ) .toList(), onChanged: (v) { setState(() { _preserveDateOnStatusChange(_status, v); _status = v; _coerceGenderForStatus(); _dirty = true; }); if (v == 'sans_garde') { _applySansGardeStatus(); } }, ), ), ValidationLabeledField( label: 'Genre', field: _dropdownField( key: ValueKey('genre-$_status'), value: _gender, hint: 'Veuillez sélectionner le genre', items: _availableGenders .map((g) => MapEntry(g, _genderLabel(g))) .toList(), onChanged: (v) => setState(() { _gender = v; _dirty = true; }), ), ), ], ); } Widget _consentPhotoSwitch() { return SwitchListTile( contentPadding: EdgeInsets.zero, dense: true, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, title: const Text( 'Consentement photo', style: TextStyle(fontSize: 13), ), value: _consentPhoto, onChanged: _busy ? null : (v) => setState(() { _consentPhoto = v; _dirty = true; }), ); } Future _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) { final maxRowW = c.maxWidth; final maxRowH = c.maxHeight; final idealPhotoW = maxRowH * AdminAmPhotoFrame.idPhotoAspectRatio + 16; final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth) .clamp(0.0, double.infinity); var photoW = idealPhotoW.clamp(_photoColumnMinWidth, _photoColumnMaxWidth); if (photoW > maxPhotoW) photoW = maxPhotoW; return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: photoW, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: AdminAmPhotoFrame( 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), _consentPhotoSwitch(), ], ), ), const SizedBox(width: _photoProGap), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _fieldsGrid(), Expanded( child: Padding( padding: const EdgeInsets.only(top: 16), child: Align( alignment: Alignment.bottomCenter, child: _placementBlock(), ), ), ), ], ), ), ], ); }, ); } Widget _scolariseCard() { return Container( height: _placementHeight, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFFE8F4FD), const Color(0xFFDCEEFB), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF90CAF9)), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.school_rounded, size: 32, color: Colors.blue.shade700, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( enfantStatusLabel('scolarise', gender: _gender), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.blue.shade900, ), ), const SizedBox(height: 4), Text( 'Enfant scolarisé — pas de garde chez une assistante maternelle', style: TextStyle( fontSize: 13, color: Colors.blue.shade800.withValues(alpha: 0.85), ), ), ], ), ), Icon(Icons.auto_stories_outlined, size: 28, color: Colors.blue.shade400), ], ), ); } Widget _emptyAmSlot() { return Material( color: Colors.transparent, child: InkWell( onTap: _busy ? null : _attachAm, 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.face_outlined, size: 30, color: Colors.grey.shade400, ), ), const SizedBox(width: 16), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Aucune assistante maternelle', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Colors.grey.shade800, ), ), const SizedBox(height: 4), Text( 'Cliquer pour choisir une assistante', style: TextStyle( fontSize: 13, color: Colors.grey.shade600, ), ), ], ), ), Icon(Icons.add_circle_outline, color: Colors.grey.shade500), ], ), ), ), ); } Widget _linkedAmCard() { final am = _linkedAm!; final zone = (am.residenceCity ?? '').trim(); final agrement = (am.approvalNumber ?? '').trim(); final subtitle = [ if (zone.isNotEmpty) 'Zone : $zone', if (agrement.isNotEmpty) 'Agrément : $agrement', ].join(' · '); return Material( color: Colors.transparent, child: InkWell( onTap: _busy ? null : _openLinkedAm, borderRadius: BorderRadius.circular(10), child: Container( height: _placementHeight, width: double.infinity, decoration: BoxDecoration( color: const Color(0xFFF7F3FC), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFD4C4EF)), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ _amAvatar(am.user.photoUrl), const SizedBox(width: 16), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( am.user.fullName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFF4A2F7A), ), ), if (subtitle.isNotEmpty) ...[ const SizedBox(height: 4), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: Colors.grey.shade700, ), ), ], ], ), ), IconButton( icon: const Icon(Icons.open_in_new, size: 20), tooltip: 'Ouvrir la fiche AM', onPressed: _busy ? null : _openLinkedAm, ), IconButton( icon: Icon(Icons.link_off, size: 20, color: Colors.orange.shade800), tooltip: 'Détacher', onPressed: _busy ? null : _detachAm, ), ], ), ), ), ); } Widget _amAvatar(String? photoUrl) { const size = 56.0; const bg = Color(0xFFEDE5FA); const iconColor = Color(0xFF6B3FA0); final url = ApiConfig.absoluteMediaUrl(photoUrl); if (url.isEmpty) { return Container( width: size, height: size, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.face, size: 30, color: iconColor), ); } return ClipRRect( borderRadius: BorderRadius.circular(12), child: AuthNetworkImage( url: url, width: size, height: size, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: size, height: size, color: bg, child: const Icon(Icons.face, size: 30, color: iconColor), ), ), ); } String get _placementTitle { if (widget.isCreating) return 'Sélection du dossier de la famille'; return _isScolarise ? 'Scolarisation' : 'Assistante maternelle'; } Widget _placementBlock() { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( _placementTitle, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey.shade800, ), ), const SizedBox(height: 8), if (widget.isCreating) _familyPlacementSection() else _placementSection(), ], ); } Future _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(); if (_loadingAm) { return SizedBox( height: _placementHeight, width: double.infinity, child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.grey.shade600, ), ), ), ); } // Sans garde : toujours le cadre vide (comme après détachement AM). if (_isSansGarde) return _emptyAmSlot(); if (_linkedAm != null) return _linkedAmCard(); return _emptyAmSlot(); } Widget _buildFooter() { return Row( children: [ if (_canDelete) OutlinedButton( onPressed: _busy ? null : _delete, style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade700, ), child: _deleting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Supprimer'), ), const Spacer(), TextButton( onPressed: _busy ? null : () => Navigator.of(context).pop(), child: const Text('Fermer'), ), const SizedBox(width: 12), ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: !_canSubmit || _busy ? null : _save, child: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( widget.isCreating ? 'Créer' : (_dirty ? 'Sauvegarder' : 'Aucune modification'), ), ), ], ); } String _genderLabel(String gender) { switch (gender) { case 'H': return 'Garçon'; case 'F': return 'Fille'; case 'Autre': return 'Inconnu'; default: return gender; } } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: _modalWidth), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(18, 16, 4, 0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _headerTitle(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), if (_parentsSubtitle() != null) ...[ const SizedBox(height: 4), _parentsSubtitle()!, ], ], ), ), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 40, minHeight: 40, ), icon: const Icon(Icons.close), onPressed: _busy ? null : () => Navigator.of(context).pop(), tooltip: 'Fermer', ), ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(height: _mainRowHeight, child: _mainRow()), const SizedBox(height: 12), _buildFooter(), ], ), ), ], ), ), ); } }