From 1d774f29eb870c93d6954cfbc42329d3f7a5779c Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 3 Feb 2026 17:38:54 +0100 Subject: [PATCH] feat(#78): Migrer PersonalInfoFormScreen vers infrastructure multi-modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration du widget PersonalInfoFormScreen pour utiliser la nouvelle infrastructure générique : **Modifications PersonalInfoFormScreen:** - Ajout paramètre DisplayMode mode (editable/readonly) - Utilisation de DisplayConfig pour détecter mobile/desktop - Utilisation de FormFieldRow pour layout responsive - Adaptation automatique carte vertical/horizontal - Boutons navigation adaptés mobile/desktop - Conservation de toutes les fonctionnalités (toggles, validation, etc.) **Corrections infrastructure:** - base_form_screen.dart: Correction paramètres ImageButton (bg, textColor) - form_field_wrapper.dart: Correction paramètres CustomAppTextField - Gestion correcte des types nullables (TextInputType) **Résultat:** ✅ Compilation sans erreurs ✅ Layout responsive fonctionnel ✅ Mode editable opérationnel ✅ Prêt pour mode readonly (récaps) Référence: #78 Co-authored-by: Cursor --- frontend/lib/widgets/base_form_screen.dart | 6 +- frontend/lib/widgets/form_field_wrapper.dart | 6 +- .../widgets/personal_info_form_screen.dart | 530 +++++++++++------- 3 files changed, 326 insertions(+), 216 deletions(-) diff --git a/frontend/lib/widgets/base_form_screen.dart b/frontend/lib/widgets/base_form_screen.dart index d97fc53..fb10a51 100644 --- a/frontend/lib/widgets/base_form_screen.dart +++ b/frontend/lib/widgets/base_form_screen.dart @@ -196,8 +196,9 @@ class BaseFormScreen extends StatelessWidget { // Bouton Précédent HoverReliefWidget( child: ImageButton( - imagePath: 'assets/images/btn_green.png', + bg: 'assets/images/btn_green.png', text: 'Précédent', + textColor: Colors.white, onPressed: () => Navigator.pushNamed(context, previousRoute), width: config.isMobile ? 120 : 150, height: config.isMobile ? 40 : 50, @@ -207,8 +208,9 @@ class BaseFormScreen extends StatelessWidget { // Bouton Suivant/Soumettre HoverReliefWidget( child: ImageButton( - imagePath: 'assets/images/btn_green.png', + bg: 'assets/images/btn_green.png', text: submitButtonText ?? 'Suivant', + textColor: Colors.white, onPressed: config.isReadonly ? onSubmit : () { // En mode éditable, valider avant de soumettre onSubmit(); diff --git a/frontend/lib/widgets/form_field_wrapper.dart b/frontend/lib/widgets/form_field_wrapper.dart index ed601c7..cece43d 100644 --- a/frontend/lib/widgets/form_field_wrapper.dart +++ b/frontend/lib/widgets/form_field_wrapper.dart @@ -148,10 +148,10 @@ class FormFieldWrapper extends StatelessWidget { // Champ de saisie CustomAppTextField( controller: controller!, + labelText: label, hintText: hint ?? label, - onChanged: onChanged, - maxLines: maxLines ?? 1, - keyboardType: keyboardType, + keyboardType: keyboardType ?? TextInputType.text, + fieldWidth: double.infinity, ), ], ), diff --git a/frontend/lib/widgets/personal_info_form_screen.dart b/frontend/lib/widgets/personal_info_form_screen.dart index 9881f57..8f423d9 100644 --- a/frontend/lib/widgets/personal_info_form_screen.dart +++ b/frontend/lib/widgets/personal_info_form_screen.dart @@ -4,8 +4,9 @@ import 'package:go_router/go_router.dart'; import 'dart:math' as math; import 'custom_app_text_field.dart'; -import 'app_custom_checkbox.dart'; +import 'form_field_wrapper.dart'; import '../models/card_assets.dart'; +import '../config/display_config.dart'; /// Modèle de données pour le formulaire class PersonalInfoData { @@ -29,7 +30,9 @@ class PersonalInfoData { } /// Widget générique pour les formulaires d'informations personnelles +/// Supporte mode éditable et readonly, responsive mobile/desktop class PersonalInfoFormScreen extends StatefulWidget { + final DisplayMode mode; // editable ou readonly final String stepText; // Ex: "Étape 1/5" final String title; // Ex: "Informations du Parent Principal" final CardColorHorizontal cardColor; @@ -46,6 +49,7 @@ class PersonalInfoFormScreen extends StatefulWidget { const PersonalInfoFormScreen({ super.key, + this.mode = DisplayMode.editable, required this.stepText, required this.title, required this.cardColor, @@ -120,7 +124,7 @@ class _PersonalInfoFormScreenState extends State { } void _handleSubmit() { - if (_formKey.currentState!.validate()) { + if (widget.mode == DisplayMode.readonly || _formKey.currentState!.validate()) { final data = PersonalInfoData( firstName: _firstNameController.text, lastName: _lastNameController.text, @@ -142,6 +146,7 @@ class _PersonalInfoFormScreenState extends State { @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; + final config = DisplayConfig.fromContext(context, mode: widget.mode); return Scaffold( body: Stack( @@ -168,12 +173,19 @@ class _PersonalInfoFormScreenState extends State { ), const SizedBox(height: 30), Container( - width: screenSize.width * 0.6, - padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), - constraints: const BoxConstraints(minHeight: 570), + width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, + padding: EdgeInsets.symmetric( + vertical: config.isMobile ? 30 : 50, + horizontal: config.isMobile ? 20 : 50, + ), + constraints: BoxConstraints(minHeight: config.isMobile ? 400 : 570), decoration: BoxDecoration( image: DecorationImage( - image: AssetImage(widget.cardColor.path), + image: AssetImage( + config.isMobile + ? _getVerticalCardAsset() + : widget.cardColor.path + ), fit: BoxFit.fill, ), ), @@ -182,185 +194,12 @@ class _PersonalInfoFormScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Toggles "Ajouter Parent 2" et "Même Adresse" (uniquement pour Parent 2) - if (widget.showSecondPersonToggle) ...[ - Row( - children: [ - Expanded( - flex: 12, - child: Row( - children: [ - const Icon(Icons.person_add_alt_1, size: 20), - const SizedBox(width: 8), - Flexible( - child: Text( - 'Ajouter Parent 2 ?', - style: GoogleFonts.merienda(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), - ), - const Spacer(), - Switch( - value: _hasSecondPerson, - onChanged: (value) { - setState(() { - _hasSecondPerson = value; - _fieldsEnabled = value; - }); - }, - activeColor: Theme.of(context).primaryColor, - ), - ], - ), - ), - const Expanded(flex: 1, child: SizedBox()), - if (widget.showSameAddressCheckbox) - Expanded( - flex: 12, - child: Row( - children: [ - Icon( - Icons.home_work_outlined, - size: 20, - color: _fieldsEnabled ? null : Colors.grey, - ), - const SizedBox(width: 8), - Flexible( - child: Text( - 'Même Adresse ?', - style: GoogleFonts.merienda( - color: _fieldsEnabled ? null : Colors.grey, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const Spacer(), - Switch( - value: _sameAddress, - onChanged: _fieldsEnabled ? (value) { - setState(() { - _sameAddress = value ?? false; - _updateAddressFields(); - }); - } : null, - activeColor: Theme.of(context).primaryColor, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - ], - Row( - children: [ - Expanded( - flex: 12, - child: CustomAppTextField( - controller: _lastNameController, - labelText: 'Nom', - hintText: 'Votre nom de famille', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled, - ), - ), - const Expanded(flex: 1, child: SizedBox()), - Expanded( - flex: 12, - child: CustomAppTextField( - controller: _firstNameController, - labelText: 'Prénom', - hintText: 'Votre prénom', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled, - ), - ), - ], - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - flex: 12, - child: CustomAppTextField( - controller: _phoneController, - labelText: 'Téléphone', - keyboardType: TextInputType.phone, - hintText: 'Votre numéro de téléphone', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled, - ), - ), - const Expanded(flex: 1, child: SizedBox()), - Expanded( - flex: 12, - child: CustomAppTextField( - controller: _emailController, - labelText: 'Email', - keyboardType: TextInputType.emailAddress, - hintText: 'Votre adresse e-mail', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled, - ), - ), - ], - ), - const SizedBox(height: 32), - CustomAppTextField( - controller: _addressController, - labelText: 'Adresse (N° et Rue)', - hintText: 'Numéro et nom de votre rue', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled && !_sameAddress, - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - flex: 1, - child: CustomAppTextField( - controller: _postalCodeController, - labelText: 'Code Postal', - keyboardType: TextInputType.number, - hintText: 'Code postal', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled && !_sameAddress, - ), - ), - const Expanded(flex: 1, child: SizedBox()), - Expanded( - flex: 4, - child: CustomAppTextField( - controller: _cityController, - labelText: 'Ville', - hintText: 'Votre ville', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - enabled: _fieldsEnabled && !_sameAddress, - ), - ), - ], - ), + // Toggles "Ajouter Parent 2" et "Même Adresse" (uniquement en mode éditable) + if (config.isEditable && widget.showSecondPersonToggle) + _buildToggles(context, config), + + // Champs du formulaire + _buildFormFields(context, config), ], ), ), @@ -369,37 +208,306 @@ class _PersonalInfoFormScreenState extends State { ), ), ), - // Chevrons - 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), + // Chevrons de navigation + 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(widget.previousRoute); + } + }, + tooltip: 'Retour', ), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go(widget.previousRoute); - } - }, - tooltip: 'Retour', ), - ), - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: _handleSubmit, - tooltip: 'Suivant', + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _handleSubmit, + tooltip: 'Suivant', + ), + ), + ], + // Boutons mobile en bas + if (config.isMobile) + Positioned( + bottom: 20, + left: 20, + right: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + child: const Text('Précédent'), + ), + ElevatedButton( + onPressed: _handleSubmit, + child: const Text('Suivant'), + ), + ], + ), ), - ), ], ), ); } + + /// Construit les toggles (Parent 2 / Même adresse) + Widget _buildToggles(BuildContext context, DisplayConfig config) { + if (config.isMobile) { + // Layout vertical sur mobile + return Column( + children: [ + _buildSecondPersonToggle(context), + if (widget.showSameAddressCheckbox) ...[ + const SizedBox(height: 16), + _buildSameAddressToggle(context), + ], + const SizedBox(height: 32), + ], + ); + } else { + // Layout horizontal sur desktop + return Column( + children: [ + Row( + children: [ + Expanded( + flex: 12, + child: _buildSecondPersonToggle(context), + ), + const Expanded(flex: 1, child: SizedBox()), + if (widget.showSameAddressCheckbox) + Expanded( + flex: 12, + child: _buildSameAddressToggle(context), + ), + ], + ), + const SizedBox(height: 32), + ], + ); + } + } + + Widget _buildSecondPersonToggle(BuildContext context) { + return Row( + children: [ + const Icon(Icons.person_add_alt_1, size: 20), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Ajouter Parent 2 ?', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + Switch( + value: _hasSecondPerson, + onChanged: (value) { + setState(() { + _hasSecondPerson = value; + _fieldsEnabled = value; + }); + }, + activeColor: Theme.of(context).primaryColor, + ), + ], + ); + } + + Widget _buildSameAddressToggle(BuildContext context) { + return Row( + children: [ + Icon( + Icons.home_work_outlined, + size: 20, + color: _fieldsEnabled ? null : Colors.grey, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Même Adresse ?', + style: GoogleFonts.merienda( + color: _fieldsEnabled ? null : Colors.grey, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + Switch( + value: _sameAddress, + onChanged: _fieldsEnabled ? (value) { + setState(() { + _sameAddress = value ?? false; + _updateAddressFields(); + }); + } : null, + activeColor: Theme.of(context).primaryColor, + ), + ], + ); + } + + /// Construit les champs du formulaire avec la nouvelle infrastructure + Widget _buildFormFields(BuildContext context, DisplayConfig config) { + return Column( + children: [ + // Nom et Prénom + FormFieldRow( + config: config, + fields: [ + _buildField( + config: config, + label: 'Nom', + controller: _lastNameController, + hint: 'Votre nom de famille', + enabled: _fieldsEnabled, + ), + _buildField( + config: config, + label: 'Prénom', + controller: _firstNameController, + hint: 'Votre prénom', + enabled: _fieldsEnabled, + ), + ], + ), + SizedBox(height: config.isMobile ? 16 : 32), + + // Téléphone et Email + FormFieldRow( + config: config, + fields: [ + _buildField( + config: config, + label: 'Téléphone', + controller: _phoneController, + hint: 'Votre numéro de téléphone', + keyboardType: TextInputType.phone, + enabled: _fieldsEnabled, + ), + _buildField( + config: config, + label: 'Email', + controller: _emailController, + hint: 'Votre adresse e-mail', + keyboardType: TextInputType.emailAddress, + enabled: _fieldsEnabled, + ), + ], + ), + SizedBox(height: config.isMobile ? 16 : 32), + + // Adresse + _buildField( + config: config, + label: 'Adresse (N° et Rue)', + controller: _addressController, + hint: 'Numéro et nom de votre rue', + enabled: _fieldsEnabled && !_sameAddress, + ), + SizedBox(height: config.isMobile ? 16 : 32), + + // Code Postal et Ville + FormFieldRow( + config: config, + fields: [ + Flexible( + flex: 1, + child: _buildField( + config: config, + label: 'Code Postal', + controller: _postalCodeController, + hint: 'Code postal', + keyboardType: TextInputType.number, + enabled: _fieldsEnabled && !_sameAddress, + ), + ), + Flexible( + flex: 4, + child: _buildField( + config: config, + label: 'Ville', + controller: _cityController, + hint: 'Votre ville', + enabled: _fieldsEnabled && !_sameAddress, + ), + ), + ], + ), + ], + ); + } + + /// Construit un champ individuel (éditable ou readonly) + Widget _buildField({ + required DisplayConfig config, + required String label, + required TextEditingController controller, + String? hint, + TextInputType? keyboardType, + bool enabled = true, + }) { + if (config.isReadonly) { + // Mode readonly : utiliser FormFieldWrapper + return FormFieldWrapper( + config: config, + label: label, + value: controller.text, + ); + } else { + // Mode éditable : utiliser CustomAppTextField existant pour garder le style + return CustomAppTextField( + controller: controller, + labelText: label, + hintText: hint ?? label, + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: config.isMobile ? 18.0 : 22.0, + inputFontSize: config.isMobile ? 16.0 : 20.0, + keyboardType: keyboardType ?? TextInputType.text, + enabled: enabled, + ); + } + } + + /// Retourne l'asset de carte vertical correspondant à la couleur + String _getVerticalCardAsset() { + switch (widget.cardColor) { + case CardColorHorizontal.blue: + return CardColorVertical.blue.path; + case CardColorHorizontal.green: + return CardColorVertical.green.path; + case CardColorHorizontal.lavender: + return CardColorVertical.lavender.path; + case CardColorHorizontal.lime: + return CardColorVertical.lime.path; + case CardColorHorizontal.peach: + return CardColorVertical.peach.path; + case CardColorHorizontal.pink: + return CardColorVertical.pink.path; + case CardColorHorizontal.red: + return CardColorVertical.red.path; + } + } }