diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index 6d97e45..93c0bf7 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -7,6 +7,7 @@ import 'dart:math' as math; import '../../models/am_registration_data.dart'; import '../../models/card_assets.dart'; import '../../config/display_config.dart'; +import '../../services/auth_service.dart'; import '../../widgets/hover_relief_widget.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_navigation_button.dart'; @@ -22,6 +23,28 @@ class AmRegisterStep4Screen extends StatefulWidget { } class _AmRegisterStep4ScreenState extends State { + bool _isSubmitting = false; + + Future _submitAMRegistration(AmRegistrationData registrationData) async { + if (_isSubmitting) return; + setState(() => _isSubmitting = true); + try { + await AuthService.registerAM(registrationData); + if (!mounted) return; + _showConfirmationModal(context); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e is Exception ? e.toString().replaceFirst('Exception: ', '') : 'Erreur lors de l\'inscription'), + backgroundColor: Colors.red.shade700, + ), + ); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + @override Widget build(BuildContext context) { final registrationData = Provider.of(context); @@ -90,12 +113,9 @@ class _AmRegisterStep4ScreenState extends State { Expanded( child: HoverReliefWidget( child: CustomNavigationButton( - text: 'Soumettre', + text: _isSubmitting ? 'Envoi...' : 'Soumettre', style: NavigationButtonStyle.green, - onPressed: () { - print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); - _showConfirmationModal(context); - }, + onPressed: () => _submitAMRegistration(registrationData), width: double.infinity, height: 50, fontSize: 16, @@ -106,17 +126,14 @@ class _AmRegisterStep4ScreenState extends State { ), ) else - ImageButton( + ImageButton( bg: 'assets/images/bg_green.png', - text: 'Soumettre ma demande', + text: _isSubmitting ? 'Envoi...' : 'Soumettre ma demande', textColor: const Color(0xFF2D6A4F), width: 350, height: 50, fontSize: 18, - onPressed: () { - print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); - _showConfirmationModal(context); - }, + onPressed: () => _submitAMRegistration(registrationData), ), ], ), diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index 7a44678..f9b0e75 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -1,9 +1,12 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/user.dart'; +import '../models/am_registration_data.dart'; import 'api/api_config.dart'; import 'api/tokenService.dart'; +import '../utils/nir_utils.dart'; class AuthService { static const String _currentUserKey = 'current_user'; @@ -133,6 +136,70 @@ class AuthService { await prefs.setString(_currentUserKey, jsonEncode(user.toJson())); } + /// Inscription AM complète (POST /auth/register/am). + /// En cas de succès (201), aucune donnée utilisateur retournée ; rediriger vers login. + static Future registerAM(AmRegistrationData data) async { + String? photoBase64; + if (data.photoPath != null && data.photoPath!.isNotEmpty && !data.photoPath!.startsWith('assets/')) { + try { + final file = File(data.photoPath!); + if (await file.exists()) { + final bytes = await file.readAsBytes(); + photoBase64 = 'data:image/jpeg;base64,${base64Encode(bytes)}'; + } + } catch (_) {} + } + + final body = { + 'email': data.email, + 'prenom': data.firstName, + 'nom': data.lastName, + 'telephone': data.phone, + '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, + '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, + 'nir': normalizeNir(data.nir), + 'numero_agrement': data.agrementNumber, + 'capacite_accueil': data.capacity ?? 1, + 'biographie': data.presentationText.isNotEmpty ? data.presentationText : null, + 'acceptation_cgu': data.cguAccepted, + 'acceptation_privacy': data.cguAccepted, + }; + + final response = await http.post( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerAM}'), + headers: ApiConfig.headers, + body: jsonEncode(body), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + return; + } + + final decoded = response.body.isNotEmpty ? jsonDecode(response.body) : null; + final message = _extractErrorMessage(decoded, response.statusCode); + throw Exception(message); + } + + /// Extrait le message d'erreur des réponses NestJS (message string, array, ou objet). + static String _extractErrorMessage(dynamic decoded, int statusCode) { + const fallback = 'Erreur lors de l\'inscription'; + if (decoded == null || decoded is! Map) return '$fallback ($statusCode)'; + final msg = decoded['message']; + if (msg == null) return decoded['error'] as String? ?? '$fallback ($statusCode)'; + if (msg is String) return msg; + if (msg is List) return msg.map((e) => e.toString()).join('. ').trim(); + if (msg is Map && msg['message'] != null) return msg['message'].toString(); + return '$fallback ($statusCode)'; + } + /// Rafraîchit le profil utilisateur depuis l'API static Future refreshCurrentUser() async { final token = await TokenService.getToken(); diff --git a/frontend/lib/utils/nir_utils.dart b/frontend/lib/utils/nir_utils.dart index ea8d072..cfed04b 100644 --- a/frontend/lib/utils/nir_utils.dart +++ b/frontend/lib/utils/nir_utils.dart @@ -49,15 +49,11 @@ String nirToRaw(String normalized) { return s; } -/// Formate pour affichage : 1 12 34 56 789 012-34 ou 1 12 34 2A 789 012-34 +/// Formate pour affichage : 1 12 34 56 789 012-34 ou 1 12 34 2A 789 012-34 (Corse). String formatNir(String raw) { final r = nirToRaw(raw); if (r.length < 15) return r; - final dept = r.substring(5, 7); - final isCorsica = dept == '2A' || dept == '2B'; - if (isCorsica) { - return '${r.substring(0, 5)} ${r.substring(5, 7)} ${r.substring(7, 10)} ${r.substring(10, 13)}-${r.substring(13, 15)}'; - } + // Même structure pour tous : sexe + année + mois + département + commune + ordre-clé. return '${r.substring(0, 1)} ${r.substring(1, 3)} ${r.substring(3, 5)} ${r.substring(5, 7)} ${r.substring(7, 10)} ${r.substring(10, 13)}-${r.substring(13, 15)}'; } @@ -67,7 +63,7 @@ bool _isFormatValid(String raw) { final dept = raw.substring(5, 7); final restDigits = raw.substring(0, 5) + (dept == '2A' ? '19' : dept == '2B' ? '18' : dept) + raw.substring(7, 15); if (!RegExp(r'^[12]\d{12}\d{2}$').hasMatch(restDigits)) return false; - return RegExp(r'^[12]\d{4}(?:\d{2}|2A|2B)\d{6}$').hasMatch(raw); + return RegExp(r'^[12]\d{4}(?:\d{2}|2A|2B)\d{8}$').hasMatch(raw); } /// Calcule la clé de contrôle (97 - (NIR13 mod 97)). Pour 2A→19, 2B→18.