feat(frontend): Refonte infrastructure formulaires multi-modes

- Support des modes Desktop/Mobile et Édition/Lecture seule
- Refactoring des widgets de formulaire (PersonalInfo, ProfessionalInfo, Presentation, ChildCard)
- Mise à jour des écrans de récapitulatif (ParentStep5, AmStep4)
- Ajout de navigation (Précédent/Soumettre) sur mobile

Closes #78

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-07 14:51:33 +01:00
co-authored by Cursor
parent 45bd8a9ef1
commit b18d5c8a9e
23 changed files with 3799 additions and 1213 deletions
@@ -1,41 +1,18 @@
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/hover_relief_widget.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_navigation_button.dart';
import '../../widgets/personal_info_form_screen.dart';
import '../../widgets/professional_info_form_screen.dart';
import '../../widgets/presentation_form_screen.dart';
class AmRegisterStep4Screen extends StatefulWidget {
const AmRegisterStep4Screen({super.key});
@@ -49,6 +26,7 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
Widget build(BuildContext context) {
final registrationData = Provider.of<AmRegistrationData>(context);
final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
return Scaffold(
body: Stack(
@@ -60,7 +38,9 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
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,50 +50,167 @@ 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),
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/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
_showConfirmationModal(context);
},
),
// Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop
if (config.isMobile)
Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Row(
children: [
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Précédent',
style: NavigationButtonStyle.purple,
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/am-register-step3');
}
},
width: double.infinity,
height: 50,
fontSize: 16,
),
),
),
const SizedBox(width: 16),
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Soumettre',
style: NavigationButtonStyle.green,
onPressed: () {
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
_showConfirmationModal(context);
},
width: double.infinity,
height: 50,
fontSize: 16,
),
),
),
],
),
)
else
ImageButton(
bg: 'assets/images/bg_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
_showConfirmationModal(context);
},
),
],
),
),
),
),
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<void>(
context: context,
@@ -141,199 +238,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',
),
],
),
),
);
}
}