[#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:
2026-04-11 18:07:24 +02:00
parent cde676c4f9
commit fdd1e06e77
49 changed files with 3179 additions and 828 deletions
@@ -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,