refactor(inscription): Refonte complète du processus d'inscription - Modèles etdonnées: Suppression de placeholder_registration_data.dart, ajout de user_registration_data.dart, data_generator.dart et card_assets.dart - Interface utilisateur: Refonte des écrans d'inscription, amélioration des widgets, ajout de cartes colorées - Assets: Ajout de nouvelles cartes colorées - Configuration: Mise à jour de pubspec.yaml et app_router.dart

This commit is contained in:
Julien Martin 2025-05-12 12:00:49 +02:00
parent acb602643a
commit 1496f7f174
32 changed files with 1035 additions and 717 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,25 @@
enum CardColorVertical {
red('assets/cards/card_red.png'),
pink('assets/cards/card_pink.png'),
peach('assets/cards/card_peach.png'),
lime('assets/cards/card_lime.png'),
lavender('assets/cards/card_lavender.png'),
green('assets/cards/card_green.png'),
blue('assets/cards/card_blue.png');
final String path;
const CardColorVertical(this.path);
}
enum CardColorHorizontal {
red('assets/cards/card_red_h.png'),
pink('assets/cards/card_pink_h.png'),
peach('assets/cards/card_peach_h.png'),
lime('assets/cards/card_lime_h.png'),
lavender('assets/cards/card_lavender_h.png'),
green('assets/cards/card_green_h.png'),
blue('assets/cards/card_blue_h.png');
final String path;
const CardColorHorizontal(this.path);
}

View File

@ -1,18 +0,0 @@
// frontend/lib/models/placeholder_registration_data.dart
class PlaceholderRegistrationData {
final String? parent1Name;
// Ajoutez ici d'autres champs au fur et à mesure que nous définissons les données nécessaires
// pour parent 1, parent 2, enfants, motivation
// Exemple de champ pour savoir si le parent 2 existe
final bool parent2Exists;
final List<String> childrenNames; // Juste un exemple, à remplacer par une vraie structure enfant
final String? motivationText;
PlaceholderRegistrationData({
this.parent1Name,
this.parent2Exists = false, // Valeur par défaut
this.childrenNames = const [], // Valeur par défaut
this.motivationText,
});
}

View File

@ -0,0 +1,97 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
class ParentData {
String firstName;
String lastName;
String address; // Rue et numéro
String postalCode; // Ajout
String city; // Ajout
String phone;
String email;
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
File? profilePicture; // Chemin ou objet File
ParentData({
this.firstName = '',
this.lastName = '',
this.address = '', // Rue
this.postalCode = '', // Ajout
this.city = '', // Ajout
this.phone = '',
this.email = '',
this.password = '',
this.profilePicture,
});
}
class ChildData {
String firstName;
String lastName;
String dob; // Date de naissance ou prévisionnelle
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
ChildData({
this.firstName = '',
this.lastName = '',
this.dob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur
});
}
class UserRegistrationData {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
UserRegistrationData({
ParentData? parent1Data,
this.parent2,
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
}
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
void updateChild(int index, ChildData child) {
if (index >= 0 && index < children.length) {
children[index] = child;
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
}
// Accepter les CGU
void acceptCGU() {
cguAccepted = true;
}
}

View File

@ -8,7 +8,7 @@ import '../screens/auth/parent_register_step3_screen.dart';
import '../screens/auth/parent_register_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/home/home_screen.dart';
import '../models/placeholder_registration_data.dart';
import '../models/user_registration_data.dart';
class AppRouter {
static const String login = '/login';
@ -23,6 +23,12 @@ class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
bool slideTransition = false;
Object? args = settings.arguments;
Widget buildErrorScreen(String step) {
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
return const ParentRegisterStep1Screen();
}
switch (settings.name) {
case login:
@ -30,32 +36,43 @@ class AppRouter {
break;
case registerChoice:
screen = const RegisterChoiceScreen();
slideTransition = true; // Activer la transition pour cet écran
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
slideTransition = true; // Activer la transition pour cet écran
slideTransition = true;
break;
case parentRegisterStep2:
screen = const ParentRegisterStep2Screen();
if (args is UserRegistrationData) {
screen = ParentRegisterStep2Screen(registrationData: args);
} else {
screen = buildErrorScreen('2');
}
slideTransition = true;
break;
case parentRegisterStep3:
screen = const ParentRegisterStep3Screen();
if (args is UserRegistrationData) {
screen = ParentRegisterStep3Screen(registrationData: args);
} else {
screen = buildErrorScreen('3');
}
slideTransition = true;
break;
case parentRegisterStep4:
screen = const ParentRegisterStep4Screen();
if (args is UserRegistrationData) {
screen = ParentRegisterStep4Screen(registrationData: args);
} else {
screen = buildErrorScreen('4');
}
slideTransition = true;
break;
case parentRegisterStep5:
final args = settings.arguments as PlaceholderRegistrationData?;
if (args != null) {
if (args is UserRegistrationData) {
screen = ParentRegisterStep5Screen(registrationData: args);
} else {
print("Erreur: Données d'inscription manquantes pour l'étape 5");
screen = const RegisterChoiceScreen();
screen = buildErrorScreen('5');
}
slideTransition = true;
break;
case home:
screen = const HomeScreen();
@ -72,22 +89,16 @@ class AppRouter {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0); // Glisse depuis la droite
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut; // Animation douce
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
return SlideTransition(position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 400), // Durée de la transition
transitionDuration: const Duration(milliseconds: 400),
);
} else {
// Transition par défaut pour les autres écrans
return MaterialPageRoute(builder: (_) => screen);
}
}

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur de données
import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@ -11,17 +15,42 @@ class ParentRegisterStep1Screen extends StatefulWidget {
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// Contrôleurs pour les champs
// Contrôleurs pour les champs (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
_generateAndFillData();
}
void _generateAndFillData() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
// Utilisation des méthodes publiques de DataGenerator
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
@override
void dispose() {
@ -61,13 +90,13 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
children: [
// Indicateur d'étape (à rendre dynamique)
Text(
'Étape 1/X',
'Étape 1/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Merci de renseigner les informations du premier parent :',
'Informations du Parent Principal',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -80,10 +109,11 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
// Carte jaune contenant le formulaire
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: const BoxDecoration(
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/card_yellow_h.png'),
image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill,
),
),
@ -94,35 +124,49 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
children: [
Row(
children: [
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Votre nom de famille')),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Votre prénom')),
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone')),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail')),
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe')),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe')),
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Mot de passe requis';
if (value.length < 6) return '6 caractères minimum';
return null;
})),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Ne correspond pas';
return null;
})),
],
),
const SizedBox(height: 20),
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Numéro et nom de votre rue'),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal')),
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Votre ville')),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
@ -157,8 +201,19 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// TODO: Sauvegarder les données du parent 1
Navigator.pushNamed(context, '/parent-register/step2'); // Naviguer vers l'étape 2
_registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text, // Rue
postalCode: _postalCodeController.text, // Ajout
city: _cityController.text, // Ajout
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
}
},
tooltip: 'Suivant',
@ -168,59 +223,4 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
),
);
}
// Widget pour construire les champs de texte avec le fond personnalisé
Widget _buildTextField(
TextEditingController controller,
String label, {
TextInputType? keyboardType,
bool obscureText = false,
String? hintText,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label :',
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 5),
Container(
height: 50,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black87),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
hintText: hintText ?? label,
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
),
validator: (value) {
// Validation désactivée
return null;
/*
if (value == null || value.isEmpty) {
return 'Ce champ est obligatoire';
}
// TODO: Ajouter des validations spécifiques (email, téléphone, mot de passe)
if (label == 'Confirmation' && value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
*/
},
),
),
],
);
}
}

