From 5d7eb9eb36ce5c1825074260a5f4e2f3e42ef97b Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 3 Feb 2026 16:17:49 +0100 Subject: [PATCH 01/12] =?UTF-8?q?fix(#79):=20Supprimer=20toutes=20les=20r?= =?UTF-8?q?=C3=A9f=C3=A9rences=20obsol=C3=A8tes=20=C3=A0=20Nanny?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrections suite au merge master: - Suppression imports nanny_register_step*.dart (fichiers supprimés) - Suppression variable nannyRegistrationDataNotifier - Suppression section Nanny Registration Flow complète - Ajout import go_router dans login_screen.dart L'application compile maintenant sans erreurs. Référence: #79 Co-authored-by: Cursor --- frontend/lib/config/app_router.dart | 45 --------------------- frontend/lib/screens/auth/login_screen.dart | 1 + 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/frontend/lib/config/app_router.dart b/frontend/lib/config/app_router.dart index d456ad6..7bd6358 100644 --- a/frontend/lib/config/app_router.dart +++ b/frontend/lib/config/app_router.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; // Models import '../models/user_registration_data.dart'; -import '../models/nanny_registration_data.dart'; import '../models/am_registration_data.dart'; // Screens @@ -15,11 +14,6 @@ import '../screens/auth/parent_register_step2_screen.dart'; import '../screens/auth/parent_register_step3_screen.dart'; import '../screens/auth/parent_register_step4_screen.dart'; import '../screens/auth/parent_register_step5_screen.dart'; -import '../screens/auth/nanny_register_step1_screen.dart'; -import '../screens/auth/nanny_register_step2_screen.dart'; -import '../screens/auth/nanny_register_step3_screen.dart'; -import '../screens/auth/nanny_register_step4_screen.dart'; -import '../screens/auth/nanny_register_confirmation_screen.dart'; import '../screens/auth/am_register_step1_screen.dart'; import '../screens/auth/am_register_step2_screen.dart'; import '../screens/auth/am_register_step3_screen.dart'; @@ -33,7 +27,6 @@ import '../screens/unknown_screen.dart'; // For ShellRoute, creating them here and passing via .value is common. final userRegistrationDataNotifier = UserRegistrationData(); -final nannyRegistrationDataNotifier = NannyRegistrationData(); final amRegistrationDataNotifier = AmRegistrationData(); class AppRouter { @@ -84,44 +77,6 @@ class AppRouter { path: '/parent-register-step5', builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(), ), - GoRoute( - path: '/parent-register-confirmation', - builder: (BuildContext context, GoRouterState state) => const NannyRegisterConfirmationScreen(), - ), - ], - ), - - // --- Nanny Registration Flow --- - ShellRoute( - builder: (context, state, child) { - return ChangeNotifierProvider.value( - value: nannyRegistrationDataNotifier, - child: child, - ); - }, - routes: [ - GoRoute( - path: '/nanny-register-step1', - builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep1Screen(), - ), - GoRoute( - path: '/nanny-register-step2', - builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep2Screen(), - ), - GoRoute( - path: '/nanny-register-step3', - builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep3Screen(), - ), - GoRoute( - path: '/nanny-register-step4', - builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep4Screen(), - ), - GoRoute( - path: '/nanny-register-confirmation', - builder: (BuildContext context, GoRouterState state) { - return const NannyRegisterConfirmationScreen(); - }, - ), ], ), diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index 23d7af1..74c33b1 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:url_launcher/url_launcher.dart'; +import 'package:go_router/go_router.dart'; import 'package:p_tits_pas/services/bug_report_service.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_app_text_field.dart'; From 890619ff594a52b68bb329828a792be008f10499 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 3 Feb 2026 17:33:29 +0100 Subject: [PATCH 02/12] =?UTF-8?q?feat(#78):=20Cr=C3=A9er=20infrastructure?= =?UTF-8?q?=20g=C3=A9n=C3=A9rique=20pour=20formulaires=20multi-modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle architecture centralisée pour tous les formulaires : **Configuration centrale (display_config.dart):** - DisplayMode enum (editable/readonly) - LayoutType enum (mobile/desktop) - DisplayConfig class pour configuration complète - LayoutHelper avec utilitaires (détection, spacing, etc.) - Breakpoint: 600px (mobile < 600px reste toujours vertical) **Widgets génériques (form_field_wrapper.dart):** - FormFieldWrapper: champ auto-adaptatif (TextField ou Text readonly) - FormFieldRow: ligne responsive (horizontal desktop, vertical mobile) **Structure de page (base_form_screen.dart):** - BaseFormScreen: layout complet avec carte, boutons, navigation - Gestion auto des assets carte (horizontal/vertical selon layout) **Avantages:** ✅ Code unique pour editable + readonly + mobile + desktop ✅ Logique centralisée (aucune duplication) ✅ Héritage automatique via DisplayConfig propagé ✅ API simple et cohérente Prochaine étape: Migration des widgets existants Référence: #78 Co-authored-by: Cursor --- frontend/lib/config/display_config.dart | 106 +++++++++ frontend/lib/widgets/README_FORM_WIDGETS.md | 220 ++++++++++++++++++ frontend/lib/widgets/base_form_screen.dart | 223 +++++++++++++++++++ frontend/lib/widgets/form_field_wrapper.dart | 208 +++++++++++++++++ 4 files changed, 757 insertions(+) create mode 100644 frontend/lib/config/display_config.dart create mode 100644 frontend/lib/widgets/README_FORM_WIDGETS.md create mode 100644 frontend/lib/widgets/base_form_screen.dart create mode 100644 frontend/lib/widgets/form_field_wrapper.dart diff --git a/frontend/lib/config/display_config.dart b/frontend/lib/config/display_config.dart new file mode 100644 index 0000000..7085bec --- /dev/null +++ b/frontend/lib/config/display_config.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +/// Mode d'affichage d'un formulaire +enum DisplayMode { + /// Mode éditable (formulaire d'inscription) + editable, + + /// Mode lecture seule (récapitulatif) + readonly, +} + +/// Configuration d'affichage pour les widgets de formulaire +class DisplayConfig { + /// Mode d'affichage (editable/readonly) + final DisplayMode mode; + + /// Type de layout détecté (mobile/desktop) + final LayoutType layoutType; + + const DisplayConfig({ + required this.mode, + required this.layoutType, + }); + + /// Crée une config à partir du contexte + factory DisplayConfig.fromContext( + BuildContext context, { + required DisplayMode mode, + }) { + return DisplayConfig( + mode: mode, + layoutType: LayoutHelper.getLayoutType(context), + ); + } + + /// Est en mode éditable + bool get isEditable => mode == DisplayMode.editable; + + /// Est en mode lecture seule + bool get isReadonly => mode == DisplayMode.readonly; + + /// Est en layout mobile + bool get isMobile => layoutType == LayoutType.mobile; + + /// Est en layout desktop + bool get isDesktop => layoutType == LayoutType.desktop; + + /// Layout vertical (mobile) + bool get isVerticalLayout => isMobile; + + /// Layout horizontal (desktop) + bool get isHorizontalLayout => isDesktop; +} + +/// Type de layout +enum LayoutType { + /// Mobile (< 600px) - toujours vertical + mobile, + + /// Desktop/Tablette (≥ 600px) - horizontal + desktop, +} + +/// Utilitaires pour la détection de layout +class LayoutHelper { + /// Seuil de largeur pour mobile/desktop (en pixels) + static const double mobileBreakpoint = 600.0; + + /// Détermine le type de layout selon la largeur d'écran + static LayoutType getLayoutType(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return width < mobileBreakpoint + ? LayoutType.mobile + : LayoutType.desktop; + } + + /// Vérifie si on est sur mobile + static bool isMobile(BuildContext context) { + return getLayoutType(context) == LayoutType.mobile; + } + + /// Vérifie si on est sur desktop + static bool isDesktop(BuildContext context) { + return getLayoutType(context) == LayoutType.desktop; + } + + /// Retourne un espacement adapté au layout + static double getSpacing(BuildContext context, { + double mobileSpacing = 12.0, + double desktopSpacing = 20.0, + }) { + return isMobile(context) ? mobileSpacing : desktopSpacing; + } + + /// Retourne une largeur max adaptée au layout + static double getMaxWidth(BuildContext context, { + double? mobileMaxWidth, + double? desktopMaxWidth, + }) { + if (isMobile(context)) { + return mobileMaxWidth ?? double.infinity; + } else { + return desktopMaxWidth ?? 1200.0; + } + } +} diff --git a/frontend/lib/widgets/README_FORM_WIDGETS.md b/frontend/lib/widgets/README_FORM_WIDGETS.md new file mode 100644 index 0000000..0ee8d47 --- /dev/null +++ b/frontend/lib/widgets/README_FORM_WIDGETS.md @@ -0,0 +1,220 @@ +# Infrastructure générique pour les formulaires + +## 📋 Vue d'ensemble + +Cette infrastructure permet de créer des formulaires qui s'adaptent automatiquement : +- **Mode éditable** (inscription) vs **lecture seule** (récapitulatif) +- **Layout mobile** (vertical, < 600px) vs **desktop** (horizontal, ≥ 600px) +- **Mobile reste toujours vertical**, même en rotation paysage + +## 🏗️ Architecture + +### 1. `display_config.dart` - Configuration centrale + +```dart +// Mode d'affichage +enum DisplayMode { + editable, // Formulaire éditable + readonly, // Récapitulatif +} + +// Type de layout +enum LayoutType { + mobile, // < 600px, toujours vertical + desktop, // ≥ 600px, horizontal +} + +// Configuration complète +DisplayConfig config = DisplayConfig.fromContext( + context, + mode: DisplayMode.editable, +); +``` + +### 2. `form_field_wrapper.dart` - Champs génériques + +#### FormFieldWrapper +Widget pour afficher un champ unique qui s'adapte automatiquement. + +**Mode éditable :** +```dart +FormFieldWrapper( + config: config, + label: 'Prénom', + value: '', + controller: firstNameController, + onChanged: (value) => {}, + hint: 'Entrez votre prénom', +) +``` + +**Mode readonly :** +```dart +FormFieldWrapper( + config: config, + label: 'Prénom', + value: 'Jean', +) +``` + +#### FormFieldRow +Widget pour afficher plusieurs champs sur une ligne (desktop) ou en colonne (mobile). + +```dart +FormFieldRow( + config: config, + fields: [ + FormFieldWrapper(...), + FormFieldWrapper(...), + ], +) +``` + +### 3. `base_form_screen.dart` - Structure de page générique + +Encapsule toute la structure d'une page de formulaire : +- En-tête (étape + titre) +- Carte avec fond adapté (horizontal/vertical) +- Boutons de navigation +- Gestion automatique du layout + +```dart +BaseFormScreen( + config: DisplayConfig.fromContext( + context, + mode: DisplayMode.editable, + ), + stepText: 'Étape 1/4', + title: 'Informations personnelles', + cardColor: CardColorHorizontal.blue, + previousRoute: '/previous', + onSubmit: () => _handleSubmit(), + content: Column( + children: [ + FormFieldRow( + config: config, + fields: [ + FormFieldWrapper(...), + FormFieldWrapper(...), + ], + ), + ], + ), +) +``` + +## 📱 Comportement responsive + +### Breakpoint : 600px + +| Largeur écran | LayoutType | Orientation carte | Disposition champs | +|--------------|------------|-------------------|-------------------| +| < 600px | mobile | Verticale | Colonne | +| ≥ 600px | desktop | Horizontale | Ligne | + +### Règle importante +**Sur mobile, le layout reste TOUJOURS vertical**, même si l'utilisateur tourne son téléphone en mode paysage. + +## 🎨 Utilisation dans un widget de formulaire + +### Exemple : PersonalInfoFormScreen + +```dart +class PersonalInfoFormScreen extends StatefulWidget { + final DisplayMode mode; + final PersonalInfoData? initialData; + final Function(PersonalInfoData) onSubmit; + + // ... +} + +class _PersonalInfoFormScreenState extends State { + late TextEditingController _firstNameController; + late TextEditingController _lastNameController; + + @override + Widget build(BuildContext context) { + final config = DisplayConfig.fromContext( + context, + mode: widget.mode, + ); + + return BaseFormScreen( + config: config, + stepText: 'Étape 1/4', + title: 'Informations personnelles', + cardColor: CardColorHorizontal.blue, + previousRoute: '/previous', + onSubmit: _handleSubmit, + content: Column( + children: [ + FormFieldRow( + config: config, + fields: [ + FormFieldWrapper( + config: config, + label: 'Prénom', + value: _firstNameController.text, + controller: config.isEditable ? _firstNameController : null, + onChanged: config.isEditable ? (v) => setState(() {}) : null, + ), + FormFieldWrapper( + config: config, + label: 'Nom', + value: _lastNameController.text, + controller: config.isEditable ? _lastNameController : null, + onChanged: config.isEditable ? (v) => setState(() {}) : null, + ), + ], + ), + ], + ), + ); + } + + void _handleSubmit() { + final data = PersonalInfoData( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + ); + widget.onSubmit(data); + } +} +``` + +## ✅ Avantages + +1. **Code unique** : Un seul widget pour éditable + readonly + mobile + desktop +2. **Cohérence** : Tous les formulaires se comportent de la même façon +3. **Maintenance** : Modification centralisée de l'UI +4. **Performance** : Pas de rebuild inutile, layout déterminé au build +5. **Simplicité** : API claire et prévisible + +## 🔧 Utilitaires disponibles + +```dart +// Détecter le type de layout +bool isMobile = LayoutHelper.isMobile(context); +bool isDesktop = LayoutHelper.isDesktop(context); + +// Espacement adaptatif +double spacing = LayoutHelper.getSpacing( + context, + mobileSpacing: 12.0, + desktopSpacing: 20.0, +); + +// Largeur max adaptative +double maxWidth = LayoutHelper.getMaxWidth(context); +``` + +## 🚀 Migration des widgets existants + +Pour migrer un widget existant vers cette infrastructure : + +1. Ajouter paramètre `DisplayMode mode` +2. Créer `DisplayConfig.fromContext(context, mode: widget.mode)` +3. Remplacer la structure Scaffold par `BaseFormScreen` +4. Remplacer les champs par `FormFieldWrapper` +5. Grouper les champs avec `FormFieldRow` +6. Tester en mode editable + readonly + mobile + desktop diff --git a/frontend/lib/widgets/base_form_screen.dart b/frontend/lib/widgets/base_form_screen.dart new file mode 100644 index 0000000..d97fc53 --- /dev/null +++ b/frontend/lib/widgets/base_form_screen.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../config/display_config.dart'; +import '../models/card_assets.dart'; +import 'hover_relief_widget.dart'; +import 'image_button.dart'; + +/// Widget de base générique pour tous les écrans de formulaire +/// Gère automatiquement le layout, les boutons de navigation, etc. +class BaseFormScreen extends StatelessWidget { + /// Configuration d'affichage + final DisplayConfig config; + + /// Texte de l'étape (ex: "Étape 1/4") + final String stepText; + + /// Titre du formulaire + final String title; + + /// Couleur de la carte (horizontal pour desktop) + final CardColorHorizontal cardColor; + + /// Contenu du formulaire + final Widget content; + + /// Texte du bouton de soumission (par défaut "Suivant") + final String? submitButtonText; + + /// Callback de soumission + final VoidCallback onSubmit; + + /// Route précédente (pour le bouton retour) + final String previousRoute; + + /// Widget supplémentaire au-dessus du contenu (ex: toggle) + final Widget? headerWidget; + + /// Widget supplémentaire en dessous du contenu (ex: checkbox CGU) + final Widget? footerWidget; + + /// Padding personnalisé pour le contenu + final EdgeInsets? contentPadding; + + const BaseFormScreen({ + super.key, + required this.config, + required this.stepText, + required this.title, + required this.cardColor, + required this.content, + required this.onSubmit, + required this.previousRoute, + this.submitButtonText, + this.headerWidget, + this.footerWidget, + this.contentPadding, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFF8E1), + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all( + LayoutHelper.getSpacing(context, + mobileSpacing: 16.0, + desktopSpacing: 32.0, + ), + ), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: LayoutHelper.getMaxWidth(context), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Texte de l'étape + Text( + stepText, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 14 : 16, + fontWeight: FontWeight.w500, + color: const Color(0xFF6D4C41), + ), + ), + const SizedBox(height: 8), + + // Titre + Text( + title, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 24 : 32, + fontWeight: FontWeight.bold, + color: const Color(0xFF4A4A4A), + ), + ), + const SizedBox(height: 24), + + // Header widget (si fourni) + if (headerWidget != null) ...[ + headerWidget!, + const SizedBox(height: 16), + ], + + // Carte principale + _buildCard(context), + + const SizedBox(height: 24), + + // Footer widget (si fourni) + if (footerWidget != null) ...[ + footerWidget!, + const SizedBox(height: 24), + ], + + // Boutons de navigation + _buildNavigationButtons(context), + ], + ), + ), + ), + ), + ), + ); + } + + /// Construit la carte principale + Widget _buildCard(BuildContext context) { + final effectivePadding = contentPadding ?? + EdgeInsets.all( + LayoutHelper.getSpacing(context, + mobileSpacing: 16.0, + desktopSpacing: 32.0, + ), + ); + + if (config.isMobile) { + // Carte verticale sur mobile + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(_getVerticalCardAsset()), + fit: BoxFit.fill, + ), + ), + child: Padding( + padding: effectivePadding, + child: content, + ), + ); + } else { + // Carte horizontale sur desktop + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(cardColor.path), + fit: BoxFit.fill, + ), + ), + child: Padding( + padding: effectivePadding, + child: content, + ), + ); + } + } + + /// Retourne l'asset de carte vertical correspondant à la couleur + String _getVerticalCardAsset() { + // Mapping couleur horizontale -> verticale + switch (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; + } + } + + /// Construit les boutons de navigation + Widget _buildNavigationButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Bouton Précédent + HoverReliefWidget( + child: ImageButton( + imagePath: 'assets/images/btn_green.png', + text: 'Précédent', + onPressed: () => Navigator.pushNamed(context, previousRoute), + width: config.isMobile ? 120 : 150, + height: config.isMobile ? 40 : 50, + ), + ), + + // Bouton Suivant/Soumettre + HoverReliefWidget( + child: ImageButton( + imagePath: 'assets/images/btn_green.png', + text: submitButtonText ?? 'Suivant', + onPressed: config.isReadonly ? onSubmit : () { + // En mode éditable, valider avant de soumettre + onSubmit(); + }, + width: config.isMobile ? 120 : 150, + height: config.isMobile ? 40 : 50, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/widgets/form_field_wrapper.dart b/frontend/lib/widgets/form_field_wrapper.dart new file mode 100644 index 0000000..ed601c7 --- /dev/null +++ b/frontend/lib/widgets/form_field_wrapper.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../config/display_config.dart'; +import 'custom_app_text_field.dart'; + +/// Widget générique pour afficher un champ de formulaire +/// S'adapte automatiquement selon le DisplayConfig (editable/readonly, mobile/desktop) +class FormFieldWrapper extends StatelessWidget { + /// Configuration d'affichage + final DisplayConfig config; + + /// Label du champ + final String label; + + /// Valeur actuelle + final String value; + + /// Controller pour le mode éditable + final TextEditingController? controller; + + /// Callback de changement (mode éditable) + final ValueChanged? onChanged; + + /// Hint du champ (mode éditable) + final String? hint; + + /// Nombre de lignes (pour textarea) + final int? maxLines; + + /// Type de clavier + final TextInputType? keyboardType; + + /// Widget personnalisé à afficher (override le champ standard) + final Widget? customWidget; + + const FormFieldWrapper({ + super.key, + required this.config, + required this.label, + required this.value, + this.controller, + this.onChanged, + this.hint, + this.maxLines, + this.keyboardType, + this.customWidget, + }); + + @override + Widget build(BuildContext context) { + if (config.isReadonly) { + return _buildReadonlyField(context); + } else { + return _buildEditableField(context); + } + } + + /// Construit un champ en mode lecture seule + Widget _buildReadonlyField(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: LayoutHelper.getSpacing(context, + mobileSpacing: 8.0, + desktopSpacing: 12.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label + Text( + label, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 14 : 16, + fontWeight: FontWeight.bold, + color: const Color(0xFF4A4A4A), + ), + ), + const SizedBox(height: 6), + + // Valeur + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFE0E0E0), + width: 1, + ), + ), + child: Text( + value.isEmpty ? '-' : value, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 14 : 16, + color: value.isEmpty + ? const Color(0xFFBDBDBD) + : const Color(0xFF2C2C2C), + ), + ), + ), + ], + ), + ); + } + + /// Construit un champ en mode éditable + Widget _buildEditableField(BuildContext context) { + // Si un widget personnalisé est fourni, l'utiliser + if (customWidget != null) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: LayoutHelper.getSpacing(context, + mobileSpacing: 8.0, + desktopSpacing: 12.0, + ), + ), + child: customWidget, + ); + } + + // Sinon, utiliser le champ standard + return Padding( + padding: EdgeInsets.symmetric( + vertical: LayoutHelper.getSpacing(context, + mobileSpacing: 8.0, + desktopSpacing: 12.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label + Text( + label, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 14 : 16, + fontWeight: FontWeight.w600, + color: const Color(0xFF4A4A4A), + ), + ), + const SizedBox(height: 8), + + // Champ de saisie + CustomAppTextField( + controller: controller!, + hintText: hint ?? label, + onChanged: onChanged, + maxLines: maxLines ?? 1, + keyboardType: keyboardType, + ), + ], + ), + ); + } +} + +/// Widget générique pour afficher une ligne de champs +/// S'adapte automatiquement: horizontal sur desktop, vertical sur mobile +class FormFieldRow extends StatelessWidget { + /// Configuration d'affichage + final DisplayConfig config; + + /// Liste des champs à afficher + final List fields; + + /// Espacement entre les champs + final double? spacing; + + const FormFieldRow({ + super.key, + required this.config, + required this.fields, + this.spacing, + }); + + @override + Widget build(BuildContext context) { + final effectiveSpacing = spacing ?? + LayoutHelper.getSpacing(context, + mobileSpacing: 12.0, + desktopSpacing: 20.0, + ); + + if (config.isMobile) { + // Layout vertical sur mobile + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: fields, + ); + } else { + // Layout horizontal sur desktop + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < fields.length; i++) ...[ + Expanded(child: fields[i]), + if (i < fields.length - 1) SizedBox(width: effectiveSpacing), + ], + ], + ); + } + } +} From 1d774f29eb870c93d6954cfbc42329d3f7a5779c Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 3 Feb 2026 17:38:54 +0100 Subject: [PATCH 03/12] 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; + } + } } From a57993a90f1d414b88c89cd93dd97fbe0d602483 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:01:43 +0100 Subject: [PATCH 04/12] feat(#78): Finaliser layout mobile responsive de PersonalInfoFormScreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout mobile complètement repensé avec séparation desktop/mobile : **Layout Desktop (_buildDesktopFields) :** - Champs par paires horizontales (Row avec Expanded) - Code Postal + Ville avec ratio flex 2:5 - Espacement 32px entre lignes - Taille police : 22px labels, 20px input **Layout Mobile (_buildMobileFields) :** - Tous les champs empilés verticalement (Column pure) - Chaque champ prend toute la largeur - Espacement 12px entre champs (compact) - Taille police : 15px labels, 14px input - Hauteur champs réduite : 45px **Nouveau widget CustomNavigationButton :** - Widget réutilisable pour boutons navigation - Enum NavigationButtonStyle (green/purple) - Utilise assets images comme fond - Bouton "Précédent" : fond lavande, texte violet foncé - Bouton "Suivant" : fond vert, texte vert foncé **Boutons mobile :** - Positionnés sous la carte (dans le scroll) - Aligned avec les marges de la carte (5% de chaque côté) - Prennent toute la largeur avec Expanded - Écart de 16px entre les deux - Utilisation de CustomNavigationButton **Optimisations mobile :** - Padding carte réduit : 20px vertical (vs 40px initial) - Toggles compacts (Switch scale 0.85) - Titre : 18px (vs 24px desktop) - Étape : 13px (vs 16px desktop) Référence: #78 Co-authored-by: Cursor --- .../lib/widgets/custom_navigation_button.dart | 87 +++++ .../widgets/personal_info_form_screen.dart | 363 ++++++++++++------ 2 files changed, 335 insertions(+), 115 deletions(-) create mode 100644 frontend/lib/widgets/custom_navigation_button.dart diff --git a/frontend/lib/widgets/custom_navigation_button.dart b/frontend/lib/widgets/custom_navigation_button.dart new file mode 100644 index 0000000..b132c1b --- /dev/null +++ b/frontend/lib/widgets/custom_navigation_button.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// Style de bouton de navigation +enum NavigationButtonStyle { + green, // Bouton vert avec texte vert foncé + purple, // Bouton violet avec texte violet foncé +} + +/// Widget de bouton de navigation personnalisé +/// Utilise les assets existants pour le fond +class CustomNavigationButton extends StatelessWidget { + final String text; + final VoidCallback onPressed; + final NavigationButtonStyle style; + final double? width; + final double height; + final double fontSize; + + const CustomNavigationButton({ + super.key, + required this.text, + required this.onPressed, + this.style = NavigationButtonStyle.green, + this.width, + this.height = 50, + this.fontSize = 16, + }); + + @override + Widget build(BuildContext context) { + final backgroundImage = _getBackgroundImage(); + final textColor = _getTextColor(); + + return SizedBox( + width: width, + height: height, + child: Stack( + children: [ + // Fond avec image + Positioned.fill( + child: Image.asset( + backgroundImage, + fit: BoxFit.fill, + ), + ), + // Bouton cliquable + Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Center( + child: Text( + text, + style: GoogleFonts.merienda( + color: textColor, + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ); + } + + String _getBackgroundImage() { + switch (style) { + case NavigationButtonStyle.green: + return 'assets/images/bg_green.png'; + case NavigationButtonStyle.purple: + return 'assets/images/bg_lavender.png'; + } + } + + Color _getTextColor() { + switch (style) { + case NavigationButtonStyle.green: + return const Color(0xFF2E7D32); // Vert foncé + case NavigationButtonStyle.purple: + return const Color(0xFF5E35B1); // Violet foncé + } + } +} diff --git a/frontend/lib/widgets/personal_info_form_screen.dart b/frontend/lib/widgets/personal_info_form_screen.dart index 8f423d9..e8e109f 100644 --- a/frontend/lib/widgets/personal_info_form_screen.dart +++ b/frontend/lib/widgets/personal_info_form_screen.dart @@ -5,6 +5,8 @@ import 'dart:math' as math; import 'custom_app_text_field.dart'; import 'form_field_wrapper.dart'; +import 'hover_relief_widget.dart'; +import 'custom_navigation_button.dart'; import '../models/card_assets.dart'; import '../config/display_config.dart'; @@ -160,25 +162,30 @@ class _PersonalInfoFormScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), - const SizedBox(height: 10), + Text( + widget.stepText, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 13 : 16, + color: Colors.black54, + ), + ), + SizedBox(height: config.isMobile ? 6 : 10), Text( widget.title, style: GoogleFonts.merienda( - fontSize: 24, + fontSize: config.isMobile ? 18 : 24, fontWeight: FontWeight.bold, color: Colors.black87, ), textAlign: TextAlign.center, ), - const SizedBox(height: 30), + SizedBox(height: config.isMobile ? 16 : 30), Container( width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, padding: EdgeInsets.symmetric( - vertical: config.isMobile ? 30 : 50, - horizontal: config.isMobile ? 20 : 50, + vertical: config.isMobile ? 20 : 50, + horizontal: config.isMobile ? 24 : 50, ), - constraints: BoxConstraints(minHeight: config.isMobile ? 400 : 570), decoration: BoxDecoration( image: DecorationImage( image: AssetImage( @@ -204,11 +211,57 @@ class _PersonalInfoFormScreenState extends State { ), ), ), + + // Boutons mobile sous la carte (dans le scroll) + if (config.isMobile) ...[ + const SizedBox(height: 20), + Padding( + padding: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.05, // Même marge que la carte (0.9 = 0.05 de chaque côté) + ), + child: Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + const SizedBox(width: 16), // Écart entre les boutons + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Suivant', + style: NavigationButtonStyle.green, + onPressed: _handleSubmit, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], ], ), ), ), - // Chevrons de navigation + // Chevrons de navigation (desktop uniquement) if (!config.isMobile) ...[ Positioned( top: screenSize.height / 2 - 20, @@ -239,32 +292,6 @@ class _PersonalInfoFormScreenState extends State { ), ), ], - // 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'), - ), - ], - ), - ), ], ), ); @@ -273,15 +300,15 @@ class _PersonalInfoFormScreenState extends State { /// Construit les toggles (Parent 2 / Même adresse) Widget _buildToggles(BuildContext context, DisplayConfig config) { if (config.isMobile) { - // Layout vertical sur mobile + // Layout vertical sur mobile - toggles compacts return Column( children: [ - _buildSecondPersonToggle(context), + _buildSecondPersonToggle(context, config), if (widget.showSameAddressCheckbox) ...[ - const SizedBox(height: 16), - _buildSameAddressToggle(context), + const SizedBox(height: 12), + _buildSameAddressToggle(context, config), ], - const SizedBox(height: 32), + const SizedBox(height: 24), ], ); } else { @@ -292,13 +319,13 @@ class _PersonalInfoFormScreenState extends State { children: [ Expanded( flex: 12, - child: _buildSecondPersonToggle(context), + child: _buildSecondPersonToggle(context, config), ), const Expanded(flex: 1, child: SizedBox()), if (widget.showSameAddressCheckbox) Expanded( flex: 12, - child: _buildSameAddressToggle(context), + child: _buildSameAddressToggle(context, config), ), ], ), @@ -308,61 +335,69 @@ class _PersonalInfoFormScreenState extends State { } } - Widget _buildSecondPersonToggle(BuildContext context) { + Widget _buildSecondPersonToggle(BuildContext context, DisplayConfig config) { return Row( children: [ - const Icon(Icons.person_add_alt_1, size: 20), - const SizedBox(width: 8), - Flexible( + Icon(Icons.person_add_alt_1, size: config.isMobile ? 18 : 20), + SizedBox(width: config.isMobile ? 6 : 8), + Expanded( child: Text( 'Ajouter Parent 2 ?', - style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + style: GoogleFonts.merienda( + fontWeight: FontWeight.bold, + fontSize: config.isMobile ? 14 : 16, + ), overflow: TextOverflow.ellipsis, ), ), - const Spacer(), - Switch( - value: _hasSecondPerson, - onChanged: (value) { - setState(() { - _hasSecondPerson = value; - _fieldsEnabled = value; - }); - }, - activeColor: Theme.of(context).primaryColor, + Transform.scale( + scale: config.isMobile ? 0.85 : 1.0, + child: Switch( + value: _hasSecondPerson, + onChanged: (value) { + setState(() { + _hasSecondPerson = value; + _fieldsEnabled = value; + }); + }, + activeColor: Theme.of(context).primaryColor, + ), ), ], ); } - Widget _buildSameAddressToggle(BuildContext context) { + Widget _buildSameAddressToggle(BuildContext context, DisplayConfig config) { return Row( children: [ Icon( Icons.home_work_outlined, - size: 20, + size: config.isMobile ? 18 : 20, color: _fieldsEnabled ? null : Colors.grey, ), - const SizedBox(width: 8), - Flexible( + SizedBox(width: config.isMobile ? 6 : 8), + Expanded( child: Text( 'Même Adresse ?', style: GoogleFonts.merienda( color: _fieldsEnabled ? null : Colors.grey, + fontSize: config.isMobile ? 14 : 16, ), overflow: TextOverflow.ellipsis, ), ), - const Spacer(), - Switch( - value: _sameAddress, - onChanged: _fieldsEnabled ? (value) { - setState(() { - _sameAddress = value ?? false; - _updateAddressFields(); - }); - } : null, - activeColor: Theme.of(context).primaryColor, + Transform.scale( + scale: config.isMobile ? 0.85 : 1.0, + child: Switch( + value: _sameAddress, + onChanged: _fieldsEnabled ? (value) { + setState(() { + _sameAddress = value ?? false; + _updateAddressFields(); + }); + } : null, + activeColor: Theme.of(context).primaryColor, + ), ), ], ); @@ -370,53 +405,70 @@ class _PersonalInfoFormScreenState extends State { /// Construit les champs du formulaire avec la nouvelle infrastructure Widget _buildFormFields(BuildContext context, DisplayConfig config) { + if (config.isMobile) { + return _buildMobileFields(context, config); + } else { + return _buildDesktopFields(context, config); + } + } + + /// Layout DESKTOP : champs côte à côte (horizontal) + Widget _buildDesktopFields(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, + Row( + children: [ + Expanded( + child: _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, + const SizedBox(width: 20), + Expanded( + child: _buildField( + config: config, + label: 'Prénom', + controller: _firstNameController, + hint: 'Votre prénom', + enabled: _fieldsEnabled, + ), ), ], ), - SizedBox(height: config.isMobile ? 16 : 32), + const SizedBox(height: 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, + Row( + children: [ + Expanded( + child: _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, + const SizedBox(width: 20), + Expanded( + child: _buildField( + config: config, + label: 'Email', + controller: _emailController, + hint: 'Votre adresse e-mail', + keyboardType: TextInputType.emailAddress, + enabled: _fieldsEnabled, + ), ), ], ), - SizedBox(height: config.isMobile ? 16 : 32), + const SizedBox(height: 32), // Adresse _buildField( @@ -426,14 +478,13 @@ class _PersonalInfoFormScreenState extends State { hint: 'Numéro et nom de votre rue', enabled: _fieldsEnabled && !_sameAddress, ), - SizedBox(height: config.isMobile ? 16 : 32), + const SizedBox(height: 32), // Code Postal et Ville - FormFieldRow( - config: config, - fields: [ - Flexible( - flex: 1, + Row( + children: [ + Expanded( + flex: 2, child: _buildField( config: config, label: 'Code Postal', @@ -443,8 +494,9 @@ class _PersonalInfoFormScreenState extends State { enabled: _fieldsEnabled && !_sameAddress, ), ), - Flexible( - flex: 4, + const SizedBox(width: 20), + Expanded( + flex: 5, child: _buildField( config: config, label: 'Ville', @@ -459,6 +511,86 @@ class _PersonalInfoFormScreenState extends State { ); } + /// Layout MOBILE : tous les champs empilés verticalement + Widget _buildMobileFields(BuildContext context, DisplayConfig config) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Nom + _buildField( + config: config, + label: 'Nom', + controller: _lastNameController, + hint: 'Votre nom de famille', + enabled: _fieldsEnabled, + ), + const SizedBox(height: 12), + + // Prénom + _buildField( + config: config, + label: 'Prénom', + controller: _firstNameController, + hint: 'Votre prénom', + enabled: _fieldsEnabled, + ), + const SizedBox(height: 12), + + // Téléphone + _buildField( + config: config, + label: 'Téléphone', + controller: _phoneController, + hint: 'Votre numéro de téléphone', + keyboardType: TextInputType.phone, + enabled: _fieldsEnabled, + ), + const SizedBox(height: 12), + + // Email + _buildField( + config: config, + label: 'Email', + controller: _emailController, + hint: 'Votre adresse e-mail', + keyboardType: TextInputType.emailAddress, + enabled: _fieldsEnabled, + ), + const SizedBox(height: 12), + + // Adresse + _buildField( + config: config, + label: 'Adresse (N° et Rue)', + controller: _addressController, + hint: 'Numéro et nom de votre rue', + enabled: _fieldsEnabled && !_sameAddress, + ), + const SizedBox(height: 12), + + // Code Postal + _buildField( + config: config, + label: 'Code Postal', + controller: _postalCodeController, + hint: 'Code postal', + keyboardType: TextInputType.number, + enabled: _fieldsEnabled && !_sameAddress, + ), + const SizedBox(height: 12), + + // Ville + _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, @@ -476,15 +608,16 @@ class _PersonalInfoFormScreenState extends State { value: controller.text, ); } else { - // Mode éditable : utiliser CustomAppTextField existant pour garder le style + // Mode éditable : style adapté mobile/desktop 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, + fieldHeight: config.isMobile ? 45.0 : 53.0, + labelFontSize: config.isMobile ? 15.0 : 22.0, + inputFontSize: config.isMobile ? 14.0 : 20.0, keyboardType: keyboardType ?? TextInputType.text, enabled: enabled, ); From b79f8c7e64424a093dff8c3273c1c110fe93ac49 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:01:54 +0100 Subject: [PATCH 05/12] =?UTF-8?q?refactor(#78):=20Renommer=20assets=20imag?= =?UTF-8?q?es=20pour=20usage=20g=C3=A9n=C3=A9rique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renommage des assets pour permettre leur utilisation aussi bien pour les boutons que pour les champs : **Images renommées :** - input_field_bg.png → bg_beige.png - input_field_jaune.png → bg_yellow.png - input_field_lavande.png → bg_lavender.png - btn_green.png → bg_green.png **Fichiers mis à jour (8) :** - custom_app_text_field.dart (champs de formulaire) - custom_navigation_button.dart (nouveau widget boutons) - base_form_screen.dart (structure de page) - login_screen.dart - change_password_dialog.dart - am_register_step4_screen.dart - parent_register_step5_screen.dart - summary_screen.dart **Avantages :** ✅ Noms génériques et cohérents ✅ Réutilisabilité boutons ET champs ✅ Maintenance facilitée Référence: #78 Co-authored-by: Cursor --- .../images/{input_field_bg.png => bg_beige.png} | Bin .../assets/images/{btn_green.png => bg_green.png} | Bin .../{input_field_lavande.png => bg_lavender.png} | Bin .../images/{input_field_jaune.png => bg_yellow.png} | Bin .../lib/screens/auth/am_register_step4_screen.dart | 2 +- frontend/lib/screens/auth/login_screen.dart | 2 +- .../screens/auth/parent_register_step5_screen.dart | 2 +- .../lib/widgets/auth/change_password_dialog.dart | 2 +- frontend/lib/widgets/base_form_screen.dart | 4 ++-- frontend/lib/widgets/custom_app_text_field.dart | 6 +++--- frontend/lib/widgets/summary_screen.dart | 4 ++-- 11 files changed, 11 insertions(+), 11 deletions(-) rename frontend/assets/images/{input_field_bg.png => bg_beige.png} (100%) rename frontend/assets/images/{btn_green.png => bg_green.png} (100%) rename frontend/assets/images/{input_field_lavande.png => bg_lavender.png} (100%) rename frontend/assets/images/{input_field_jaune.png => bg_yellow.png} (100%) diff --git a/frontend/assets/images/input_field_bg.png b/frontend/assets/images/bg_beige.png similarity index 100% rename from frontend/assets/images/input_field_bg.png rename to frontend/assets/images/bg_beige.png diff --git a/frontend/assets/images/btn_green.png b/frontend/assets/images/bg_green.png similarity index 100% rename from frontend/assets/images/btn_green.png rename to frontend/assets/images/bg_green.png diff --git a/frontend/assets/images/input_field_lavande.png b/frontend/assets/images/bg_lavender.png similarity index 100% rename from frontend/assets/images/input_field_lavande.png rename to frontend/assets/images/bg_lavender.png diff --git a/frontend/assets/images/input_field_jaune.png b/frontend/assets/images/bg_yellow.png similarity index 100% rename from frontend/assets/images/input_field_jaune.png rename to frontend/assets/images/bg_yellow.png diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index 8ed3b36..3dc293e 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -78,7 +78,7 @@ class _AmRegisterStep4ScreenState extends State { const SizedBox(height: 40), ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: 'Soumettre ma demande', textColor: const Color(0xFF2D6A4F), width: 350, diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index 74c33b1..cd883e4 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -242,7 +242,7 @@ class _LoginPageState extends State { child: _isLoading ? const CircularProgressIndicator() : ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', width: 300, height: 40, text: 'Se connecter', diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart index 5f939f7..9c81ae1 100644 --- a/frontend/lib/screens/auth/parent_register_step5_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -87,7 +87,7 @@ class _ParentRegisterStep5ScreenState extends State { _buildMotivationCard(context, registrationData.motivationText), const SizedBox(height: 40), ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: 'Soumettre ma demande', textColor: const Color(0xFF2D6A4F), width: 350, diff --git a/frontend/lib/widgets/auth/change_password_dialog.dart b/frontend/lib/widgets/auth/change_password_dialog.dart index 363ed71..f9f50a6 100644 --- a/frontend/lib/widgets/auth/change_password_dialog.dart +++ b/frontend/lib/widgets/auth/change_password_dialog.dart @@ -265,7 +265,7 @@ class _ChangePasswordDialogState extends State { child: _isLoading ? const CircularProgressIndicator() : ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', width: 250, height: 40, text: 'Changer le mot de passe', diff --git a/frontend/lib/widgets/base_form_screen.dart b/frontend/lib/widgets/base_form_screen.dart index fb10a51..608bffd 100644 --- a/frontend/lib/widgets/base_form_screen.dart +++ b/frontend/lib/widgets/base_form_screen.dart @@ -196,7 +196,7 @@ class BaseFormScreen extends StatelessWidget { // Bouton Précédent HoverReliefWidget( child: ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: 'Précédent', textColor: Colors.white, onPressed: () => Navigator.pushNamed(context, previousRoute), @@ -208,7 +208,7 @@ class BaseFormScreen extends StatelessWidget { // Bouton Suivant/Soumettre HoverReliefWidget( child: ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: submitButtonText ?? 'Suivant', textColor: Colors.white, onPressed: config.isReadonly ? onSubmit : () { diff --git a/frontend/lib/widgets/custom_app_text_field.dart b/frontend/lib/widgets/custom_app_text_field.dart index 5f700da..2492cab 100644 --- a/frontend/lib/widgets/custom_app_text_field.dart +++ b/frontend/lib/widgets/custom_app_text_field.dart @@ -54,12 +54,12 @@ class _CustomAppTextFieldState extends State { String getBackgroundImagePath() { switch (widget.style) { case CustomAppTextFieldStyle.lavande: - return 'assets/images/input_field_lavande.png'; + return 'assets/images/bg_lavender.png'; case CustomAppTextFieldStyle.jaune: - return 'assets/images/input_field_jaune.png'; + return 'assets/images/bg_yellow.png'; case CustomAppTextFieldStyle.beige: default: - return 'assets/images/input_field_bg.png'; + return 'assets/images/bg_beige.png'; } } diff --git a/frontend/lib/widgets/summary_screen.dart b/frontend/lib/widgets/summary_screen.dart index a623891..7a69f3e 100644 --- a/frontend/lib/widgets/summary_screen.dart +++ b/frontend/lib/widgets/summary_screen.dart @@ -38,7 +38,7 @@ class SummaryScreen extends StatelessWidget { actions: [ Center( child: ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: 'OK', textColor: const Color(0xFF2D6A4F), width: 150, @@ -89,7 +89,7 @@ class SummaryScreen extends StatelessWidget { const SizedBox(height: 20), ImageButton( - bg: 'assets/images/btn_green.png', + bg: 'assets/images/bg_green.png', text: submitButtonText, textColor: const Color(0xFF2D6A4F), width: 350, From f8bd911c0202883ed1a1f4b50f8e74eef337aa55 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:12:42 +0100 Subject: [PATCH 06/12] feat(#78): Migrer ProfessionalInfoFormScreen vers infrastructure multi-modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adaptation responsive du formulaire d'informations professionnelles (AM Step 2) : - Desktop : photo gauche (270px) + champs droite - Mobile : layout vertical avec photo 200px + champs empilés - Boutons CustomNavigationButton (violet/vert) sous la carte en mobile - Chevrons uniquement en mode desktop - Espacement adapté (12px mobile vs 32px desktop) - Tailles de police adaptées Co-authored-by: Cursor --- .../professional_info_form_screen.dart | 563 ++++++++++++------ 1 file changed, 384 insertions(+), 179 deletions(-) diff --git a/frontend/lib/widgets/professional_info_form_screen.dart b/frontend/lib/widgets/professional_info_form_screen.dart index dbc19dd..7843863 100644 --- a/frontend/lib/widgets/professional_info_form_screen.dart +++ b/frontend/lib/widgets/professional_info_form_screen.dart @@ -5,9 +5,11 @@ import 'package:intl/intl.dart'; import 'dart:math' as math; import 'dart:io'; import '../models/card_assets.dart'; +import '../config/display_config.dart'; import 'custom_app_text_field.dart'; import 'app_custom_checkbox.dart'; import 'hover_relief_widget.dart'; +import 'custom_navigation_button.dart'; /// Données pour le formulaire d'informations professionnelles class ProfessionalInfoData { @@ -36,7 +38,9 @@ class ProfessionalInfoData { /// Widget générique pour le formulaire d'informations professionnelles /// Utilisé pour l'inscription des Assistantes Maternelles +/// Supporte mode éditable et readonly, responsive mobile/desktop class ProfessionalInfoFormScreen extends StatefulWidget { + final DisplayMode mode; final String stepText; final String title; final CardColorHorizontal cardColor; @@ -47,6 +51,7 @@ class ProfessionalInfoFormScreen extends StatefulWidget { const ProfessionalInfoFormScreen({ super.key, + this.mode = DisplayMode.editable, required this.stepText, required this.title, required this.cardColor, @@ -163,16 +168,7 @@ class _ProfessionalInfoFormScreenState extends State @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; - final Color baseCardColorForShadow = Colors.green.shade300; - final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); - final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); - - ImageProvider? currentImageProvider; - if (_photoFile != null) { - currentImageProvider = FileImage(_photoFile!); - } else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) { - currentImageProvider = AssetImage(_photoPathFramework!); - } + final config = DisplayConfig.fromContext(context, mode: widget.mode); return Scaffold( body: Stack( @@ -186,201 +182,410 @@ class _ProfessionalInfoFormScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), - const SizedBox(height: 10), + Text( + widget.stepText, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 13 : 16, + color: Colors.black54, + ), + ), + SizedBox(height: config.isMobile ? 6 : 10), Text( widget.title, style: GoogleFonts.merienda( - fontSize: 24, + fontSize: config.isMobile ? 18 : 24, fontWeight: FontWeight.bold, color: Colors.black87, ), textAlign: TextAlign.center, ), - const SizedBox(height: 30), + SizedBox(height: config.isMobile ? 16 : 30), Container( - width: screenSize.width * 0.6, - padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), - constraints: const BoxConstraints(minHeight: 650), + width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, + padding: EdgeInsets.symmetric( + vertical: config.isMobile ? 20 : 50, + horizontal: config.isMobile ? 24 : 50, + ), decoration: BoxDecoration( - image: DecorationImage(image: AssetImage(widget.cardColor.path), fit: BoxFit.fill), + image: DecorationImage( + image: AssetImage( + config.isMobile + ? _getVerticalCardAsset() + : widget.cardColor.path + ), + fit: BoxFit.fill, + ), ), child: Form( key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Colonne Gauche: Photo et Checkbox - SizedBox( - width: 300, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - HoverReliefWidget( - onPressed: _pickPhoto, - borderRadius: BorderRadius.circular(10.0), - initialShadowColor: initialPhotoShadow, - hoverShadowColor: hoverPhotoShadow, - child: SizedBox( - height: 270, - width: 270, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - image: currentImageProvider != null - ? DecorationImage(image: currentImageProvider, fit: BoxFit.cover) - : null, - ), - child: currentImageProvider == null - ? Image.asset('assets/images/photo.png', fit: BoxFit.contain) - : null, - ), - ), - ), - const SizedBox(height: 10), - AppCustomCheckbox( - label: 'J\'accepte l\'utilisation\nde ma photo.', - value: _photoConsent, - onChanged: (val) => setState(() => _photoConsent = val ?? false), - ), - ], - ), - ), - const SizedBox(width: 30), - // Colonne Droite: Champs de naissance - Expanded( - child: Column( - children: [ - CustomAppTextField( - controller: _birthCityController, - labelText: 'Ville de naissance', - hintText: 'Votre ville de naissance', - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) => v!.isEmpty ? 'Ville requise' : null, - ), - const SizedBox(height: 32), - CustomAppTextField( - controller: _birthCountryController, - labelText: 'Pays de naissance', - hintText: 'Votre pays de naissance', - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) => v!.isEmpty ? 'Pays requis' : null, - ), - const SizedBox(height: 32), - CustomAppTextField( - controller: _dateOfBirthController, - labelText: 'Date de naissance', - hintText: 'JJ/MM/AAAA', - readOnly: true, - onTap: () => _selectDate(context), - suffixIcon: Icons.calendar_today, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) => _selectedDate == null ? 'Date requise' : null, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - CustomAppTextField( - controller: _nirController, - labelText: 'N° Sécurité Sociale (NIR)', - hintText: 'Votre NIR à 13 chiffres', - keyboardType: TextInputType.number, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) { - if (v == null || v.isEmpty) return 'NIR requis'; - if (v.length != 13) return 'Le NIR doit contenir 13 chiffres'; - if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3'; - return null; - }, - ), - const SizedBox(height: 32), - Row( - children: [ - Expanded( - child: CustomAppTextField( - controller: _agrementController, - labelText: 'N° d\'agrément', - hintText: 'Votre numéro d\'agrément', - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) => v!.isEmpty ? 'Agrément requis' : null, - ), - ), - const SizedBox(width: 20), - Expanded( - child: CustomAppTextField( - controller: _capacityController, - labelText: 'Capacité d\'accueil', - hintText: 'Ex: 3', - keyboardType: TextInputType.number, - fieldWidth: double.infinity, - labelFontSize: 22.0, - inputFontSize: 20.0, - validator: (v) { - if (v == null || v.isEmpty) return 'Capacité requise'; - final n = int.tryParse(v); - if (n == null || n <= 0) return 'Nombre invalide'; - return null; - }, - ), - ), - ], - ), - ], - ), + child: config.isMobile + ? _buildMobileFields(context, config) + : _buildDesktopFields(context, config), ), ), + // Boutons mobile sous la carte + if (config.isMobile) ...[ + const SizedBox(height: 20), + _buildMobileButtons(context, config, screenSize), + const SizedBox(height: 10), + ], ], ), ), ), - // Chevron Gauche (Retour) - 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 desktop uniquement + if (!config.isMobile) ...[ + // Chevron Gauche (Retour) + 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: 'Précédent', ), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go(widget.previousRoute); - } - }, - tooltip: 'Précédent', + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _submitForm, + tooltip: 'Suivant', + ), + ), + ], + ], + ), + ); + } + + /// Layout DESKTOP : Photo à gauche, champs à droite + Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Photo + Checkbox à gauche + SizedBox( + width: 300, + child: _buildPhotoSection(context, config), + ), + const SizedBox(width: 30), + // Champs à droite + Expanded( + child: Column( + children: [ + _buildField( + config: config, + label: 'Ville de naissance', + controller: _birthCityController, + hint: 'Votre ville de naissance', + validator: (v) => v!.isEmpty ? 'Ville requise' : null, + ), + const SizedBox(height: 32), + _buildField( + config: config, + label: 'Pays de naissance', + controller: _birthCountryController, + hint: 'Votre pays de naissance', + validator: (v) => v!.isEmpty ? 'Pays requis' : null, + ), + const SizedBox(height: 32), + _buildField( + config: config, + label: 'Date de naissance', + controller: _dateOfBirthController, + hint: 'JJ/MM/AAAA', + readOnly: true, + onTap: () => _selectDate(context), + suffixIcon: Icons.calendar_today, + validator: (v) => _selectedDate == null ? 'Date requise' : null, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + _buildField( + config: config, + label: 'N° Sécurité Sociale (NIR)', + controller: _nirController, + hint: 'Votre NIR à 13 chiffres', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return 'NIR requis'; + if (v.length != 13) return 'Le NIR doit contenir 13 chiffres'; + if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3'; + return null; + }, + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: _buildField( + config: config, + label: 'N° d\'agrément', + controller: _agrementController, + hint: 'Votre numéro d\'agrément', + validator: (v) => v!.isEmpty ? 'Agrément requis' : null, + ), + ), + const SizedBox(width: 20), + Expanded( + child: _buildField( + config: config, + label: 'Capacité d\'accueil', + controller: _capacityController, + hint: 'Ex: 3', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return 'Capacité requise'; + final n = int.tryParse(v); + if (n == null || n <= 0) return 'Nombre invalide'; + return null; + }, + ), + ), + ], + ), + ], + ); + } + + /// Layout MOBILE : Tout empilé verticalement + Widget _buildMobileFields(BuildContext context, DisplayConfig config) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Photo + Checkbox en premier + _buildPhotoSection(context, config), + const SizedBox(height: 20), + + _buildField( + config: config, + label: 'Ville de naissance', + controller: _birthCityController, + hint: 'Votre ville de naissance', + validator: (v) => v!.isEmpty ? 'Ville requise' : null, + ), + const SizedBox(height: 12), + + _buildField( + config: config, + label: 'Pays de naissance', + controller: _birthCountryController, + hint: 'Votre pays de naissance', + validator: (v) => v!.isEmpty ? 'Pays requis' : null, + ), + const SizedBox(height: 12), + + _buildField( + config: config, + label: 'Date de naissance', + controller: _dateOfBirthController, + hint: 'JJ/MM/AAAA', + readOnly: true, + onTap: () => _selectDate(context), + suffixIcon: Icons.calendar_today, + validator: (v) => _selectedDate == null ? 'Date requise' : null, + ), + const SizedBox(height: 12), + + _buildField( + config: config, + label: 'N° Sécurité Sociale (NIR)', + controller: _nirController, + hint: 'Votre NIR à 13 chiffres', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return 'NIR requis'; + if (v.length != 13) return 'Le NIR doit contenir 13 chiffres'; + if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3'; + return null; + }, + ), + const SizedBox(height: 12), + + _buildField( + config: config, + label: 'N° d\'agrément', + controller: _agrementController, + hint: 'Votre numéro d\'agrément', + validator: (v) => v!.isEmpty ? 'Agrément requis' : null, + ), + const SizedBox(height: 12), + + _buildField( + config: config, + label: 'Capacité d\'accueil', + controller: _capacityController, + hint: 'Ex: 3', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return 'Capacité requise'; + final n = int.tryParse(v); + if (n == null || n <= 0) return 'Nombre invalide'; + return null; + }, + ), + ], + ); + } + + /// Section photo + checkbox + Widget _buildPhotoSection(BuildContext context, DisplayConfig config) { + final Color baseCardColorForShadow = Colors.green.shade300; + final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); + final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); + + ImageProvider? currentImageProvider; + if (_photoFile != null) { + currentImageProvider = FileImage(_photoFile!); + } else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) { + currentImageProvider = AssetImage(_photoPathFramework!); + } + + final photoSize = config.isMobile ? 200.0 : 270.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + HoverReliefWidget( + onPressed: _pickPhoto, + borderRadius: BorderRadius.circular(10.0), + initialShadowColor: initialPhotoShadow, + hoverShadowColor: hoverPhotoShadow, + child: SizedBox( + height: photoSize, + width: photoSize, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: currentImageProvider != null + ? DecorationImage(image: currentImageProvider, fit: BoxFit.cover) + : null, + ), + child: currentImageProvider == null + ? Image.asset('assets/images/photo.png', fit: BoxFit.contain) + : null, ), ), - // Chevron Droit (Suivant) - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: _submitForm, - tooltip: 'Suivant', + ), + const SizedBox(height: 10), + AppCustomCheckbox( + label: 'J\'accepte l\'utilisation\nde ma photo.', + value: _photoConsent, + onChanged: (val) => setState(() => _photoConsent = val ?? false), + ), + ], + ); + } + + /// Construit un champ individuel + Widget _buildField({ + required DisplayConfig config, + required String label, + required TextEditingController controller, + String? hint, + TextInputType? keyboardType, + bool readOnly = false, + VoidCallback? onTap, + IconData? suffixIcon, + String? Function(String?)? validator, + }) { + return CustomAppTextField( + controller: controller, + labelText: label, + hintText: hint ?? label, + fieldWidth: double.infinity, + fieldHeight: config.isMobile ? 45.0 : 53.0, + labelFontSize: config.isMobile ? 15.0 : 22.0, + inputFontSize: config.isMobile ? 14.0 : 20.0, + keyboardType: keyboardType ?? TextInputType.text, + readOnly: readOnly, + onTap: onTap, + suffixIcon: suffixIcon, + validator: validator, + ); + } + + /// Boutons mobile + Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.05, + ), + child: Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Suivant', + style: NavigationButtonStyle.green, + onPressed: _submitForm, + width: double.infinity, + height: 50, + fontSize: 16, + ), ), ), ], ), ); } + + /// 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; + } + } } From bdecbc2c1d5d90746fc0daaa242864a6c22e1290 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:19:43 +0100 Subject: [PATCH 07/12] feat(#78): Migrer PresentationFormScreen vers infrastructure multi-modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adaptation responsive du formulaire de présentation (AM Step 3) : - Desktop : Layout horizontal avec scroll global (format 2:1) - Mobile : Layout plein écran sans scroll global - Header fixe (titre + étape) - Carte occupe tout l'espace vertical disponible - Seul le champ texte interne est scrollable - Boutons fixes en bas - Checkbox CGU adaptée (texte raccourci + scale 0.85 en mobile) - Chevrons uniquement en mode desktop Co-authored-by: Cursor --- .../lib/widgets/presentation_form_screen.dart | 345 +++++++++++++----- 1 file changed, 262 insertions(+), 83 deletions(-) diff --git a/frontend/lib/widgets/presentation_form_screen.dart b/frontend/lib/widgets/presentation_form_screen.dart index 8008f32..17a049f 100644 --- a/frontend/lib/widgets/presentation_form_screen.dart +++ b/frontend/lib/widgets/presentation_form_screen.dart @@ -5,9 +5,15 @@ import 'dart:math' as math; import 'custom_decorated_text_field.dart'; import 'app_custom_checkbox.dart'; +import 'custom_navigation_button.dart'; +import 'hover_relief_widget.dart'; import '../models/card_assets.dart'; +import '../config/display_config.dart'; +/// Widget générique pour le formulaire de présentation avec texte libre + CGU +/// Supporte mode éditable et readonly, responsive mobile/desktop class PresentationFormScreen extends StatefulWidget { + final DisplayMode mode; final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5" final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande" final CardColorHorizontal cardColor; @@ -19,6 +25,7 @@ class PresentationFormScreen extends StatefulWidget { const PresentationFormScreen({ super.key, + this.mode = DisplayMode.editable, required this.stepText, required this.title, required this.cardColor, @@ -66,9 +73,7 @@ class _PresentationFormScreenState extends State { @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; - final cardWidth = screenSize.width * 0.6; - final double imageAspectRatio = 2.0; - final cardHeight = cardWidth / imageAspectRatio; + final config = DisplayConfig.fromContext(context, mode: widget.mode); return Scaffold( body: Stack( @@ -76,94 +81,268 @@ class _PresentationFormScreenState extends State { Positioned.fill( child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.stepText, - style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), - ), - const SizedBox(height: 20), - Text( - widget.title, - style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Container( - width: cardWidth, - height: cardHeight, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(widget.cardColor.path), - fit: BoxFit.fill, - ), - ), - child: Padding( - padding: const EdgeInsets.all(40.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: CustomDecoratedTextField( - controller: _textController, - hintText: widget.textFieldHint, - fieldHeight: cardHeight * 0.6, - maxLines: 10, - expandDynamically: true, - fontSize: 18.0, - ), - ), - const SizedBox(height: 20), - AppCustomCheckbox( - label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', - value: _cguAccepted, - onChanged: (value) => setState(() => _cguAccepted = value ?? false), - ), - ], - ), - ), - ), - ], + config.isMobile + ? _buildMobileLayout(context, config, screenSize) + : _buildDesktopLayout(context, config, screenSize), + // Chevrons desktop uniquement + if (!config.isMobile) ...[ + // Chevron Gauche (Retour) + 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', + ), + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _cguAccepted ? _handleSubmit : null, + tooltip: 'Suivant', + ), + ), + ], + ], + ), + ); + } + + /// Layout MOBILE : Plein écran sans scroll global + Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize) { + return Column( + children: [ + // Header fixe + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Column( + children: [ + Text( + widget.stepText, + style: GoogleFonts.merienda( + fontSize: 13, + color: Colors.black54, + ), + ), + const SizedBox(height: 6), + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 16), + // Carte qui prend tout l'espace restant + Expanded( + child: _buildMobileCard(context, config, screenSize), + ), + // Boutons en bas + const SizedBox(height: 20), + _buildMobileButtons(context, config, screenSize), + const SizedBox(height: 10), + ], + ); + } + + /// Layout DESKTOP : Avec scroll + Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize) { + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.stepText, + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 20), + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + _buildDesktopCard(context, config, screenSize), + ], + ), + ), + ); + } + + /// Carte DESKTOP : Format horizontal 2:1 + Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { + final cardWidth = screenSize.width * 0.6; + final double imageAspectRatio = 2.0; + final cardHeight = cardWidth / imageAspectRatio; + + return Container( + width: cardWidth, + height: cardHeight, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(widget.cardColor.path), + fit: BoxFit.fill, + ), + ), + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: CustomDecoratedTextField( + controller: _textController, + hintText: widget.textFieldHint, + fieldHeight: cardHeight * 0.6, + maxLines: 10, + expandDynamically: true, + fontSize: 18.0, + ), + ), + const SizedBox(height: 20), + AppCustomCheckbox( + label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', + value: _cguAccepted, + onChanged: (value) => setState(() => _cguAccepted = value ?? false), + ), + ], + ), + ), + ); + } + + /// Carte MOBILE : Prend tout l'espace disponible + Widget _buildMobileCard(BuildContext context, DisplayConfig config, Size screenSize) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(_getVerticalCardAsset()), + fit: BoxFit.fill, + ), + ), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: Column( + children: [ + // Champ de texte qui prend l'espace disponible et scrollable + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return CustomDecoratedTextField( + controller: _textController, + hintText: widget.textFieldHint, + fieldHeight: constraints.maxHeight, + maxLines: 100, // Grande valeur pour permettre le scroll + expandDynamically: false, + fontSize: 14.0, + ); + }, + ), + ), + const SizedBox(height: 16), + // Checkbox en bas + Transform.scale( + scale: 0.85, + child: AppCustomCheckbox( + label: 'J\'accepte les CGU et la\nPolitique de confidentialité', + value: _cguAccepted, + onChanged: (value) => setState(() => _cguAccepted = value ?? false), + ), + ), + ], + ), + ), + ); + } + + /// Boutons mobile + Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.05, + ), + child: Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + width: double.infinity, + height: 50, + fontSize: 16, ), ), ), - // Chevron Gauche (Retour) - 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), + const SizedBox(width: 16), + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Suivant', + style: NavigationButtonStyle.green, + onPressed: _handleSubmit, + width: double.infinity, + height: 50, + fontSize: 16, ), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go(widget.previousRoute); - } - }, - tooltip: 'Retour', - ), - ), - // Chevron Droit (Suivant) - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: _cguAccepted ? _handleSubmit : null, - tooltip: 'Suivant', ), ), ], ), ); } + + /// 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; + } + } } From eea94769bf5991e03a5896e2fddd454a10ac1cd4 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 11:43:26 +0100 Subject: [PATCH 08/12] 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, ), ), From 64527066802a02e944b870f1dd0354ef565dc10e Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 4 Feb 2026 14:33:04 +0100 Subject: [PATCH 09/12] fix(#78): Ajustements UI ChildCardWidget et ParentStep3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Réduction de la taille des polices et des champs dans la carte enfant (Mobile/Desktop) pour éviter l'overflow. - Restauration de la taille du bouton "+" en mode Desktop (100px). Co-authored-by: Cursor --- .../auth/parent_register_step3_screen.dart | 4 +- frontend/lib/widgets/child_card_widget.dart | 116 +++++++++++++----- 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart index 3d6441d..12e472b 100644 --- a/frontend/lib/screens/auth/parent_register_step3_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -433,12 +433,12 @@ class _ParentRegisterStep3ScreenState extends State { ), ); } else { - // Bouton Ajouter + // Bouton Ajouter Desktop (Gros bouton) return Center( child: HoverReliefWidget( onPressed: () => _addChild(registrationData), borderRadius: BorderRadius.circular(15), - child: Image.asset('assets/images/plus.png', height: 80, width: 80), + child: Image.asset('assets/images/plus.png', height: 100, width: 100, fit: BoxFit.contain), ), ); } diff --git a/frontend/lib/widgets/child_card_widget.dart b/frontend/lib/widgets/child_card_widget.dart index bd086fe..3b37c75 100644 --- a/frontend/lib/widgets/child_card_widget.dart +++ b/frontend/lib/widgets/child_card_widget.dart @@ -5,6 +5,7 @@ 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 'app_custom_checkbox.dart'; import 'hover_relief_widget.dart'; import '../config/display_config.dart'; @@ -23,6 +24,8 @@ class ChildCardWidget extends StatefulWidget { final ValueChanged onToggleIsUnborn; final VoidCallback onRemove; final bool canBeRemoved; + final DisplayMode mode; + final VoidCallback? onEdit; const ChildCardWidget({ required Key key, @@ -37,6 +40,8 @@ class ChildCardWidget extends StatefulWidget { required this.onToggleIsUnborn, required this.onRemove, required this.canBeRemoved, + this.mode = DisplayMode.editable, + this.onEdit, }) : super(key: key); @override @@ -88,7 +93,7 @@ class _ChildCardWidgetState extends State { @override Widget build(BuildContext context) { - final config = DisplayConfig.fromContext(context); + final config = DisplayConfig.fromContext(context, mode: widget.mode); final scaleFactor = config.isMobile ? 0.9 : 1.1; // Réduire légèrement sur mobile final File? currentChildImage = widget.childData.imageFile; @@ -101,7 +106,7 @@ class _ChildCardWidgetState extends State { return Container( width: 345.0 * scaleFactor, - height: config.isMobile ? null : 570.0 * scaleFactor, // Hauteur auto sur mobile + height: config.isMobile ? null : 600.0 * scaleFactor, // Hauteur augmentée pour éviter l'overflow padding: EdgeInsets.all(22.0 * scaleFactor), decoration: BoxDecoration( image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill), @@ -113,7 +118,7 @@ class _ChildCardWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ HoverReliefWidget( - onPressed: widget.onPickImage, + onPressed: config.isReadonly ? null : widget.onPickImage, borderRadius: BorderRadius.circular(10), initialShadowColor: initialPhotoShadow, hoverShadowColor: hoverPhotoShadow, @@ -130,7 +135,7 @@ class _ChildCardWidgetState extends State { ), ), ), - SizedBox(height: 12.0 * scaleFactor), + SizedBox(height: 10.0 * scaleFactor), // Réduit de 12 à 10 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -143,58 +148,58 @@ class _ChildCardWidgetState extends State { ), Transform.scale( scale: config.isMobile ? 0.8 : 1.0, - child: Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), + child: Switch( + value: widget.childData.isUnbornChild, + onChanged: config.isReadonly ? null : widget.onToggleIsUnborn, + activeColor: Theme.of(context).primaryColor, + ), ), ], ), - SizedBox(height: 9.0 * scaleFactor), - CustomAppTextField( + SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 + _buildField( + config: config, + scaleFactor: scaleFactor, + label: 'Prénom', controller: _firstNameController, - labelText: 'Prénom', - hintText: 'Facultatif si à naître', + hint: 'Facultatif si à naître', isRequired: !widget.childData.isUnbornChild, - 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, ), - SizedBox(height: 6.0 * scaleFactor), - CustomAppTextField( + SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 + _buildField( + config: config, + scaleFactor: scaleFactor, + label: 'Nom', controller: _lastNameController, - labelText: 'Nom', - hintText: 'Nom de l\'enfant', - enabled: true, - fieldHeight: config.isMobile ? 45.0 : 55.0 * scaleFactor, - labelFontSize: config.isMobile ? 14.0 : 22.0, - inputFontSize: config.isMobile ? 14.0 : 20.0, + hint: 'Nom de l\'enfant', ), - SizedBox(height: 9.0 * scaleFactor), - CustomAppTextField( + SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 + _buildField( + config: config, + scaleFactor: scaleFactor, + label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', controller: _dobController, - labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', - hintText: 'JJ/MM/AAAA', - readOnly: true, - onTap: widget.onDateSelect, + hint: 'JJ/MM/AAAA', + readOnly: true, // Toujours readonly pour le TextField (date picker) + onTap: config.isReadonly ? null : widget.onDateSelect, suffixIcon: Icons.calendar_today, - fieldHeight: config.isMobile ? 45.0 : 55.0 * scaleFactor, - labelFontSize: config.isMobile ? 14.0 : 22.0, - inputFontSize: config.isMobile ? 14.0 : 20.0, ), - SizedBox(height: 11.0 * scaleFactor), + SizedBox(height: 10.0 * scaleFactor), // Réduit de 11 à 10 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppCustomCheckbox( label: 'Consentement photo', value: widget.childData.photoConsent, - onChanged: widget.onTogglePhotoConsent, + onChanged: config.isReadonly ? (v) {} : widget.onTogglePhotoConsent, checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, fontSize: config.isMobile ? 13.0 : 16.0, ), - SizedBox(height: 6.0 * scaleFactor), + SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 AppCustomCheckbox( label: 'Naissance multiple', value: widget.childData.multipleBirth, - onChanged: widget.onToggleMultipleBirth, + onChanged: config.isReadonly ? (v) {} : widget.onToggleMultipleBirth, checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, fontSize: config.isMobile ? 13.0 : 16.0, ), @@ -202,7 +207,7 @@ class _ChildCardWidgetState extends State { ), ], ), - if (widget.canBeRemoved) + if (widget.canBeRemoved && !config.isReadonly) Positioned( top: -5, right: -5, child: InkWell( @@ -216,8 +221,51 @@ class _ChildCardWidgetState extends State { ), ), ), + + 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', + ), + ), ], ), ); } + + 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, + }) { + if (config.isReadonly) { + return FormFieldWrapper( + config: config, + label: label, + value: controller.text, + ); + } else { + return CustomAppTextField( + controller: controller, + 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, + ); + } + } } From 08612c455d4adb6cc6c08f11e6344080d413a12a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sat, 7 Feb 2026 13:29:11 +0100 Subject: [PATCH 10/12] Fix recap screens layout (desktop/mobile) and widget styles - Restore horizontal 2:1 layout for desktop readonly cards - Implement adaptive height for mobile readonly cards - Fix spacing and margins on mobile recap screens - Update field styles to use beige background - Adjust ChildCardWidget width for mobile editing - Fix compilation errors and duplicate methods Co-authored-by: Cursor --- .../auth/am_register_step4_screen.dart | 354 ++++--------- .../auth/parent_register_step5_screen.dart | 495 +++++------------- frontend/lib/widgets/child_card_widget.dart | 342 +++++++++++- frontend/lib/widgets/form_field_wrapper.dart | 15 +- .../widgets/personal_info_form_screen.dart | 398 ++++++++++++-- .../lib/widgets/presentation_form_screen.dart | 276 +++++++++- .../professional_info_form_screen.dart | 378 +++++++++++-- frontend/lib/widgets/summary_screen.dart | 7 +- 8 files changed, 1542 insertions(+), 723 deletions(-) diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index 3dc293e..909ea88 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -1,41 +1,16 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../models/am_registration_data.dart'; -import '../../widgets/image_button.dart'; -import '../../models/card_assets.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; +import 'dart:math' as math; -// Méthode helper pour afficher un champ de type "lecture seule" stylisé -Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { - const FontWeight labelFontWeight = FontWeight.w600; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), - const SizedBox(height: 4), - Container( - width: double.infinity, - height: multiLine ? null : fieldHeight, - constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, - padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), - fit: BoxFit.fill, - ), - ), - child: Text( - value.isNotEmpty ? value : '-', - style: GoogleFonts.merienda(fontSize: labelFontSize), - maxLines: multiLine ? null : 1, - overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, - ), - ), - ], - ); -} +import '../../models/am_registration_data.dart'; +import '../../models/card_assets.dart'; +import '../../config/display_config.dart'; +import '../../widgets/image_button.dart'; +import '../../widgets/personal_info_form_screen.dart'; +import '../../widgets/professional_info_form_screen.dart'; +import '../../widgets/presentation_form_screen.dart'; class AmRegisterStep4Screen extends StatefulWidget { const AmRegisterStep4Screen({super.key}); @@ -49,6 +24,7 @@ class _AmRegisterStep4ScreenState extends State { Widget build(BuildContext context) { final registrationData = Provider.of(context); final screenSize = MediaQuery.of(context).size; + final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly); return Scaffold( body: Stack( @@ -60,7 +36,9 @@ class _AmRegisterStep4ScreenState extends State { child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 40.0), child: Padding( - padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + padding: EdgeInsets.symmetric( + horizontal: config.isMobile ? 0 : screenSize.width / 4.0 + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -70,18 +48,23 @@ class _AmRegisterStep4ScreenState extends State { Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), const SizedBox(height: 30), - _buildPersonalInfoCard(context, registrationData), - const SizedBox(height: 20), - _buildProfessionalInfoCard(context, registrationData), - const SizedBox(height: 20), - _buildPresentationCard(context, registrationData), + // Carte 1: Informations personnelles + _buildPersonalInfo(context, registrationData), + const SizedBox(height: 30), + + // Carte 2: Informations professionnelles + _buildProfessionalInfo(context, registrationData), + const SizedBox(height: 30), + + // Carte 3: Présentation + _buildPresentation(context, registrationData), const SizedBox(height: 40), ImageButton( bg: 'assets/images/bg_green.png', text: 'Soumettre ma demande', textColor: const Color(0xFF2D6A4F), - width: 350, + width: config.isMobile ? 300 : 350, height: 50, fontSize: 18, onPressed: () { @@ -94,26 +77,94 @@ class _AmRegisterStep4ScreenState extends State { ), ), ), - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go('/am-register-step3'); - } - }, - tooltip: 'Retour', + // 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('/am-register-step3'); + } + }, + tooltip: 'Retour', + ), ), - ), ], ), ); } + Widget _buildPersonalInfo(BuildContext context, AmRegistrationData data) { + return PersonalInfoFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Informations personnelles', + cardColor: CardColorHorizontal.blue, + initialData: PersonalInfoData( + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone, + email: data.email, + address: data.streetAddress, + postalCode: data.postalCode, + city: data.city, + ), + onSubmit: (d, {hasSecondPerson, sameAddress}) {}, // No-op en readonly + previousRoute: '', + onEdit: () => context.go('/am-register-step1'), + ); + } + + Widget _buildProfessionalInfo(BuildContext context, AmRegistrationData data) { + return ProfessionalInfoFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Informations professionnelles', + cardColor: CardColorHorizontal.green, + initialData: ProfessionalInfoData( + // TODO: Gérer photoPath vs photoFile correctement + photoPath: null, // Pas d'accès facile au fichier ici, on verra + dateOfBirth: data.dateOfBirth, + birthCity: data.birthCity, + birthCountry: data.birthCountry, + nir: data.nir, + agrementNumber: data.agrementNumber, + capacity: data.capacity, + photoConsent: data.photoConsent, + ), + onSubmit: (d) {}, + previousRoute: '', + onEdit: () => context.go('/am-register-step2'), + ); + } + + Widget _buildPresentation(BuildContext context, AmRegistrationData data) { + return PresentationFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Présentation & CGU', + cardColor: CardColorHorizontal.peach, + textFieldHint: '', + initialText: data.presentationText, + initialCguAccepted: data.cguAccepted, + previousRoute: '', + onSubmit: (t, c) {}, + onEdit: () => context.go('/am-register-step3'), + ); + } + void _showConfirmationModal(BuildContext context) { showDialog( context: context, @@ -141,199 +192,4 @@ class _AmRegisterStep4ScreenState extends State { }, ); } - - // Carte Informations personnelles - Widget _buildPersonalInfoCard(BuildContext context, AmRegistrationData data) { - const double verticalSpacing = 28.0; - const double labelFontSize = 22.0; - - List details = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - _buildDisplayFieldValue(context, "Adresse:", "${data.streetAddress}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), - const SizedBox(height: verticalSpacing), - _buildDisplayFieldValue(context, "Consentement photo:", data.photoConsent ? "Oui" : "Non", labelFontSize: labelFontSize), - ]; - - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.blue.path, - title: 'Informations personnelles', - content: details, - onEdit: () => context.go('/am-register-step1'), - ); - } - - // Carte Informations professionnelles - Widget _buildProfessionalInfoCard(BuildContext context, AmRegistrationData data) { - const double verticalSpacing = 28.0; - const double labelFontSize = 22.0; - - String formattedDate = '-'; - if (data.dateOfBirth != null) { - formattedDate = '${data.dateOfBirth!.day.toString().padLeft(2, '0')}/${data.dateOfBirth!.month.toString().padLeft(2, '0')}/${data.dateOfBirth!.year}'; - } - String birthPlace = '${data.birthCity}, ${data.birthCountry}'.trim(); - if (birthPlace == ',') birthPlace = '-'; - - List details = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Date de naissance:", formattedDate, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Lieu de naissance:", birthPlace, labelFontSize: labelFontSize, multiLine: true)), - ], - ), - const SizedBox(height: verticalSpacing), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "N° Sécurité Sociale:", data.nir, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "N° Agrément:", data.agrementNumber, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - _buildDisplayFieldValue(context, "Capacité d'accueil:", data.capacity?.toString() ?? '-', labelFontSize: labelFontSize), - ]; - - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.green.path, - title: 'Informations professionnelles', - content: details, - onEdit: () => context.go('/am-register-step2'), - ); - } - - // Carte Présentation & CGU - Widget _buildPresentationCard(BuildContext context, AmRegistrationData data) { - const double labelFontSize = 22.0; - - List details = [ - Text( - 'Votre présentation (facultatif) :', - style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 8), - Container( - width: double.infinity, - constraints: const BoxConstraints(minHeight: 80.0), - padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), - fit: BoxFit.fill, - ), - ), - child: Text( - data.presentationText.isNotEmpty ? data.presentationText : 'Aucune présentation rédigée.', - style: GoogleFonts.merienda( - fontSize: 18, - fontStyle: data.presentationText.isNotEmpty ? FontStyle.normal : FontStyle.italic, - color: data.presentationText.isNotEmpty ? Colors.black87 : Colors.black54, - ), - ), - ), - const SizedBox(height: 20), - Row( - children: [ - Icon( - data.cguAccepted ? Icons.check_circle : Icons.cancel, - color: data.cguAccepted ? Colors.green : Colors.red, - size: 24, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - data.cguAccepted ? 'CGU acceptées' : 'CGU non acceptées', - style: GoogleFonts.merienda(fontSize: 18), - ), - ), - ], - ), - ]; - - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.peach.path, - title: 'Présentation & CGU', - content: details, - onEdit: () => context.go('/am-register-step3'), - ); - } -} - -// Widget générique _SummaryCard -class _SummaryCard extends StatelessWidget { - final String backgroundImagePath; - final String title; - final List content; - final VoidCallback onEdit; - - const _SummaryCard({ - super.key, - required this.backgroundImagePath, - required this.title, - required this.content, - required this.onEdit, - }); - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 2.0, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(backgroundImagePath), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(15), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.center, - child: Text( - title, - style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), - ), - ), - const SizedBox(height: 12), - ...content, - ], - ), - ), - IconButton( - icon: const Icon(Icons.edit, color: Colors.black54, size: 28), - onPressed: onEdit, - tooltip: 'Modifier', - ), - ], - ), - ), - ); - } } diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart index 9c81ae1..e198b84 100644 --- a/frontend/lib/screens/auth/parent_register_step5_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -1,45 +1,16 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle -import '../../widgets/image_button.dart'; // Import du ImageButton -import '../../models/card_assets.dart'; // Import des enums de cartes -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; +import 'dart:math' as math; -// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé -Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { - const FontWeight labelFontWeight = FontWeight.w600; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), - const SizedBox(height: 4), - Container( - width: double.infinity, // Prendra la largeur allouée par son parent (Expanded) - height: multiLine ? null : fieldHeight, // Hauteur flexible pour multiligne, sinon fixe - constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, // Hauteur min pour multiligne - padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), // Ajuster au besoin - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), // Image de fond du champ - fit: BoxFit.fill, - ), - // Si votre image input_field_bg.png a des coins arrondis intrinsèques, ce borderRadius n'est pas nécessaire - // ou doit correspondre. Sinon, pour une image rectangulaire, vous pouvez l'ajouter. - // borderRadius: BorderRadius.circular(12), - ), - child: Text( - value.isNotEmpty ? value : '-', - style: GoogleFonts.merienda(fontSize: labelFontSize), - maxLines: multiLine ? null : 1, // Permet plusieurs lignes si multiLine est true - overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, - ), - ), - ], - ); -} +import '../../models/user_registration_data.dart'; +import '../../models/card_assets.dart'; +import '../../config/display_config.dart'; +import '../../widgets/image_button.dart'; +import '../../widgets/personal_info_form_screen.dart'; +import '../../widgets/child_card_widget.dart'; +import '../../widgets/presentation_form_screen.dart'; class ParentRegisterStep5Screen extends StatefulWidget { const ParentRegisterStep5Screen({super.key}); @@ -53,9 +24,7 @@ class _ParentRegisterStep5ScreenState extends State { Widget build(BuildContext context) { final registrationData = Provider.of(context); final screenSize = MediaQuery.of(context).size; - final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran) - final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) - final cardHeight = cardWidth / imageAspectRatio; + final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly); return Scaffold( body: Stack( @@ -65,9 +34,11 @@ class _ParentRegisterStep5ScreenState extends State { ), Center( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici - child: Padding( // Ajout du Padding horizontal externe - padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: config.isMobile ? 0 : screenSize.width / 4.0 + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -77,20 +48,35 @@ class _ParentRegisterStep5ScreenState extends State { Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), const SizedBox(height: 30), - _buildParent1Card(context, registrationData.parent1), + // Carte Parent 1 + _buildParent1(context, registrationData), const SizedBox(height: 20), + + // Carte Parent 2 (si présent) if (registrationData.parent2 != null) ...[ - _buildParent2Card(context, registrationData.parent2!), + _buildParent2(context, registrationData), const SizedBox(height: 20), ], - ..._buildChildrenCards(context, registrationData.children), - _buildMotivationCard(context, registrationData.motivationText), + + // Cartes Enfants + ...registrationData.children.asMap().entries.map((entry) => + Column( + children: [ + _buildChildCard(context, entry.value, entry.key), + const SizedBox(height: 20), + ], + ) + ), + + // Carte Motivation + _buildMotivation(context, registrationData), const SizedBox(height: 40), + ImageButton( bg: 'assets/images/bg_green.png', text: 'Soumettre ma demande', textColor: const Color(0xFF2D6A4F), - width: 350, + width: config.isMobile ? 300 : 350, height: 50, fontSize: 18, onPressed: () { @@ -103,26 +89,117 @@ class _ParentRegisterStep5ScreenState extends State { ), ), ), - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go('/parent-register-step4'); - } - }, - tooltip: 'Retour', + // 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-step4'); + } + }, + tooltip: 'Retour', + ), ), - ), ], ), ); } + Widget _buildParent1(BuildContext context, UserRegistrationData data) { + return PersonalInfoFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Informations du Parent Principal', + cardColor: CardColorHorizontal.peach, + initialData: PersonalInfoData( + firstName: data.parent1.firstName, + lastName: data.parent1.lastName, + phone: data.parent1.phone, + email: data.parent1.email, + address: data.parent1.address, + postalCode: data.parent1.postalCode, + city: data.parent1.city, + ), + onSubmit: (d, {hasSecondPerson, sameAddress}) {}, + previousRoute: '', + onEdit: () => context.go('/parent-register-step1'), + ); + } + + Widget _buildParent2(BuildContext context, UserRegistrationData data) { + if (data.parent2 == null) return const SizedBox(); + return PersonalInfoFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Informations du Deuxième Parent', + cardColor: CardColorHorizontal.blue, + initialData: PersonalInfoData( + firstName: data.parent2!.firstName, + lastName: data.parent2!.lastName, + phone: data.parent2!.phone, + email: data.parent2!.email, + address: data.parent2!.address, + postalCode: data.parent2!.postalCode, + city: data.parent2!.city, + ), + onSubmit: (d, {hasSecondPerson, sameAddress}) {}, + previousRoute: '', + onEdit: () => context.go('/parent-register-step2'), + ); + } + + Widget _buildChildCard(BuildContext context, ChildData child, int index) { + // Note: Le titre est maintenant intégré dans la carte ChildCardWidget en mode readonly + return Column( + children: [ + ChildCardWidget( + key: ValueKey('child_readonly_$index'), + childData: child, + childIndex: index, + mode: DisplayMode.readonly, + onPickImage: () {}, + onDateSelect: () {}, + onFirstNameChanged: (v) {}, + onLastNameChanged: (v) {}, + onTogglePhotoConsent: (v) {}, + onToggleMultipleBirth: (v) {}, + onToggleIsUnborn: (v) {}, + onRemove: () {}, + canBeRemoved: false, + onEdit: () => context.go('/parent-register-step3', extra: {'childIndex': index}), + ), + ], + ); + } + + Widget _buildMotivation(BuildContext context, UserRegistrationData data) { + return PresentationFormScreen( + mode: DisplayMode.readonly, + embedContentOnly: true, + stepText: '', + title: 'Votre Motivation', + cardColor: CardColorHorizontal.green, // Changé de pink à green + textFieldHint: '', + initialText: data.motivationText, + initialCguAccepted: true, // Toujours true ici car déjà passé + previousRoute: '', + onSubmit: (t, c) {}, + onEdit: () => context.go('/parent-register-step4'), + ); + } + void _showConfirmationModal(BuildContext context) { showDialog( context: context, @@ -141,8 +218,7 @@ class _ParentRegisterStep5ScreenState extends State { TextButton( child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), onPressed: () { - Navigator.of(dialogContext).pop(); // Ferme la modale - // Utiliser go_router pour la navigation + Navigator.of(dialogContext).pop(); context.go('/login'); }, ), @@ -151,294 +227,5 @@ class _ParentRegisterStep5ScreenState extends State { }, ); } - - // Méthode pour construire la carte Parent 1 - Widget _buildParent1Card(BuildContext context, ParentData data) { - const double verticalSpacing = 28.0; // Espacement vertical augmenté - const double labelFontSize = 22.0; // Taille de label augmentée - - List details = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - _buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), - ]; - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.peach.path, - title: 'Parent Principal', - content: details, - onEdit: () => context.go('/parent-register-step1'), - ); - } - - // Méthode pour construire la carte Parent 2 - Widget _buildParent2Card(BuildContext context, ParentData data) { - const double verticalSpacing = 28.0; - const double labelFontSize = 22.0; - List details = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), - const SizedBox(width: 20), - Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), - ], - ), - const SizedBox(height: verticalSpacing), - _buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), - ]; - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.blue.path, - title: 'Deuxième Parent', - content: details, - onEdit: () => context.go('/parent-register-step2'), - ); - } - - // Méthode pour construire les cartes Enfants - List _buildChildrenCards(BuildContext context, List children) { - return children.asMap().entries.map((entry) { - int index = entry.key; - ChildData child = entry.value; - - CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere( - (e) => e.name == child.cardColor.name, - orElse: () => CardColorHorizontal.lavender, - ); - - return Padding( - padding: const EdgeInsets.only(bottom: 20.0), - child: Stack( - children: [ - AspectRatio( - aspectRatio: 2.0, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(cardColorHorizontal.path), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(15), - ), - child: Column( - children: [ - // Titre centré dans la carte - Row( - children: [ - Expanded( - child: Text( - 'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''), - style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - ), - ), - IconButton( - icon: const Icon(Icons.edit, color: Colors.black54, size: 28), - onPressed: () { - context.go('/parent-register-step3', extra: {'childIndex': index}); - }, - tooltip: 'Modifier', - ), - ], - ), - const SizedBox(height: 18), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // IMAGE SANS CADRE BLANC, PREND LA HAUTEUR - Expanded( - flex: 1, - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(18), - child: AspectRatio( - aspectRatio: 1, - child: (child.imageFile != null) - ? (kIsWeb - ? Image.network(child.imageFile!.path, fit: BoxFit.cover) - : Image.file(child.imageFile!, fit: BoxFit.cover)) - : Image.asset('assets/images/photo.png', fit: BoxFit.contain), - ), - ), - ), - ), - const SizedBox(width: 32), - // INFOS À DROITE (2/3) - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0), - const SizedBox(height: 12), - _buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0), - const SizedBox(height: 12), - _buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 18), - // Ligne des consentements - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - Checkbox( - value: child.photoConsent, - onChanged: null, - ), - Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)), - ], - ), - const SizedBox(width: 32), - Row( - children: [ - Checkbox( - value: child.multipleBirth, - onChanged: null, - ), - Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)), - ], - ), - ], - ), - ], - ), - ), - ), - ], - ), - ); - }).toList(); - } - - // Méthode pour construire la carte Motivation - Widget _buildMotivationCard(BuildContext context, String motivation) { - List details = [ - Text(motivation.isNotEmpty ? motivation : 'Aucune motivation renseignée.', - style: GoogleFonts.merienda(fontSize: 18), - maxLines: 4, - overflow: TextOverflow.ellipsis) - ]; - return _SummaryCard( - backgroundImagePath: CardColorHorizontal.pink.path, - title: 'Votre Motivation', - content: details, - onEdit: () => context.go('/parent-register-step4'), - ); - } - - // Helper pour afficher une ligne de détail (police et agencement amélioré) - Widget _buildDetailRow(String label, String value) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "$label: ", - style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600), - ), - Expanded( - child: Text( - value.isNotEmpty ? value : '-', - style: GoogleFonts.merienda(fontSize: 18), - softWrap: true, - ), - ), - ], - ), - ); - } } - -// Widget générique _SummaryCard (ajusté) -class _SummaryCard extends StatelessWidget { - final String backgroundImagePath; - final String title; - final List content; - final VoidCallback onEdit; - - const _SummaryCard({ - super.key, - required this.backgroundImagePath, - required this.title, - required this.content, - required this.onEdit, - }); - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 2.0, // Le ratio largeur/hauteur de nos images de fond - child: Container( - padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(backgroundImagePath), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(15), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, // Pour que la colonne prenne la hauteur du contenu - children: [ - Align( // Centrer le titre - alignment: Alignment.center, - child: Text( - title, - style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), // Police légèrement augmentée - ), - ), - const SizedBox(height: 12), // Espacement ajusté après le titre - ...content, - ], - ), - ), - IconButton( - icon: const Icon(Icons.edit, color: Colors.black54, size: 28), // Icône un peu plus grande - onPressed: onEdit, - tooltip: 'Modifier', - ), - ], - ), - ), - ); - } -} \ No newline at end of file + \ No newline at end of file diff --git a/frontend/lib/widgets/child_card_widget.dart b/frontend/lib/widgets/child_card_widget.dart index 3b37c75..18bdf72 100644 --- a/frontend/lib/widgets/child_card_widget.dart +++ b/frontend/lib/widgets/child_card_widget.dart @@ -94,19 +94,34 @@ class _ChildCardWidgetState extends State { @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 File? currentChildImage = widget.childData.imageFile; - // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond + // ... (reste du code existant pour mobile/editable) final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender ? Colors.purple.shade200 - : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs + : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); return Container( - width: 345.0 * scaleFactor, - height: config.isMobile ? null : 600.0 * scaleFactor, // Hauteur augmentée pour éviter l'overflow + width: config.isMobile ? double.infinity : screenSize.width * 0.6, + // 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), @@ -117,6 +132,7 @@ class _ChildCardWidgetState extends State { Column( mainAxisSize: MainAxisSize.min, children: [ + // ... (contenu existant) HoverReliefWidget( onPressed: config.isReadonly ? null : widget.onPickImage, borderRadius: BorderRadius.circular(10), @@ -135,7 +151,7 @@ class _ChildCardWidgetState extends State { ), ), ), - SizedBox(height: 10.0 * scaleFactor), // Réduit de 12 à 10 + SizedBox(height: 10.0 * scaleFactor), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -156,7 +172,7 @@ class _ChildCardWidgetState extends State { ), ], ), - SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 + SizedBox(height: 8.0 * scaleFactor), _buildField( config: config, scaleFactor: scaleFactor, @@ -165,7 +181,7 @@ class _ChildCardWidgetState extends State { hint: 'Facultatif si à naître', isRequired: !widget.childData.isUnbornChild, ), - SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 + SizedBox(height: 5.0 * scaleFactor), _buildField( config: config, scaleFactor: scaleFactor, @@ -173,18 +189,18 @@ class _ChildCardWidgetState extends State { controller: _lastNameController, hint: 'Nom de l\'enfant', ), - SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 + 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, // Toujours readonly pour le TextField (date picker) + readOnly: true, onTap: config.isReadonly ? null : widget.onDateSelect, suffixIcon: Icons.calendar_today, ), - SizedBox(height: 10.0 * scaleFactor), // Réduit de 11 à 10 + SizedBox(height: 10.0 * scaleFactor), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -195,7 +211,7 @@ class _ChildCardWidgetState extends State { checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, fontSize: config.isMobile ? 13.0 : 16.0, ), - SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 + SizedBox(height: 5.0 * scaleFactor), AppCustomCheckbox( label: 'Naissance multiple', value: widget.childData.multipleBirth, @@ -236,6 +252,310 @@ class _ChildCardWidgetState extends State { ); } + /// 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 File? currentChildImage = widget.childData.imageFile; + 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) + Expanded( + flex: 1, + child: Center( + child: 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: currentChildImage != null + ? (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(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: 18), + + // Consentements + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppCustomCheckbox( + label: 'Consentement photo', + value: widget.childData.photoConsent, + onChanged: (v) {}, // Readonly + checkboxSize: 22.0, + fontSize: 16.0, + ), + const SizedBox(width: 32), + AppCustomCheckbox( + label: 'Naissance multiple', + value: widget.childData.multipleBirth, + onChanged: (v) {}, // Readonly + checkboxSize: 22.0, + fontSize: 16.0, + ), + ], + ), + ], + ), + ), + ), + ); + } + + + + /// Carte en mode readonly MOBILE avec hauteur adaptative + Widget _buildReadonlyMobileCard(BuildContext context, DisplayConfig config) { + final File? currentChildImage = widget.childData.imageFile; + + 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: currentChildImage != null + ? (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: 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: 16), + + // Consentements + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AppCustomCheckbox( + label: 'Consentement photo', + value: widget.childData.photoConsent, + onChanged: (v) {}, + checkboxSize: 20.0, + fontSize: 14.0, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + AppCustomCheckbox( + label: 'Naissance multiple', + value: widget.childData.multipleBirth, + onChanged: (v) {}, + checkboxSize: 20.0, + fontSize: 14.0, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + + 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', + ), + ), + ], + ), + ); + } + + /// 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, diff --git a/frontend/lib/widgets/form_field_wrapper.dart b/frontend/lib/widgets/form_field_wrapper.dart index cece43d..77bb5c1 100644 --- a/frontend/lib/widgets/form_field_wrapper.dart +++ b/frontend/lib/widgets/form_field_wrapper.dart @@ -78,7 +78,7 @@ class FormFieldWrapper extends StatelessWidget { ), const SizedBox(height: 6), - // Valeur + // Valeur avec fond beige Container( width: double.infinity, padding: const EdgeInsets.symmetric( @@ -86,20 +86,17 @@ class FormFieldWrapper extends StatelessWidget { vertical: 12, ), decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: const Color(0xFFE0E0E0), - width: 1, + image: const DecorationImage( + image: AssetImage('assets/images/bg_beige.png'), + fit: BoxFit.fill, ), + borderRadius: BorderRadius.circular(8), ), child: Text( value.isEmpty ? '-' : value, style: GoogleFonts.merienda( fontSize: config.isMobile ? 14 : 16, - color: value.isEmpty - ? const Color(0xFFBDBDBD) - : const Color(0xFF2C2C2C), + color: const Color(0xFF2C2C2C), ), ), ), diff --git a/frontend/lib/widgets/personal_info_form_screen.dart b/frontend/lib/widgets/personal_info_form_screen.dart index e8e109f..3ae6454 100644 --- a/frontend/lib/widgets/personal_info_form_screen.dart +++ b/frontend/lib/widgets/personal_info_form_screen.dart @@ -48,6 +48,8 @@ class PersonalInfoFormScreen extends StatefulWidget { final bool showSameAddressCheckbox; // Afficher "Même adresse que parent 1" final bool? initialSameAddress; final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse" + final bool embedContentOnly; // Si true, affiche seulement la carte (sans scaffold/fond/titre) + final VoidCallback? onEdit; // Callback pour le bouton d'édition (si affiché) const PersonalInfoFormScreen({ super.key, @@ -63,6 +65,8 @@ class PersonalInfoFormScreen extends StatefulWidget { this.showSameAddressCheckbox = false, this.initialSameAddress, this.referenceAddressData, + this.embedContentOnly = false, + this.onEdit, }); @override @@ -150,6 +154,10 @@ class _PersonalInfoFormScreenState extends State { final screenSize = MediaQuery.of(context).size; final config = DisplayConfig.fromContext(context, mode: widget.mode); + if (widget.embedContentOnly) { + return _buildCard(context, config, screenSize); + } + return Scaffold( body: Stack( children: [ @@ -180,37 +188,7 @@ class _PersonalInfoFormScreenState extends State { textAlign: TextAlign.center, ), SizedBox(height: config.isMobile ? 16 : 30), - Container( - width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, - padding: EdgeInsets.symmetric( - vertical: config.isMobile ? 20 : 50, - horizontal: config.isMobile ? 24 : 50, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - config.isMobile - ? _getVerticalCardAsset() - : widget.cardColor.path - ), - fit: BoxFit.fill, - ), - ), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 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), - ], - ), - ), - ), + _buildCard(context, config, screenSize), // Boutons mobile sous la carte (dans le scroll) if (config.isMobile) ...[ @@ -297,6 +275,198 @@ class _PersonalInfoFormScreenState extends State { ); } + Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) { + // En mode readonly desktop avec embedContentOnly, utiliser le layout ancien (AspectRatio 2:1) + if (config.isReadonly && !config.isMobile && widget.embedContentOnly) { + return _buildReadonlyDesktopCard(context, config, screenSize); + } + + // En mode readonly mobile avec embedContentOnly, forcer le ratio 1:2 (Vertical) + if (config.isReadonly && config.isMobile && widget.embedContentOnly) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), // Marge 5% + child: _buildMobileReadonlyCard(context, config, screenSize), + ); + } + + // Mode normal (éditable ou mobile non-récap) + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, + padding: EdgeInsets.symmetric( + vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50), + horizontal: config.isMobile ? 24 : 50, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + config.isMobile + ? _getVerticalCardAsset() + : widget.cardColor.path + ), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.embedContentOnly) ...[ + Text( + // Titre raccourci sur mobile en mode readonly pour laisser place au bouton edit + (config.isMobile && config.isReadonly && widget.title.contains('Parent Principal')) + ? 'Parent Principal' + : widget.title, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 18 : 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ], + + if (config.isEditable && widget.showSecondPersonToggle) + _buildToggles(context, config), + + _buildFormFields(context, config), + ], + ), + ), + ), + if (config.isReadonly && widget.onEdit != null) + Positioned( + top: 10, + right: 10, + child: IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: widget.onEdit, + tooltip: 'Modifier', + ), + ), + ], + ); + } + + /// Carte en mode readonly MOBILE avec hauteur adaptative + Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) { + return Container( + width: double.infinity, + // Pas de height fixe, s'adapte au contenu + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(_getVerticalCardAsset()), + fit: BoxFit.fill, // Fill pour que l'image s'étire selon la hauteur du contenu + ), + borderRadius: BorderRadius.circular(15), + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, // Important : prend le min de place + children: [ + // Titre + Edit Button + Row( + children: [ + Expanded( + child: Text( + (widget.title.contains('Parent Principal') ? 'Parent Principal' : (widget.title.contains('Deuxième Parent') ? 'Deuxième Parent' : widget.title)), + 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 (plus d'Expanded ici car parent non contraint) + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: _buildFormFields(context, config), + ), + ], + ), + + 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', + ), + ), + ], + ), + ); + } + + /// Carte en mode readonly desktop avec AspectRatio 2:1 (format de l'ancien récapitulatif) + Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { + // Largeur de la carte : 50% de l'écran (comme l'ancien système) + 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(widget.cardColor.path), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre centré + Align( + alignment: Alignment.center, + child: Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + const SizedBox(height: 55), // Espace très augmenté pour pousser les champs vers le bas + // Champs du formulaire + _buildFormFields(context, config), + ], + ), + ), + // Bouton d'édition à droite + if (widget.onEdit != null) + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: widget.onEdit, + tooltip: 'Modifier', + ), + ], + ), + ), + ), + ); + } + /// Construit les toggles (Parent 2 / Même adresse) Widget _buildToggles(BuildContext context, DisplayConfig config) { if (config.isMobile) { @@ -305,10 +475,10 @@ class _PersonalInfoFormScreenState extends State { children: [ _buildSecondPersonToggle(context, config), if (widget.showSameAddressCheckbox) ...[ - const SizedBox(height: 12), + const SizedBox(height: 5), // Réduit de 12 à 5 _buildSameAddressToggle(context, config), ], - const SizedBox(height: 24), + const SizedBox(height: 10), // Réduit de 24 à 10 ], ); } else { @@ -392,7 +562,7 @@ class _PersonalInfoFormScreenState extends State { value: _sameAddress, onChanged: _fieldsEnabled ? (value) { setState(() { - _sameAddress = value ?? false; + _sameAddress = value; _updateAddressFields(); }); } : null, @@ -414,6 +584,14 @@ class _PersonalInfoFormScreenState extends State { /// Layout DESKTOP : champs côte à côte (horizontal) Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { + // En mode readonly, utiliser l'ancien layout du récapitulatif + if (config.isReadonly) { + return _buildReadonlyDesktopFields(context); + } + + // Mode éditable : layout normal + final double verticalSpacing = 32.0; + return Column( children: [ // Nom et Prénom @@ -440,7 +618,7 @@ class _PersonalInfoFormScreenState extends State { ), ], ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), // Téléphone et Email Row( @@ -468,7 +646,7 @@ class _PersonalInfoFormScreenState extends State { ), ], ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), // Adresse _buildField( @@ -478,7 +656,7 @@ class _PersonalInfoFormScreenState extends State { hint: 'Numéro et nom de votre rue', enabled: _fieldsEnabled && !_sameAddress, ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), // Code Postal et Ville Row( @@ -511,8 +689,154 @@ class _PersonalInfoFormScreenState extends State { ); } + /// Layout DESKTOP en mode READONLY : réplique l'ancien design du récapitulatif + Widget _buildReadonlyDesktopFields(BuildContext context) { + const double verticalSpacing = 20.0; // Réduit pour compacter le bas + const double labelFontSize = 22.0; + const double valueFontSize = 18.0; // Taille du texte dans les champs + + return Column( + children: [ + // Ligne 1 : Nom + Prénom + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildDisplayFieldValue( + context, + 'Nom :', + _lastNameController.text, + labelFontSize: labelFontSize, + valueFontSize: valueFontSize, + ), + ), + const SizedBox(width: 20), + Expanded( + child: _buildDisplayFieldValue( + context, + 'Prénom :', + _firstNameController.text, + labelFontSize: labelFontSize, + valueFontSize: valueFontSize, + ), + ), + ], + ), + const SizedBox(height: verticalSpacing), + + // Ligne 2 : Téléphone + Email + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildDisplayFieldValue( + context, + 'Téléphone :', + _phoneController.text, + labelFontSize: labelFontSize, + valueFontSize: valueFontSize, + ), + ), + const SizedBox(width: 20), + Expanded( + child: _buildDisplayFieldValue( + context, + 'Email :', + _emailController.text, + multiLine: true, + labelFontSize: labelFontSize, + valueFontSize: valueFontSize, + ), + ), + ], + ), + const SizedBox(height: verticalSpacing), + + // Ligne 3 : Adresse complète (adresse + CP + ville) + _buildDisplayFieldValue( + context, + 'Adresse :', + "${_addressController.text}\n${_postalCodeController.text} ${_cityController.text}".trim(), + multiLine: true, + fieldHeight: 80, + labelFontSize: labelFontSize, + valueFontSize: valueFontSize, + ), + ], + ); + } + + /// Helper pour afficher un champ en lecture seule avec le style de l'ancien récap + Widget _buildDisplayFieldValue( + BuildContext context, + String label, + String value, { + bool multiLine = false, + double fieldHeight = 50.0, + double labelFontSize = 18.0, + double valueFontSize = 18.0, // Taille du texte dans le champ + }) { + const FontWeight labelFontWeight = FontWeight.w600; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.merienda( + fontSize: labelFontSize, + fontWeight: labelFontWeight, + ), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: multiLine ? null : fieldHeight, + constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, + 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: valueFontSize), + maxLines: multiLine ? null : 1, + overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, + ), + ), + ], + ); + } + /// Layout MOBILE : tous les champs empilés verticalement Widget _buildMobileFields(BuildContext context, DisplayConfig config) { + // Mode Readonly Mobile : Layout compact et groupé + if (config.isReadonly) { + // NOTE: FormFieldWrapper ajoute déjà un padding vertical (8px mobile), + // donc on n'ajoute pas de SizedBox supplémentaire ici pour éviter un double espacement. + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildField(config: config, label: 'Nom', controller: _lastNameController), + _buildField(config: config, label: 'Prénom', controller: _firstNameController), + _buildField(config: config, label: 'Téléphone', controller: _phoneController), + _buildField(config: config, label: 'Email', controller: _emailController), + // Adresse complète en un seul bloc multiligne + FormFieldWrapper( + config: config, + label: 'Adresse', + value: "${_addressController.text}\n${_postalCodeController.text} ${_cityController.text}".trim(), + maxLines: 3, + ), + ], + ); + } + + // Mode Editable Mobile : Layout standard avec champs séparés return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/frontend/lib/widgets/presentation_form_screen.dart b/frontend/lib/widgets/presentation_form_screen.dart index 17a049f..ace8df1 100644 --- a/frontend/lib/widgets/presentation_form_screen.dart +++ b/frontend/lib/widgets/presentation_form_screen.dart @@ -23,6 +23,9 @@ class PresentationFormScreen extends StatefulWidget { final String previousRoute; final Function(String text, bool cguAccepted) onSubmit; + final bool embedContentOnly; + final VoidCallback? onEdit; + const PresentationFormScreen({ super.key, this.mode = DisplayMode.editable, @@ -34,6 +37,8 @@ class PresentationFormScreen extends StatefulWidget { required this.initialCguAccepted, required this.previousRoute, required this.onSubmit, + this.embedContentOnly = false, + this.onEdit, }); @override @@ -75,6 +80,10 @@ class _PresentationFormScreenState extends State { final screenSize = MediaQuery.of(context).size; final config = DisplayConfig.fromContext(context, mode: widget.mode); + if (widget.embedContentOnly) { + return _buildCard(context, config, screenSize); + } + return Scaffold( body: Stack( children: [ @@ -154,7 +163,7 @@ class _PresentationFormScreenState extends State { const SizedBox(height: 16), // Carte qui prend tout l'espace restant Expanded( - child: _buildMobileCard(context, config, screenSize), + child: _buildCard(context, config, screenSize), ), // Boutons en bas const SizedBox(height: 20), @@ -188,13 +197,228 @@ class _PresentationFormScreenState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 30), - _buildDesktopCard(context, config, screenSize), + _buildCard(context, config, screenSize), ], ), ), ); } + /// Wrapper pour la carte (Mobile ou Desktop) + Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) { + // Si mode Readonly Desktop : Layout spécial "Vintage" horizontal (2:1) + if (config.isReadonly && !config.isMobile && widget.embedContentOnly) { + return _buildReadonlyDesktopCard(context, config, screenSize); + } + + // Si mode Readonly Mobile : Layout spécial "Vintage" vertical (1:2) + if (config.isReadonly && config.isMobile && widget.embedContentOnly) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: _buildMobileReadonlyCard(context, config, screenSize), + ); + } + + final Widget cardContent = config.isMobile + ? _buildMobileCard(context, config, screenSize) + : _buildDesktopCard(context, config, screenSize); + + return Stack( + clipBehavior: Clip.none, + children: [ + if (widget.embedContentOnly) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 18 : 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + cardContent, + ], + ) + else + cardContent, + + if (config.isReadonly && widget.onEdit != null) + Positioned( + top: widget.embedContentOnly ? 50 : 10, + right: 10, + child: IconButton( + icon: const Icon(Icons.edit, color: Colors.black54), + onPressed: widget.onEdit, + tooltip: 'Modifier', + ), + ), + ], + ); + } + + /// Carte en mode readonly MOBILE avec hauteur adaptative + Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) { + return Container( + width: double.infinity, + // Pas de height fixe, s'adapte au contenu + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(_getVerticalCardAsset()), + fit: BoxFit.fill, // Fill pour que l'image s'étire selon la hauteur du contenu + ), + borderRadius: BorderRadius.circular(15), + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, // S'adapte au contenu + children: [ + // Titre + Edit Button + Row( + children: [ + Expanded( + child: Text( + widget.title, + 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 (Texte scrollable + Checkbox) + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Champ texte scrollable + // On utilise ConstrainedBox pour limiter la hauteur max + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), // Max height pour éviter une carte infinie + child: CustomDecoratedTextField( + controller: _textController, + hintText: widget.textFieldHint, + fieldHeight: null, // Flexible + maxLines: 100, + expandDynamically: true, // Scrollable + fontSize: 14.0, + readOnly: config.isReadonly, + ), + ), + const SizedBox(height: 16), + // Checkbox + Transform.scale( + scale: 0.85, + child: AppCustomCheckbox( + label: 'J\'accepte les CGU et la\nPolitique de confidentialité', + value: _cguAccepted, + onChanged: (v) {}, + ), + ), + ], + ), + ), + ], + ), + + 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', + ), + ), + ], + ), + ); + } + + /// Carte en mode readonly desktop avec AspectRatio 2:1 (format de l'ancien récapitulatif) + Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { + // Largeur de la carte : 50% de l'écran + 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(widget.cardColor.path), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + children: [ + // Titre + Edit Button + Row( + children: [ + Expanded( + child: Text( + widget.title, + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + 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), + + // Texte de motivation + Expanded( + child: CustomDecoratedTextField( + controller: _textController, + hintText: '', + fieldHeight: double.infinity, // Remplit l'espace disponible + maxLines: 10, + expandDynamically: false, // Fixe pour le readonly + fontSize: 18.0, + readOnly: true, + ), + ), + const SizedBox(height: 20), + + // CGU + AppCustomCheckbox( + label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', + value: _cguAccepted, + onChanged: (v) {}, // Readonly + checkboxSize: 22.0, + fontSize: 16.0, + ), + ], + ), + ), + ), + ); + } + /// Carte DESKTOP : Format horizontal 2:1 Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { final cardWidth = screenSize.width * 0.6; @@ -223,13 +447,14 @@ class _PresentationFormScreenState extends State { maxLines: 10, expandDynamically: true, fontSize: 18.0, + readOnly: config.isReadonly, ), ), const SizedBox(height: 20), AppCustomCheckbox( label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', value: _cguAccepted, - onChanged: (value) => setState(() => _cguAccepted = value ?? false), + onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), ), ], ), @@ -239,6 +464,26 @@ class _PresentationFormScreenState extends State { /// Carte MOBILE : Prend tout l'espace disponible Widget _buildMobileCard(BuildContext context, DisplayConfig config, Size screenSize) { + // Le contenu du champ texte + Widget textFieldContent = LayoutBuilder( + builder: (context, constraints) { + // En mode embed (récap), constraints.maxHeight peut être infini, donc on fixe une hauteur par défaut + // En mode standalone, on utilise la hauteur disponible + double height = constraints.maxHeight; + if (height.isInfinite) height = 200.0; + + return CustomDecoratedTextField( + controller: _textController, + hintText: widget.textFieldHint, + fieldHeight: height, + maxLines: 100, + expandDynamically: false, + fontSize: 14.0, + readOnly: config.isReadonly, + ); + }, + ); + return Padding( padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), child: Container( @@ -251,21 +496,14 @@ class _PresentationFormScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), child: Column( children: [ - // Champ de texte qui prend l'espace disponible et scrollable - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return CustomDecoratedTextField( - controller: _textController, - hintText: widget.textFieldHint, - fieldHeight: constraints.maxHeight, - maxLines: 100, // Grande valeur pour permettre le scroll - expandDynamically: false, - fontSize: 14.0, - ); - }, - ), - ), + // Champ de texte + if (widget.embedContentOnly) + // En mode récapitulatif, on donne une hauteur fixe pour éviter l'erreur d'Expanded + SizedBox(height: 200, child: textFieldContent) + else + // En mode écran complet, on prend tout l'espace restant + Expanded(child: textFieldContent), + const SizedBox(height: 16), // Checkbox en bas Transform.scale( @@ -273,7 +511,7 @@ class _PresentationFormScreenState extends State { child: AppCustomCheckbox( label: 'J\'accepte les CGU et la\nPolitique de confidentialité', value: _cguAccepted, - onChanged: (value) => setState(() => _cguAccepted = value ?? false), + onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), ), ), ], diff --git a/frontend/lib/widgets/professional_info_form_screen.dart b/frontend/lib/widgets/professional_info_form_screen.dart index 7843863..24b8f56 100644 --- a/frontend/lib/widgets/professional_info_form_screen.dart +++ b/frontend/lib/widgets/professional_info_form_screen.dart @@ -7,6 +7,7 @@ import 'dart:io'; import '../models/card_assets.dart'; import '../config/display_config.dart'; import 'custom_app_text_field.dart'; +import 'form_field_wrapper.dart'; import 'app_custom_checkbox.dart'; import 'hover_relief_widget.dart'; import 'custom_navigation_button.dart'; @@ -48,6 +49,8 @@ class ProfessionalInfoFormScreen extends StatefulWidget { final String previousRoute; final Function(ProfessionalInfoData) onSubmit; final Future Function()? onPickPhoto; + final bool embedContentOnly; + final VoidCallback? onEdit; const ProfessionalInfoFormScreen({ super.key, @@ -59,6 +62,8 @@ class ProfessionalInfoFormScreen extends StatefulWidget { required this.previousRoute, required this.onSubmit, this.onPickPhoto, + this.embedContentOnly = false, + this.onEdit, }); @override @@ -170,6 +175,10 @@ class _ProfessionalInfoFormScreenState extends State final screenSize = MediaQuery.of(context).size; final config = DisplayConfig.fromContext(context, mode: widget.mode); + if (widget.embedContentOnly) { + return _buildCard(context, config, screenSize); + } + return Scaffold( body: Stack( children: [ @@ -200,29 +209,8 @@ class _ProfessionalInfoFormScreenState extends State textAlign: TextAlign.center, ), SizedBox(height: config.isMobile ? 16 : 30), - Container( - width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, - padding: EdgeInsets.symmetric( - vertical: config.isMobile ? 20 : 50, - horizontal: config.isMobile ? 24 : 50, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - config.isMobile - ? _getVerticalCardAsset() - : widget.cardColor.path - ), - fit: BoxFit.fill, - ), - ), - child: Form( - key: _formKey, - child: config.isMobile - ? _buildMobileFields(context, config) - : _buildDesktopFields(context, config), - ), - ), + _buildCard(context, config, screenSize), + // Boutons mobile sous la carte if (config.isMobile) ...[ const SizedBox(height: 20), @@ -271,8 +259,308 @@ class _ProfessionalInfoFormScreenState extends State ); } + Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) { + // Si mode Readonly Desktop : Layout spécial "Vintage" horizontal + if (config.isReadonly && !config.isMobile && widget.embedContentOnly) { + return _buildReadonlyDesktopCard(context, config, screenSize); + } + + // Si mode Readonly Mobile : Layout spécial "Vintage" vertical + if (config.isReadonly && config.isMobile && widget.embedContentOnly) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: _buildMobileReadonlyCard(context, config, screenSize), + ); + } + + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, + padding: EdgeInsets.symmetric( + vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50), + horizontal: config.isMobile ? 24 : 50, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + config.isMobile + ? _getVerticalCardAsset() + : widget.cardColor.path + ), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.embedContentOnly) ...[ + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: config.isMobile ? 18 : 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ], + config.isMobile + ? _buildMobileFields(context, config) + : _buildDesktopFields(context, config), + ], + ), + ), + ), + if (config.isReadonly && widget.onEdit != null) + Positioned( + top: 10, + right: 10, + child: IconButton( + icon: const Icon(Icons.edit, color: Colors.black54), + onPressed: widget.onEdit, + tooltip: 'Modifier', + ), + ), + ], + ); + } + + /// Carte en mode readonly MOBILE avec hauteur adaptative + Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) { + 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(_getVerticalCardAsset()), + fit: BoxFit.fill, // Fill pour s'adapter + ), + borderRadius: BorderRadius.circular(15), + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Titre + Edit Button + Row( + children: [ + Expanded( + child: Text( + widget.title, + 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: _buildMobileFields(context, config), + ), + ], + ), + + 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', + ), + ), + ], + ), + ); + } + + /// Carte en mode readonly desktop avec AspectRatio 2:1 + Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { + 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(widget.cardColor.path), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + children: [ + // Titre + Edit Button + Row( + children: [ + Expanded( + child: Text( + widget.title, + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + 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 + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // PHOTO (1/3) + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + 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: _photoFile != null + ? Image.file(_photoFile!, fit: BoxFit.cover) + : (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/') + ? Image.asset(_photoPathFramework!, fit: BoxFit.contain) + : Image.asset('assets/images/photo.png', fit: BoxFit.contain)), + ), + ), + ), + const SizedBox(height: 10), + AppCustomCheckbox( + label: 'J\'accepte l\'utilisation\nde ma photo.', + value: _photoConsent, + onChanged: (v) {}, // Readonly + checkboxSize: 22.0, + fontSize: 14.0, + ), + ], + ), + ), + const SizedBox(width: 32), + + // CHAMPS (2/3) - Layout optimisé compact + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Ligne 1 : Ville + Pays + Row( + children: [ + Expanded(child: _buildReadonlyField('Ville de naissance', _birthCityController.text)), + const SizedBox(width: 16), + Expanded(child: _buildReadonlyField('Pays de naissance', _birthCountryController.text)), + ], + ), + const SizedBox(height: 12), + + // Ligne 2 : Date + NIR (NIR prend plus de place si possible ou 50/50) + Row( + children: [ + Expanded(flex: 2, child: _buildReadonlyField('Date de naissance', _dateOfBirthController.text)), + const SizedBox(width: 16), + Expanded(flex: 3, child: _buildReadonlyField('NIR', _nirController.text)), + ], + ), + const SizedBox(height: 12), + + // Ligne 3 : Agrément + Capacité + Row( + children: [ + Expanded(flex: 3, child: _buildReadonlyField('N° Agrément', _agrementController.text)), + const SizedBox(width: 16), + Expanded(flex: 2, child: _buildReadonlyField('Capacité', _capacityController.text)), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + /// Helper pour champ Readonly style "Beige" + Widget _buildReadonlyField(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.merienda(fontSize: 18.0, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 45.0, // Hauteur réduite pour compacter + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.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: 16.0), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + /// Layout DESKTOP : Photo à gauche, champs à droite Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { + final double verticalSpacing = config.isReadonly ? 16.0 : 32.0; + return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -296,7 +584,7 @@ class _ProfessionalInfoFormScreenState extends State hint: 'Votre ville de naissance', validator: (v) => v!.isEmpty ? 'Ville requise' : null, ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), _buildField( config: config, label: 'Pays de naissance', @@ -304,7 +592,7 @@ class _ProfessionalInfoFormScreenState extends State hint: 'Votre pays de naissance', validator: (v) => v!.isEmpty ? 'Pays requis' : null, ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), _buildField( config: config, label: 'Date de naissance', @@ -320,7 +608,7 @@ class _ProfessionalInfoFormScreenState extends State ), ], ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), _buildField( config: config, label: 'N° Sécurité Sociale (NIR)', @@ -334,7 +622,7 @@ class _ProfessionalInfoFormScreenState extends State return null; }, ), - const SizedBox(height: 32), + SizedBox(height: verticalSpacing), Row( children: [ Expanded( @@ -509,20 +797,28 @@ class _ProfessionalInfoFormScreenState extends State IconData? suffixIcon, String? Function(String?)? validator, }) { - return CustomAppTextField( - controller: controller, - labelText: label, - hintText: hint ?? label, - fieldWidth: double.infinity, - fieldHeight: config.isMobile ? 45.0 : 53.0, - labelFontSize: config.isMobile ? 15.0 : 22.0, - inputFontSize: config.isMobile ? 14.0 : 20.0, - keyboardType: keyboardType ?? TextInputType.text, - readOnly: readOnly, - onTap: onTap, - suffixIcon: suffixIcon, - validator: validator, - ); + if (config.isReadonly) { + return FormFieldWrapper( + config: config, + label: label, + value: controller.text, + ); + } else { + return CustomAppTextField( + controller: controller, + labelText: label, + hintText: hint ?? label, + fieldWidth: double.infinity, + fieldHeight: config.isMobile ? 45.0 : 53.0, + labelFontSize: config.isMobile ? 15.0 : 22.0, + inputFontSize: config.isMobile ? 14.0 : 20.0, + keyboardType: keyboardType ?? TextInputType.text, + readOnly: readOnly, + onTap: onTap, + suffixIcon: suffixIcon, + validator: validator, + ); + } } /// Boutons mobile diff --git a/frontend/lib/widgets/summary_screen.dart b/frontend/lib/widgets/summary_screen.dart index 7a69f3e..7f75fdb 100644 --- a/frontend/lib/widgets/summary_screen.dart +++ b/frontend/lib/widgets/summary_screen.dart @@ -230,11 +230,12 @@ Widget buildDisplayFieldValue( height: multiLine ? null : fieldHeight, constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/bg_beige.png'), fit: BoxFit.fill, ), + borderRadius: BorderRadius.circular(8), ), child: Text( value.isNotEmpty ? value : '-', From dfe91ed77211d869451c41e597d9b9aa4fe2216b Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sat, 7 Feb 2026 14:44:17 +0100 Subject: [PATCH 11/12] Add navigation buttons to mobile recap screens and fix child card width - Add 'Previous' and 'Submit' buttons to mobile recap screens (Parent & AM) - Fix imports for navigation buttons and widgets - Adjust ChildCardWidget width to fill available space on mobile editing Co-authored-by: Cursor --- .../auth/am_register_step4_screen.dart | 70 +++++++++++++++---- .../auth/parent_register_step5_screen.dart | 70 +++++++++++++++---- 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index 909ea88..6d97e45 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -7,7 +7,9 @@ import 'dart:math' as math; import '../../models/am_registration_data.dart'; import '../../models/card_assets.dart'; import '../../config/display_config.dart'; +import '../../widgets/hover_relief_widget.dart'; import '../../widgets/image_button.dart'; +import '../../widgets/custom_navigation_button.dart'; import '../../widgets/personal_info_form_screen.dart'; import '../../widgets/professional_info_form_screen.dart'; import '../../widgets/presentation_form_screen.dart'; @@ -60,18 +62,62 @@ class _AmRegisterStep4ScreenState extends State { _buildPresentation(context, registrationData), const SizedBox(height: 40), - ImageButton( - bg: 'assets/images/bg_green.png', - text: 'Soumettre ma demande', - textColor: const Color(0xFF2D6A4F), - width: config.isMobile ? 300 : 350, - height: 50, - fontSize: 18, - onPressed: () { - print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); - _showConfirmationModal(context); - }, - ), + // Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop + if (config.isMobile) + Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/am-register-step3'); + } + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Soumettre', + style: NavigationButtonStyle.green, + onPressed: () { + print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); + _showConfirmationModal(context); + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + ], + ), + ) + else + ImageButton( + bg: 'assets/images/bg_green.png', + text: 'Soumettre ma demande', + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); + _showConfirmationModal(context); + }, + ), ], ), ), diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart index e198b84..71047e1 100644 --- a/frontend/lib/screens/auth/parent_register_step5_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -7,7 +7,9 @@ import 'dart:math' as math; import '../../models/user_registration_data.dart'; import '../../models/card_assets.dart'; import '../../config/display_config.dart'; +import '../../widgets/hover_relief_widget.dart'; import '../../widgets/image_button.dart'; +import '../../widgets/custom_navigation_button.dart'; import '../../widgets/personal_info_form_screen.dart'; import '../../widgets/child_card_widget.dart'; import '../../widgets/presentation_form_screen.dart'; @@ -72,18 +74,62 @@ class _ParentRegisterStep5ScreenState extends State { _buildMotivation(context, registrationData), const SizedBox(height: 40), - ImageButton( - bg: 'assets/images/bg_green.png', - text: 'Soumettre ma demande', - textColor: const Color(0xFF2D6A4F), - width: config.isMobile ? 300 : 350, - height: 50, - fontSize: 18, - onPressed: () { - print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); - _showConfirmationModal(context); - }, - ), + // Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop + if (config.isMobile) + Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: Row( + children: [ + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Précédent', + style: NavigationButtonStyle.purple, + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/parent-register-step4'); + } + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: HoverReliefWidget( + child: CustomNavigationButton( + text: 'Soumettre', + style: NavigationButtonStyle.green, + onPressed: () { + print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); + _showConfirmationModal(context); + }, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), + ), + ], + ), + ) + else + ImageButton( + bg: 'assets/images/bg_green.png', + text: 'Soumettre ma demande', + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); + _showConfirmationModal(context); + }, + ), ], ), ), From 6ad88cbbc6f0958d0a547b912ef6a47f8217c8cf Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sat, 7 Feb 2026 14:46:34 +0100 Subject: [PATCH 12/12] docs: Update ticket list with #78 (Multi-mode forms refactoring) Co-authored-by: Cursor --- docs/23_LISTE-TICKETS.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 6bb9a32..4ed66e1 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -20,7 +20,7 @@ | **P4** | 4 tickets | ~24h | Tests & Documentation | | **CRITIQUES** | 6 tickets | ~13h | Upload, Logs, Infra, CDC | | **JURIDIQUE** | 1 ticket | ~8h | Rédaction CGU/Privacy | -| **TOTAL** | **61 tickets** | **~173h** | | +| **TOTAL** | **62 tickets** | **~181h** | | --- @@ -892,6 +892,27 @@ Créer l'écran de consultation des logs système (optionnel pour v1.1). --- +### Ticket #78 : [Frontend] Refonte Infrastructure Formulaires Multi-modes ✅ +**Estimation** : 8h +**Labels** : `frontend`, `p3`, `refactoring`, `ux` +**Statut** : ✅ TERMINÉ (Fermé le 2026-01-27) + +**Description** : +Créer une infrastructure générique pour gérer les formulaires en modes multiples (Editable / Readonly / Mobile / Desktop) afin d'harmoniser l'UI et faciliter la maintenance. + +**Tâches** : +- [x] Créer `DisplayConfig` et `DisplayMode` (editable, readonly) +- [x] Créer `FormFieldWrapper` pour affichage uniforme readonly +- [x] Migrer `PersonalInfoFormScreen` (Parents/AM) +- [x] Migrer `ChildCardWidget` (Enfants) +- [x] Migrer `ProfessionalInfoFormScreen` (AM) +- [x] Migrer `PresentationFormScreen` (Motivation/CGU) +- [x] Implémenter layout "Vintage" (2:1) pour Desktop Readonly +- [x] Implémenter layout adaptatif pour Mobile Readonly +- [x] Harmoniser styles (champs beiges, polices) + +--- + ## 🔵 PRIORITÉ 4 : Tests & Documentation ### Ticket #52 : [Tests] Tests unitaires Backend @@ -1116,7 +1137,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit - **P0 (Bloquant BDD)** : 7 tickets (~5h) - **P1 (Bloquant Config)** : 7 tickets (~22h) - **P2 (Backend)** : 18 tickets (~50h) -- **P3 (Frontend)** : 17 tickets (~52h) ← +1 ticket logs admin +- **P3 (Frontend)** : 18 tickets (~60h) ← +1 ticket logs admin, +1 ticket refonte formulaires - **P4 (Tests/Doc)** : 4 tickets (~24h) - **Critiques** : 6 tickets (~13h) ← -2 email, +1 logs, +1 CDC - **Juridique** : 1 ticket (~8h) @@ -1124,7 +1145,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit ### Par domaine - **BDD** : 7 tickets - **Backend** : 23 tickets ← +1 logs -- **Frontend** : 17 tickets ← +1 logs admin +- **Frontend** : 18 tickets ← +1 logs admin, +1 refonte formulaires - **Tests** : 3 tickets - **Documentation** : 5 tickets ← +1 amendement CDC - **Infra** : 2 tickets @@ -1134,7 +1155,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit - ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire - ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel - ✅ **Ajouté** : Ticket #56 "Écran Logs Admin" - Optionnel Phase 1.1 -- ✅ **Ajouté** : Ticket #60 "Amendement CDC v1.4 - Suppression SMS" - Simplification +- ✅ **Ajouté** : Ticket #78 "Refonte Infrastructure Formulaires" - Harmonisation UI/UX ---