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 ---
|
||||
bool get isStep1Complete =>
|
||||
firstName.isNotEmpty &&
|
||||
lastName.isNotEmpty &&
|
||||
streetAddress.isNotEmpty && // Modifié
|
||||
postalCode.isNotEmpty && // Nouveau
|
||||
city.isNotEmpty && // Nouveau
|
||||
firstName.trim().length >= 2 &&
|
||||
lastName.trim().length >= 2 &&
|
||||
streetAddress.isNotEmpty &&
|
||||
postalCode.isNotEmpty &&
|
||||
city.isNotEmpty &&
|
||||
phone.isNotEmpty &&
|
||||
email.isNotEmpty;
|
||||
// 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/'));
|
||||
|
||||
bool get isStep2Complete =>
|
||||
(_hasUserPhoto ? photoConsent == true : true) &&
|
||||
_hasUserPhoto &&
|
||||
photoConsent == true &&
|
||||
dateOfBirth != null &&
|
||||
birthCity.isNotEmpty &&
|
||||
birthCountry.isNotEmpty &&
|
||||
nir.isNotEmpty && // Basic check, could add validation
|
||||
birthCity.trim().length >= 2 &&
|
||||
birthCountry.trim().length >= 2 &&
|
||||
nir.isNotEmpty &&
|
||||
agrementNumber.isNotEmpty &&
|
||||
agreementDate != null &&
|
||||
capacity != null &&
|
||||
|
||||
@ -28,6 +28,7 @@ class AmRegisterStep1Screen extends StatelessWidget {
|
||||
title: 'Vos informations personnelles',
|
||||
cardColor: CardColorHorizontal.blue,
|
||||
initialData: initialData,
|
||||
minPersonNameLength: 2,
|
||||
previousRoute: '/register-choice',
|
||||
onSubmit: (data, {hasSecondPerson, sameAddress}) {
|
||||
registrationData.updateIdentityInfo(
|
||||
|
||||
@ -27,6 +27,19 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
||||
|
||||
Future<void> _submitAMRegistration(AmRegistrationData registrationData) async {
|
||||
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);
|
||||
try {
|
||||
await AuthService.registerAM(registrationData);
|
||||
|
||||
@ -168,6 +168,13 @@ class AuthService {
|
||||
if (data.placesAvailable == null) {
|
||||
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? 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>{
|
||||
'email': data.email,
|
||||
'prenom': data.firstName,
|
||||
@ -201,14 +212,14 @@ class AuthService {
|
||||
'adresse': data.streetAddress.isNotEmpty ? data.streetAddress : null,
|
||||
'code_postal': data.postalCode.isNotEmpty ? data.postalCode : null,
|
||||
'ville': data.city.isNotEmpty ? data.city : null,
|
||||
if (photoBase64 != null) 'photo_base64': photoBase64,
|
||||
if (photoBase64 != null) 'photo_filename': photoFilename ?? 'photo_am.jpg',
|
||||
'photo_base64': photoBase64,
|
||||
'photo_filename': photoFilename ?? 'photo_am.jpg',
|
||||
'consentement_photo': data.photoConsent,
|
||||
'date_naissance': data.dateOfBirth != null
|
||||
? '${data.dateOfBirth!.year}-${data.dateOfBirth!.month.toString().padLeft(2, '0')}-${data.dateOfBirth!.day.toString().padLeft(2, '0')}'
|
||||
: null,
|
||||
'lieu_naissance_ville': data.birthCity.isNotEmpty ? data.birthCity : null,
|
||||
'lieu_naissance_pays': data.birthCountry.isNotEmpty ? data.birthCountry : null,
|
||||
'lieu_naissance_ville': lieuVille,
|
||||
'lieu_naissance_pays': lieuPays,
|
||||
'nir': normalizeNir(data.nir),
|
||||
'numero_agrement': data.agrementNumber,
|
||||
'date_agrement':
|
||||
|
||||
@ -55,6 +55,8 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
||||
final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse"
|
||||
final bool embedContentOnly; // Si true, affiche seulement la carte (sans scaffold/fond/titre)
|
||||
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({
|
||||
super.key,
|
||||
@ -72,6 +74,7 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
||||
this.referenceAddressData,
|
||||
this.embedContentOnly = false,
|
||||
this.onEdit,
|
||||
this.minPersonNameLength,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -214,6 +217,16 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
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é.
|
||||
String? _validateFrenchPhoneIfSecondParentNeeded(String? value) {
|
||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||
@ -837,6 +850,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusLastName,
|
||||
focusNode: _lastNameFocus,
|
||||
fieldValidator: widget.minPersonNameLength != null
|
||||
? _validatePersonNameWithMinLength
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
@ -849,6 +865,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusFirstName,
|
||||
focusNode: _firstNameFocus,
|
||||
fieldValidator: widget.minPersonNameLength != null
|
||||
? _validatePersonNameWithMinLength
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1106,6 +1125,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusLastName,
|
||||
focusNode: _lastNameFocus,
|
||||
fieldValidator: widget.minPersonNameLength != null
|
||||
? _validatePersonNameWithMinLength
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@ -1118,6 +1140,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusFirstName,
|
||||
focusNode: _firstNameFocus,
|
||||
fieldValidator: widget.minPersonNameLength != null
|
||||
? _validatePersonNameWithMinLength
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
|
||||
@ -189,6 +189,20 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
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) {
|
||||
if (v == null || v.isEmpty) return 'Places disponibles requises';
|
||||
final n = int.tryParse(v);
|
||||
@ -281,7 +295,13 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
_applyPlaceNameFormat(_birthCityController);
|
||||
_applyPlaceNameFormat(_birthCountryController);
|
||||
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(
|
||||
const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')),
|
||||
);
|
||||
@ -295,8 +315,8 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
photoFilename: _photoFilename,
|
||||
photoConsent: _photoConsent,
|
||||
dateOfBirth: _selectedDate,
|
||||
birthCity: _birthCityController.text,
|
||||
birthCountry: _birthCountryController.text,
|
||||
birthCity: _birthCityController.text.trim(),
|
||||
birthCountry: _birthCountryController.text.trim(),
|
||||
nir: normalizeNir(_nirController.text),
|
||||
agrementNumber: _agrementController.text,
|
||||
agreementDate: _selectedAgreementDate,
|
||||
@ -753,7 +773,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
label: 'Ville de naissance',
|
||||
controller: _birthCityController,
|
||||
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
||||
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
||||
validator: _validateBirthCity,
|
||||
focusNode: _birthCityFocus,
|
||||
),
|
||||
SizedBox(height: verticalSpacing),
|
||||
@ -762,7 +782,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
label: 'Pays de naissance',
|
||||
controller: _birthCountryController,
|
||||
hint: 'Ex. France, Maroc…',
|
||||
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
||||
validator: _validateBirthCountry,
|
||||
focusNode: _birthCountryFocus,
|
||||
),
|
||||
],
|
||||
@ -875,7 +895,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
label: 'Ville de naissance',
|
||||
controller: _birthCityController,
|
||||
hint: 'Ex. Ajaccio, Paris, Casablanca…',
|
||||
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
||||
validator: _validateBirthCity,
|
||||
focusNode: _birthCityFocus,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@ -885,7 +905,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
label: 'Pays de naissance',
|
||||
controller: _birthCountryController,
|
||||
hint: 'Ex. France, Maroc…',
|
||||
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
||||
validator: _validateBirthCountry,
|
||||
focusNode: _birthCountryFocus,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user