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 <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-07 13:29:11 +01:00
parent 6452706680
commit 08612c455d
8 changed files with 1542 additions and 723 deletions

View File

@ -1,41 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.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:provider/provider.dart';
import 'package:go_router/go_router.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é import '../../models/am_registration_data.dart';
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { import '../../models/card_assets.dart';
const FontWeight labelFontWeight = FontWeight.w600; import '../../config/display_config.dart';
import '../../widgets/image_button.dart';
return Column( import '../../widgets/personal_info_form_screen.dart';
crossAxisAlignment: CrossAxisAlignment.start, import '../../widgets/professional_info_form_screen.dart';
children: [ import '../../widgets/presentation_form_screen.dart';
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,
),
),
],
);
}
class AmRegisterStep4Screen extends StatefulWidget { class AmRegisterStep4Screen extends StatefulWidget {
const AmRegisterStep4Screen({super.key}); const AmRegisterStep4Screen({super.key});
@ -49,6 +24,7 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final registrationData = Provider.of<AmRegistrationData>(context); final registrationData = Provider.of<AmRegistrationData>(context);
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
return Scaffold( return Scaffold(
body: Stack( body: Stack(
@ -60,7 +36,9 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), padding: EdgeInsets.symmetric(
horizontal: config.isMobile ? 0 : screenSize.width / 4.0
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -70,18 +48,23 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildPersonalInfoCard(context, registrationData), // Carte 1: Informations personnelles
const SizedBox(height: 20), _buildPersonalInfo(context, registrationData),
_buildProfessionalInfoCard(context, registrationData), const SizedBox(height: 30),
const SizedBox(height: 20),
_buildPresentationCard(context, registrationData), // Carte 2: Informations professionnelles
_buildProfessionalInfo(context, registrationData),
const SizedBox(height: 30),
// Carte 3: Présentation
_buildPresentation(context, registrationData),
const SizedBox(height: 40), const SizedBox(height: 40),
ImageButton( ImageButton(
bg: 'assets/images/bg_green.png', bg: 'assets/images/bg_green.png',
text: 'Soumettre ma demande', text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F), textColor: const Color(0xFF2D6A4F),
width: 350, width: config.isMobile ? 300 : 350,
height: 50, height: 50,
fontSize: 18, fontSize: 18,
onPressed: () { onPressed: () {
@ -94,26 +77,94 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
), ),
), ),
), ),
Positioned( // Chevrons desktop uniquement
top: screenSize.height / 2 - 20, if (!config.isMobile)
left: 40, Positioned(
child: IconButton( top: screenSize.height / 2 - 20,
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), left: 40,
onPressed: () { child: IconButton(
if (context.canPop()) { icon: Transform(
context.pop(); alignment: Alignment.center,
} else { transform: Matrix4.rotationY(math.pi),
context.go('/am-register-step3'); child: Image.asset('assets/images/chevron_right.png', height: 40),
} ),
}, onPressed: () {
tooltip: 'Retour', 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) { void _showConfirmationModal(BuildContext context) {
showDialog<void>( showDialog<void>(
context: context, context: context,
@ -141,199 +192,4 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
}, },
); );
} }
// Carte Informations personnelles
Widget _buildPersonalInfoCard(BuildContext context, AmRegistrationData data) {
const double verticalSpacing = 28.0;
const double labelFontSize = 22.0;
List<Widget> 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<Widget> 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<Widget> 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<Widget> 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',
),
],
),
),
);
}
} }

View File

