feat(front): inscription parent — genre obligatoire H/F/Autre (#101)

- ChildData.genre + copyWith ; liste déroulante sur la carte enfant
- Validation et payload API alignés sur GenreType
- Étape 3 : mises à jour via copyWith (préservation du genre)

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-03-28 18:08:32 +01:00
parent de60a001cc
commit cd2bf63b8a
5 changed files with 145 additions and 66 deletions

View File

@ -1,7 +1,6 @@
import 'dart:io'; // Pour File import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
// import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé // import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé
class ParentData { class ParentData {
@ -32,6 +31,8 @@ class ChildData {
String firstName; String firstName;
String lastName; String lastName;
String dob; // Date de naissance ou prévisionnelle String dob; // Date de naissance ou prévisionnelle
/// Valeurs API : `H`, `F`, `Autre` (GenreType backend). Vide tant que non choisi.
String genre;
bool photoConsent; bool photoConsent;
bool multipleBirth; bool multipleBirth;
bool isUnbornChild; bool isUnbornChild;
@ -42,12 +43,37 @@ class ChildData {
this.firstName = '', this.firstName = '',
this.lastName = '', this.lastName = '',
this.dob = '', this.dob = '',
this.genre = '',
this.photoConsent = false, this.photoConsent = false,
this.multipleBirth = false, this.multipleBirth = false,
this.isUnbornChild = false, this.isUnbornChild = false,
this.imageFile, this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur required this.cardColor, // Rendre requis dans le constructeur
}); });
ChildData copyWith({
String? firstName,
String? lastName,
String? dob,
String? genre,
bool? photoConsent,
bool? multipleBirth,
bool? isUnbornChild,
File? imageFile,
CardColorVertical? cardColor,
}) {
return ChildData(
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
dob: dob ?? this.dob,
genre: genre ?? this.genre,
photoConsent: photoConsent ?? this.photoConsent,
multipleBirth: multipleBirth ?? this.multipleBirth,
isUnbornChild: isUnbornChild ?? this.isUnbornChild,
imageFile: imageFile ?? this.imageFile,
cardColor: cardColor ?? this.cardColor,
);
}
} }
// Nouvelle classe pour les détails bancaires // Nouvelle classe pour les détails bancaires

View File

