From 08612c455d4adb6cc6c08f11e6344080d413a12a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sat, 7 Feb 2026 13:29:11 +0100 Subject: [PATCH] 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 : '-',