feat(#78): Migrer ParentRegisterStep3Screen (Enfants) vers infrastructure multi-modes

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 <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-04 11:43:26 +01:00
parent bdecbc2c1d
commit eea94769bf
4 changed files with 223 additions and 44 deletions

View File

@ -25,7 +25,7 @@ class DisplayConfig {
/// Crée une config à partir du contexte /// Crée une config à partir du contexte
factory DisplayConfig.fromContext( factory DisplayConfig.fromContext(
BuildContext context, { BuildContext context, {
required DisplayMode mode, DisplayMode mode = DisplayMode.editable,
}) { }) {
return DisplayConfig( return DisplayConfig(
mode: mode, mode: mode,

View File

@ -5,9 +5,11 @@ import 'package:image_picker/image_picker.dart';
import 'dart:io' show File; import 'dart:io' show File;
import '../../widgets/hover_relief_widget.dart'; import '../../widgets/hover_relief_widget.dart';
import '../../widgets/child_card_widget.dart'; import '../../widgets/child_card_widget.dart';
import '../../widgets/custom_navigation_button.dart';
import '../../models/user_registration_data.dart'; import '../../models/user_registration_data.dart';
import '../../utils/data_generator.dart'; import '../../utils/data_generator.dart';
import '../../models/card_assets.dart'; import '../../models/card_assets.dart';
import '../../config/display_config.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -204,14 +206,157 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */); final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: DisplayMode.editable);
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), 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( 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, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
@ -306,13 +451,18 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
), ),
), );
// Chevrons de navigation }
Positioned(
top: screenSize.height / 2 - 20, /// Boutons navigation mobile
left: 40, Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
child: IconButton( return Row(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), children: [
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Précédent',
style: NavigationButtonStyle.purple,
onPressed: () { onPressed: () {
if (context.canPop()) { if (context.canPop()) {
context.pop(); context.pop();
@ -320,22 +470,28 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
context.go('/parent-register-step2'); context.go('/parent-register-step2');
} }
}, },
tooltip: 'Retour', width: double.infinity,
height: 50,
fontSize: 16,
), ),
), ),
Positioned( ),
top: screenSize.height / 2 - 20, const SizedBox(width: 16),
right: 40, Expanded(
child: IconButton( child: HoverReliefWidget(
icon: Image.asset('assets/images/chevron_right.png', height: 40), child: CustomNavigationButton(
text: 'Suivant',
style: NavigationButtonStyle.green,
onPressed: () { onPressed: () {
context.go('/parent-register-step4'); context.go('/parent-register-step4');
}, },
tooltip: 'Suivant', width: double.infinity,
height: 50,
fontSize: 16,
), ),
), ),
], ),
), ],
); );
} }
} }

View File

@ -7,6 +7,7 @@ class AppCustomCheckbox extends StatelessWidget {
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final double checkboxSize; final double checkboxSize;
final double checkmarkSizeFactor; final double checkmarkSizeFactor;
final double fontSize;
const AppCustomCheckbox({ const AppCustomCheckbox({
super.key, super.key,
@ -15,6 +16,7 @@ class AppCustomCheckbox extends StatelessWidget {
required this.onChanged, required this.onChanged,
this.checkboxSize = 20.0, this.checkboxSize = 20.0,
this.checkmarkSizeFactor = 1.4, this.checkmarkSizeFactor = 1.4,
this.fontSize = 16.0,
}); });
@override @override
@ -51,7 +53,7 @@ class AppCustomCheckbox extends StatelessWidget {
Flexible( Flexible(
child: Text( child: Text(
label, label,
style: GoogleFonts.merienda(fontSize: 16), style: GoogleFonts.merienda(fontSize: fontSize),
overflow: TextOverflow.ellipsis, // Gérer le texte long overflow: TextOverflow.ellipsis, // Gérer le texte long
), ),
), ),

View File

@ -7,6 +7,7 @@ import '../models/card_assets.dart';
import 'custom_app_text_field.dart'; import 'custom_app_text_field.dart';
import 'app_custom_checkbox.dart'; import 'app_custom_checkbox.dart';
import 'hover_relief_widget.dart'; import 'hover_relief_widget.dart';
import '../config/display_config.dart';
/// Widget pour afficher et éditer une carte enfant /// Widget pour afficher et éditer une carte enfant
/// Utilisé dans le workflow d'inscription des parents /// Utilisé dans le workflow d'inscription des parents
@ -87,6 +88,9 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
@override @override
Widget build(BuildContext context) { 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; final File? currentChildImage = widget.childData.imageFile;
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond // 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 final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
@ -96,12 +100,12 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container( return Container(
width: 345.0 * 1.1, // 379.5 width: 345.0 * scaleFactor,
height: 570.0 * 1.2, // 684.0 height: config.isMobile ? null : 570.0 * scaleFactor, // Hauteur auto sur mobile
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2 padding: EdgeInsets.all(22.0 * scaleFactor),
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover), image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill),
borderRadius: BorderRadius.circular(20 * 1.1), // 22 borderRadius: BorderRadius.circular(20 * scaleFactor),
), ),
child: Stack( child: Stack(
children: [ children: [
@ -114,43 +118,56 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
initialShadowColor: initialPhotoShadow, initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow, hoverShadowColor: hoverPhotoShadow,
child: SizedBox( child: SizedBox(
height: 200.0, height: 200.0 * (config.isMobile ? 0.8 : 1.0),
width: 200.0, width: 200.0 * (config.isMobile ? 0.8 : 1.0),
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5 padding: EdgeInsets.all(5.0 * scaleFactor),
child: currentChildImage != null 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), : 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)), Text(
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), '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( CustomAppTextField(
controller: _firstNameController, controller: _firstNameController,
labelText: 'Prénom', labelText: 'Prénom',
hintText: 'Facultatif si à naître', hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild, 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( CustomAppTextField(
controller: _lastNameController, controller: _lastNameController,
labelText: 'Nom', labelText: 'Nom',
hintText: 'Nom de l\'enfant', hintText: 'Nom de l\'enfant',
enabled: true, 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( CustomAppTextField(
controller: _dobController, controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
@ -158,9 +175,11 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
readOnly: true, readOnly: true,
onTap: widget.onDateSelect, onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today, 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( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -168,14 +187,16 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
label: 'Consentement photo', label: 'Consentement photo',
value: widget.childData.photoConsent, value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent, 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( AppCustomCheckbox(
label: 'Naissance multiple', label: 'Naissance multiple',
value: widget.childData.multipleBirth, value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth, 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<ChildCardWidget> {
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
child: Image.asset( child: Image.asset(
'assets/images/red_cross2.png', 'assets/images/red_cross2.png',
width: 36, width: config.isMobile ? 30 : 36,
height: 36, height: config.isMobile ? 30 : 36,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),