[Frontend] Refactorisation inscription Parents avec Provider (#38 #39)

Refactorisation complète du parcours d'inscription des parents pour utiliser
Provider au lieu du passage de données par paramètres de navigation.

Modifications principales :
- Utilisation de Provider pour partager UserRegistrationData entre les étapes
- Simplification du routeur (suppression des paramètres)
- Amélioration de la persistance des données entre les étapes
- Meilleure expérience utilisateur lors de la navigation

Fichiers modifiés :
- models/user_registration_data.dart : Modèle avec ChangeNotifier
- screens/auth/parent_register_step1-5_screen.dart : Intégration Provider
- navigation/app_router.dart : Simplification du routing
- main.dart : Configuration du Provider
- login_screen.dart : Ajout navigation vers inscription
- register_choice_screen.dart : Navigation vers parcours parent/AM
- utils/data_generator.dart : Génération de données de test

Refs: #38 (Étape 3 Enfants), #39 (Étapes 4-6 Finalisation)
This commit is contained in:
2026-01-27 16:44:23 +01:00
parent dbd56637e1
commit 29bee9fa80
11 changed files with 505 additions and 325 deletions
@@ -4,69 +4,153 @@ import '../../models/user_registration_data.dart'; // Utilisation du vrai modèl
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 '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
const FontWeight labelFontWeight = FontWeight.w600;
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
bool showLabel = label.isNotEmpty && labelFontSize > 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel)
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
if (showLabel)
const SizedBox(height: 4),
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
multiLine && fieldHeight == null
? Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
maxLines: null, // Permettre un nombre illimité de lignes
),
),
),
)
: Container(
width: double.infinity,
height: multiLine ? null : fieldHeight,
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
maxLines: multiLine ? null : 1,
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
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 StatelessWidget {
final UserRegistrationData registrationData;
class ParentRegisterStep5Screen extends StatefulWidget {
const ParentRegisterStep5Screen({super.key});
const ParentRegisterStep5Screen({super.key, required this.registrationData});
@override
_ParentRegisterStep5ScreenState createState() => _ParentRegisterStep5ScreenState();
}
class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context);
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
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 finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_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('/parent-register-step4');
}
},
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// Utiliser go_router pour la navigation
context.go('/login');
},
),
],
);
},
);
}
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
@@ -98,7 +182,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
onEdit: () => context.go('/parent-register-step1'),
);
}
@@ -131,7 +215,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
onEdit: () => context.go('/parent-register-step2'),
);
}
@@ -176,10 +260,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: () {
Navigator.of(context).pushNamed(
'/parent-register/step3',
arguments: registrationData,
);
context.go('/parent-register-step3', extra: {'childIndex': index});
},
tooltip: 'Modifier',
),
@@ -264,23 +345,17 @@ class ParentRegisterStep5Screen extends StatelessWidget {
// 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.green.path,
backgroundImagePath: CardColorHorizontal.pink.path,
title: 'Votre Motivation',
content: [
Expanded(
child: CustomDecoratedTextField(
controller: TextEditingController(text: motivation),
hintText: 'Aucune motivation renseignée.',
fieldHeight: 200,
maxLines: 10,
expandDynamically: true,
readOnly: true,
fontSize: 18.0,
),
),
],
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
content: details,
onEdit: () => context.go('/parent-register-step4'),
);
}
@@ -306,102 +381,6 @@ class ParentRegisterStep5Screen extends StatelessWidget {
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
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 finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_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: () => Navigator.pop(context), // Retour à l'étape 4
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// TODO: Naviguer vers l'écran de connexion ou tableau de bord
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
},
),
],
);
},
);
}
}
// Widget générique _SummaryCard (ajusté)
@@ -422,7 +401,7 @@ class _SummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.0,
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(
@@ -432,30 +411,31 @@ class _SummaryCard extends StatelessWidget {
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: onEdit,
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Column(
children: content,
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',
),
],
),
),