From eea94769bf5991e03a5896e2fddd454a10ac1cd4 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:43:26 +0100 Subject: [PATCH] feat(#78): Migrer ParentRegisterStep3Screen (Enfants) vers infrastructure multi-modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adaptation responsive du formulaire "Informations Enfants" (Parent Step 3) : - Desktop : Conservation du layout horizontal avec scroll et effets de fondu - Mobile : Layout vertical avec cartes empilées - Header fixe - Bouton "+" carré (50px) centré à la fin de la liste - Boutons navigation intégrés au scroll - Cartes enfants adaptées (scale 0.9, polices réduites) - Mise à jour DisplayConfig (mode optionnel par défaut) - Mise à jour AppCustomCheckbox (paramètre fontSize) Co-authored-by: Cursor --- frontend/lib/config/display_config.dart | 2 +- .../auth/parent_register_step3_screen.dart | 192 ++++++++++++++++-- frontend/lib/widgets/app_custom_checkbox.dart | 4 +- frontend/lib/widgets/child_card_widget.dart | 69 ++++--- 4 files changed, 223 insertions(+), 44 deletions(-) diff --git a/frontend/lib/config/display_config.dart b/frontend/lib/config/display_config.dart index 7085bec..420d6b8 100644 --- a/frontend/lib/config/display_config.dart +++ b/frontend/lib/config/display_config.dart @@ -25,7 +25,7 @@ class DisplayConfig { /// Crée une config à partir du contexte factory DisplayConfig.fromContext( BuildContext context, { - required DisplayMode mode, + DisplayMode mode = DisplayMode.editable, }) { return DisplayConfig( mode: mode, diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart index 1f62801..3d6441d 100644 --- a/frontend/lib/screens/auth/parent_register_step3_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -5,9 +5,11 @@ import 'package:image_picker/image_picker.dart'; import 'dart:io' show File; import '../../widgets/hover_relief_widget.dart'; import '../../widgets/child_card_widget.dart'; +import '../../widgets/custom_navigation_button.dart'; import '../../models/user_registration_data.dart'; import '../../utils/data_generator.dart'; import '../../models/card_assets.dart'; +import '../../config/display_config.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; @@ -204,14 +206,157 @@ class _ParentRegisterStep3ScreenState extends State { Widget build(BuildContext context) { final registrationData = Provider.of(context /*, listen: true par défaut */); final screenSize = MediaQuery.of(context).size; + final config = DisplayConfig.fromContext(context, mode: DisplayMode.editable); + return Scaffold( body: Stack( children: [ Positioned.fill( child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), ), - Center( + config.isMobile + ? _buildMobileLayout(context, config, screenSize, registrationData) + : _buildDesktopLayout(context, config, screenSize, registrationData), + // Chevrons desktop uniquement + if (!config.isMobile) ...[ + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/parent-register-step2'); + } + }, + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + context.go('/parent-register-step4'); + }, + tooltip: 'Suivant', + ), + ), + ], + ], + ), + ); + } + + /// Layout MOBILE : Cartes empilées verticalement + Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) { + return Column( + children: [ + // Header fixe + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Column( + children: [ + Text( + 'Étape 3/5', + style: GoogleFonts.merienda(fontSize: 13, color: Colors.black54), + ), + const SizedBox(height: 6), + Text( + 'Informations Enfants', + style: GoogleFonts.merienda( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 16), + // Liste scrollable des cartes + boutons + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), child: Column( + children: [ + // Générer les cartes enfants + for (int index = 0; index < registrationData.children.length; index++) ...[ + ChildCardWidget( + key: ValueKey(registrationData.children[index].hashCode), + childData: registrationData.children[index], + childIndex: index, + onPickImage: () => _pickImage(index, registrationData), + onDateSelect: () => _selectDate(context, index, registrationData), + onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( + firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, + multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor + ))), + onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( + firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, + multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor + ))), + onTogglePhotoConsent: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue, + multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onToggleMultipleBirth: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent, + multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onToggleIsUnborn: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue), + photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue, + imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onRemove: () => _removeChild(index, registrationData), + canBeRemoved: registrationData.children.length > 1, + ), + const SizedBox(height: 16), + ], + + // Bouton "+" carré à la fin de la liste + Center( + child: HoverReliefWidget( + onPressed: () => _addChild(registrationData), + borderRadius: BorderRadius.circular(15), + child: Container( + width: 50, + height: 50, + child: Image.asset('assets/images/plus.png', fit: BoxFit.contain), + ), + ), + ), + + const SizedBox(height: 30), + // Boutons navigation en bas du scroll + _buildMobileButtons(context, config, screenSize), + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ); + } + + /// Layout DESKTOP : Scroll horizontal avec fondu + Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) { + return Center( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), @@ -306,13 +451,18 @@ class _ParentRegisterStep3ScreenState extends State { const SizedBox(height: 20), ], ), - ), - // Chevrons de navigation - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), + ); + } + + /// Boutons navigation mobile + Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) { + return Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, onPressed: () { if (context.canPop()) { context.pop(); @@ -320,22 +470,28 @@ class _ParentRegisterStep3ScreenState extends State { context.go('/parent-register-step2'); } }, - tooltip: 'Retour', + width: double.infinity, + height: 50, + fontSize: 16, ), ), - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), + ), + const SizedBox(width: 16), + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Suivant', + style: NavigationButtonStyle.green, onPressed: () { - context.go('/parent-register-step4'); + context.go('/parent-register-step4'); }, - tooltip: 'Suivant', + width: double.infinity, + height: 50, + fontSize: 16, ), ), - ], - ), + ), + ], ); } } \ No newline at end of file diff --git a/frontend/lib/widgets/app_custom_checkbox.dart b/frontend/lib/widgets/app_custom_checkbox.dart index f5e0276..a52640c 100644 --- a/frontend/lib/widgets/app_custom_checkbox.dart +++ b/frontend/lib/widgets/app_custom_checkbox.dart @@ -7,6 +7,7 @@ class AppCustomCheckbox extends StatelessWidget { final ValueChanged onChanged; final double checkboxSize; final double checkmarkSizeFactor; + final double fontSize; const AppCustomCheckbox({ super.key, @@ -15,6 +16,7 @@ class AppCustomCheckbox extends StatelessWidget { required this.onChanged, this.checkboxSize = 20.0, this.checkmarkSizeFactor = 1.4, + this.fontSize = 16.0, }); @override @@ -51,7 +53,7 @@ class AppCustomCheckbox extends StatelessWidget { Flexible( child: Text( label, - style: GoogleFonts.merienda(fontSize: 16), + style: GoogleFonts.merienda(fontSize: fontSize), overflow: TextOverflow.ellipsis, // Gérer le texte long ), ), diff --git a/frontend/lib/widgets/child_card_widget.dart b/frontend/lib/widgets/child_card_widget.dart index 93f6044..bd086fe 100644 --- a/frontend/lib/widgets/child_card_widget.dart +++ b/frontend/lib/widgets/child_card_widget.dart @@ -7,6 +7,7 @@ import '../models/card_assets.dart'; import 'custom_app_text_field.dart'; import 'app_custom_checkbox.dart'; import 'hover_relief_widget.dart'; +import '../config/display_config.dart'; /// Widget pour afficher et éditer une carte enfant /// Utilisé dans le workflow d'inscription des parents @@ -87,6 +88,9 @@ class _ChildCardWidgetState extends State { @override Widget build(BuildContext context) { + final config = DisplayConfig.fromContext(context); + final scaleFactor = config.isMobile ? 0.9 : 1.1; // Réduire légèrement sur mobile + final File? currentChildImage = widget.childData.imageFile; // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender @@ -96,12 +100,12 @@ class _ChildCardWidgetState extends State { final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); return Container( - width: 345.0 * 1.1, // 379.5 - height: 570.0 * 1.2, // 684.0 - padding: const EdgeInsets.all(22.0 * 1.1), // 24.2 + width: 345.0 * scaleFactor, + height: config.isMobile ? null : 570.0 * scaleFactor, // Hauteur auto sur mobile + padding: EdgeInsets.all(22.0 * scaleFactor), decoration: BoxDecoration( - image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover), - borderRadius: BorderRadius.circular(20 * 1.1), // 22 + image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill), + borderRadius: BorderRadius.circular(20 * scaleFactor), ), child: Stack( children: [ @@ -114,43 +118,56 @@ class _ChildCardWidgetState extends State { initialShadowColor: initialPhotoShadow, hoverShadowColor: hoverPhotoShadow, child: SizedBox( - height: 200.0, - width: 200.0, + height: 200.0 * (config.isMobile ? 0.8 : 1.0), + width: 200.0 * (config.isMobile ? 0.8 : 1.0), child: Center( child: Padding( - padding: const EdgeInsets.all(5.0 * 1.1), // 5.5 + padding: EdgeInsets.all(5.0 * scaleFactor), child: currentChildImage != null - ? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover)) + ? ClipRRect(borderRadius: BorderRadius.circular(10 * scaleFactor), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover)) : Image.asset('assets/images/photo.png', fit: BoxFit.contain), ), ), ), ), - const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo + SizedBox(height: 12.0 * scaleFactor), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)), - Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), + 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: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), + ), ], ), - const SizedBox(height: 9.0 * 1.1), // 9.9 + SizedBox(height: 9.0 * scaleFactor), CustomAppTextField( controller: _firstNameController, labelText: 'Prénom', hintText: 'Facultatif si à naître', isRequired: !widget.childData.isUnbornChild, - fieldHeight: 55.0 * 1.1, // 60.5 + fieldHeight: config.isMobile ? 45.0 : 55.0 * scaleFactor, + labelFontSize: config.isMobile ? 14.0 : 22.0, // Police réduite mobile + inputFontSize: config.isMobile ? 14.0 : 20.0, ), - const SizedBox(height: 6.0 * 1.1), // 6.6 + SizedBox(height: 6.0 * scaleFactor), CustomAppTextField( controller: _lastNameController, labelText: 'Nom', hintText: 'Nom de l\'enfant', enabled: true, - fieldHeight: 55.0 * 1.1, // 60.5 + fieldHeight: config.isMobile ? 45.0 : 55.0 * scaleFactor, + labelFontSize: config.isMobile ? 14.0 : 22.0, + inputFontSize: config.isMobile ? 14.0 : 20.0, ), - const SizedBox(height: 9.0 * 1.1), // 9.9 + SizedBox(height: 9.0 * scaleFactor), CustomAppTextField( controller: _dobController, labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', @@ -158,9 +175,11 @@ class _ChildCardWidgetState extends State { readOnly: true, onTap: widget.onDateSelect, suffixIcon: Icons.calendar_today, - fieldHeight: 55.0 * 1.1, // 60.5 + fieldHeight: config.isMobile ? 45.0 : 55.0 * scaleFactor, + labelFontSize: config.isMobile ? 14.0 : 22.0, + inputFontSize: config.isMobile ? 14.0 : 20.0, ), - const SizedBox(height: 11.0 * 1.1), // 12.1 + SizedBox(height: 11.0 * scaleFactor), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -168,14 +187,16 @@ class _ChildCardWidgetState extends State { label: 'Consentement photo', value: widget.childData.photoConsent, onChanged: widget.onTogglePhotoConsent, - checkboxSize: 22.0 * 1.1, // 24.2 + checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, + fontSize: config.isMobile ? 13.0 : 16.0, ), - const SizedBox(height: 6.0 * 1.1), // 6.6 + SizedBox(height: 6.0 * scaleFactor), AppCustomCheckbox( label: 'Naissance multiple', value: widget.childData.multipleBirth, onChanged: widget.onToggleMultipleBirth, - checkboxSize: 22.0 * 1.1, // 24.2 + checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, + fontSize: config.isMobile ? 13.0 : 16.0, ), ], ), @@ -189,8 +210,8 @@ class _ChildCardWidgetState extends State { customBorder: const CircleBorder(), child: Image.asset( 'assets/images/red_cross2.png', - width: 36, - height: 36, + width: config.isMobile ? 30 : 36, + height: config.isMobile ? 30 : 36, fit: BoxFit.contain, ), ),