import 'package:flutter/material.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/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/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. class AdminChildDetailModal extends StatefulWidget { final EnfantAdminModel enfant; final VoidCallback? onSaved; final VoidCallback? onDeleted; const AdminChildDetailModal({ super.key, required this.enfant, this.onSaved, this.onDeleted, }); @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 _placementBusy = false; bool _loadingAm = true; String? _currentUserRole; AssistanteMaternelleModel? _linkedAm; 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 => (_currentUserRole ?? '').toLowerCase() == 'super_admin'; bool get _busy => _saving || _deleting || _placementBusy; @override void initState() { super.initState(); final e = widget.enfant; _prenomCtrl = TextEditingController(text: e.firstName ?? ''); _nomCtrl = TextEditingController(text: e.lastName ?? ''); _birthCtrl = TextEditingController( text: formatIsoDateFr(e.birthDate, ifEmpty: ''), ); _dueCtrl = TextEditingController( text: formatIsoDateFr(e.dueDate, ifEmpty: ''), ); _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); } _prenomCtrl.addListener(_onNameChanged); _nomCtrl.addListener(_onNameChanged); _loadCurrentUserRole(); _loadLinkedAm(); } void _onNameChanged() => setState(() {}); Future _loadLinkedAm() async { setState(() => _loadingAm = true); try { final am = await UserService.findAmForEnfant(widget.enfant.id); if (!mounted) return; setState(() { _linkedAm = am; _loadingAm = false; }); } catch (_) { if (!mounted) return; setState(() => _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 (!_availableGenders.contains(_gender)) { _gender = 'H'; } } void _markDirty() { if (!_dirty) setState(() => _dirty = true); } String? _dateToIso(String text) => parseFrDateToIso(text); String _headerTitle() { 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(); } String? _parentsSubtitle() { final names = widget.enfant.parentLinks .map((l) { final n = (l.parentName ?? '').trim(); return n.isNotEmpty ? n : 'Parent rattaché'; }) .where((s) => s.isNotEmpty) .toList(); if (names.isEmpty) return null; return 'Responsables : ${names.join(', ')}'; } Future _save() async { if (!_dirty) return; setState(() => _saving = true); try { await UserService.updateEnfant( enfantId: widget.enfant.id, body: { 'first_name': _prenomCtrl.text.trim(), 'last_name': _nomCtrl.text.trim(), '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(); 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 _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 { await UserService.deleteEnfant(widget.enfant.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: () { _loadLinkedAm(); widget.onSaved?.call(); }, ), ); } Future _attachAm() async { List all; try { all = await UserService.getAssistantesMaternelles(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); return; } 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; } final candidates = all; if (candidates.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Aucune assistante maternelle disponible')), ); return; } if (!mounted) return; final selected = await showDialog( context: context, builder: (ctx) => SimpleDialog( title: const Text('Choisir une assistante maternelle'), children: candidates .map( (am) => SimpleDialogOption( onPressed: () => Navigator.pop(ctx, am), child: Text(am.user.fullName), ), ) .toList(), ), ); if (selected == null || !mounted) return; setState(() => _placementBusy = true); try { // Sans garde : éventuel lien résiduel à clôturer avant rattachement. final previousAm = _linkedAm; if (previousAm != null) { await UserService.detachEnfantFromAm( amUserId: previousAm.user.id, enfantId: widget.enfant.id, ); } await UserService.attachEnfantToAm( amUserId: selected.user.id, enfantId: widget.enfant.id, ); if (!mounted) return; setState(() { // Le back passe déjà sans_garde → garde à l'attach ; on aligne l'UI. if (_status == 'sans_garde') { _status = 'garde'; } }); await _loadLinkedAm(); widget.onSaved?.call(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Assistante maternelle rattachée')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } finally { if (mounted) setState(() => _placementBusy = false); } } 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 ?', ), 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(() => _placementBusy = true); try { await UserService.detachEnfantFromAm( amUserId: am.user.id, enfantId: widget.enfant.id, ); if (!mounted) return; setState(() { // Le back repasse en sans_garde sauf a_naitre / scolarise. if (_status != 'a_naitre' && _status != 'scolarise') { _status = 'sans_garde'; } _linkedAm = null; }); widget.onSaved?.call(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Assistante maternelle détachée')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } finally { if (mounted) setState(() => _placementBusy = false); } } /// Passe en « Sans garde » : cadre vide + détachement AM si besoin. Future _applySansGardeStatus() async { final am = _linkedAm; if (am == null) return; setState(() => _placementBusy = true); try { await UserService.detachEnfantFromAm( amUserId: am.user.id, enfantId: widget.enfant.id, ); if (!mounted) return; setState(() => _linkedAm = null); widget.onSaved?.call(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } finally { if (mounted) setState(() => _placementBusy = false); } } InputDecoration _inputDecoration({String? hint}) { return ValidationFieldDecoration.input(hint: hint).copyWith( isDense: false, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ); } Widget _dropdownField({ Key? key, required String value, required List> items, required ValueChanged onChanged, }) { final safeValue = items.any((e) => e.key == value) ? value : items.first.key; return SizedBox( height: _fieldHeight, child: DropdownButtonFormField( key: key, value: safeValue, 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}) { return SizedBox( height: _fieldHeight, child: TextField( key: key, controller: controller, textAlignVertical: TextAlignVertical.center, style: const TextStyle(color: Colors.black87, fontSize: 14), decoration: _inputDecoration(hint: hint), ), ); } Widget _fieldsGrid() { return ValidationEditableSection( rowLayout: _fieldsRowLayout, fields: [ ValidationLabeledField( label: 'Prénom', field: _textField(_prenomCtrl), ), ValidationLabeledField( label: 'Nom', field: _textField(_nomCtrl), ), ValidationLabeledField( label: _dateFieldLabel, field: _textField( _activeDateCtrl, hint: 'jj/mm/aaaa', key: ValueKey(_status), ), ), ValidationLabeledField( label: 'Statut', field: _dropdownField( value: _status, items: enfantStatusValues .map( (s) => MapEntry( s, enfantStatusLabel(s, gender: _gender), ), ) .toList(), onChanged: (v) { setState(() { _status = v; _coerceGenderForStatus(); _dirty = true; }); if (v == 'sans_garde') { _applySansGardeStatus(); } }, ), ), ValidationLabeledField( label: 'Genre', field: _dropdownField( key: ValueKey('genre-$_status'), value: _gender, 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; }), ); } 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.enfant.photoUrl, ), ), 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( 'Scolarisé', 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 => _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), _placementSection(), ], ); } Widget _placementSection() { if (!_showsAmPlacement) return _scolariseCard(); if (_loadingAm || _placementBusy) { 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: !_dirty || _busy ? null : _save, child: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text(_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), Text( _parentsSubtitle()!, style: const TextStyle( fontSize: 13, color: Colors.black54, ), ), ], ], ), ), 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(), ], ), ), ], ), ), ); } }