feat(inscription): email accusé réception, photos enfants, formulaires identité
- Backend: mail après inscription parent avec n° dossier, UPLOAD_PHOTOS_DIR, réponse API - Frontend: imageBytes + payload photo, utils email/code postal/téléphone, champs dédiés - Formulaires admin, login (focus), personal_info, child_card, custom_app_text_field Made-with: Cursor
This commit is contained in:
@@ -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,6 +3,7 @@ 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';
|
||||
@@ -152,10 +153,19 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
if (pickedFile != null) {
|
||||
if (childIndex < registrationData.children.length) {
|
||||
final oldChild = registrationData.children[childIndex];
|
||||
final updatedChild = oldChild.copyWith(imageFile: File(pickedFile.path));
|
||||
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"); }
|
||||
}
|
||||
|
||||
@@ -285,13 +295,14 @@ 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));
|
||||
registrationData.updateChild(
|
||||
index, c.copyWith(imageFile: null, imageBytes: null));
|
||||
}),
|
||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||
onFirstNameChanged: (value) => setState(() {
|
||||
@@ -392,13 +403,14 @@ 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));
|
||||
registrationData.updateChild(
|
||||
index, c.copyWith(imageFile: null, imageBytes: null));
|
||||
}),
|
||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||
onFirstNameChanged: (value) => setState(() {
|
||||
|
||||
Reference in New Issue
Block a user