feat(#112): reprise après refus via lien e-mail (/reprise?token=)

Branche le flux front : chargement du dossier, session reprise, wizards
parent/AM préremplis et resoumission via reprise-resoumettre.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 16:28:41 +02:00
co-authored by Cursor
parent c226c2fcdf
commit 671da71752
12 changed files with 575 additions and 3 deletions
+86
View File
@@ -8,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
import '../models/am_registration_data.dart';
import '../models/user_registration_data.dart';
import '../models/reprise_dossier.dart';
import '../utils/parent_registration_payload.dart';
import 'api/api_config.dart';
import 'api/tokenService.dart';
@@ -258,6 +259,91 @@ class AuthService {
throw Exception(msg);
}
/// Charge le dossier pour reprise (lien e-mail). GET /auth/reprise-dossier. Ticket #112.
static Future<RepriseDossier> getRepriseDossier(String token) async {
final cleaned = token.trim();
if (cleaned.isEmpty) {
throw Exception('Lien invalide ou expiré.');
}
final uri = Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.repriseDossier}',
).replace(queryParameters: {'token': cleaned});
late final http.Response response;
try {
response = await http.get(uri, headers: ApiConfig.headers);
} on http.ClientException {
throw Exception(
'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
);
}
if (response.statusCode == 404) {
throw Exception('Lien invalide ou expiré.');
}
if (response.statusCode != 200) {
final decoded = _tryDecodeJsonMap(response.body);
throw Exception(_extractErrorMessage(decoded, response.statusCode));
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) {
throw Exception('Réponse invalide du serveur.');
}
return RepriseDossier.fromJson(decoded);
}
/// Resoumission après refus. PATCH /auth/reprise-resoumettre. Ticket #112.
static Future<void> resoumettreReprise({
required String token,
String? prenom,
String? nom,
String? telephone,
String? adresse,
String? ville,
String? codePostal,
String? photoUrl,
}) async {
final cleaned = token.trim();
if (cleaned.isEmpty) {
throw Exception('Lien invalide ou expiré.');
}
final body = <String, dynamic>{'token': cleaned};
if (prenom != null && prenom.trim().isNotEmpty) body['prenom'] = prenom.trim();
if (nom != null && nom.trim().isNotEmpty) body['nom'] = nom.trim();
if (telephone != null && telephone.trim().isNotEmpty) {
body['telephone'] = telephone.trim();
}
if (adresse != null && adresse.trim().isNotEmpty) {
body['adresse'] = adresse.trim();
}
if (ville != null && ville.trim().isNotEmpty) body['ville'] = ville.trim();
if (codePostal != null && codePostal.trim().isNotEmpty) {
body['code_postal'] = codePostal.trim();
}
if (photoUrl != null && photoUrl.trim().isNotEmpty) {
body['photo_url'] = photoUrl.trim();
}
late final http.Response response;
try {
response = await http.patch(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseResoumettre}'),
headers: ApiConfig.headers,
body: jsonEncode(body),
);
} on http.ClientException {
throw Exception(
'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
);
}
if (response.statusCode == 404) {
throw Exception('Lien invalide ou expiré.');
}
if (response.statusCode == 200 || response.statusCode == 201) {
return;
}
final decoded = _tryDecodeJsonMap(response.body);
throw Exception(_extractErrorMessage(decoded, response.statusCode));
}
/// Déconnexion de l'utilisateur
static Future<void> logout() async {
await TokenService.clearAll();