View File

@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep2Screen extends StatefulWidget {
const ParentRegisterStep2Screen({super.key});
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
const ParentRegisterStep2Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
@ -11,25 +17,54 @@ class ParentRegisterStep2Screen extends StatefulWidget {
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
// TODO: Recevoir les infos du parent 1 pour pré-remplir l'adresse
// String? _parent1Address;
// String? _parent1PostalCode;
// String? _parent1City;
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
bool _addParent2 = false; // Par défaut, on n'ajoute pas le parent 2
bool _sameAddressAsParent1 = false;
// Contrôleurs pour les champs du parent 2
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
if (_addParent2) {
_generateAndFillParent2Data();
}
}
void _generateAndFillParent2Data() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
}
}
@override
void dispose() {
@ -44,10 +79,8 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_cityController.dispose();
super.dispose();
}
// Helper pour activer/désactiver tous les champs sauf l'adresse
bool get _parent2FieldsEnabled => _addParent2;
// Helper pour activer/désactiver les champs d'adresse
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override
@ -57,174 +90,104 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
return Scaffold(
body: Stack(
children: [
// Fond papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
// Contenu centré
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape
Text(
'Étape 2/X', // Mettre à jour le numéro d'étape total
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Renseignez les informations du deuxième parent (optionnel) :',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// Carte bleue contenant le formulaire
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: const BoxDecoration( // Retour à la décoration
image: DecorationImage(
image: AssetImage('assets/images/card_blue_h.png'), // Utilisation de l'image horizontale
fit: BoxFit.fill,
),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
),
// Suppression du Stack et Transform.rotate
child: Form(
key: _formKey,
child: SingleChildScrollView( // Le SingleChildScrollView redevient l'enfant direct
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- Interrupteurs sur une ligne ---
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Option 1: Ajouter Parent 2
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.person_add_alt_1, size: 20),
const SizedBox(width: 8),
Flexible(
child: Text(
'Ajouter Parent 2 ?',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
_buildCustomSwitch(
value: _addParent2,
onChanged: (bool? newValue) {
final bool actualValue = newValue ?? false;
setState(() {
_addParent2 = actualValue;
if (!_addParent2) {
_formKey.currentState?.reset();
_lastNameController.clear();
_firstNameController.clear();
_phoneController.clear();
_emailController.clear();
_passwordController.clear();
_confirmPasswordController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
}
});
},
),
],
),
),
const SizedBox(width: 10),
// Option 2: Même Adresse
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), // Griser l'icône si désactivé
const SizedBox(width: 8),
Flexible(
child: Text(
'Même Adresse ?',
style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), // Griser le texte si désactivé
overflow: TextOverflow.ellipsis,
),
),
_buildCustomSwitch(
value: _sameAddressAsParent1,
onChanged: _addParent2 ? (bool? newValue) {
final bool actualValue = newValue ?? false;
setState(() {
_sameAddressAsParent1 = actualValue;
if (_sameAddressAsParent1) {
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
// TODO: Pré-remplir
}
});
} : null,
),
],
),
),
],
),
const SizedBox(height: 25), // Espacement ajusté après les switchs
// --- Champs du Parent 2 (conditionnels) ---
// Nom & Prénom
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 12,
child: Row(children: [
const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
Expanded(flex: 1, child: const SizedBox()),
Expanded(
flex: 12,
child: Row(children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
const SizedBox(width: 8),
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
_sameAddressAsParent1 = val ?? false;
if (_sameAddressAsParent1) {
_addressController.text = _registrationData.parent1.address;
_postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.text = _registrationData.parent1.city;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
}
}) : null, activeColor: Theme.of(context).primaryColor),
]),
),
]),
const SizedBox(height: 25),
Row(
children: [
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Nom du deuxième parent', enabled: _parent2FieldsEnabled)),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Prénom du deuxième parent', enabled: _parent2FieldsEnabled)),
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
// Téléphone & Email
Row(
children: [
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son numéro de téléphone', enabled: _parent2FieldsEnabled)),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son adresse e-mail', enabled: _parent2FieldsEnabled)),
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
// Mot de passe
Row(
children: [
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled)),
const SizedBox(width: 20),
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmer son mot de passe', enabled: _parent2FieldsEnabled)),
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
],
),
const SizedBox(height: 20),
// --- Champs Adresse (conditionnels) ---
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled)),
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled)),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
@ -236,50 +199,39 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
),
),
),
// Chevron de navigation gauche (Retour)
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: () => Navigator.pop(context), // Retour à l'étape 1
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
// Chevron de navigation droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
// Si on n'ajoute pas de parent 2, on passe directement
if (!_addParent2) {
// Naviguer vers l'étape 3 (enfants)
print('Passer à l\'étape 3 (enfants) - Sans Parent 2');
Navigator.pushNamed(context, '/parent-register/step3');
return;
}
// Si on ajoute un parent 2
// Valider seulement si on n'utilise PAS la même adresse
bool isFormValid = true;
// TODO: Remettre la validation quand elle sera prête
/*
if (!_sameAddressAsParent1) {
isFormValid = _formKey.currentState?.validate() ?? false;
}
*/
if (isFormValid) {
// TODO: Sauvegarder les données du parent 2
print('Passer à l\'étape 3 (enfants) - Avec Parent 2');
Navigator.pushNamed(context, '/parent-register/step3');
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_registrationData.updateParent2(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
} else {
_registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
}
},
tooltip: 'Suivant',
@ -290,85 +242,14 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
);
}
// --- NOUVEAU WIDGET ---
// Widget pour construire un switch personnalisé avec images
Widget _buildCustomSwitch({required bool value, required ValueChanged<bool?>? onChanged}) {
// --- DEBUG ---
print("Building Custom Switch with value: $value");
// -------------
const double switchHeight = 25.0;
const double switchWidth = 40.0;
return InkWell(
onTap: onChanged != null ? () => onChanged(!value) : null,
child: Opacity(
// Griser le switch si désactivé
opacity: onChanged != null ? 1.0 : 0.5,
child: Image.asset(
value ? 'assets/images/switch_on.png' : 'assets/images/switch_off.png',
height: switchHeight,
width: switchWidth,
fit: BoxFit.contain, // Ou BoxFit.fill selon le rendu souhaité
),
),
);
}
// Widget pour construire les champs de texte (identique à l'étape 1)
Widget _buildTextField(
TextEditingController controller,
String label, {
TextInputType? keyboardType,
bool obscureText = false,
String? hintText,
bool enabled = true,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label :',
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 5),
Container(
height: 50,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
enabled: enabled,
style: GoogleFonts.merienda(fontSize: 16, color: enabled ? Colors.black87 : Colors.grey),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
hintText: hintText ?? label,
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
),
validator: (value) {
if (!enabled) return null; // Ne pas valider si désactivé
// Le reste de la validation (commentée précédemment)
return null;
/*
if (value == null || value.isEmpty) {
return 'Ce champ est obligatoire';
}
// TODO: Validations spécifiques
if (label == 'Confirmation' && value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
*/
},
),
),
],
);
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
setState(() {});
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
import 'package:image_picker/image_picker.dart';
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
@ -8,92 +9,69 @@ import 'dart:io' show File, Platform; // Ajout de Platform
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
// Classe de données pour un enfant
class _ChildFormData {
final Key key; // Pour aider Flutter à identifier les widgets dans une liste
final TextEditingController firstNameController;
final TextEditingController lastNameController;
final TextEditingController dobController;
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
_ChildFormData({
required this.key,
String initialFirstName = '',
String initialLastName = '',
String initialDob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
}) : firstNameController = TextEditingController(text: initialFirstName),
lastNameController = TextEditingController(text: initialLastName),
dobController = TextEditingController(text: initialDob);
// Méthode pour disposer les contrôleurs
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
dobController.dispose();
}
}
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
class ParentRegisterStep3Screen extends StatefulWidget {
const ParentRegisterStep3Screen({super.key});
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep3Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
// Liste pour stocker les données de chaque enfant
List<_ChildFormData> _childrenDataList = [];
final ScrollController _scrollController = ScrollController(); // Ajout du ScrollController
late UserRegistrationData _registrationData; // Stocke l'état complet
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
bool _isScrollable = false;
bool _showLeftFade = false;
bool _showRightFade = false;
static const double _fadeExtent = 0.05; // Pourcentage de la vue pour le fondu (5%)
static const double _fadeExtent = 0.05; // Pourcentage de fondu
// Liste ordonnée des couleurs de cartes pour les enfants
static const List<CardColorVertical> _childCardColors = [
CardColorVertical.lavender, // Premier enfant toujours lavande
CardColorVertical.pink,
CardColorVertical.peach,
CardColorVertical.lime,
CardColorVertical.red,
CardColorVertical.green,
CardColorVertical.blue,
];
// Utilisation de GlobalKey pour les cartes enfants si validation complexe future
// Map<int, GlobalKey<FormState>> _childFormKeys = {};
@override
void initState() {
super.initState();
_addChild();
_registrationData = widget.registrationData;
// S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées
if (_registrationData.children.isEmpty) {
_addChild();
}
_scrollController.addListener(_scrollListener);
// Appel initial pour définir l'état des fondus après le premier layout
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
@override
void dispose() {
// Disposer les contrôleurs de tous les enfants
for (var childData in _childrenDataList) {
childData.dispose();
}
_scrollController.removeListener(_scrollListener); // Ne pas oublier de retirer le listener
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (!_scrollController.hasClients) return; // S'assurer que le controller est attaché
if (!_scrollController.hasClients) return;
final position = _scrollController.position;
final newIsScrollable = position.maxScrollExtent > 0.0;
// Le fondu à gauche est affiché si on a scrollé plus loin que la moitié de la zone de fondu
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
// Le fondu à droite est affiché s'il reste à scroller plus que la moitié de la zone de fondu
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
if (newIsScrollable != _isScrollable ||
newShowLeftFade != _showLeftFade ||
newShowRightFade != _showRightFade) {
if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) {
setState(() {
_isScrollable = newIsScrollable;
_showLeftFade = newShowLeftFade;
@ -103,110 +81,99 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
}
void _addChild() {
String initialLastName = '';
if (_childrenDataList.isNotEmpty) {
initialLastName = _childrenDataList.first.lastNameController.text;
}
setState(() {
_childrenDataList.add(_ChildFormData(
key: UniqueKey(),
initialLastName: initialLastName,
));
bool isUnborn = DataGenerator.boolean();
// Déterminer la couleur de la carte pour le nouvel enfant
final cardColor = _childCardColors[_registrationData.children.length % _childCardColors.length];
final newChild = ChildData(
lastName: _registrationData.parent1.lastName, // Hérite du nom de famille du parent 1
firstName: DataGenerator.firstName(),
dob: DataGenerator.dob(isUnborn: isUnborn),
isUnbornChild: isUnborn,
photoConsent: DataGenerator.boolean(),
multipleBirth: DataGenerator.boolean(),
cardColor: cardColor, // Assigner la couleur
// imageFile: null, // Pas d'image générée pour l'instant
);
_registrationData.addChild(newChild);
// Ajouter une clé de formulaire si nécessaire
// _childFormKeys[_registrationData.children.length - 1] = GlobalKey<FormState>();
});
// S'assurer que le listener est appelé après la mise à jour de l'UI
// et faire défiler vers la fin si possible
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollListener(); // Mettre à jour l'état des fondus
_scrollListener();
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
});
}
// Méthode pour sélectionner une image (devra être adaptée pour l'index)
void _removeChild(int index) {
if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) {
setState(() {
_registrationData.children.removeAt(index);
// Supprimer aussi la clé de formulaire associée si utilisée
// _childFormKeys.remove(index);
// Il faudrait aussi décaler les clés des enfants suivants si on utilise les index comme clés de map
});
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
}
Future<void> _pickImage(int childIndex) async {
final ImagePicker picker = ImagePicker();
try {
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 70,
maxWidth: 1024,
maxHeight: 1024,
);
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
if (pickedFile != null) {
setState(() {
if (childIndex < _childrenDataList.length) {
_childrenDataList[childIndex].imageFile = File(pickedFile.path);
if (childIndex < _registrationData.children.length) {
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
}
});
} // Fin de if (pickedFile != null)
} catch (e) {
print("Erreur lors de la sélection de l'image: $e");
}
}
} catch (e) { print("Erreur image: $e"); }
}
Future<void> _selectDate(BuildContext context, int childIndex) async {
final _ChildFormData currentChild = _childrenDataList[childIndex];
final ChildData currentChild = _registrationData.children[childIndex];
final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980);
DateTime lastDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
if (currentChild.isUnbornChild) {
firstDatePickerDate = now;
lastDatePickerDate = now.add(const Duration(days: 300));
if (currentChild.dobController.text.isNotEmpty) {
firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dobController.text.split('/');
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) { /* Ignorer */ }
} catch (e) {}
}
} else {
if (currentChild.dobController.text.isNotEmpty) {
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dobController.text.split('/');
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) { /* Ignorer */ }
} catch (e) {}
}
}
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDatePickerDate,
firstDate: firstDatePickerDate,
lastDate: lastDatePickerDate,
locale: const Locale('fr', 'FR'),
context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
currentChild.dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
}
}
void _removeChild(Key key) {
setState(() {
// Trouver et supprimer l'enfant par sa clé, et s'assurer qu'il en reste au moins un.
if (_childrenDataList.length > 1) {
_childrenDataList.removeWhere((child) => child.key == key);
}
});
// S'assurer que le listener est appelé après la mise à jour de l'UI
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
@ -217,101 +184,79 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/X', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Merci de renseigner les informations de/vos enfant(s) :',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Padding( // Ajout du Padding pour les marges latérales
padding: const EdgeInsets.symmetric(horizontal: 150.0), // Marge de 150px de chaque côté
child: SizedBox(
height: 500,
child: ShaderMask(
shaderCallback: (Rect bounds) {
// Déterminer les couleurs du gradient en fonction de l'état de défilement
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
// Si ce n'est pas scrollable du tout, pas de fondu.
if (!_isScrollable) {
return LinearGradient(
colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black],
stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],
).createShader(bounds);
}
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
leftFade, // Bord gauche
Colors.black, // Devient opaque
Colors.black, // Reste opaque
rightFade // Bord droit
],
stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], // 5% de fondu sur chaque bord
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Scrollbar( // Ajout du Scrollbar
controller: _scrollController, // Utiliser le même contrôleur
thumbVisibility: true, // Rendre la thumb toujours visible pour le web si souhaité, ou la laisser adaptative
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _childrenDataList.length + 1,
itemBuilder: (context, index) {
if (index < _childrenDataList.length) {
// Carte Enfant
return Padding(
padding: const EdgeInsets.only(right: 20.0), // Espace entre les cartes
child: _ChildCardWidget(
key: _childrenDataList[index].key, // Passer la clé unique
childData: _childrenDataList[index],
childIndex: index,
onPickImage: () => _pickImage(index),
onDateSelect: () => _selectDate(context, index),
onTogglePhotoConsent: (newValue) {
setState(() => _childrenDataList[index].photoConsent = newValue);
},
onToggleMultipleBirth: (newValue) {
setState(() => _childrenDataList[index].multipleBirth = newValue);
},
onToggleIsUnborn: (newValue) {
setState(() => _childrenDataList[index].isUnbornChild = newValue);
},
onRemove: () => _removeChild(_childrenDataList[index].key),
canBeRemoved: _childrenDataList.length > 1,
),
);
} else {
// Bouton Ajouter
return Center( // Pour centrer le bouton dans l'espace disponible
child: HoverReliefWidget(
onPressed: _addChild,
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
);
}
},
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations Enfants',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 150.0),
child: SizedBox(
height: 684.0,
child: ShaderMask(
shaderCallback: (Rect bounds) {
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
if (!_isScrollable) { return LinearGradient(colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); }
return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: <Color>[ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _registrationData.children.length + 1,
itemBuilder: (context, index) {
if (index < _registrationData.children.length) {
// Carte Enfant
return Padding(
padding: const EdgeInsets.only(right: 20.0),
child: _ChildCardWidget(
key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
childData: _registrationData.children[index],
childIndex: index,
onPickImage: () => _pickImage(index),
onDateSelect: () => _selectDate(context, index),
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
onToggleIsUnborn: (newValue) => setState(() {
_registrationData.children[index].isUnbornChild = newValue;
// Générer une nouvelle date si on change le statut
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
}),
onRemove: () => _removeChild(index),
canBeRemoved: _registrationData.children.length > 1,
),
);
} else {
// Bouton Ajouter
return Center(
child: HoverReliefWidget(
onPressed: _addChild,
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
);
}
},
),
),
),
),
const SizedBox(height: 20), // Espace optionnel après la liste
],
),
),
const SizedBox(height: 20),
],
),
),
// Chevrons de navigation
@ -330,8 +275,8 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
print('Passer à l\'étape 4 (Situation familiale et CGU)');
Navigator.pushNamed(context, '/parent-register/step4');
// TODO: Validation (si nécessaire)
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
},
tooltip: 'Suivant',
),
@ -342,24 +287,28 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
}
}
// Nouveau Widget pour la carte enfant
class _ChildCardWidget extends StatelessWidget {
final _ChildFormData childData;
final int childIndex; // Utile pour certains callbacks ou logging
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
final ChildData childData;
final int childIndex;
final VoidCallback onPickImage;
final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged;
final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn;
final VoidCallback onRemove; // Callback pour supprimer la carte
final bool canBeRemoved; // Pour afficher/cacher le bouton de suppression
final VoidCallback onRemove;
final bool canBeRemoved;
const _ChildCardWidget({
required Key key, // Important pour le ListView.builder
required Key key,
required this.childData,
required this.childIndex,
required this.onPickImage,
required this.onDateSelect,
required this.onFirstNameChanged,
required this.onLastNameChanged,
required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth,
required this.onToggleIsUnborn,
@ -367,105 +316,158 @@ class _ChildCardWidget extends StatelessWidget {
required this.canBeRemoved,
}) : super(key: key);
@override
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
}
class _ChildCardWidgetState extends State<_ChildCardWidget> {
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _dobController;
@override
void initState() {
super.initState();
// Initialiser les contrôleurs avec les données du widget
_firstNameController = TextEditingController(text: widget.childData.firstName);
_lastNameController = TextEditingController(text: widget.childData.lastName);
_dobController = TextEditingController(text: widget.childData.dob);
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
}
@override
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Mettre à jour les contrôleurs si les données externes changent
// (peut arriver si on recharge l'état global)
if (widget.childData.firstName != _firstNameController.text) {
_firstNameController.text = widget.childData.firstName;
}
if (widget.childData.lastName != _lastNameController.text) {
_lastNameController.text = widget.childData.lastName;
}
if (widget.childData.dob != _dobController.text) {
_dobController.text = widget.childData.dob;
}
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final File? currentChildImage = childData.imageFile;
final Color baseLavandeColor = Colors.purple.shade200;
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
final File? currentChildImage = widget.childData.imageFile;
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
? Colors.purple.shade200
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container(
width: 300,
padding: const EdgeInsets.all(20),
width: 345.0 * 1.1, // 379.5
height: 570.0 * 1.2, // 684.0
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
decoration: BoxDecoration(
image: const DecorationImage(image: AssetImage('assets/images/card_lavander.png'), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20),
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20 * 1.1), // 22
),
child: Stack( // Stack pour pouvoir superposer le bouton de suppression
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: onPickImage,
onPressed: widget.onPickImage,
borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow,
child: SizedBox(
height: 100, width: 100,
height: 200.0,
width: 200.0,
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0),
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
child: currentChildImage != null
? ClipRRect(borderRadius: BorderRadius.circular(10), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: 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: 5),
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600)),
Switch(value: childData.isUnbornChild, onChanged: onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
],
),
const SizedBox(height: 8),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: childData.firstNameController,
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !childData.isUnbornChild,
isRequired: !widget.childData.isUnbornChild,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 5),
const SizedBox(height: 6.0 * 1.1), // 6.6
CustomAppTextField(
controller: childData.lastNameController,
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 8),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: childData.dobController,
labelText: childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: onDateSelect,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 10),
const SizedBox(height: 11.0 * 1.1), // 12.1
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomCheckbox( // Utilisation du nouveau widget
AppCustomCheckbox(
label: 'Consentement photo',
value: childData.photoConsent,
onChanged: onTogglePhotoConsent,
value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
),
const SizedBox(height: 5),
AppCustomCheckbox( // Utilisation du nouveau widget
const SizedBox(height: 6.0 * 1.1), // 6.6
AppCustomCheckbox(
label: 'Naissance multiple',
value: childData.multipleBirth,
onChanged: onToggleMultipleBirth,
value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
),
],
),
],
),
if (canBeRemoved) // Afficher le bouton de suppression conditionnellement
if (widget.canBeRemoved)
Positioned(
top: -5, // Ajuster pour le positionnement visuel
right: -5, // Ajuster pour le positionnement visuel
top: -5, right: -5,
child: InkWell(
onTap: onRemove,
customBorder: const CircleBorder(), // Pour un effet de clic circulaire
onTap: widget.onRemove,
customBorder: const CircleBorder(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8), // Fond rouge pour le bouton X
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: Colors.red.withOpacity(0.8), shape: BoxShape.circle),
child: const Icon(Icons.close, color: Colors.white, size: 18),
),
),

