import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import '../models/user_registration_data.dart'; import '../models/card_assets.dart'; import 'custom_app_text_field.dart'; import 'form_field_wrapper.dart'; import 'registration_photo_slot.dart'; import '../config/display_config.dart'; import 'package:p_tits_pas/utils/name_format_utils.dart'; const String _photoConsentTooltip = 'Obligatoire : cochez cette case pour autoriser l’utilisation de la photo de l’enfant.\n' 'Suivi du dossier et organisation de l’accueil (affichage interne, outils pédagogiques).\n' 'Dans le respect de la politique de confidentialité.'; bool _hasChildPhoto(ChildData c) { return registrationPhotoSlotHasImage( imageBytes: c.imageBytes, imageFile: c.imageFile, imagePathOrAsset: null, ); } Widget _buildChildPhotoImage(ChildData c, {required BoxFit fit}) { final bytes = c.imageBytes; if (bytes != null && bytes.isNotEmpty) { return Image.memory(bytes, fit: fit); } final f = c.imageFile; if (f != null) { return kIsWeb ? Image.network(f.path, fit: fit) : Image.file(f, fit: fit); } return Image.asset('assets/images/photo.png', fit: BoxFit.contain); } /// Widget pour afficher et éditer une carte enfant /// Utilisé dans le workflow d'inscription des parents class ChildCardWidget extends StatefulWidget { final ChildData childData; final int childIndex; final VoidCallback onPickImage; /// Retire la photo sélectionnée (placeholder à la place). final VoidCallback onClearImage; final VoidCallback onDateSelect; final ValueChanged onFirstNameChanged; final ValueChanged onLastNameChanged; /// `H`, `F` ou `Autre` (API). « Inconnu » à l’UI = `Autre`. final ValueChanged onGenreChanged; final ValueChanged onTogglePhotoConsent; final ValueChanged onToggleIsUnborn; final VoidCallback onRemove; final bool canBeRemoved; final DisplayMode mode; final VoidCallback? onEdit; const ChildCardWidget({ required Key key, required this.childData, required this.childIndex, required this.onPickImage, required this.onClearImage, required this.onDateSelect, required this.onFirstNameChanged, required this.onLastNameChanged, required this.onGenreChanged, required this.onTogglePhotoConsent, required this.onToggleIsUnborn, required this.onRemove, required this.canBeRemoved, this.mode = DisplayMode.editable, this.onEdit, }) : super(key: key); @override State createState() => _ChildCardWidgetState(); } class _ChildCardWidgetState extends State { /// Largeur desktop : proportionnelle au côté du carré photo (512×1024 sur les PNG → portrait ~1:2 ; même idée qu’avant #78 : 345×1,1 pour 200 px de côté). static double _desktopEditableCardWidth({ required double photoSide, required double maxWidth, required double scaleFactor, }) { final fromPhoto = photoSide * (345.0 * 1.1 / 200.0); final minForFields = 300.0 + 44.0 * scaleFactor; return math.min(maxWidth, math.max(minForFields, fromPhoto)); } late TextEditingController _firstNameController; late TextEditingController _lastNameController; late TextEditingController _dobController; FocusNode? _firstNameFocus; FocusNode? _lastNameFocus; @override void initState() { super.initState(); // Initialiser les contrôleurs avec les données du widget _firstNameController = TextEditingController(text: widget.childData.firstName); _lastNameController = TextEditingController(text: widget.childData.lastName); _dobController = TextEditingController(text: widget.childData.dob); // Ajouter des listeners pour mettre à jour les données sources via les callbacks _firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text)); _lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text)); // Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici if (widget.mode == DisplayMode.editable) { _firstNameFocus = FocusNode(); _lastNameFocus = FocusNode(); _firstNameFocus!.addListener(_onFirstNameFocusChange); _lastNameFocus!.addListener(_onLastNameFocusChange); } } void _onFirstNameFocusChange() { if (_firstNameFocus == null || _firstNameFocus!.hasFocus) { return; } _applyPersonNameFormat(_firstNameController); } void _onLastNameFocusChange() { if (_lastNameFocus == null || _lastNameFocus!.hasFocus) { return; } _applyPersonNameFormat(_lastNameController); } void _applyPersonNameFormat(TextEditingController controller) { final formatted = formatPersonNameCase(controller.text); if (formatted == controller.text) { return; } controller.value = TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } @override void didUpdateWidget(covariant ChildCardWidget oldWidget) { super.didUpdateWidget(oldWidget); // Mettre à jour les contrôleurs si les données externes changent // (peut arriver si on recharge l'état global) if (widget.childData.firstName != _firstNameController.text) { _firstNameController.text = widget.childData.firstName; } if (widget.childData.lastName != _lastNameController.text) { _lastNameController.text = widget.childData.lastName; } if (widget.childData.dob != _dobController.text) { _dobController.text = widget.childData.dob; } } @override void dispose() { _firstNameFocus?.removeListener(_onFirstNameFocusChange); _lastNameFocus?.removeListener(_onLastNameFocusChange); _firstNameFocus?.dispose(); _lastNameFocus?.dispose(); _firstNameController.dispose(); _lastNameController.dispose(); _dobController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final config = DisplayConfig.fromContext(context, mode: widget.mode); final screenSize = MediaQuery.of(context).size; final scaleFactor = config.isMobile ? 0.9 : 1.1; // Réduire légèrement sur mobile // Si mode Readonly Desktop : Layout spécial "Vintage" horizontal if (config.isReadonly && !config.isMobile) { return _buildReadonlyDesktopCard(context, config, screenSize); } // Si mode Readonly Mobile : Layout spécial "Vintage" vertical (1:2) if (config.isReadonly && config.isMobile) { return Padding( padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), child: _buildReadonlyMobileCard(context, config), ); } final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender ? Colors.purple.shade200 : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); final double photoSide = 200.0 * (config.isMobile ? 0.8 : 1.0); final double editableCardWidth = config.isMobile ? double.infinity : _desktopEditableCardWidth( photoSide: photoSide, maxWidth: screenSize.width * 0.92, scaleFactor: scaleFactor, ); return Container( width: editableCardWidth, // On retire la hauteur fixe pour laisser le contenu définir la taille, comme les autres cartes // height: config.isMobile ? null : 600.0 * scaleFactor, padding: EdgeInsets.all(22.0 * scaleFactor), decoration: BoxDecoration( image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill), borderRadius: BorderRadius.circular(20 * scaleFactor), ), child: Stack( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ RegistrationPhotoSlot( side: photoSide, scaleFactor: scaleFactor, imageBytes: widget.childData.imageBytes, imageFile: widget.childData.imageFile, onTapPick: !config.isReadonly ? widget.onPickImage : null, onClear: !config.isReadonly ? widget.onClearImage : null, baseShadowColor: baseCardColorForShadow, isMobile: config.isMobile, ), SizedBox(height: 8.0 * scaleFactor), _buildPhotoConsentRow( context: context, config: config, labelFontSize: config.isMobile ? 13.0 : 16.0, ), SizedBox(height: 10.0 * scaleFactor), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Enfant à naître ?', style: GoogleFonts.merienda( fontSize: config.isMobile ? 14 : 16 * scaleFactor, fontWeight: FontWeight.w600 ) ), Transform.scale( scale: config.isMobile ? 0.8 : 1.0, child: Switch( value: widget.childData.isUnbornChild, onChanged: config.isReadonly ? null : widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor, ), ), ], ), SizedBox(height: 8.0 * scaleFactor), _buildGenreSegment(context, config, scaleFactor), SizedBox(height: 8.0 * scaleFactor), _buildField( config: config, scaleFactor: scaleFactor, label: 'Prénom', controller: _firstNameController, hint: widget.childData.isUnbornChild ? 'Facultatif' : 'Prénom', isRequired: !widget.childData.isUnbornChild, focusNode: _firstNameFocus, ), SizedBox(height: 5.0 * scaleFactor), _buildField( config: config, scaleFactor: scaleFactor, label: 'Nom', controller: _lastNameController, hint: 'Nom de l\'enfant', focusNode: _lastNameFocus, ), SizedBox(height: 8.0 * scaleFactor), _buildField( config: config, scaleFactor: scaleFactor, label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', controller: _dobController, hint: 'JJ/MM/AAAA', readOnly: true, onTap: config.isReadonly ? null : widget.onDateSelect, suffixIcon: Icons.calendar_today, ), ], ), if (widget.canBeRemoved && !config.isReadonly) Positioned( top: -5, right: -5, child: InkWell( onTap: widget.onRemove, customBorder: const CircleBorder(), child: Image.asset( 'assets/images/cross.png', width: config.isMobile ? 30 : 36, height: config.isMobile ? 30 : 36, fit: BoxFit.contain, ), ), ), if (config.isReadonly && widget.onEdit != null) Positioned( top: -5, right: -5, child: IconButton( icon: const Icon(Icons.edit, color: Colors.black54), onPressed: widget.onEdit, tooltip: 'Modifier', ), ), ], ), ); } /// Layout SPÉCIAL Readonly Desktop (Ancien Design Horizontal) Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { // Convertir la couleur verticale (pour mobile) en couleur horizontale (pour desktop/récap) // On mappe les couleurs verticales vers horizontales String horizontalCardAsset = CardColorHorizontal.lavender.path; // Par défaut // Mapping manuel simple if (widget.childData.cardColor.path.contains('lavender')) horizontalCardAsset = CardColorHorizontal.lavender.path; else if (widget.childData.cardColor.path.contains('blue')) horizontalCardAsset = CardColorHorizontal.blue.path; else if (widget.childData.cardColor.path.contains('green')) horizontalCardAsset = CardColorHorizontal.green.path; else if (widget.childData.cardColor.path.contains('lime')) horizontalCardAsset = CardColorHorizontal.lime.path; else if (widget.childData.cardColor.path.contains('peach')) horizontalCardAsset = CardColorHorizontal.peach.path; else if (widget.childData.cardColor.path.contains('pink')) horizontalCardAsset = CardColorHorizontal.pink.path; else if (widget.childData.cardColor.path.contains('red')) horizontalCardAsset = CardColorHorizontal.red.path; final cardWidth = screenSize.width / 2.0; return SizedBox( width: cardWidth, child: AspectRatio( aspectRatio: 2.0, child: Container( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(horizontalCardAsset), fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(15), ), child: Column( children: [ // Titre + Edit Button Row( children: [ Expanded( child: Text( 'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''), style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), ), if (widget.onEdit != null) IconButton( icon: const Icon(Icons.edit, color: Colors.black54, size: 28), onPressed: widget.onEdit, tooltip: 'Modifier', ), ], ), const SizedBox(height: 18), // Contenu principal : Photo + Champs Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // PHOTO (1/3) + consentement sous la photo Expanded( flex: 1, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ AspectRatio( aspectRatio: 1, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(18), child: _hasChildPhoto(widget.childData) ? _buildChildPhotoImage(widget.childData, fit: BoxFit.cover) : Image.asset('assets/images/photo.png', fit: BoxFit.contain), ), ), ), const SizedBox(height: 12), _buildPhotoConsentRow( context: context, config: config, labelFontSize: 16.0, ), ], ), ), ), const SizedBox(width: 32), // CHAMPS (2/3) Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ _buildReadonlyField('Prénom :', _firstNameController.text), const SizedBox(height: 12), _buildReadonlyField('Nom :', _lastNameController.text), const SizedBox(height: 12), _buildReadonlyField( widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :', _dobController.text ), const SizedBox(height: 12), _buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)), ], ), ), ], ), ), ], ), ), ), ); } /// Carte en mode readonly MOBILE avec hauteur adaptative Widget _buildReadonlyMobileCard(BuildContext context, DisplayConfig config) { return Container( width: double.infinity, // Pas de height fixe padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(widget.childData.cardColor.path), // Image verticale fit: BoxFit.fill, // Fill pour s'adapter ), borderRadius: BorderRadius.circular(15), ), child: Stack( children: [ Column( mainAxisSize: MainAxisSize.min, // S'adapte au contenu children: [ // Titre + Edit Button Row( children: [ Expanded( child: Text( 'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''), style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center, ), ), if (widget.onEdit != null) const SizedBox(width: 28), ], ), // Contenu aligné en haut Padding( padding: const EdgeInsets.only(top: 20.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ // Photo SizedBox( height: 150, width: 150, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: _hasChildPhoto(widget.childData) ? _buildChildPhotoImage(widget.childData, fit: BoxFit.cover) : Image.asset('assets/images/photo.png', fit: BoxFit.contain), ), ), ), const SizedBox(height: 10), _buildPhotoConsentRow( context: context, config: config, labelFontSize: 14.0, ), const SizedBox(height: 16), // Champs _buildReadonlyField('Prénom :', _firstNameController.text), const SizedBox(height: 8), _buildReadonlyField('Nom :', _lastNameController.text), const SizedBox(height: 8), _buildReadonlyField( widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :', _dobController.text ), const SizedBox(height: 8), _buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)), ], ), ), ], ), if (widget.onEdit != null) Positioned( top: 0, right: 0, child: IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: const Icon(Icons.edit, color: Colors.black54, size: 24), onPressed: widget.onEdit, tooltip: 'Modifier', ), ), ], ), ); } Widget _buildPhotoConsentRow({ required BuildContext context, required DisplayConfig config, required double labelFontSize, }) { final readonly = config.isReadonly; final primary = Theme.of(context).colorScheme.primary; return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Checkbox( value: widget.childData.photoConsent, onChanged: readonly ? null : (v) { if (v != null) widget.onTogglePhotoConsent(v); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, activeColor: primary, ), Flexible( child: Text( 'Consentement photo', style: GoogleFonts.merienda(fontSize: labelFontSize), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), Tooltip( message: _photoConsentTooltip, waitDuration: const Duration(milliseconds: 300), triggerMode: TooltipTriggerMode.tap, showDuration: const Duration(seconds: 6), child: Icon( Icons.info_outline, size: labelFontSize * 1.2, color: Colors.black54, ), ), ], ); } static String _genreDisplayLabel(String genre) { switch (genre) { case 'F': return 'Fille'; case 'H': return 'Garçon'; case 'Autre': return 'Inconnu'; default: return '—'; } } /// Fille / Garçon ; « Inconnu » (`Autre`) uniquement si enfant à naître. Widget _buildGenreSegment(BuildContext context, DisplayConfig config, double scaleFactor) { if (config.isReadonly) { return const SizedBox.shrink(); } final selected = widget.childData.genre; final isUnborn = widget.childData.isUnbornChild; final fontSize = config.isMobile ? 13.0 : 14.0 * scaleFactor; final padV = config.isMobile ? 6.0 : 8.0 * scaleFactor; Widget seg(String label, String api) { final on = selected == api; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 3), child: OutlinedButton( onPressed: () => widget.onGenreChanged(api), style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: padV), visualDensity: VisualDensity.compact, backgroundColor: on ? Colors.black.withOpacity(0.08) : Colors.transparent, foregroundColor: Colors.black87, side: BorderSide( width: on ? 2 : 1, color: on ? Theme.of(context).primaryColor : Colors.black38, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), child: Text( label, style: GoogleFonts.merienda(fontSize: fontSize, fontWeight: on ? FontWeight.w700 : FontWeight.w500), textAlign: TextAlign.center, ), ), ), ); } return Row( children: [ seg('Fille', 'F'), seg('Garçon', 'H'), if (isUnborn) seg('Inconnu', 'Autre'), ], ); } /// Helper pour champ Readonly style "Beige" Widget _buildReadonlyField(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: GoogleFonts.merienda(fontSize: 22.0, fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Container( width: double.infinity, height: 50.0, padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), decoration: BoxDecoration( image: const DecorationImage( image: AssetImage('assets/images/bg_beige.png'), fit: BoxFit.fill, ), borderRadius: BorderRadius.circular(8), ), child: Text( value.isNotEmpty ? value : '-', style: GoogleFonts.merienda(fontSize: 18.0), overflow: TextOverflow.ellipsis, ), ), ], ); } Widget _buildField({ required DisplayConfig config, required double scaleFactor, required String label, required TextEditingController controller, String? hint, bool isRequired = false, bool readOnly = false, VoidCallback? onTap, IconData? suffixIcon, FocusNode? focusNode, }) { if (config.isReadonly) { return FormFieldWrapper( config: config, label: label, value: controller.text, ); } else { return CustomAppTextField( controller: controller, focusNode: focusNode, labelText: label, hintText: hint ?? label, isRequired: isRequired, fieldHeight: config.isMobile ? 40.0 : 50.0 * scaleFactor, // Hauteur réduite labelFontSize: config.isMobile ? 12.0 : 18.0, // Police réduite inputFontSize: config.isMobile ? 13.0 : 16.0, // Police réduite readOnly: readOnly, onTap: onTap, suffixIcon: suffixIcon, ); } } }