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
@@ -5,9 +5,11 @@ import 'package:image_picker/image_picker.dart';
import 'dart:io' show File;
import '../../widgets/hover_relief_widget.dart';
import '../../widgets/child_card_widget.dart';
import '../../widgets/custom_navigation_button.dart';
import '../../models/user_registration_data.dart';
import '../../utils/data_generator.dart';
import '../../models/card_assets.dart';
import '../../config/display_config.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
@@ -204,14 +206,157 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: DisplayMode.editable);
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
config.isMobile
? _buildMobileLayout(context, config, screenSize, registrationData)
: _buildDesktopLayout(context, config, screenSize, registrationData),
// Chevrons desktop uniquement
if (!config.isMobile) ...[
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step2');
}
},
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
context.go('/parent-register-step4');
},
tooltip: 'Suivant',
),
),
],
],
),
);
}
/// Layout MOBILE : Cartes empilées verticalement
Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
return Column(
children: [
// Header fixe
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Column(
children: [
Text(
'Étape 3/5',
style: GoogleFonts.merienda(fontSize: 13, color: Colors.black54),
),
const SizedBox(height: 6),
Text(
'Informations Enfants',
style: GoogleFonts.merienda(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 16),
// Liste scrollable des cartes + boutons
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Column(
children: [
// Générer les cartes enfants
for (int index = 0; index < registrationData.children.length; index++) ...[
ChildCardWidget(
key: ValueKey(registrationData.children[index].hashCode),
childData: registrationData.children[index],
childIndex: index,
onPickImage: () => _pickImage(index, registrationData),
onDateSelect: () => _selectDate(context, index, registrationData),
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
))),
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
))),
onTogglePhotoConsent: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onToggleMultipleBirth: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onToggleIsUnborn: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onRemove: () => _removeChild(index, registrationData),
canBeRemoved: registrationData.children.length > 1,
),
const SizedBox(height: 16),
],
// Bouton "+" carré à la fin de la liste
Center(
child: HoverReliefWidget(
onPressed: () => _addChild(registrationData),
borderRadius: BorderRadius.circular(15),
child: Container(
width: 50,
height: 50,
child: Image.asset('assets/images/plus.png', fit: BoxFit.contain),
),
),
),
const SizedBox(height: 30),
// Boutons navigation en bas du scroll
_buildMobileButtons(context, config, screenSize),
const SizedBox(height: 20),
],
),
),
),
],
);
}
/// Layout DESKTOP : Scroll horizontal avec fondu
Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
@@ -288,12 +433,12 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
),
);
} else {
// Bouton Ajouter
// Bouton Ajouter Desktop (Gros bouton)
return Center(
child: HoverReliefWidget(
onPressed: () => _addChild(registrationData),
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
child: Image.asset('assets/images/plus.png', height: 100, width: 100, fit: BoxFit.contain),
),
);
}
@@ -306,13 +451,18 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
const SizedBox(height: 20),
],
),
),
// Chevrons de navigation
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
);
}
/// Boutons navigation mobile
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
return Row(
children: [
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Précédent',
style: NavigationButtonStyle.purple,
onPressed: () {
if (context.canPop()) {
context.pop();
@@ -320,22 +470,28 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
context.go('/parent-register-step2');
}
},
tooltip: 'Retour',
width: double.infinity,
height: 50,
fontSize: 16,
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
),
const SizedBox(width: 16),
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Suivant',
style: NavigationButtonStyle.green,
onPressed: () {
context.go('/parent-register-step4');
context.go('/parent-register-step4');
},
tooltip: 'Suivant',
width: double.infinity,
height: 50,
fontSize: 16,
),
),
],
),
),
],
);
}
}