View File

@ -3,18 +3,30 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
import 'package:p_tits_pas/models/placeholder_registration_data.dart';
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
import '../../models/user_registration_data.dart'; // Import du vrai modèle
import '../../utils/data_generator.dart'; // Import du générateur
class ParentRegisterStep4Screen extends StatefulWidget {
const ParentRegisterStep4Screen({super.key});
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep4Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
}
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
final _motivationController = TextEditingController();
bool _cguAccepted = false;
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
}
@override
void dispose() {
@ -113,7 +125,7 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
),
const SizedBox(height: 20),
Text(
'Merci de motiver votre demande création de compte :',
'Motivation de votre demande',
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
@ -132,35 +144,23 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
const SizedBox(height: 30),
GestureDetector(
onTap: () {
// Si on clique sur la zone et que ce n'est pas encore accepté,
// ou si c'est déjà accepté (l'utilisateur veut peut-être revoir les CGU)
_showCGUModal();
if (!_cguAccepted) {
_showCGUModal();
}
},
child: AppCustomCheckbox(
label: 'J\'accepte les conditions générales d\'utilisation',
value: _cguAccepted,
onChanged: (newValue) {
// La logique d'ouverture de la modale est déclenchée par le GestureDetector externe.
// La modale mettra à jour _cguAccepted et reconstruira le widget.
// Si newValue est true (ce qui signifie que la modale a é acceptée et _cguAccepted mis à jour),
// on n'a rien à faire de plus ici.
// Si newValue est false (l'utilisateur a réussi à la décocher d'une manière ou d'une autre,
// ce qui ne devrait pas arriver car on ouvre toujours la modale),
// on pourrait vouloir forcer l'affichage de la modale aussi.
// Pour l'instant, on se fie au fait que la modale gère l'acceptation.
if (!_cguAccepted) { // Si pas encore accepté, la modale doit s'ouvrir
_showCGUModal();
} else if (newValue == false) { // Si on essaie de décocher
// Optionnel: Forcer la non-acceptation et rouvrir la modale ?
// Pour l'instant, on ne fait rien, la modale gère.
if (!_cguAccepted) {
_showCGUModal();
} else {
setState(() => _cguAccepted = false);
}
},
// Vous pouvez ajuster checkboxSize et checkmarkSizeFactor si nécessaire
),
),
const SizedBox(height: 40),
// On ajoutera un Form et un bouton de soumission principal plus tard
// Pour l'instant, les chevrons servent à la navigation simple
],
),
),
@ -182,19 +182,16 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted
? () {
print('Motivation: ${_motivationController.text}');
print('CGU acceptées: $_cguAccepted');
// TODO: Rassembler toutes les données des étapes précédentes
final dummyData = PlaceholderRegistrationData(parent1Name: "Parent 1 Nom (Exemple)");
_registrationData.updateMotivation(_motivationController.text);
_registrationData.acceptCGU();
Navigator.pushNamed(
context,
'/parent-register/step5',
arguments: dummyData // Passer les données (ici factices)
arguments: _registrationData
);
}
: null, // Désactiver si les CGU ne sont pas acceptées
: null,
tooltip: 'Suivant',
),
),

