[#101] [Frontend] Inscription parent — API, soumission et validation
Squash merge de develop vers master. Livrables principaux (ticket #101 et mise au point associée) : - Branchement du formulaire d'inscription parent sur POST /api/v1/auth/register/parent - Payload DTO (parents, enfants, photos base64, CGU) et services Auth - Parcours gestionnaire : cartes dossiers, wizard validation famille, images authentifiées - Scripts d'inscription test (Martin, Durand/Rousseau, Lecomte) ; .gitignore .cursor/ Inclut également les ajustements develop fusionnés dans ce lot (inscription AM, champs relais, etc.). Closes #101 Made-with: Cursor
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||
|
||||
class AdminCreateDialog extends StatefulWidget {
|
||||
final AppUser? initialUser;
|
||||
@@ -61,11 +63,10 @@ class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
final base = _required(value, 'Email');
|
||||
if (base != null) return base;
|
||||
final email = value!.trim();
|
||||
final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
|
||||
if (!ok) return 'Format email invalide';
|
||||
return null;
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateEmail(value, allowEmpty: true);
|
||||
}
|
||||
|
||||
String? _validatePassword(String? value) {
|
||||
@@ -78,6 +79,17 @@ class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateTelephone(String? value) {
|
||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
final base = _required(value, 'Téléphone');
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_isSubmitting) return;
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
@@ -305,13 +317,9 @@ class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return TextFormField(
|
||||
return EmailTextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
label: 'Email',
|
||||
validator: _validateEmail,
|
||||
);
|
||||
}
|
||||
@@ -346,19 +354,10 @@ class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
||||
}
|
||||
|
||||
Widget _buildTelephoneField() {
|
||||
return TextFormField(
|
||||
return FrenchPhoneTextFormField(
|
||||
controller: _telephoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
FrenchPhoneNumberFormatter(),
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) => _required(v, 'Téléphone'),
|
||||
label: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
validator: _validateTelephone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:p_tits_pas/models/relais_model.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/relais_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
@@ -163,11 +165,10 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
final base = _required(value, 'Email');
|
||||
if (base != null) return base;
|
||||
final email = value!.trim();
|
||||
final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
|
||||
if (!ok) return 'Format email invalide';
|
||||
return null;
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateEmail(value, allowEmpty: true);
|
||||
}
|
||||
|
||||
String? _validatePassword(String? value) {
|
||||
@@ -185,15 +186,10 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
return null;
|
||||
}
|
||||
final base = _required(value, 'Téléphone');
|
||||
if (base != null) return base;
|
||||
final digits = normalizePhone(value!);
|
||||
if (digits.length != 10) {
|
||||
return 'Le téléphone doit contenir 10 chiffres';
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
if (!digits.startsWith('0')) {
|
||||
return 'Le téléphone doit commencer par 0';
|
||||
}
|
||||
return null;
|
||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
String _toTitleCase(String raw) {
|
||||
@@ -536,14 +532,10 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return TextFormField(
|
||||
return EmailTextFormField(
|
||||
controller: _emailController,
|
||||
readOnly: widget.readOnly,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
label: 'Email',
|
||||
validator: widget.readOnly ? null : _validateEmail,
|
||||
);
|
||||
}
|
||||
@@ -584,21 +576,10 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
}
|
||||
|
||||
Widget _buildTelephoneField() {
|
||||
return TextFormField(
|
||||
return FrenchPhoneTextFormField(
|
||||
controller: _telephoneController,
|
||||
readOnly: widget.readOnly,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: widget.readOnly
|
||||
? null
|
||||
: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
FrenchPhoneNumberFormatter(),
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
label: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
validator: widget.readOnly ? null : _validatePhone,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,29 +13,15 @@ class AmRegisterStep1Screen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final registrationData = Provider.of<AmRegistrationData>(context, listen: false);
|
||||
|
||||
// Données de test : Marie DUBOIS (jeu de test 03_seed_test_data.sql / docs/test-data)
|
||||
PersonalInfoData initialData;
|
||||
if (registrationData.firstName.isEmpty) {
|
||||
initialData = PersonalInfoData(
|
||||
firstName: 'Marie',
|
||||
lastName: 'DUBOIS',
|
||||
phone: '0696345678',
|
||||
email: 'marie.dubois@ptits-pas.fr',
|
||||
address: '25 Rue de la République',
|
||||
postalCode: '95870',
|
||||
city: 'Bezons',
|
||||
);
|
||||
} else {
|
||||
initialData = PersonalInfoData(
|
||||
firstName: registrationData.firstName,
|
||||
lastName: registrationData.lastName,
|
||||
phone: registrationData.phone,
|
||||
email: registrationData.email,
|
||||
address: registrationData.streetAddress,
|
||||
postalCode: registrationData.postalCode,
|
||||
city: registrationData.city,
|
||||
);
|
||||
}
|
||||
final initialData = PersonalInfoData(
|
||||
firstName: registrationData.firstName,
|
||||
lastName: registrationData.lastName,
|
||||
phone: registrationData.phone,
|
||||
email: registrationData.email,
|
||||
address: registrationData.streetAddress,
|
||||
postalCode: registrationData.postalCode,
|
||||
city: registrationData.city,
|
||||
);
|
||||
|
||||
return PersonalInfoFormScreen(
|
||||
stepText: 'Étape 1/4',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import '../../models/am_registration_data.dart';
|
||||
import '../../models/card_assets.dart';
|
||||
@@ -17,24 +15,9 @@ class AmRegisterStep2Screen extends StatefulWidget {
|
||||
|
||||
class _AmRegisterStep2ScreenState extends State<AmRegisterStep2Screen> {
|
||||
String? _photoPathFramework;
|
||||
File? _photoFile;
|
||||
|
||||
Future<void> _pickPhoto() async {
|
||||
// TODO: Remplacer par la vraie logique ImagePicker
|
||||
// final imagePicker = ImagePicker();
|
||||
// final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery);
|
||||
// if (pickedFile != null) {
|
||||
// setState(() {
|
||||
// _photoFile = File(pickedFile.path);
|
||||
// _photoPathFramework = pickedFile.path;
|
||||
// });
|
||||
// } else {
|
||||
setState(() {
|
||||
_photoPathFramework = 'assets/images/icon_assmat.png';
|
||||
_photoFile = null;
|
||||
});
|
||||
// }
|
||||
print("Photo sélectionnée: $_photoPathFramework");
|
||||
// TODO: brancher ImagePicker ; ne pas préremplir de chemin factice
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -53,20 +36,6 @@ class _AmRegisterStep2ScreenState extends State<AmRegisterStep2Screen> {
|
||||
capacity: registrationData.capacity,
|
||||
);
|
||||
|
||||
// Données de test : Marie DUBOIS (jeu de test 03_seed_test_data.sql / docs/test-data)
|
||||
if (registrationData.dateOfBirth == null && registrationData.nir.isEmpty) {
|
||||
initialData = ProfessionalInfoData(
|
||||
photoPath: 'assets/images/icon_assmat.png',
|
||||
photoConsent: true,
|
||||
dateOfBirth: DateTime(1980, 6, 8),
|
||||
birthCity: 'Bezons',
|
||||
birthCountry: 'France',
|
||||
nir: '280062A00100191',
|
||||
agrementNumber: 'AGR-2019-095001',
|
||||
capacity: 4,
|
||||
);
|
||||
}
|
||||
|
||||
return ProfessionalInfoFormScreen(
|
||||
stepText: 'Étape 2/4',
|
||||
title: 'Vos informations professionnelles',
|
||||
|
||||
@@ -13,22 +13,13 @@ class AmRegisterStep3Screen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final data = Provider.of<AmRegistrationData>(context, listen: false);
|
||||
|
||||
// Données de test : Marie DUBOIS (jeu de test 03_seed_test_data.sql / docs/test-data)
|
||||
String initialText = data.presentationText;
|
||||
bool initialCgu = data.cguAccepted;
|
||||
|
||||
if (initialText.isEmpty) {
|
||||
initialText = 'Assistante maternelle agréée depuis 2019. Spécialité bébés 0-18 mois. Accueil bienveillant et cadre sécurisant. 2 places disponibles.';
|
||||
initialCgu = true;
|
||||
}
|
||||
|
||||
return PresentationFormScreen(
|
||||
stepText: 'Étape 3/4',
|
||||
title: 'Présentation et Conditions',
|
||||
cardColor: CardColorHorizontal.peach,
|
||||
textFieldHint: 'Ex: Disponible immédiatement, 10 ans d\'expérience, formation premiers secours...',
|
||||
initialText: initialText,
|
||||
initialCguAccepted: initialCgu,
|
||||
initialText: data.presentationText,
|
||||
initialCguAccepted: data.cguAccepted,
|
||||
previousRoute: '/am-register-step2',
|
||||
onSubmit: (text, cguAccepted) {
|
||||
data.updatePresentationAndCgu(
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/bug_report_service.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import '../../widgets/image_button.dart';
|
||||
import '../../widgets/custom_app_text_field.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
@@ -21,6 +22,10 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final GlobalKey<FormFieldState<String>> _emailFormKey =
|
||||
GlobalKey<FormFieldState<String>>();
|
||||
late final FocusNode _emailFocus;
|
||||
late final FocusNode _passwordFocus;
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
@@ -36,22 +41,49 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_desktopRiverLogoDimensionsFuture = _getImageDimensions();
|
||||
_emailFocus = FocusNode();
|
||||
_passwordFocus = FocusNode();
|
||||
_emailFocus.addListener(_onEmailFocusChange);
|
||||
}
|
||||
|
||||
void _onEmailFocusChange() {
|
||||
if (_emailFocus.hasFocus) {
|
||||
return;
|
||||
}
|
||||
// Reporter au frame suivant : une mise à jour synchrone du controller pendant
|
||||
// un transfert de focus (Tab) peut casser la navigation au clavier.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _emailFocus.hasFocus) {
|
||||
return;
|
||||
}
|
||||
final normalized = normalizeEmailText(_emailController.text);
|
||||
if (normalized != _emailController.text) {
|
||||
_emailController.value = TextEditingValue(
|
||||
text: normalized,
|
||||
selection: TextSelection.collapsed(offset: normalized.length),
|
||||
);
|
||||
}
|
||||
_emailFormKey.currentState?.validate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailFocus.removeListener(_onEmailFocusChange);
|
||||
_emailFocus.dispose();
|
||||
_passwordFocus.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
final v = value ?? '';
|
||||
final v = value?.trim() ?? '';
|
||||
if (v.isEmpty) {
|
||||
return 'Veuillez entrer votre email';
|
||||
return 'Veuillez entrer votre adresse e-mail.';
|
||||
}
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(v)) {
|
||||
return 'Veuillez entrer un email valide';
|
||||
if (!isValidEmailFormat(v)) {
|
||||
return 'L’adresse e-mail n’est pas valide.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -205,53 +237,75 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||
child: AutofillGroup(
|
||||
child: AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Champs côte à côte
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomAppTextField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 53,
|
||||
fieldWidth: double.infinity,
|
||||
FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(1),
|
||||
child: CustomAppTextField(
|
||||
formFieldKey: _emailFormKey,
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocus,
|
||||
labelText: 'Email',
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType:
|
||||
TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
inputFormatters: const [
|
||||
EmailMaxLengthFormatter(),
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (_) =>
|
||||
_passwordFocus.requestFocus(),
|
||||
validator: _validateEmail,
|
||||
style:
|
||||
CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 53,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
labelText: 'Mot de passe',
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password,
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted:
|
||||
_handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 53,
|
||||
fieldWidth: double.infinity,
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(2),
|
||||
child: CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
labelText: 'Mot de passe',
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password,
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted:
|
||||
_handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style:
|
||||
CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 53,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -461,46 +515,68 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
horizontal: 24, vertical: 20),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: AutofillGroup(
|
||||
child: AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppTextField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
showLabel: false,
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
labelText: 'Mot de passe',
|
||||
showLabel: false,
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: _handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
child: FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(1),
|
||||
child: CustomAppTextField(
|
||||
formFieldKey: _emailFormKey,
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocus,
|
||||
labelText: 'Email',
|
||||
showLabel: false,
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType:
|
||||
TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
inputFormatters: const [
|
||||
EmailMaxLengthFormatter(),
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (_) =>
|
||||
_passwordFocus.requestFocus(),
|
||||
validator: _validateEmail,
|
||||
style:
|
||||
CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(2),
|
||||
child: CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
labelText: 'Mot de passe',
|
||||
showLabel: false,
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted:
|
||||
_handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style:
|
||||
CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
@@ -571,6 +647,7 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 8),
|
||||
child: Wrap(
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../models/user_registration_data.dart';
|
||||
import '../../utils/data_generator.dart';
|
||||
import '../../widgets/personal_info_form_screen.dart';
|
||||
import '../../models/card_assets.dart';
|
||||
|
||||
@@ -15,31 +14,15 @@ class ParentRegisterStep1Screen extends StatelessWidget {
|
||||
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
|
||||
final parent1 = registrationData.parent1;
|
||||
|
||||
// Générer des données de test si vide
|
||||
PersonalInfoData initialData;
|
||||
if (parent1.firstName.isEmpty) {
|
||||
final genFirstName = DataGenerator.firstName();
|
||||
final genLastName = DataGenerator.lastName();
|
||||
initialData = PersonalInfoData(
|
||||
firstName: genFirstName,
|
||||
lastName: genLastName,
|
||||
phone: DataGenerator.phone(),
|
||||
email: DataGenerator.email(genFirstName, genLastName),
|
||||
address: DataGenerator.address(),
|
||||
postalCode: DataGenerator.postalCode(),
|
||||
city: DataGenerator.city(),
|
||||
);
|
||||
} else {
|
||||
initialData = PersonalInfoData(
|
||||
firstName: parent1.firstName,
|
||||
lastName: parent1.lastName,
|
||||
phone: parent1.phone,
|
||||
email: parent1.email,
|
||||
address: parent1.address,
|
||||
postalCode: parent1.postalCode,
|
||||
city: parent1.city,
|
||||
);
|
||||
}
|
||||
final initialData = PersonalInfoData(
|
||||
firstName: parent1.firstName,
|
||||
lastName: parent1.lastName,
|
||||
phone: parent1.phone,
|
||||
email: parent1.email,
|
||||
address: parent1.address,
|
||||
postalCode: parent1.postalCode,
|
||||
city: parent1.city,
|
||||
);
|
||||
|
||||
return PersonalInfoFormScreen(
|
||||
stepText: 'Étape 1/5',
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../models/user_registration_data.dart';
|
||||
import '../../utils/data_generator.dart';
|
||||
import '../../widgets/personal_info_form_screen.dart';
|
||||
import '../../models/card_assets.dart';
|
||||
|
||||
@@ -19,21 +18,17 @@ class ParentRegisterStep2Screen extends StatelessWidget {
|
||||
bool hasParent2 = parent2 != null;
|
||||
bool sameAddress = false;
|
||||
|
||||
// Générer des données de test si vide
|
||||
PersonalInfoData initialData;
|
||||
if (parent2 == null || parent2.firstName.isEmpty) {
|
||||
final genFirstName = DataGenerator.firstName();
|
||||
final genLastName = DataGenerator.lastName();
|
||||
sameAddress = DataGenerator.boolean();
|
||||
|
||||
sameAddress = false;
|
||||
initialData = PersonalInfoData(
|
||||
firstName: genFirstName,
|
||||
lastName: genLastName,
|
||||
phone: DataGenerator.phone(),
|
||||
email: DataGenerator.email(genFirstName, genLastName),
|
||||
address: sameAddress ? parent1.address : DataGenerator.address(),
|
||||
postalCode: sameAddress ? parent1.postalCode : DataGenerator.postalCode(),
|
||||
city: sameAddress ? parent1.city : DataGenerator.city(),
|
||||
firstName: parent2?.firstName ?? '',
|
||||
lastName: parent2?.lastName ?? '',
|
||||
phone: parent2?.phone ?? '',
|
||||
email: parent2?.email ?? '',
|
||||
address: parent2?.address ?? '',
|
||||
postalCode: parent2?.postalCode ?? '',
|
||||
city: parent2?.city ?? '',
|
||||
);
|
||||
} else {
|
||||
sameAddress = (parent2.address == parent1.address &&
|
||||
|
||||
@@ -3,15 +3,16 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io' show File;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import '../../widgets/hover_relief_widget.dart';
|
||||
import '../../widgets/child_card_widget.dart';
|
||||
import '../../widgets/custom_navigation_button.dart';
|
||||
import '../../models/user_registration_data.dart';
|
||||
import '../../utils/data_generator.dart';
|
||||
import '../../models/card_assets.dart';
|
||||
import '../../config/display_config.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||
|
||||
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||
// final UserRegistrationData registrationData; // Supprimé
|
||||
@@ -75,6 +76,22 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Même logique que nom / prénom parent : normalisation avant passage à l’étape suivante.
|
||||
void _normalizeChildrenNamesAndGoToStep4(
|
||||
BuildContext context,
|
||||
UserRegistrationData registrationData,
|
||||
) {
|
||||
for (var i = 0; i < registrationData.children.length; i++) {
|
||||
final c = registrationData.children[i];
|
||||
final fn = formatPersonNameCase(c.firstName);
|
||||
final ln = formatPersonNameCase(c.lastName);
|
||||
if (fn != c.firstName || ln != c.lastName) {
|
||||
registrationData.updateChild(i, c.copyWith(firstName: fn, lastName: ln));
|
||||
}
|
||||
}
|
||||
context.go('/parent-register-step4');
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final position = _scrollController.position;
|
||||
@@ -92,8 +109,6 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
|
||||
void _addChild(UserRegistrationData registrationData) { // Prend registrationData
|
||||
setState(() {
|
||||
bool isUnborn = DataGenerator.boolean();
|
||||
|
||||
// Trouver la première couleur non utilisée
|
||||
CardColorVertical cardColor = _childCardColors.firstWhere(
|
||||
(color) => !_usedColors.contains(color),
|
||||
@@ -102,11 +117,11 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
|
||||
final newChild = ChildData(
|
||||
lastName: registrationData.parent1.lastName,
|
||||
firstName: DataGenerator.firstName(),
|
||||
dob: DataGenerator.dob(isUnborn: isUnborn),
|
||||
isUnbornChild: isUnborn,
|
||||
photoConsent: DataGenerator.boolean(),
|
||||
multipleBirth: DataGenerator.boolean(),
|
||||
firstName: '',
|
||||
dob: '',
|
||||
isUnbornChild: false,
|
||||
photoConsent: false,
|
||||
multipleBirth: false,
|
||||
cardColor: cardColor,
|
||||
);
|
||||
registrationData.addChild(newChild);
|
||||
@@ -134,23 +149,27 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
try {
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 60,
|
||||
maxWidth: 900,
|
||||
maxHeight: 900,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
if (childIndex < registrationData.children.length) {
|
||||
final oldChild = registrationData.children[childIndex];
|
||||
final updatedChild = ChildData(
|
||||
firstName: oldChild.firstName,
|
||||
lastName: oldChild.lastName,
|
||||
dob: oldChild.dob,
|
||||
photoConsent: oldChild.photoConsent,
|
||||
multipleBirth: oldChild.multipleBirth,
|
||||
isUnbornChild: oldChild.isUnbornChild,
|
||||
imageFile: File(pickedFile.path),
|
||||
cardColor: oldChild.cardColor,
|
||||
);
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
if (bytes.isEmpty) return;
|
||||
File? file;
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
final f = File(pickedFile.path);
|
||||
if (await f.exists()) file = f;
|
||||
} catch (_) {}
|
||||
}
|
||||
final updatedChild = oldChild.copyWith(imageBytes: bytes, imageFile: file);
|
||||
registrationData.updateChild(childIndex, updatedChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { print("Erreur image: $e"); }
|
||||
}
|
||||
|
||||
@@ -188,15 +207,8 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
);
|
||||
if (picked != null) {
|
||||
final oldChild = registrationData.children[childIndex];
|
||||
final updatedChild = ChildData(
|
||||
firstName: oldChild.firstName,
|
||||
lastName: oldChild.lastName,
|
||||
dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
|
||||
photoConsent: oldChild.photoConsent,
|
||||
multipleBirth: oldChild.multipleBirth,
|
||||
isUnbornChild: oldChild.isUnbornChild,
|
||||
imageFile: oldChild.imageFile,
|
||||
cardColor: oldChild.cardColor,
|
||||
final updatedChild = oldChild.copyWith(
|
||||
dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
|
||||
);
|
||||
registrationData.updateChild(childIndex, updatedChild);
|
||||
}
|
||||
@@ -287,40 +299,42 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
// Générer les cartes enfants
|
||||
for (int index = 0; index < registrationData.children.length; index++) ...[
|
||||
ChildCardWidget(
|
||||
key: ValueKey(registrationData.children[index].hashCode),
|
||||
key: ValueKey('parent_register_child_$index'),
|
||||
childData: registrationData.children[index],
|
||||
childIndex: index,
|
||||
onPickImage: () => _pickImage(index, registrationData),
|
||||
onClearImage: () => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(
|
||||
index, c.copyWith(imageFile: null, imageBytes: null));
|
||||
}),
|
||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||
))),
|
||||
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||
))),
|
||||
onFirstNameChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(firstName: value));
|
||||
}),
|
||||
onLastNameChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||||
}),
|
||||
onGenreChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(genre: value));
|
||||
}),
|
||||
onTogglePhotoConsent: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
|
||||
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
},
|
||||
onToggleMultipleBirth: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
|
||||
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||||
},
|
||||
onToggleIsUnborn: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
|
||||
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
|
||||
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
var g = oldChild.genre;
|
||||
if (!newValue && g == 'Autre') {
|
||||
g = '';
|
||||
}
|
||||
registrationData.updateChild(
|
||||
index,
|
||||
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||||
);
|
||||
},
|
||||
onRemove: () => _removeChild(index, registrationData),
|
||||
canBeRemoved: registrationData.children.length > 1,
|
||||
@@ -343,7 +357,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
|
||||
const SizedBox(height: 30),
|
||||
// Boutons navigation en bas du scroll
|
||||
_buildMobileButtons(context, config, screenSize),
|
||||
_buildMobileButtons(context, config, screenSize, registrationData),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
@@ -393,41 +407,43 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: ChildCardWidget(
|
||||
key: ValueKey(registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
|
||||
key: ValueKey('parent_register_child_$index'),
|
||||
childData: registrationData.children[index],
|
||||
childIndex: index,
|
||||
onPickImage: () => _pickImage(index, registrationData),
|
||||
onClearImage: () => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(
|
||||
index, c.copyWith(imageFile: null, imageBytes: null));
|
||||
}),
|
||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||
))),
|
||||
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||
))),
|
||||
onFirstNameChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(firstName: value));
|
||||
}),
|
||||
onLastNameChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||||
}),
|
||||
onGenreChanged: (value) => setState(() {
|
||||
final c = registrationData.children[index];
|
||||
registrationData.updateChild(index, c.copyWith(genre: value));
|
||||
}),
|
||||
onTogglePhotoConsent: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
|
||||
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
},
|
||||
onToggleMultipleBirth: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
|
||||
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||||
},
|
||||
onToggleIsUnborn: (newValue) {
|
||||
final oldChild = registrationData.children[index];
|
||||
registrationData.updateChild(index, ChildData(
|
||||
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
|
||||
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
|
||||
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||
));
|
||||
},
|
||||
final oldChild = registrationData.children[index];
|
||||
var g = oldChild.genre;
|
||||
if (!newValue && g == 'Autre') {
|
||||
g = '';
|
||||
}
|
||||
registrationData.updateChild(
|
||||
index,
|
||||
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||||
);
|
||||
},
|
||||
onRemove: () => _removeChild(index, registrationData),
|
||||
canBeRemoved: registrationData.children.length > 1,
|
||||
),
|
||||
@@ -455,7 +471,12 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
}
|
||||
|
||||
/// Boutons navigation mobile
|
||||
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||
Widget _buildMobileButtons(
|
||||
BuildContext context,
|
||||
DisplayConfig config,
|
||||
Size screenSize,
|
||||
UserRegistrationData registrationData,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -483,7 +504,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
text: 'Suivant',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: () {
|
||||
context.go('/parent-register-step4');
|
||||
_normalizeChildrenNamesAndGoToStep4(context, registrationData);
|
||||
},
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../models/user_registration_data.dart';
|
||||
import '../../widgets/presentation_form_screen.dart';
|
||||
import '../../models/card_assets.dart';
|
||||
import '../../utils/data_generator.dart';
|
||||
|
||||
class ParentRegisterStep4Screen extends StatelessWidget {
|
||||
const ParentRegisterStep4Screen({super.key});
|
||||
@@ -14,22 +13,13 @@ class ParentRegisterStep4Screen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
|
||||
|
||||
// Générer un texte de test si vide
|
||||
String initialText = registrationData.motivationText;
|
||||
bool initialCgu = registrationData.cguAccepted;
|
||||
|
||||
if (initialText.isEmpty) {
|
||||
initialText = DataGenerator.motivation();
|
||||
initialCgu = true;
|
||||
}
|
||||
|
||||
return PresentationFormScreen(
|
||||
stepText: 'Étape 4/5',
|
||||
title: 'Motivation de votre demande',
|
||||
cardColor: CardColorHorizontal.green,
|
||||
textFieldHint: 'Écrivez ici pour motiver votre demande...',
|
||||
initialText: initialText,
|
||||
initialCguAccepted: initialCgu,
|
||||
initialText: registrationData.motivationText,
|
||||
initialCguAccepted: registrationData.cguAccepted,
|
||||
previousRoute: '/parent-register-step3',
|
||||
onSubmit: (text, cguAccepted) {
|
||||
registrationData.updateMotivation(text);
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../widgets/custom_navigation_button.dart';
|
||||
import '../../widgets/personal_info_form_screen.dart';
|
||||
import '../../widgets/child_card_widget.dart';
|
||||
import '../../widgets/presentation_form_screen.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
|
||||
class ParentRegisterStep5Screen extends StatefulWidget {
|
||||
const ParentRegisterStep5Screen({super.key});
|
||||
@@ -22,6 +23,36 @@ class ParentRegisterStep5Screen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
||||
bool _isSubmitting = false;
|
||||
|
||||
Future<void> _submitRegistration(BuildContext context, UserRegistrationData data) async {
|
||||
if (_isSubmitting) return;
|
||||
setState(() => _isSubmitting = true);
|
||||
try {
|
||||
await AuthService.registerParent(data);
|
||||
if (!context.mounted) return;
|
||||
_showSuccessModal(context);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
final msg = e.toString().replaceAll('Exception: ', '');
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('Envoi impossible', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
||||
content: Text(msg, style: GoogleFonts.merienda(fontSize: 14)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final registrationData = Provider.of<UserRegistrationData>(context);
|
||||
@@ -102,12 +133,11 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
||||
Expanded(
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Soumettre',
|
||||
text: _isSubmitting ? 'Envoi…' : 'Soumettre',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: () {
|
||||
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
||||
_showConfirmationModal(context);
|
||||
},
|
||||
onPressed: _isSubmitting
|
||||
? () {}
|
||||
: () => _submitRegistration(context, registrationData),
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
@@ -118,18 +148,20 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
||||
),
|
||||
)
|
||||
else
|
||||
ImageButton(
|
||||
bg: 'assets/images/bg_green.png',
|
||||
text: 'Soumettre ma demande',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
width: 350,
|
||||
height: 50,
|
||||
fontSize: 18,
|
||||
onPressed: () {
|
||||
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
||||
_showConfirmationModal(context);
|
||||
},
|
||||
),
|
||||
_isSubmitting
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ImageButton(
|
||||
bg: 'assets/images/bg_green.png',
|
||||
text: 'Soumettre ma demande',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
width: 350,
|
||||
height: 50,
|
||||
fontSize: 18,
|
||||
onPressed: () => _submitRegistration(context, registrationData),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -216,11 +248,12 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
||||
childIndex: index,
|
||||
mode: DisplayMode.readonly,
|
||||
onPickImage: () {},
|
||||
onClearImage: () {},
|
||||
onDateSelect: () {},
|
||||
onFirstNameChanged: (v) {},
|
||||
onLastNameChanged: (v) {},
|
||||
onGenreChanged: (v) {},
|
||||
onTogglePhotoConsent: (v) {},
|
||||
onToggleMultipleBirth: (v) {},
|
||||
onToggleIsUnborn: (v) {},
|
||||
onRemove: () {},
|
||||
canBeRemoved: false,
|
||||
@@ -239,14 +272,14 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
||||
cardColor: CardColorHorizontal.green, // Changé de pink à green
|
||||
textFieldHint: '',
|
||||
initialText: data.motivationText,
|
||||
initialCguAccepted: true, // Toujours true ici car déjà passé
|
||||
initialCguAccepted: data.cguAccepted,
|
||||
previousRoute: '',
|
||||
onSubmit: (t, c) {},
|
||||
onEdit: () => context.go('/parent-register-step4'),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConfirmationModal(BuildContext context) {
|
||||
void _showSuccessModal(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
||||
Reference in New Issue
Block a user