feat(am): inscription photo (picker, bytes web, photo_filename) (#120)

- ProfessionalInfoFormScreen : ImagePicker par défaut, MemoryImage, consentement si photo réelle
- AmRegistrationData : photoBytes + photoFilename ; récap étape 4
- registerAM : base64 + filename, erreurs réseau/JSON comme parents
- docs: 23_LISTE-TICKETS — issue #120

Made-with: Cursor
This commit is contained in:
2026-04-13 13:19:31 +02:00
parent cdae9b5f7c
commit 01c1be8f16
6 changed files with 147 additions and 60 deletions
+54 -14
View File
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
@@ -13,6 +15,13 @@ import '../utils/nir_utils.dart';
class AuthService {
static const String _currentUserKey = 'current_user';
static String? _basenameFromPath(String path) {
final s = path.replaceAll(r'\', '/');
final i = s.lastIndexOf('/');
final name = i >= 0 ? s.substring(i + 1) : s;
return name.isEmpty ? null : name;
}
/// Connexion de l'utilisateur
/// Retourne l'utilisateur connecté avec le flag changement_mdp_obligatoire
static Future<AppUser> login(String email, String password) async {
@@ -142,17 +151,29 @@ class AuthService {
/// En cas de succès (201), aucune donnée utilisateur retournée ; rediriger vers login.
static Future<void> 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 (_) {}
String? photoFilename;
if (data.photoBytes != null && data.photoBytes!.isNotEmpty) {
photoBase64 = 'data:image/jpeg;base64,${base64Encode(data.photoBytes!)}';
final fn = (data.photoFilename ?? '').trim();
photoFilename = fn.isNotEmpty ? fn : 'photo_am.jpg';
} else if (data.photoPath != null &&
data.photoPath!.isNotEmpty &&
!data.photoPath!.startsWith('assets/')) {
if (!kIsWeb) {
try {
final file = File(data.photoPath!);
if (await file.exists()) {
final bytes = await file.readAsBytes();
photoBase64 = 'data:image/jpeg;base64,${base64Encode(bytes)}';
photoFilename =
_basenameFromPath(data.photoPath!) ?? 'photo_am.jpg';
}
} catch (_) {}
}
}
final body = {
final body = <String, dynamic>{
'email': data.email,
'prenom': data.firstName,
'nom': data.lastName,
@@ -161,6 +182,7 @@ class AuthService {
'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',
'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')}'
@@ -175,11 +197,29 @@ class AuthService {
'acceptation_privacy': data.cguAccepted,
};
final response = await http.post(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerAM}'),
headers: ApiConfig.headers,
body: jsonEncode(body),
);
final String encodedBody;
try {
encodedBody = jsonEncode(body);
} catch (_) {
throw Exception(
'Impossible de préparer lenvoi (données trop volumineuses ou invalides). '
'Réessayez avec une photo plus légère.',
);
}
late final http.Response response;
try {
response = await http.post(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerAM}'),
headers: ApiConfig.headers,
body: encodedBody,
);
} on http.ClientException {
throw Exception(
'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau, '
'un pare-feu ou un bloqueur, puis réessayez.',
);
}
if (response.statusCode == 200 || response.statusCode == 201) {
return;