@ -135,16 +135,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
if (pickedFile != null) { if (pickedFile != null) {
if (childIndex < registrationData.children.length) { if (childIndex < registrationData.children.length) {
final oldChild = registrationData.children[childIndex]; final oldChild = registrationData.children[childIndex];
final updatedChild = ChildData( final updatedChild = oldChild.copyWith(imageFile: File(pickedFile.path));
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,
);
registrationData.updateChild(childIndex, updatedChild); registrationData.updateChild(childIndex, updatedChild);
} }
} }
@ -185,15 +176,8 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
); );
if (picked != null) { if (picked != null) {
final oldChild = registrationData.children[childIndex]; final oldChild = registrationData.children[childIndex];
final updatedChild = ChildData( final updatedChild = oldChild.copyWith(
firstName: oldChild.firstName, dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
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,
); );
registrationData.updateChild(childIndex, updatedChild); registrationData.updateChild(childIndex, updatedChild);
} }
@ -289,35 +273,29 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
childIndex: index, childIndex: index,
onPickImage: () => _pickImage(index, registrationData), onPickImage: () => _pickImage(index, registrationData),
onDateSelect: () => _selectDate(context, index, registrationData), onDateSelect: () => _selectDate(context, index, registrationData),
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( onFirstNameChanged: (value) => setState(() {
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, final c = registrationData.children[index];
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor registrationData.updateChild(index, c.copyWith(firstName: value));
))), }),
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( onLastNameChanged: (value) => setState(() {
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, final c = registrationData.children[index];
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor registrationData.updateChild(index, c.copyWith(lastName: value));
))), }),
onGenreChanged: (value) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, oldChild.copyWith(genre: value));
},
onTogglePhotoConsent: (newValue) { onTogglePhotoConsent: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
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) { onToggleMultipleBirth: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(multipleBirth: newValue));
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
}, },
onToggleIsUnborn: (newValue) { onToggleIsUnborn: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(isUnbornChild: newValue));
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob,
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
}, },
onRemove: () => _removeChild(index, registrationData), onRemove: () => _removeChild(index, registrationData),
canBeRemoved: registrationData.children.length > 1, canBeRemoved: registrationData.children.length > 1,
@ -395,36 +373,30 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
childIndex: index, childIndex: index,
onPickImage: () => _pickImage(index, registrationData), onPickImage: () => _pickImage(index, registrationData),
onDateSelect: () => _selectDate(context, index, registrationData), onDateSelect: () => _selectDate(context, index, registrationData),
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( onFirstNameChanged: (value) => setState(() {
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, final c = registrationData.children[index];
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor registrationData.updateChild(index, c.copyWith(firstName: value));
))), }),
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( onLastNameChanged: (value) => setState(() {
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, final c = registrationData.children[index];
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor registrationData.updateChild(index, c.copyWith(lastName: value));
))), }),
onGenreChanged: (value) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, oldChild.copyWith(genre: value));
},
onTogglePhotoConsent: (newValue) { onTogglePhotoConsent: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
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) { onToggleMultipleBirth: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(multipleBirth: newValue));
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
}, },
onToggleIsUnborn: (newValue) { onToggleIsUnborn: (newValue) {
final oldChild = registrationData.children[index]; final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData( registrationData.updateChild(index, oldChild.copyWith(isUnbornChild: newValue));
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, },
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onRemove: () => _removeChild(index, registrationData), onRemove: () => _removeChild(index, registrationData),
canBeRemoved: registrationData.children.length > 1, canBeRemoved: registrationData.children.length > 1,
), ),

View File

@ -251,6 +251,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
onDateSelect: () {}, onDateSelect: () {},
onFirstNameChanged: (v) {}, onFirstNameChanged: (v) {},
onLastNameChanged: (v) {}, onLastNameChanged: (v) {},
onGenreChanged: (_) {},
onTogglePhotoConsent: (v) {}, onTogglePhotoConsent: (v) {},
onToggleMultipleBirth: (v) {}, onToggleMultipleBirth: (v) {},
onToggleIsUnborn: (v) {}, onToggleIsUnborn: (v) {},

View File

