From d550e57cb8e30adab378e2dc8a2c17988971ea01 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Mon, 13 Apr 2026 13:32:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(inscription-am):=20=C3=A9tape=201/2=20oblig?= =?UTF-8?q?atoires=20align=C3=A9es=20API=20(min=202=20car.,=20photo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - É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 --- frontend/lib/models/am_registration_data.dart | 19 ++++++----- .../auth/am_register_step1_screen.dart | 1 + .../auth/am_register_step4_screen.dart | 13 +++++++ frontend/lib/services/auth_service.dart | 19 ++++++++--- .../widgets/personal_info_form_screen.dart | 25 ++++++++++++++ .../professional_info_form_screen.dart | 34 +++++++++++++++---- 6 files changed, 91 insertions(+), 20 deletions(-) diff --git a/frontend/lib/models/am_registration_data.dart b/frontend/lib/models/am_registration_data.dart index 6bc88e6..44604df 100644 --- a/frontend/lib/models/am_registration_data.dart +++ b/frontend/lib/models/am_registration_data.dart @@ -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 && diff --git a/frontend/lib/screens/auth/am_register_step1_screen.dart b/frontend/lib/screens/auth/am_register_step1_screen.dart index 86df609..66129e6 100644 --- a/frontend/lib/screens/auth/am_register_step1_screen.dart +++ b/frontend/lib/screens/auth/am_register_step1_screen.dart @@ -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( diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index b49cd8c..90f536f 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -27,6 +27,19 @@ class _AmRegisterStep4ScreenState extends State { Future _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); diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index 99d52aa..77f9587 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -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 = { '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': diff --git a/frontend/lib/widgets/personal_info_form_screen.dart b/frontend/lib/widgets/personal_info_form_screen.dart index 181cc82..94c9888 100644 --- a/frontend/lib/widgets/personal_info_form_screen.dart +++ b/frontend/lib/widgets/personal_info_form_screen.dart @@ -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 { 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 { enabled: _fieldsEnabled, focusOrder: _focusLastName, focusNode: _lastNameFocus, + fieldValidator: widget.minPersonNameLength != null + ? _validatePersonNameWithMinLength + : null, ), ), const SizedBox(width: 20), @@ -849,6 +865,9 @@ class _PersonalInfoFormScreenState extends State { enabled: _fieldsEnabled, focusOrder: _focusFirstName, focusNode: _firstNameFocus, + fieldValidator: widget.minPersonNameLength != null + ? _validatePersonNameWithMinLength + : null, ), ), ], @@ -1106,6 +1125,9 @@ class _PersonalInfoFormScreenState extends State { enabled: _fieldsEnabled, focusOrder: _focusLastName, focusNode: _lastNameFocus, + fieldValidator: widget.minPersonNameLength != null + ? _validatePersonNameWithMinLength + : null, ), const SizedBox(height: 12), @@ -1118,6 +1140,9 @@ class _PersonalInfoFormScreenState extends State { enabled: _fieldsEnabled, focusOrder: _focusFirstName, focusNode: _firstNameFocus, + fieldValidator: widget.minPersonNameLength != null + ? _validatePersonNameWithMinLength + : null, ), const SizedBox(height: 12), diff --git a/frontend/lib/widgets/professional_info_form_screen.dart b/frontend/lib/widgets/professional_info_form_screen.dart index 811f746..5a111cb 100644 --- a/frontend/lib/widgets/professional_info_form_screen.dart +++ b/frontend/lib/widgets/professional_info_form_screen.dart @@ -189,6 +189,20 @@ class _ProfessionalInfoFormScreenState extends State 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 _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 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 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 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 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 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),