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_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart'; import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/home/home_screen.dart'; import '../screens/home/home_screen.dart';
import '../models/placeholder_registration_data.dart'; import '../models/user_registration_data.dart';
class AppRouter { class AppRouter {
static const String login = '/login'; static const String login = '/login';
@ -23,6 +23,12 @@ class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen; Widget screen;
bool slideTransition = false; 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) { switch (settings.name) {
case login: case login:
@ -30,32 +36,43 @@ class AppRouter {
break; break;
case registerChoice: case registerChoice:
screen = const RegisterChoiceScreen(); screen = const RegisterChoiceScreen();
slideTransition = true; // Activer la transition pour cet écran slideTransition = true;
break; break;
case parentRegisterStep1: case parentRegisterStep1:
screen = const ParentRegisterStep1Screen(); screen = const ParentRegisterStep1Screen();
slideTransition = true; // Activer la transition pour cet écran slideTransition = true;
break; break;
case parentRegisterStep2: case parentRegisterStep2:
screen = const ParentRegisterStep2Screen(); if (args is UserRegistrationData) {
screen = ParentRegisterStep2Screen(registrationData: args);
} else {
screen = buildErrorScreen('2');
}
slideTransition = true; slideTransition = true;
break; break;
case parentRegisterStep3: case parentRegisterStep3:
screen = const ParentRegisterStep3Screen(); if (args is UserRegistrationData) {
screen = ParentRegisterStep3Screen(registrationData: args);
} else {
screen = buildErrorScreen('3');
}
slideTransition = true; slideTransition = true;
break; break;
case parentRegisterStep4: case parentRegisterStep4:
screen = const ParentRegisterStep4Screen(); if (args is UserRegistrationData) {
screen = ParentRegisterStep4Screen(registrationData: args);
} else {
screen = buildErrorScreen('4');
}
slideTransition = true; slideTransition = true;
break; break;
case parentRegisterStep5: case parentRegisterStep5:
final args = settings.arguments as PlaceholderRegistrationData?; if (args is UserRegistrationData) {
if (args != null) {
screen = ParentRegisterStep5Screen(registrationData: args); screen = ParentRegisterStep5Screen(registrationData: args);
} else { } else {
print("Erreur: Données d'inscription manquantes pour l'étape 5"); screen = buildErrorScreen('5');
screen = const RegisterChoiceScreen();
} }
slideTransition = true;
break; break;
case home: case home:
screen = const HomeScreen(); screen = const HomeScreen();
@ -72,22 +89,16 @@ class AppRouter {
return PageRouteBuilder( return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen, pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) { 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 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 tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween); 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 { } else {
// Transition par défaut pour les autres écrans
return MaterialPageRoute(builder: (_) => screen); return MaterialPageRoute(builder: (_) => screen);
} }
} }

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron 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 { class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key}); const ParentRegisterStep1Screen({super.key});
@ -11,17 +15,42 @@ class ParentRegisterStep1Screen extends StatefulWidget {
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> { class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>(); 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 _lastNameController = TextEditingController();
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
final _phoneController = TextEditingController(); final _phoneController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); 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 @override
void dispose() { void dispose() {
@ -61,13 +90,13 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
children: [ children: [
// Indicateur d'étape (à rendre dynamique) // Indicateur d'étape (à rendre dynamique)
Text( Text(
'Étape 1/X', 'Étape 1/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Texte d'instruction // Texte d'instruction
Text( Text(
'Merci de renseigner les informations du premier parent :', 'Informations du Parent Principal',
style: GoogleFonts.merienda( style: GoogleFonts.merienda(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -80,10 +109,11 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
// Carte jaune contenant le formulaire // Carte jaune contenant le formulaire
Container( Container(
width: screenSize.width * 0.6, width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
decoration: const BoxDecoration( constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage('assets/images/card_yellow_h.png'), image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
), ),
@ -94,35 +124,49 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Votre nom de famille')), Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20), Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Votre prénom')), Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone')), 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)),
const SizedBox(width: 20), Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail')), 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), const SizedBox(height: 20),
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Créez votre 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) {
const SizedBox(width: 20), if (value == null || value.isEmpty) return 'Mot de passe requis';
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe')), 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), 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), const SizedBox(height: 20),
Row( Row(
children: [ 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), 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), icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () { onPressed: () {
if (_formKey.currentState?.validate() ?? false) { if (_formKey.currentState?.validate() ?? false) {
// TODO: Sauvegarder les données du parent 1 _registrationData.updateParent1(
Navigator.pushNamed(context, '/parent-register/step2'); // Naviguer vers l'étape 2 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', 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:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron 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 { 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 @override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState(); State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
@ -11,25 +17,54 @@ class ParentRegisterStep2Screen extends StatefulWidget {
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> { class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
// TODO: Recevoir les infos du parent 1 pour pré-remplir l'adresse bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
// String? _parent1Address; bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// String? _parent1PostalCode;
// String? _parent1City;
bool _addParent2 = false; // Par défaut, on n'ajoute pas le parent 2 // Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
bool _sameAddressAsParent1 = false;
// Contrôleurs pour les champs du parent 2
final _lastNameController = TextEditingController(); final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
final _phoneController = TextEditingController(); final _phoneController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); 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 @override
void dispose() { void dispose() {
@ -45,9 +80,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
super.dispose(); super.dispose();
} }
// Helper pour activer/désactiver tous les champs sauf l'adresse
bool get _parent2FieldsEnabled => _addParent2; bool get _parent2FieldsEnabled => _addParent2;
// Helper pour activer/désactiver les champs d'adresse
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1; bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override @override
@ -57,174 +90,104 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
// Fond papier
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
), ),
),
// Contenu centré
Center( Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Indicateur d'étape Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
Text(
'Étape 2/X', // Mettre à jour le numéro d'étape total
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10), const SizedBox(height: 10),
// Texte d'instruction
Text( Text(
'Renseignez les informations du deuxième parent (optionnel) :', 'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda( style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// Carte bleue contenant le formulaire
Container( Container(
width: screenSize.width * 0.6, width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: const BoxDecoration( // Retour à la décoration decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
image: AssetImage('assets/images/card_blue_h.png'), // Utilisation de l'image horizontale
fit: BoxFit.fill,
), ),
),
// Suppression du Stack et Transform.rotate
child: Form( child: Form(
key: _formKey, key: _formKey,
child: SingleChildScrollView( // Le SingleChildScrollView redevient l'enfant direct child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// --- Interrupteurs sur une ligne ---
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Option 1: Ajouter Parent 2
Expanded( Expanded(
child: Row( flex: 12,
mainAxisAlignment: MainAxisAlignment.center, child: Row(children: [
children: [ const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
const Icon(Icons.person_add_alt_1, size: 20), Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
const SizedBox(width: 8), const Spacer(),
Flexible( Switch(value: _addParent2, onChanged: (val) => setState(() {
child: Text( _addParent2 = val ?? false;
'Ajouter Parent 2 ?', if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
style: GoogleFonts.merienda(fontWeight: FontWeight.bold), }), activeColor: Theme.of(context).primaryColor),
overflow: TextOverflow.ellipsis, ]),
), ),
), Expanded(flex: 1, child: const SizedBox()),
_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( Expanded(
child: Row( flex: 12,
mainAxisAlignment: MainAxisAlignment.center, child: Row(children: [
children: [ Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), // Griser l'icône si désactivé
const SizedBox(width: 8), const SizedBox(width: 8),
Flexible( Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
child: Text( const Spacer(),
'Même Adresse ?', Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), // Griser le texte si désactivé _sameAddressAsParent1 = val ?? false;
overflow: TextOverflow.ellipsis,
),
),
_buildCustomSwitch(
value: _sameAddressAsParent1,
onChanged: _addParent2 ? (bool? newValue) {
final bool actualValue = newValue ?? false;
setState(() {
_sameAddressAsParent1 = actualValue;
if (_sameAddressAsParent1) { if (_sameAddressAsParent1) {
_addressController.clear(); _addressController.text = _registrationData.parent1.address;
_postalCodeController.clear(); _postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.clear(); _cityController.text = _registrationData.parent1.city;
// TODO: Pré-remplir } else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} }
}); }) : null, activeColor: Theme.of(context).primaryColor),
} : null, ]),
), ),
], ]),
), const SizedBox(height: 25),
),
],
),
const SizedBox(height: 25), // Espacement ajusté après les switchs
// --- Champs du Parent 2 (conditionnels) ---
// Nom & Prénom
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: '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)),
const SizedBox(width: 20), Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Prénom du deuxième parent', enabled: _parent2FieldsEnabled)), 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), const SizedBox(height: 20),
// Téléphone & Email
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son numéro de téléphone', 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)),
const SizedBox(width: 20), Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son adresse e-mail', enabled: _parent2FieldsEnabled)), 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), const SizedBox(height: 20),
// Mot de passe
Row( Row(
children: [ children: [
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: '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)),
const SizedBox(width: 20), Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmer son mot de passe', enabled: _parent2FieldsEnabled)), 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), const SizedBox(height: 20),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
// --- Champs Adresse (conditionnels) ---
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
children: [ 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), 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( Positioned(
top: screenSize.height / 2 - 20, top: screenSize.height / 2 - 20,
left: 40, left: 40,
child: IconButton( child: IconButton(
icon: Transform( icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
alignment: Alignment.center, onPressed: () => Navigator.pop(context),
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context), // Retour à l'étape 1
tooltip: 'Retour', tooltip: 'Retour',
), ),
), ),
// Chevron de navigation droit (Suivant)
Positioned( Positioned(
top: screenSize.height / 2 - 20, top: screenSize.height / 2 - 20,
right: 40, right: 40,
child: IconButton( child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40), icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () { onPressed: () {
// Si on n'ajoute pas de parent 2, on passe directement if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (!_addParent2) { if (_addParent2) {
// Naviguer vers l'étape 3 (enfants) _registrationData.updateParent2(
print('Passer à l\'étape 3 (enfants) - Sans Parent 2'); ParentData(
Navigator.pushNamed(context, '/parent-register/step3'); firstName: _firstNameController.text,
return; 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);
} }
// Si on ajoute un parent 2 Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
// 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');
} }
}, },
tooltip: 'Suivant', tooltip: 'Suivant',
@ -290,85 +242,14 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
); );
} }
// --- NOUVEAU WIDGET --- void _clearParent2Fields() {
// Widget pour construire un switch personnalisé avec images _formKey.currentState?.reset();
Widget _buildCustomSwitch({required bool value, required ValueChanged<bool?>? onChanged}) { _lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
// --- DEBUG --- _emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
print("Building Custom Switch with value: $value"); _addressController.clear();
// ------------- _postalCodeController.clear();
const double switchHeight = 25.0; _cityController.clear();
const double switchWidth = 40.0; _sameAddressAsParent1 = false;
setState(() {});
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;
*/
},
),
),
],
);
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron 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 '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
// import 'package:image_cropper/image_cropper.dart'; // Supprimé // 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 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox 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 // La classe _ChildFormData est supprimée car on utilise ChildData du modèle
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();
}
}
class ParentRegisterStep3Screen extends StatefulWidget { class ParentRegisterStep3Screen extends StatefulWidget {
const ParentRegisterStep3Screen({super.key}); final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep3Screen({super.key, required this.registrationData});
@override @override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState(); State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
} }
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> { class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs late UserRegistrationData _registrationData; // Stocke l'état complet
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
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
bool _isScrollable = false; bool _isScrollable = false;
bool _showLeftFade = false; bool _showLeftFade = false;
bool _showRightFade = 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_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(); _addChild();
}
_scrollController.addListener(_scrollListener); _scrollController.addListener(_scrollListener);
// Appel initial pour définir l'état des fondus après le premier layout
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener()); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
} }
@override @override
void dispose() { void dispose() {
// Disposer les contrôleurs de tous les enfants _scrollController.removeListener(_scrollListener);
for (var childData in _childrenDataList) {
childData.dispose();
}
_scrollController.removeListener(_scrollListener); // Ne pas oublier de retirer le listener
_scrollController.dispose(); _scrollController.dispose();
super.dispose(); super.dispose();
} }
void _scrollListener() { void _scrollListener() {
if (!_scrollController.hasClients) return; // S'assurer que le controller est attaché if (!_scrollController.hasClients) return;
final position = _scrollController.position; final position = _scrollController.position;
final newIsScrollable = position.maxScrollExtent > 0.0; 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); 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)); 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(() { setState(() {
_isScrollable = newIsScrollable; _isScrollable = newIsScrollable;
_showLeftFade = newShowLeftFade; _showLeftFade = newShowLeftFade;
@ -103,110 +81,99 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
} }
void _addChild() { void _addChild() {
String initialLastName = '';
if (_childrenDataList.isNotEmpty) {
initialLastName = _childrenDataList.first.lastNameController.text;
}
setState(() { setState(() {
_childrenDataList.add(_ChildFormData( bool isUnborn = DataGenerator.boolean();
key: UniqueKey(), // Déterminer la couleur de la carte pour le nouvel enfant
initialLastName: initialLastName, final cardColor = _childCardColors[_registrationData.children.length % _childCardColors.length];
));
}); final newChild = ChildData(
// S'assurer que le listener est appelé après la mise à jour de l'UI lastName: _registrationData.parent1.lastName, // Hérite du nom de famille du parent 1
// et faire défiler vers la fin si possible firstName: DataGenerator.firstName(),
WidgetsBinding.instance.addPostFrameCallback((_) { dob: DataGenerator.dob(isUnborn: isUnborn),
_scrollListener(); // Mettre à jour l'état des fondus isUnbornChild: isUnborn,
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) { photoConsent: DataGenerator.boolean(),
_scrollController.animateTo( multipleBirth: DataGenerator.boolean(),
_scrollController.position.maxScrollExtent, cardColor: cardColor, // Assigner la couleur
duration: const Duration(milliseconds: 300), // imageFile: null, // Pas d'image générée pour l'instant
curve: Curves.easeOut,
); );
_registrationData.addChild(newChild);
// Ajouter une clé de formulaire si nécessaire
// _childFormKeys[_registrationData.children.length - 1] = GlobalKey<FormState>();
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollListener();
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
_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 { Future<void> _pickImage(int childIndex) async {
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
try { try {
final XFile? pickedFile = await picker.pickImage( final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery, source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
imageQuality: 70,
maxWidth: 1024,
maxHeight: 1024,
);
if (pickedFile != null) { if (pickedFile != null) {
setState(() { setState(() {
if (childIndex < _childrenDataList.length) { if (childIndex < _registrationData.children.length) {
_childrenDataList[childIndex].imageFile = File(pickedFile.path); _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 { Future<void> _selectDate(BuildContext context, int childIndex) async {
final _ChildFormData currentChild = _childrenDataList[childIndex]; final ChildData currentChild = _registrationData.children[childIndex];
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now; DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980); DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
DateTime lastDatePickerDate = now;
if (currentChild.isUnbornChild) { if (currentChild.isUnbornChild) {
firstDatePickerDate = now; firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
lastDatePickerDate = now.add(const Duration(days: 300)); if (currentChild.dob.isNotEmpty) {
if (currentChild.dobController.text.isNotEmpty) {
try { 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')}"); 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)) { if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate; initialDatePickerDate = parsedDate;
} }
} catch (e) { /* Ignorer */ } } catch (e) {}
} }
} else { } else {
if (currentChild.dobController.text.isNotEmpty) { if (currentChild.dob.isNotEmpty) {
try { 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')}"); 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)) { if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate; initialDatePickerDate = parsedDate;
} }
} catch (e) { /* Ignorer */ } } catch (e) {}
} }
} }
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
initialDate: initialDatePickerDate, lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
firstDate: firstDatePickerDate,
lastDate: lastDatePickerDate,
locale: const Locale('fr', 'FR'),
); );
if (picked != null) { if (picked != null) {
setState(() { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
@ -217,85 +184,64 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
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),
), ),
Center( Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Étape 3/X', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'Merci de renseigner les informations de/vos enfant(s) :', 'Informations Enfants',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Padding( // Ajout du Padding pour les marges latérales Padding(
padding: const EdgeInsets.symmetric(horizontal: 150.0), // Marge de 150px de chaque côté padding: const EdgeInsets.symmetric(horizontal: 150.0),
child: SizedBox( child: SizedBox(
height: 500, height: 684.0,
child: ShaderMask( child: ShaderMask(
shaderCallback: (Rect bounds) { 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 leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
final Color rightFade = (_isScrollable && _showRightFade) ? 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); }
// Si ce n'est pas scrollable du tout, pas de fondu. 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);
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, blendMode: BlendMode.dstIn,
child: Scrollbar( // Ajout du Scrollbar child: Scrollbar(
controller: _scrollController, // Utiliser le même contrôleur controller: _scrollController,
thumbVisibility: true, // Rendre la thumb toujours visible pour le web si souhaité, ou la laisser adaptative thumbVisibility: true,
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0), padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _childrenDataList.length + 1, itemCount: _registrationData.children.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index < _childrenDataList.length) { if (index < _registrationData.children.length) {
// Carte Enfant // Carte Enfant
return Padding( return Padding(
padding: const EdgeInsets.only(right: 20.0), // Espace entre les cartes padding: const EdgeInsets.only(right: 20.0),
child: _ChildCardWidget( child: _ChildCardWidget(
key: _childrenDataList[index].key, // Passer la clé unique key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
childData: _childrenDataList[index], childData: _registrationData.children[index],
childIndex: index, childIndex: index,
onPickImage: () => _pickImage(index), onPickImage: () => _pickImage(index),
onDateSelect: () => _selectDate(context, index), onDateSelect: () => _selectDate(context, index),
onTogglePhotoConsent: (newValue) { onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
setState(() => _childrenDataList[index].photoConsent = newValue); onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
}, onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
onToggleMultipleBirth: (newValue) { onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
setState(() => _childrenDataList[index].multipleBirth = newValue); onToggleIsUnborn: (newValue) => setState(() {
}, _registrationData.children[index].isUnbornChild = newValue;
onToggleIsUnborn: (newValue) { // Générer une nouvelle date si on change le statut
setState(() => _childrenDataList[index].isUnbornChild = newValue); _registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
}, }),
onRemove: () => _removeChild(_childrenDataList[index].key), onRemove: () => _removeChild(index),
canBeRemoved: _childrenDataList.length > 1, canBeRemoved: _registrationData.children.length > 1,
), ),
); );
} else { } else {
// Bouton Ajouter // Bouton Ajouter
return Center( // Pour centrer le bouton dans l'espace disponible return Center(
child: HoverReliefWidget( child: HoverReliefWidget(
onPressed: _addChild, onPressed: _addChild,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
@ -309,11 +255,10 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
), ),
), ),
), ),
const SizedBox(height: 20), // Espace optionnel après la liste const SizedBox(height: 20),
], ],
), ),
), ),
),
// Chevrons de navigation // Chevrons de navigation
Positioned( Positioned(
top: screenSize.height / 2 - 20, top: screenSize.height / 2 - 20,
@ -330,8 +275,8 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
child: IconButton( child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40), icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () { onPressed: () {
print('Passer à l\'étape 4 (Situation familiale et CGU)'); // TODO: Validation (si nécessaire)
Navigator.pushNamed(context, '/parent-register/step4'); Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
}, },
tooltip: 'Suivant', tooltip: 'Suivant',
), ),
@ -342,24 +287,28 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
} }
} }
// Nouveau Widget pour la carte enfant // Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
class _ChildCardWidget extends StatelessWidget { class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
final _ChildFormData childData; final ChildData childData;
final int childIndex; // Utile pour certains callbacks ou logging final int childIndex;
final VoidCallback onPickImage; final VoidCallback onPickImage;
final VoidCallback onDateSelect; final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged;
final ValueChanged<bool> onTogglePhotoConsent; final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth; final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn; final ValueChanged<bool> onToggleIsUnborn;
final VoidCallback onRemove; // Callback pour supprimer la carte final VoidCallback onRemove;
final bool canBeRemoved; // Pour afficher/cacher le bouton de suppression final bool canBeRemoved;
const _ChildCardWidget({ const _ChildCardWidget({
required Key key, // Important pour le ListView.builder required Key key,
required this.childData, required this.childData,
required this.childIndex, required this.childIndex,
required this.onPickImage, required this.onPickImage,
required this.onDateSelect, required this.onDateSelect,
required this.onFirstNameChanged,
required this.onLastNameChanged,
required this.onTogglePhotoConsent, required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth, required this.onToggleMultipleBirth,
required this.onToggleIsUnborn, required this.onToggleIsUnborn,
@ -367,105 +316,158 @@ class _ChildCardWidget extends StatelessWidget {
required this.canBeRemoved, required this.canBeRemoved,
}) : super(key: key); }) : 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final File? currentChildImage = childData.imageFile; final File? currentChildImage = widget.childData.imageFile;
final Color baseLavandeColor = Colors.purple.shade200; // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90); final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130); ? 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( return Container(
width: 300, width: 345.0 * 1.1, // 379.5
padding: const EdgeInsets.all(20), height: 570.0 * 1.2, // 684.0
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
decoration: BoxDecoration( decoration: BoxDecoration(
image: const DecorationImage(image: AssetImage('assets/images/card_lavander.png'), fit: BoxFit.cover), image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20 * 1.1), // 22
), ),
child: Stack( // Stack pour pouvoir superposer le bouton de suppression child: Stack(
children: [ children: [
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
HoverReliefWidget( HoverReliefWidget(
onPressed: onPickImage, onPressed: widget.onPickImage,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow, initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow, hoverShadowColor: hoverPhotoShadow,
child: SizedBox( child: SizedBox(
height: 100, width: 100, height: 200.0,
width: 200.0,
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
child: currentChildImage != null 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), : 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600)), Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Switch(value: childData.isUnbornChild, onChanged: onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), 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( CustomAppTextField(
controller: childData.firstNameController, controller: _firstNameController,
labelText: 'Prénom', labelText: 'Prénom',
hintText: 'Facultatif si à naître', 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( CustomAppTextField(
controller: childData.lastNameController, controller: _lastNameController,
labelText: 'Nom', labelText: 'Nom',
hintText: 'Nom de l\'enfant', hintText: 'Nom de l\'enfant',
enabled: true, enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
), ),
const SizedBox(height: 8), const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField( CustomAppTextField(
controller: childData.dobController, controller: _dobController,
labelText: childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA', hintText: 'JJ/MM/AAAA',
readOnly: true, readOnly: true,
onTap: onDateSelect, onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today, 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( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AppCustomCheckbox( // Utilisation du nouveau widget AppCustomCheckbox(
label: 'Consentement photo', label: 'Consentement photo',
value: childData.photoConsent, value: widget.childData.photoConsent,
onChanged: onTogglePhotoConsent, onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
), ),
const SizedBox(height: 5), const SizedBox(height: 6.0 * 1.1), // 6.6
AppCustomCheckbox( // Utilisation du nouveau widget AppCustomCheckbox(
label: 'Naissance multiple', label: 'Naissance multiple',
value: childData.multipleBirth, value: widget.childData.multipleBirth,
onChanged: onToggleMultipleBirth, onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
), ),
], ],
), ),
], ],
), ),
if (canBeRemoved) // Afficher le bouton de suppression conditionnellement if (widget.canBeRemoved)
Positioned( Positioned(
top: -5, // Ajuster pour le positionnement visuel top: -5, right: -5,
right: -5, // Ajuster pour le positionnement visuel
child: InkWell( child: InkWell(
onTap: onRemove, onTap: widget.onRemove,
customBorder: const CircleBorder(), // Pour un effet de clic circulaire customBorder: const CircleBorder(),
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.red.withOpacity(0.8), shape: BoxShape.circle),
color: Colors.red.withOpacity(0.8), // Fond rouge pour le bouton X
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 18), 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 '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 '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/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 { class ParentRegisterStep4Screen extends StatefulWidget {
const ParentRegisterStep4Screen({super.key}); final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep4Screen({super.key, required this.registrationData});
@override @override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState(); State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
} }
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> { class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
final _motivationController = TextEditingController(); 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 @override
void dispose() { void dispose() {
@ -113,7 +125,7 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( 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), style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -132,35 +144,23 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
const SizedBox(height: 30), const SizedBox(height: 30),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// Si on clique sur la zone et que ce n'est pas encore accepté, if (!_cguAccepted) {
// ou si c'est déjà accepté (l'utilisateur veut peut-être revoir les CGU)
_showCGUModal(); _showCGUModal();
}
}, },
child: AppCustomCheckbox( child: AppCustomCheckbox(
label: 'J\'accepte les conditions générales d\'utilisation', label: 'J\'accepte les conditions générales d\'utilisation',
value: _cguAccepted, value: _cguAccepted,
onChanged: (newValue) { onChanged: (newValue) {
// La logique d'ouverture de la modale est déclenchée par le GestureDetector externe. if (!_cguAccepted) {
// 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(); _showCGUModal();
} else if (newValue == false) { // Si on essaie de décocher } else {
// Optionnel: Forcer la non-acceptation et rouvrir la modale ? setState(() => _cguAccepted = false);
// Pour l'instant, on ne fait rien, la modale gère.
} }
}, },
// Vous pouvez ajuster checkboxSize et checkmarkSizeFactor si nécessaire
), ),
), ),
const SizedBox(height: 40), 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), icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted onPressed: _cguAccepted
? () { ? () {
print('Motivation: ${_motivationController.text}'); _registrationData.updateMotivation(_motivationController.text);
print('CGU acceptées: $_cguAccepted'); _registrationData.acceptCGU();
// TODO: Rassembler toutes les données des étapes précédentes
final dummyData = PlaceholderRegistrationData(parent1Name: "Parent 1 Nom (Exemple)");
Navigator.pushNamed( Navigator.pushNamed(
context, context,
'/parent-register/step5', '/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', tooltip: 'Suivant',
), ),
), ),

View File

@ -1,18 +1,192 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../models/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 '../../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 { class ParentRegisterStep5Screen extends StatelessWidget {
final PlaceholderRegistrationData registrationData; // Doit maintenant utiliser la version importée final UserRegistrationData registrationData;
const ParentRegisterStep5Screen({super.key, required this.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold( return Scaffold(
body: Stack( body: Stack(
@ -22,31 +196,27 @@ class ParentRegisterStep5Screen extends StatelessWidget {
), ),
Center( Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
'Étape 5/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
'Récapitulatif de votre demande',
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30), const SizedBox(height: 30),
// TODO: Construire les cartes récapitulatives ici _buildParent1Card(context, registrationData.parent1),
// _buildParent1Card(context, registrationData), const SizedBox(height: 20),
// if (registrationData.parent2Exists) _buildParent2Card(context, registrationData), if (registrationData.parent2 != null) ...[
// ..._buildChildrenCards(context, registrationData), _buildParent2Card(context, registrationData.parent2!),
// _buildMotivationCard(context, registrationData), const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
const SizedBox(height: 40), const SizedBox(height: 40),
// Utilisation du ImageButton
ImageButton( ImageButton(
bg: 'assets/images/btn_green.png', bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande', text: 'Soumettre ma demande',
@ -55,6 +225,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
height: 50, height: 50,
fontSize: 18, fontSize: 18,
onPressed: () { onPressed: () {
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_showConfirmationModal(context); _showConfirmationModal(context);
}, },
), ),
@ -62,15 +233,12 @@ class ParentRegisterStep5Screen extends StatelessWidget {
), ),
), ),
), ),
// Chevrons de navigation (uniquement retour vers étape 4) ),
Positioned( Positioned(
top: screenSize.height / 2 - 20, top: screenSize.height / 2 - 20,
left: 40, left: 40,
child: IconButton( child: IconButton(
icon: Transform.flip( icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
flipX: true,
child: Image.asset('assets/images/chevron_right.png', height: 40)
),
onPressed: () => Navigator.pop(context), // Retour à l'étape 4 onPressed: () => Navigator.pop(context), // Retour à l'étape 4
tooltip: 'Retour', 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 générique _SummaryCard (ajusté)
// Widget _buildParent2Card(BuildContext context, PlaceholderRegistrationData data) { ... } class _SummaryCard extends StatelessWidget {
// List<Widget> _buildChildrenCards(BuildContext context, PlaceholderRegistrationData data) { ... } final String backgroundImagePath;
// Widget _buildMotivationCard(BuildContext context, PlaceholderRegistrationData data) { ... } 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 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron import 'dart:math' as math; // Pour la rotation du chevron
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique 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 { class RegisterChoiceScreen extends StatelessWidget {
const RegisterChoiceScreen({super.key}); const RegisterChoiceScreen({super.key});
@ -73,9 +74,9 @@ class RegisterChoiceScreen extends StatelessWidget {
aspectRatio: 2 / 3, aspectRatio: 2 / 3,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
decoration: const BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage('assets/images/card_rose.png'), image: AssetImage(CardColorVertical.pink.path),
fit: BoxFit.fill, 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( Flexible(
child: Text( child: Text(
label, label,
style: GoogleFonts.merienda(fontSize: 14), style: GoogleFonts.merienda(fontSize: 16),
overflow: TextOverflow.ellipsis, // Gérer le texte long overflow: TextOverflow.ellipsis, // Gérer le texte long
), ),
), ),

View File

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

View File

@ -30,6 +30,7 @@ flutter:
assets: assets:
- assets/images/ # Déclarer le dossier entier - assets/images/ # Déclarer le dossier entier
- assets/cards/ # Nouveau dossier de cartes
fonts: fonts:
- family: Merienda - 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,
),