View File

@ -1,18 +1,192 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/placeholder_registration_data.dart'; // Assurez-vous que le chemin est correct
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
// La définition locale de PlaceholderRegistrationData est supprimée ici.
// 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}) {
const double detailFontSize = 18.0;
const FontWeight labelFontWeight = FontWeight.w600;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: GoogleFonts.merienda(fontSize: detailFontSize, 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: detailFontSize),
maxLines: multiLine ? null : 1, // Permet plusieurs lignes si multiLine est true
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
),
],
);
}
class ParentRegisterStep5Screen extends StatelessWidget {
final PlaceholderRegistrationData registrationData; // Doit maintenant utiliser la version importée
final UserRegistrationData registrationData;
const ParentRegisterStep5Screen({super.key, required this.registrationData});
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
const double verticalSpacing = 15.0; // Espacement vertical entre les "champs"
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName)),
const SizedBox(width: 20), // Espace entre les champs dans la Row
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true)), // Email peut nécessiter plusieurs lignes
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80), // fieldHeight est une suggestion pour 2 lignes
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
);
}
// Méthode pour construire la carte Parent 2
Widget _buildParent2Card(BuildContext context, ParentData data) {
const double verticalSpacing = 15.0;
// Structure similaire à _buildParent1Card
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
);
}
// 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;
// Convertir CardColorVertical en CardColorHorizontal pour le récapitulatif
// Ceci suppose que les noms de couleurs correspondent entre les deux enums.
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
(e) => e.name == child.cardColor.name,
orElse: () => CardColorHorizontal.lavender, // Couleur par défaut si non trouvée
);
List<Widget> details = [
_buildDetailRow('Prénom', child.firstName),
_buildDetailRow('Nom', child.lastName),
_buildDetailRow(child.isUnbornChild ? 'Date Prév.' : 'Naissance', child.dob),
_buildDetailRow('Cons. Photo', child.photoConsent ? 'Oui' : 'Non'),
_buildDetailRow('Naiss. Mult.', child.multipleBirth ? 'Oui' : 'Non'),
];
return Padding(
padding: const EdgeInsets.only(bottom: 20.0), // Espace entre les cartes enfants
child: _SummaryCard(
backgroundImagePath: cardColorHorizontal.path, // Utiliser la couleur convertie
title: 'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
content: details,
onEdit: () { /* TODO: Naviguer vers step3 et focus l'enfant index */ },
),
);
}).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: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
);
}
// 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,
),
),
],
),
);
}
@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(
@ -22,55 +196,49 @@ class ParentRegisterStep5Screen extends StatelessWidget {
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.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),
// TODO: Construire les cartes récapitulatives ici
// _buildParent1Card(context, registrationData),
// if (registrationData.parent2Exists) _buildParent2Card(context, registrationData),
// ..._buildChildrenCards(context, registrationData),
// _buildMotivationCard(context, registrationData),
const SizedBox(height: 40),
// Utilisation du ImageButton
ImageButton(
bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
_showConfirmationModal(context);
},
),
],
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);
},
),
],
),
),
),
),
// Chevrons de navigation (uniquement retour vers étape 4)
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)
),
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',
),
@ -108,10 +276,64 @@ class ParentRegisterStep5Screen extends StatelessWidget {
},
);
}
}
// TODO: Méthodes pour construire les cartes individuelles
// Widget _buildParent1Card(BuildContext context, PlaceholderRegistrationData data) { ... }
// Widget _buildParent2Card(BuildContext context, PlaceholderRegistrationData data) { ... }
// List<Widget> _buildChildrenCards(BuildContext context, PlaceholderRegistrationData data) { ... }
// Widget _buildMotivationCard(BuildContext context, PlaceholderRegistrationData data) { ... }
// 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

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique
import '../../models/card_assets.dart'; // Import des enums de cartes
class RegisterChoiceScreen extends StatelessWidget {
const RegisterChoiceScreen({super.key});
@ -73,9 +74,9 @@ class RegisterChoiceScreen extends StatelessWidget {
aspectRatio: 2 / 3,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
decoration: const BoxDecoration(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/card_rose.png'),
image: AssetImage(CardColorVertical.pink.path),
fit: BoxFit.fill,
),
),

View File

@ -0,0 +1,65 @@
import 'dart:math';
class DataGenerator {
static final Random _random = Random();
static final List<String> _firstNames = [
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules',
'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire'
];
static final List<String> _lastNames = [
'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau',
'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier'
];
static final List<String> _addressSuffixes = [
'Rue de la Paix', 'Boulevard des Rêves', 'Avenue du Soleil', 'Place des Étoiles', 'Chemin des Champs'
];
static final List<String> _motivationSnippets = [
'Nous cherchons une personne de confiance.',
'Nos horaires sont atypiques.',
'Notre enfant est plein de vie.',
'Nous souhaitons une garde à temps plein.',
'Une adaptation en douceur est primordiale pour nous.',
'Nous avons hâte de vous rencontrer.',
'La pédagogie Montessori nous intéresse.'
];
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
static String address() => "${_random.nextInt(100) + 1} ${_addressSuffixes[_random.nextInt(_addressSuffixes.length)]}";
static String postalCode() => "750${_random.nextInt(10)}${_random.nextInt(10)}";
static String city() => "Paris";
static String phone() => "06${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}";
static String email(String firstName, String lastName) => "${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com";
static String password() => "password123"; // Simple pour le test
static String dob({bool isUnborn = false}) {
final now = DateTime.now();
if (isUnborn) {
final provisionalDate = now.add(Duration(days: _random.nextInt(180) + 30)); // Entre 1 et 7 mois dans le futur
return "${provisionalDate.day.toString().padLeft(2, '0')}/${provisionalDate.month.toString().padLeft(2, '0')}/${provisionalDate.year}";
} else {
final birthYear = now.year - _random.nextInt(3); // Enfants de 0 à 2 ans
final birthMonth = _random.nextInt(12) + 1;
final birthDay = _random.nextInt(28) + 1; // Simple, évite les pbs de jours/mois
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
}
}
static bool boolean() => _random.nextBool();
static String motivation() {
int count = _random.nextInt(3) + 2; // 2 à 4 phrases
List<String> chosenSnippets = [];
while(chosenSnippets.length < count) {
String snippet = _motivationSnippets[_random.nextInt(_motivationSnippets.length)];
if (!chosenSnippets.contains(snippet)) {
chosenSnippets.add(snippet);
}
}
return chosenSnippets.join(' ');
}
}

View File

@ -51,7 +51,7 @@ class AppCustomCheckbox extends StatelessWidget {
Flexible(
child: Text(
label,
style: GoogleFonts.merienda(fontSize: 14),
style: GoogleFonts.merienda(fontSize: 16),
overflow: TextOverflow.ellipsis, // Gérer le texte long
),
),

View File

@ -12,8 +12,8 @@ class CustomAppTextField extends StatefulWidget {
final TextEditingController controller;
final String labelText;
final String hintText;
final double fieldHeight;
final double fieldWidth;
final double fieldHeight;
final bool obscureText;
final TextInputType keyboardType;
final String? Function(String?)? validator;
@ -23,14 +23,16 @@ class CustomAppTextField extends StatefulWidget {
final bool readOnly;
final VoidCallback? onTap;
final IconData? suffixIcon;
final double labelFontSize;
final double inputFontSize;
const CustomAppTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText = '',
this.fieldHeight = 50.0,
this.fieldWidth = 300.0,
this.fieldHeight = 53.0,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.validator,
@ -40,6 +42,8 @@ class CustomAppTextField extends StatefulWidget {
this.readOnly = false,
this.onTap,
this.suffixIcon,
this.labelFontSize = 18.0,
this.inputFontSize = 18.0,
});
@override
@ -61,6 +65,10 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
@override
Widget build(BuildContext context) {
const double fontHeightMultiplier = 1.2;
const double internalVerticalPadding = 16.0;
final double dynamicFieldHeight = widget.fieldHeight;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -68,7 +76,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
Text(
widget.labelText,
style: GoogleFonts.merienda(
fontSize: 14,
fontSize: widget.labelFontSize,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
@ -76,7 +84,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
const SizedBox(height: 6),
SizedBox(
width: widget.fieldWidth,
height: widget.fieldHeight,
height: dynamicFieldHeight,
child: Stack(
alignment: Alignment.centerLeft,
children: [
@ -87,7 +95,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
),
),
Padding(
padding: const EdgeInsets.only(left: 18.0, right: 18.0, bottom: 2.0),
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
child: TextFormField(
controller: widget.controller,
obscureText: widget.obscureText,
@ -95,7 +103,10 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
enabled: widget.enabled,
readOnly: widget.readOnly,
onTap: widget.onTap,
style: GoogleFonts.merienda(fontSize: 15, color: widget.enabled ? Colors.black87 : Colors.grey),
style: GoogleFonts.merienda(
fontSize: widget.inputFontSize,
color: widget.enabled ? Colors.black87 : Colors.grey
),
validator: widget.validator ??
(value) {
if (!widget.enabled || widget.readOnly) return null;
@ -106,13 +117,13 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
},
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black54.withOpacity(0.7)),
hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero,
suffixIcon: widget.suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 0.0),
child: Icon(widget.suffixIcon, color: Colors.black54, size: 20),
child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1),
)
: null,
isDense: true,

View File

@ -30,6 +30,7 @@ flutter:
assets:
- assets/images/ # Déclarer le dossier entier
- assets/cards/ # Nouveau dossier de cartes
fonts:
- family: Merienda

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,22 @@
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
),
const SizedBox(height: 6.0),
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
),
const SizedBox(height: 9.0),
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
),