@ -1,45 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.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:provider/provider.dart';
import 'package:go_router/go_router.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é import '../../models/user_registration_data.dart';
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { import '../../models/card_assets.dart';
const FontWeight labelFontWeight = FontWeight.w600; import '../../config/display_config.dart';
import '../../widgets/image_button.dart';
return Column( import '../../widgets/personal_info_form_screen.dart';
crossAxisAlignment: CrossAxisAlignment.start, import '../../widgets/child_card_widget.dart';
children: [ import '../../widgets/presentation_form_screen.dart';
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,
),
),
],
);
}
class ParentRegisterStep5Screen extends StatefulWidget { class ParentRegisterStep5Screen extends StatefulWidget {
const ParentRegisterStep5Screen({super.key}); const ParentRegisterStep5Screen({super.key});
@ -53,9 +24,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context); final registrationData = Provider.of<UserRegistrationData>(context);
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran) final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold( return Scaffold(
body: Stack( body: Stack(
@ -65,9 +34,11 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
), ),
Center( Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Padding( // Ajout du Padding horizontal externe child: Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), padding: EdgeInsets.symmetric(
horizontal: config.isMobile ? 0 : screenSize.width / 4.0
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -77,20 +48,35 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1), // Carte Parent 1
_buildParent1(context, registrationData),
const SizedBox(height: 20), const SizedBox(height: 20),
// Carte Parent 2 (si présent)
if (registrationData.parent2 != null) ...[ if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!), _buildParent2(context, registrationData),
const SizedBox(height: 20), 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), const SizedBox(height: 40),
ImageButton( ImageButton(
bg: 'assets/images/bg_green.png', bg: 'assets/images/bg_green.png',
text: 'Soumettre ma demande', text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F), textColor: const Color(0xFF2D6A4F),
width: 350, width: config.isMobile ? 300 : 350,
height: 50, height: 50,
fontSize: 18, fontSize: 18,
onPressed: () { onPressed: () {
@ -103,26 +89,117 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
), ),
), ),
), ),
Positioned( // Chevrons desktop uniquement
top: screenSize.height / 2 - 20, if (!config.isMobile)
left: 40, Positioned(
child: IconButton( top: screenSize.height / 2 - 20,
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), left: 40,
onPressed: () { child: IconButton(
if (context.canPop()) { icon: Transform(
context.pop(); alignment: Alignment.center,
} else { transform: Matrix4.rotationY(math.pi),
context.go('/parent-register-step4'); child: Image.asset('assets/images/chevron_right.png', height: 40),
} ),
}, onPressed: () {
tooltip: 'Retour', 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) { void _showConfirmationModal(BuildContext context) {
showDialog<void>( showDialog<void>(
context: context, context: context,
@ -141,8 +218,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
TextButton( TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () { onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale Navigator.of(dialogContext).pop();
// Utiliser go_router pour la navigation
context.go('/login'); context.go('/login');
}, },
), ),
@ -151,294 +227,5 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
}, },
); );
} }
// 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<Widget> 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<Widget> 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<Widget> _buildChildrenCards(BuildContext context, List<ChildData> 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<Widget> 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<Widget> 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',
),
],
),
),
);
}
}

View File

@ -94,19 +94,34 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final config = DisplayConfig.fromContext(context, mode: widget.mode); 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 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; 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 final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
? Colors.purple.shade200 ? 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 initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container( return Container(
width: 345.0 * scaleFactor, width: config.isMobile ? double.infinity : screenSize.width * 0.6,
height: config.isMobile ? null : 600.0 * scaleFactor, // Hauteur augmentée pour éviter l'overflow // 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), padding: EdgeInsets.all(22.0 * scaleFactor),
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill), image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill),
@ -117,6 +132,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// ... (contenu existant)
HoverReliefWidget( HoverReliefWidget(
onPressed: config.isReadonly ? null : widget.onPickImage, onPressed: config.isReadonly ? null : widget.onPickImage,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@ -135,7 +151,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
), ),
), ),
), ),
SizedBox(height: 10.0 * scaleFactor), // Réduit de 12 à 10 SizedBox(height: 10.0 * scaleFactor),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -156,7 +172,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
), ),
], ],
), ),
SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 SizedBox(height: 8.0 * scaleFactor),
_buildField( _buildField(
config: config, config: config,
scaleFactor: scaleFactor, scaleFactor: scaleFactor,
@ -165,7 +181,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
hint: 'Facultatif si à naître', hint: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild, isRequired: !widget.childData.isUnbornChild,
), ),
SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 SizedBox(height: 5.0 * scaleFactor),
_buildField( _buildField(
config: config, config: config,
scaleFactor: scaleFactor, scaleFactor: scaleFactor,
@ -173,18 +189,18 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
controller: _lastNameController, controller: _lastNameController,
hint: 'Nom de l\'enfant', hint: 'Nom de l\'enfant',
), ),
SizedBox(height: 8.0 * scaleFactor), // Réduit de 9 à 8 SizedBox(height: 8.0 * scaleFactor),
_buildField( _buildField(
config: config, config: config,
scaleFactor: scaleFactor, scaleFactor: scaleFactor,
label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
controller: _dobController, controller: _dobController,
hint: 'JJ/MM/AAAA', hint: 'JJ/MM/AAAA',
readOnly: true, // Toujours readonly pour le TextField (date picker) readOnly: true,
onTap: config.isReadonly ? null : widget.onDateSelect, onTap: config.isReadonly ? null : widget.onDateSelect,
suffixIcon: Icons.calendar_today, suffixIcon: Icons.calendar_today,
), ),
SizedBox(height: 10.0 * scaleFactor), // Réduit de 11 à 10 SizedBox(height: 10.0 * scaleFactor),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -195,7 +211,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor, checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor,
fontSize: config.isMobile ? 13.0 : 16.0, fontSize: config.isMobile ? 13.0 : 16.0,
), ),
SizedBox(height: 5.0 * scaleFactor), // Réduit de 6 à 5 SizedBox(height: 5.0 * scaleFactor),
AppCustomCheckbox( AppCustomCheckbox(
label: 'Naissance multiple', label: 'Naissance multiple',
value: widget.childData.multipleBirth, value: widget.childData.multipleBirth,
@ -236,6 +252,310 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
); );
} }
/// 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({ Widget _buildField({
required DisplayConfig config, required DisplayConfig config,
required double scaleFactor, required double scaleFactor,

