fix(inscription-am): étape 1/2 obligatoires alignées API (min 2 car., photo)
- Étape 1 : prénom/nom min 2 caractères (minPersonNameLength sur PersonalInfoFormScreen pour AM) - Étape 2 : ville/pays naissance min 2 ; photo réelle + consentement obligatoires ; isStep2Complete strict - registerAM : lieu naissance trim + toujours envoyer photo_base64 ; lieu_* non null si valide - Étape 4 : blocage envoi si isRegistrationComplete faux Made-with: Cursor
This commit is contained in:
parent
b4b44cb1b9
commit
d550e57cb8
@ -106,11 +106,11 @@ class AmRegistrationData extends ChangeNotifier {
|
|||||||
|
|
||||||
// --- Getters for validation or display ---
|
// --- Getters for validation or display ---
|
||||||
bool get isStep1Complete =>
|
bool get isStep1Complete =>
|
||||||
firstName.isNotEmpty &&
|
firstName.trim().length >= 2 &&
|
||||||
lastName.isNotEmpty &&
|
lastName.trim().length >= 2 &&
|
||||||
streetAddress.isNotEmpty && // Modifié
|
streetAddress.isNotEmpty &&
|
||||||
postalCode.isNotEmpty && // Nouveau
|
postalCode.isNotEmpty &&
|
||||||
city.isNotEmpty && // Nouveau
|
city.isNotEmpty &&
|
||||||
phone.isNotEmpty &&
|
phone.isNotEmpty &&
|
||||||
email.isNotEmpty;
|
email.isNotEmpty;
|
||||||
// password n'est pas requis à l'inscription (défini après validation par lien email)
|
// password n'est pas requis à l'inscription (défini après validation par lien email)
|
||||||
@ -123,11 +123,12 @@ class AmRegistrationData extends ChangeNotifier {
|
|||||||
!photoPath!.startsWith('assets/'));
|
!photoPath!.startsWith('assets/'));
|
||||||
|
|
||||||
bool get isStep2Complete =>
|
bool get isStep2Complete =>
|
||||||
(_hasUserPhoto ? photoConsent == true : true) &&
|
_hasUserPhoto &&
|
||||||
|
photoConsent == true &&
|
||||||
dateOfBirth != null &&
|
dateOfBirth != null &&
|
||||||
birthCity.isNotEmpty &&
|
birthCity.trim().length >= 2 &&
|
||||||
birthCountry.isNotEmpty &&
|
birthCountry.trim().length >= 2 &&
|
||||||
nir.isNotEmpty && // Basic check, could add validation
|
nir.isNotEmpty &&
|
||||||
agrementNumber.isNotEmpty &&
|
agrementNumber.isNotEmpty &&
|
||||||
agreementDate != null &&
|
agreementDate != null &&
|
||||||
capacity != null &&
|
capacity != null &&
|
||||||
|
|||||||
@ -28,6 +28,7 @@ class AmRegisterStep1Screen extends StatelessWidget {
|
|||||||
title: 'Vos informations personnelles',
|
title: 'Vos informations personnelles',
|
||||||
cardColor: CardColorHorizontal.blue,
|
cardColor: CardColorHorizontal.blue,
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
|
minPersonNameLength: 2,
|
||||||
previousRoute: '/register-choice',
|
previousRoute: '/register-choice',
|
||||||
onSubmit: (data, {hasSecondPerson, sameAddress}) {
|
onSubmit: (data, {hasSecondPerson, sameAddress}) {
|
||||||
registrationData.updateIdentityInfo(
|
registrationData.updateIdentityInfo(
|
||||||
|
|||||||
@ -27,6 +27,19 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
|
|
||||||
Future<void> _submitAMRegistration(AmRegistrationData registrationData) async {
|
Future<void> _submitAMRegistration(AmRegistrationData registrationData) async {
|
||||||
if (_isSubmitting) return;
|
if (_isSubmitting) return;
|
||||||
|
if (!registrationData.isRegistrationComplete) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Le dossier est incomplet. Vérifiez chaque étape (photo, consentement, lieux de naissance, etc.).',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 14),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _isSubmitting = true);
|
setState(() => _isSubmitting = true);
|
||||||
try {
|
try {
|
||||||
await AuthService.registerAM(registrationData);
|
await AuthService.registerAM(registrationData);
|
||||||
|
|||||||
@ -168,6 +168,13 @@ class AuthService {
|
|||||||
if (data.placesAvailable == null) {
|
if (data.placesAvailable == null) {
|
||||||
throw Exception('Le nombre de places disponibles est requis.');
|
throw Exception('Le nombre de places disponibles est requis.');
|
||||||
}
|
}
|
||||||
|
final lieuVille = data.birthCity.trim();
|
||||||
|
final lieuPays = data.birthCountry.trim();
|
||||||
|
if (lieuVille.length < 2 || lieuPays.length < 2) {
|
||||||
|
throw Exception(
|
||||||
|
'La ville et le pays de naissance sont requis (au moins 2 caractères chacun).',
|
||||||
|
);
|
||||||
|
}
|
||||||
String? photoBase64;
|
String? photoBase64;
|
||||||
String? photoFilename;
|
String? photoFilename;
|
||||||
|
|
||||||
@ -193,6 +200,10 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (photoBase64 == null || photoBase64.isEmpty) {
|
||||||
|
throw Exception('Une photo de profil est requise pour finaliser l’inscription.');
|
||||||
|
}
|
||||||
|
|
||||||
final body = <String, dynamic>{
|
final body = <String, dynamic>{
|
||||||
'email': data.email,
|
'email': data.email,
|
||||||
'prenom': data.firstName,
|
'prenom': data.firstName,
|
||||||
@ -201,14 +212,14 @@ class AuthService {
|
|||||||
'adresse': data.streetAddress.isNotEmpty ? data.streetAddress : null,
|
'adresse': data.streetAddress.isNotEmpty ? data.streetAddress : null,
|
||||||
'code_postal': data.postalCode.isNotEmpty ? data.postalCode : null,
|
'code_postal': data.postalCode.isNotEmpty ? data.postalCode : null,
|
||||||
'ville': data.city.isNotEmpty ? data.city : null,
|
'ville': data.city.isNotEmpty ? data.city : null,
|
||||||
if (photoBase64 != null) 'photo_base64': photoBase64,
|
'photo_base64': photoBase64,
|
||||||
if (photoBase64 != null) 'photo_filename': photoFilename ?? 'photo_am.jpg',
|
'photo_filename': photoFilename ?? 'photo_am.jpg',
|
||||||
'consentement_photo': data.photoConsent,
|
'consentement_photo': data.photoConsent,
|
||||||
'date_naissance': data.dateOfBirth != null
|
'date_naissance': data.dateOfBirth != null
|
||||||
? '${data.dateOfBirth!.year}-${data.dateOfBirth!.month.toString().padLeft(2, '0')}-${data.dateOfBirth!.day.toString().padLeft(2, '0')}'
|
? '${data.dateOfBirth!.year}-${data.dateOfBirth!.month.toString().padLeft(2, '0')}-${data.dateOfBirth!.day.toString().padLeft(2, '0')}'
|
||||||
: null,
|
: null,
|
||||||
'lieu_naissance_ville': data.birthCity.isNotEmpty ? data.birthCity : null,
|
'lieu_naissance_ville': lieuVille,
|
||||||
'lieu_naissance_pays': data.birthCountry.isNotEmpty ? data.birthCountry : null,
|
'lieu_naissance_pays': lieuPays,
|
||||||
'nir': normalizeNir(data.nir),
|
'nir': normalizeNir(data.nir),
|
||||||
'numero_agrement': data.agrementNumber,
|
'numero_agrement': data.agrementNumber,
|
||||||
'date_agrement':
|
'date_agrement':
|
||||||
|
|||||||
@ -55,6 +55,8 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
|||||||
final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse"
|
final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse"
|
||||||
final bool embedContentOnly; // Si true, affiche seulement la carte (sans scaffold/fond/titre)
|
final bool embedContentOnly; // Si true, affiche seulement la carte (sans scaffold/fond/titre)
|
||||||
final VoidCallback? onEdit; // Callback pour le bouton d'édition (si affiché)
|
final VoidCallback? onEdit; // Callback pour le bouton d'édition (si affiché)
|
||||||
|
/// Si non null (ex. 2 pour inscription AM), nom/prénom : trim + longueur minimale (aligné API).
|
||||||
|
final int? minPersonNameLength;
|
||||||
|
|
||||||
const PersonalInfoFormScreen({
|
const PersonalInfoFormScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@ -72,6 +74,7 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
|||||||
this.referenceAddressData,
|
this.referenceAddressData,
|
||||||
this.embedContentOnly = false,
|
this.embedContentOnly = false,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
|
this.minPersonNameLength,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -214,6 +217,16 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
return validateEmail(value, allowEmpty: true);
|
return validateEmail(value, allowEmpty: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prénom / nom avec longueur minimale (ex. inscription AM, aligné `MinLength` API).
|
||||||
|
String? _validatePersonNameWithMinLength(String? value) {
|
||||||
|
final min = widget.minPersonNameLength;
|
||||||
|
if (min == null) return null;
|
||||||
|
final t = (value ?? '').trim();
|
||||||
|
if (t.isEmpty) return 'Ce champ est obligatoire';
|
||||||
|
if (t.length < min) return 'Au moins $min caractères';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Téléphone / e-mail du parent 2 : pas de validation si « Ajouter Parent 2 » est désactivé.
|
/// Téléphone / e-mail du parent 2 : pas de validation si « Ajouter Parent 2 » est désactivé.
|
||||||
String? _validateFrenchPhoneIfSecondParentNeeded(String? value) {
|
String? _validateFrenchPhoneIfSecondParentNeeded(String? value) {
|
||||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||||
@ -837,6 +850,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
focusOrder: _focusLastName,
|
focusOrder: _focusLastName,
|
||||||
focusNode: _lastNameFocus,
|
focusNode: _lastNameFocus,
|
||||||
|
fieldValidator: widget.minPersonNameLength != null
|
||||||
|
? _validatePersonNameWithMinLength
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@ -849,6 +865,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
focusOrder: _focusFirstName,
|
focusOrder: _focusFirstName,
|
||||||
focusNode: _firstNameFocus,
|
focusNode: _firstNameFocus,
|
||||||
|
fieldValidator: widget.minPersonNameLength != null
|
||||||
|
? _validatePersonNameWithMinLength
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1106,6 +1125,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
focusOrder: _focusLastName,
|
focusOrder: _focusLastName,
|
||||||
focusNode: _lastNameFocus,
|
focusNode: _lastNameFocus,
|
||||||
|
fieldValidator: widget.minPersonNameLength != null
|
||||||
|
? _validatePersonNameWithMinLength
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -1118,6 +1140,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
focusOrder: _focusFirstName,
|
focusOrder: _focusFirstName,
|
||||||
focusNode: _firstNameFocus,
|
focusNode: _firstNameFocus,
|
||||||
|
fieldValidator: widget.minPersonNameLength != null
|
||||||
|
? _validatePersonNameWithMinLength
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
|||||||
@ -189,6 +189,20 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _validateBirthCity(String? v) {
|
||||||
|
final t = (v ?? '').trim();
|
||||||
|
if (t.isEmpty) return 'Ville requise';
|
||||||
|
if (t.length < 2) return 'Au moins 2 caractères';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateBirthCountry(String? v) {
|
||||||
|
final t = (v ?? '').trim();
|
||||||
|
if (t.isEmpty) return 'Pays requis';
|
||||||
|
if (t.length < 2) return 'Au moins 2 caractères';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String? _validatePlacesAvailable(String? v) {
|
String? _validatePlacesAvailable(String? v) {
|
||||||
if (v == null || v.isEmpty) return 'Places disponibles requises';
|
if (v == null || v.isEmpty) return 'Places disponibles requises';
|
||||||
final n = int.tryParse(v);
|
final n = int.tryParse(v);
|
||||||
@ -281,7 +295,13 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
_applyPlaceNameFormat(_birthCityController);
|
_applyPlaceNameFormat(_birthCityController);
|
||||||
_applyPlaceNameFormat(_birthCountryController);
|
_applyPlaceNameFormat(_birthCountryController);
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
if (_hasRealPhoto && !_photoConsent) {
|
if (!_hasRealPhoto) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Veuillez ajouter une photo de profil.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_photoConsent) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')),
|
const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')),
|
||||||
);
|
);
|
||||||
@ -295,8 +315,8 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
photoFilename: _photoFilename,
|
photoFilename: _photoFilename,
|
||||||
photoConsent: _photoConsent,
|
photoConsent: _photoConsent,
|
||||||
dateOfBirth: _selectedDate,
|
dateOfBirth: _selectedDate,
|
||||||
birthCity: _birthCityController.text,
|
birthCity: _birthCityController.text.trim(),
|
||||||
birthCountry: _birthCountryController.text,
|
birthCountry: _birthCountryController.text.trim(),
|
||||||
nir: normalizeNir(_nirController.text),
|
nir: normalizeNir(_nirController.text),
|
||||||
agrementNumber: _agrementController.text,
|
agrementNumber: _agrementController.text,
|
||||||
agreementDate: _selectedAgreementDate,
|
agreementDate: _selectedAgreementDate,
|
||||||
@ -753,7 +773,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
controller: _birthCityController,
|
controller: _birthCityController,
|
||||||
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
||||||
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
validator: _validateBirthCity,
|
||||||
focusNode: _birthCityFocus,
|
focusNode: _birthCityFocus,
|
||||||
),
|
),
|
||||||
SizedBox(height: verticalSpacing),
|
SizedBox(height: verticalSpacing),
|
||||||
@ -762,7 +782,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
controller: _birthCountryController,
|
controller: _birthCountryController,
|
||||||
hint: 'Ex. France, Maroc…',
|
hint: 'Ex. France, Maroc…',
|
||||||
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
validator: _validateBirthCountry,
|
||||||
focusNode: _birthCountryFocus,
|
focusNode: _birthCountryFocus,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -875,7 +895,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
controller: _birthCityController,
|
controller: _birthCityController,
|
||||||
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
||||||
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
validator: _validateBirthCity,
|
||||||
focusNode: _birthCityFocus,
|
focusNode: _birthCityFocus,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@ -885,7 +905,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
controller: _birthCountryController,
|
controller: _birthCountryController,
|
||||||
hint: 'Ex. France, Maroc…',
|
hint: 'Ex. France, Maroc…',
|
||||||
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
validator: _validateBirthCountry,
|
||||||
focusNode: _birthCountryFocus,
|
focusNode: _birthCountryFocus,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user