@ -8,6 +8,8 @@ class ParentRegistrationPayload {
ParentRegistrationPayload._(); ParentRegistrationPayload._();
static final RegExp _phoneFr = RegExp(r'^(\+33|0)[1-9](\d{2}){4}$'); static final RegExp _phoneFr = RegExp(r'^(\+33|0)[1-9](\d{2}){4}$');
/// Aligné sur `GenreType` (backend) : JSON exact `H`, `F`, `Autre`.
static const Set<String> apiGenres = {'H', 'F', 'Autre'};
/// Retourne un message d'erreur utilisateur, ou `null` si le formulaire est cohérent. /// Retourne un message d'erreur utilisateur, ou `null` si le formulaire est cohérent.
static String? validateForApi(UserRegistrationData d) { static String? validateForApi(UserRegistrationData d) {
@ -57,6 +59,9 @@ class ParentRegistrationPayload {
for (var i = 0; i < d.children.length; i++) { for (var i = 0; i < d.children.length; i++) {
final c = d.children[i]; final c = d.children[i];
final label = 'Enfant ${i + 1}'; final label = 'Enfant ${i + 1}';
if (!apiGenres.contains(c.genre)) {
return '$label : sélectionnez le genre (garçon, fille ou autre).';
}
if (c.isUnbornChild) { if (c.isUnbornChild) {
final due = _ddMmYyyyToIso(c.dob); final due = _ddMmYyyyToIso(c.dob);
if (due == null) { if (due == null) {
@ -130,7 +135,7 @@ class ParentRegistrationPayload {
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) { static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
final map = <String, dynamic>{ final map = <String, dynamic>{
'genre': 'F', 'genre': c.genre,
'grossesse_multiple': c.multipleBirth, 'grossesse_multiple': c.multipleBirth,
}; };

View File

@ -19,6 +19,8 @@ class ChildCardWidget extends StatefulWidget {
final VoidCallback onDateSelect; final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged; final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged; final ValueChanged<String> onLastNameChanged;
/// `H`, `F` ou `Autre` (GenreType API).
final ValueChanged<String> onGenreChanged;
final ValueChanged<bool> onTogglePhotoConsent; final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth; final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn; final ValueChanged<bool> onToggleIsUnborn;
@ -35,6 +37,7 @@ class ChildCardWidget extends StatefulWidget {
required this.onDateSelect, required this.onDateSelect,
required this.onFirstNameChanged, required this.onFirstNameChanged,
required this.onLastNameChanged, required this.onLastNameChanged,
required this.onGenreChanged,
required this.onTogglePhotoConsent, required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth, required this.onToggleMultipleBirth,
required this.onToggleIsUnborn, required this.onToggleIsUnborn,
@ -190,6 +193,8 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
hint: 'Nom de l\'enfant', hint: 'Nom de l\'enfant',
), ),
SizedBox(height: 8.0 * scaleFactor), SizedBox(height: 8.0 * scaleFactor),
_buildGenreField(context, config, scaleFactor),
SizedBox(height: 8.0 * scaleFactor),
_buildField( _buildField(
config: config, config: config,
scaleFactor: scaleFactor, scaleFactor: scaleFactor,
@ -352,6 +357,8 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildReadonlyField('Nom :', _lastNameController.text), _buildReadonlyField('Nom :', _lastNameController.text),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
const SizedBox(height: 12),
_buildReadonlyField( _buildReadonlyField(
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :', widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
_dobController.text _dobController.text
@ -467,6 +474,8 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildReadonlyField('Nom :', _lastNameController.text), _buildReadonlyField('Nom :', _lastNameController.text),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
const SizedBox(height: 8),
_buildReadonlyField( _buildReadonlyField(
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :', widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
_dobController.text _dobController.text
@ -556,6 +565,72 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
); );
} }
static String _genreDisplayLabel(String g) {
switch (g) {
case 'H':
return 'Garçon';
case 'F':
return 'Fille';
case 'Autre':
return 'Autre / ne souhaite pas préciser';
default:
return '';
}
}
Widget _buildGenreField(BuildContext context, DisplayConfig config, double scaleFactor) {
if (config.isReadonly) {
return FormFieldWrapper(
config: config,
label: 'Genre',
value: _genreDisplayLabel(widget.childData.genre),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Genre *',
style: GoogleFonts.merienda(
fontSize: config.isMobile ? 12.0 : 18.0 * scaleFactor,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4.0 * scaleFactor),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 14.0 * scaleFactor, vertical: 4.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/bg_beige.png'),
fit: BoxFit.fill,
),
borderRadius: BorderRadius.circular(8 * scaleFactor),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: widget.childData.genre.isEmpty ? null : widget.childData.genre,
hint: Text('Choisir', style: GoogleFonts.merienda(fontSize: config.isMobile ? 13.0 : 16.0)),
style: GoogleFonts.merienda(fontSize: config.isMobile ? 13.0 : 16.0, color: Colors.black87),
items: [
DropdownMenuItem(value: 'H', child: Text('Garçon', style: GoogleFonts.merienda())),
DropdownMenuItem(value: 'F', child: Text('Fille', style: GoogleFonts.merienda())),
DropdownMenuItem(
value: 'Autre',
child: Text('Autre / ne souhaite pas préciser', style: GoogleFonts.merienda()),
),
],
onChanged: (v) {
if (v != null) widget.onGenreChanged(v);
},
),
),
),
],
);
}
Widget _buildField({ Widget _buildField({
required DisplayConfig config, required DisplayConfig config,
required double scaleFactor, required double scaleFactor,