View File

@ -78,7 +78,7 @@ class FormFieldWrapper extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// Valeur // Valeur avec fond beige
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -86,20 +86,17 @@ class FormFieldWrapper extends StatelessWidget {
vertical: 12, vertical: 12,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), image: const DecorationImage(
borderRadius: BorderRadius.circular(8), image: AssetImage('assets/images/bg_beige.png'),
border: Border.all( fit: BoxFit.fill,
color: const Color(0xFFE0E0E0),
width: 1,
), ),
borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
value.isEmpty ? '-' : value, value.isEmpty ? '-' : value,
style: GoogleFonts.merienda( style: GoogleFonts.merienda(
fontSize: config.isMobile ? 14 : 16, fontSize: config.isMobile ? 14 : 16,
color: value.isEmpty color: const Color(0xFF2C2C2C),
? const Color(0xFFBDBDBD)
: const Color(0xFF2C2C2C),
), ),
), ),
), ),

View File

@ -48,6 +48,8 @@ class PersonalInfoFormScreen extends StatefulWidget {
final bool showSameAddressCheckbox; // Afficher "Même adresse que parent 1" final bool showSameAddressCheckbox; // Afficher "Même adresse que parent 1"
final bool? initialSameAddress; final bool? initialSameAddress;
final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse" 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({ const PersonalInfoFormScreen({
super.key, super.key,
@ -63,6 +65,8 @@ class PersonalInfoFormScreen extends StatefulWidget {
this.showSameAddressCheckbox = false, this.showSameAddressCheckbox = false,
this.initialSameAddress, this.initialSameAddress,
this.referenceAddressData, this.referenceAddressData,
this.embedContentOnly = false,
this.onEdit,
}); });
@override @override
@ -150,6 +154,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: widget.mode); final config = DisplayConfig.fromContext(context, mode: widget.mode);
if (widget.embedContentOnly) {
return _buildCard(context, config, screenSize);
}
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
@ -180,37 +188,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: config.isMobile ? 16 : 30), SizedBox(height: config.isMobile ? 16 : 30),
Container( _buildCard(context, config, screenSize),
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),
],
),
),
),
// Boutons mobile sous la carte (dans le scroll) // Boutons mobile sous la carte (dans le scroll)
if (config.isMobile) ...[ if (config.isMobile) ...[
@ -297,6 +275,198 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
); );
} }
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) /// Construit les toggles (Parent 2 / Même adresse)
Widget _buildToggles(BuildContext context, DisplayConfig config) { Widget _buildToggles(BuildContext context, DisplayConfig config) {
if (config.isMobile) { if (config.isMobile) {
@ -305,10 +475,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
children: [ children: [
_buildSecondPersonToggle(context, config), _buildSecondPersonToggle(context, config),
if (widget.showSameAddressCheckbox) ...[ if (widget.showSameAddressCheckbox) ...[
const SizedBox(height: 12), const SizedBox(height: 5), // Réduit de 12 à 5
_buildSameAddressToggle(context, config), _buildSameAddressToggle(context, config),
], ],
const SizedBox(height: 24), const SizedBox(height: 10), // Réduit de 24 à 10
], ],
); );
} else { } else {
@ -392,7 +562,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
value: _sameAddress, value: _sameAddress,
onChanged: _fieldsEnabled ? (value) { onChanged: _fieldsEnabled ? (value) {
setState(() { setState(() {
_sameAddress = value ?? false; _sameAddress = value;
_updateAddressFields(); _updateAddressFields();
}); });
} : null, } : null,
@ -414,6 +584,14 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
/// Layout DESKTOP : champs côte à côte (horizontal) /// Layout DESKTOP : champs côte à côte (horizontal)
Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { 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( return Column(
children: [ children: [
// Nom et Prénom // Nom et Prénom
@ -440,7 +618,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
), ),
], ],
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
// Téléphone et Email // Téléphone et Email
Row( Row(
@ -468,7 +646,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
), ),
], ],
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
// Adresse // Adresse
_buildField( _buildField(
@ -478,7 +656,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
hint: 'Numéro et nom de votre rue', hint: 'Numéro et nom de votre rue',
enabled: _fieldsEnabled && !_sameAddress, enabled: _fieldsEnabled && !_sameAddress,
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
// Code Postal et Ville // Code Postal et Ville
Row( Row(
@ -511,8 +689,154 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
); );
} }
/// 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 /// Layout MOBILE : tous les champs empilés verticalement
Widget _buildMobileFields(BuildContext context, DisplayConfig config) { 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( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [

View File

@ -23,6 +23,9 @@ class PresentationFormScreen extends StatefulWidget {
final String previousRoute; final String previousRoute;
final Function(String text, bool cguAccepted) onSubmit; final Function(String text, bool cguAccepted) onSubmit;
final bool embedContentOnly;
final VoidCallback? onEdit;
const PresentationFormScreen({ const PresentationFormScreen({
super.key, super.key,
this.mode = DisplayMode.editable, this.mode = DisplayMode.editable,
@ -34,6 +37,8 @@ class PresentationFormScreen extends StatefulWidget {
required this.initialCguAccepted, required this.initialCguAccepted,
required this.previousRoute, required this.previousRoute,
required this.onSubmit, required this.onSubmit,
this.embedContentOnly = false,
this.onEdit,
}); });
@override @override
@ -75,6 +80,10 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: widget.mode); final config = DisplayConfig.fromContext(context, mode: widget.mode);
if (widget.embedContentOnly) {
return _buildCard(context, config, screenSize);
}
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
@ -154,7 +163,7 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Carte qui prend tout l'espace restant // Carte qui prend tout l'espace restant
Expanded( Expanded(
child: _buildMobileCard(context, config, screenSize), child: _buildCard(context, config, screenSize),
), ),
// Boutons en bas // Boutons en bas
const SizedBox(height: 20), const SizedBox(height: 20),
@ -188,13 +197,228 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), 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 /// Carte DESKTOP : Format horizontal 2:1
Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
final cardWidth = screenSize.width * 0.6; final cardWidth = screenSize.width * 0.6;
@ -223,13 +447,14 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
maxLines: 10, maxLines: 10,
expandDynamically: true, expandDynamically: true,
fontSize: 18.0, fontSize: 18.0,
readOnly: config.isReadonly,
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
AppCustomCheckbox( AppCustomCheckbox(
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
value: _cguAccepted, 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<PresentationFormScreen> {
/// Carte MOBILE : Prend tout l'espace disponible /// Carte MOBILE : Prend tout l'espace disponible
Widget _buildMobileCard(BuildContext context, DisplayConfig config, Size screenSize) { 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( return Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Container( child: Container(
@ -251,21 +496,14 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
child: Column( child: Column(
children: [ children: [
// Champ de texte qui prend l'espace disponible et scrollable // Champ de texte
Expanded( if (widget.embedContentOnly)
child: LayoutBuilder( // En mode récapitulatif, on donne une hauteur fixe pour éviter l'erreur d'Expanded
builder: (context, constraints) { SizedBox(height: 200, child: textFieldContent)
return CustomDecoratedTextField( else
controller: _textController, // En mode écran complet, on prend tout l'espace restant
hintText: widget.textFieldHint, Expanded(child: textFieldContent),
fieldHeight: constraints.maxHeight,
maxLines: 100, // Grande valeur pour permettre le scroll
expandDynamically: false,
fontSize: 14.0,
);
},
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
// Checkbox en bas // Checkbox en bas
Transform.scale( Transform.scale(
@ -273,7 +511,7 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
child: AppCustomCheckbox( child: AppCustomCheckbox(
label: 'J\'accepte les CGU et la\nPolitique de confidentialité', label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
value: _cguAccepted, value: _cguAccepted,
onChanged: (value) => setState(() => _cguAccepted = value ?? false), onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
), ),
), ),
], ],

View File

@ -7,6 +7,7 @@ import 'dart:io';
import '../models/card_assets.dart'; import '../models/card_assets.dart';
import '../config/display_config.dart'; import '../config/display_config.dart';
import 'custom_app_text_field.dart'; import 'custom_app_text_field.dart';
import 'form_field_wrapper.dart';
import 'app_custom_checkbox.dart'; import 'app_custom_checkbox.dart';
import 'hover_relief_widget.dart'; import 'hover_relief_widget.dart';
import 'custom_navigation_button.dart'; import 'custom_navigation_button.dart';
@ -48,6 +49,8 @@ class ProfessionalInfoFormScreen extends StatefulWidget {
final String previousRoute; final String previousRoute;
final Function(ProfessionalInfoData) onSubmit; final Function(ProfessionalInfoData) onSubmit;
final Future<void> Function()? onPickPhoto; final Future<void> Function()? onPickPhoto;
final bool embedContentOnly;
final VoidCallback? onEdit;
const ProfessionalInfoFormScreen({ const ProfessionalInfoFormScreen({
super.key, super.key,
@ -59,6 +62,8 @@ class ProfessionalInfoFormScreen extends StatefulWidget {
required this.previousRoute, required this.previousRoute,
required this.onSubmit, required this.onSubmit,
this.onPickPhoto, this.onPickPhoto,
this.embedContentOnly = false,
this.onEdit,
}); });
@override @override
@ -170,6 +175,10 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: widget.mode); final config = DisplayConfig.fromContext(context, mode: widget.mode);
if (widget.embedContentOnly) {
return _buildCard(context, config, screenSize);
}
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
@ -200,29 +209,8 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: config.isMobile ? 16 : 30), SizedBox(height: config.isMobile ? 16 : 30),
Container( _buildCard(context, config, screenSize),
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),
),
),
// Boutons mobile sous la carte // Boutons mobile sous la carte
if (config.isMobile) ...[ if (config.isMobile) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
@ -271,8 +259,308 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
); );
} }
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 /// Layout DESKTOP : Photo à gauche, champs à droite
Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { Widget _buildDesktopFields(BuildContext context, DisplayConfig config) {
final double verticalSpacing = config.isReadonly ? 16.0 : 32.0;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -296,7 +584,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
hint: 'Votre ville de naissance', hint: 'Votre ville de naissance',
validator: (v) => v!.isEmpty ? 'Ville requise' : null, validator: (v) => v!.isEmpty ? 'Ville requise' : null,
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
_buildField( _buildField(
config: config, config: config,
label: 'Pays de naissance', label: 'Pays de naissance',
@ -304,7 +592,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
hint: 'Votre pays de naissance', hint: 'Votre pays de naissance',
validator: (v) => v!.isEmpty ? 'Pays requis' : null, validator: (v) => v!.isEmpty ? 'Pays requis' : null,
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
_buildField( _buildField(
config: config, config: config,
label: 'Date de naissance', label: 'Date de naissance',
@ -320,7 +608,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
], ],
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
_buildField( _buildField(
config: config, config: config,
label: 'N° Sécurité Sociale (NIR)', label: 'N° Sécurité Sociale (NIR)',
@ -334,7 +622,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
return null; return null;
}, },
), ),
const SizedBox(height: 32), SizedBox(height: verticalSpacing),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -509,20 +797,28 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
IconData? suffixIcon, IconData? suffixIcon,
String? Function(String?)? validator, String? Function(String?)? validator,
}) { }) {
return CustomAppTextField( if (config.isReadonly) {
controller: controller, return FormFieldWrapper(
labelText: label, config: config,
hintText: hint ?? label, label: label,
fieldWidth: double.infinity, value: controller.text,
fieldHeight: config.isMobile ? 45.0 : 53.0, );
labelFontSize: config.isMobile ? 15.0 : 22.0, } else {
inputFontSize: config.isMobile ? 14.0 : 20.0, return CustomAppTextField(
keyboardType: keyboardType ?? TextInputType.text, controller: controller,
readOnly: readOnly, labelText: label,
onTap: onTap, hintText: hint ?? label,
suffixIcon: suffixIcon, fieldWidth: double.infinity,
validator: validator, 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 /// Boutons mobile

View File

@ -230,11 +230,12 @@ Widget buildDisplayFieldValue(
height: multiLine ? null : fieldHeight, height: multiLine ? null : fieldHeight,
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: const BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'), image: AssetImage('assets/images/bg_beige.png'),
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
value.isNotEmpty ? value : '-', value.isNotEmpty ? value : '-',