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:
@@ -49,6 +49,9 @@ class ApiConfig {
|
||||
/// Ticket #127 — mot de passe oublié (back à livrer en parallèle).
|
||||
static const String forgotPassword = '/auth/forgot-password';
|
||||
static const String resetPassword = '/auth/reset-password';
|
||||
/// Ticket #112 — reprise après refus (#111 back).
|
||||
static const String repriseDossier = '/auth/reprise-dossier';
|
||||
static const String repriseResoumettre = '/auth/reprise-resoumettre';
|
||||
|
||||
// Users endpoints
|
||||
static const String users = '/users';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:p_tits_pas/models/am_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/reprise_dossier.dart';
|
||||
import 'package:p_tits_pas/models/user_registration_data.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
|
||||
/// Contexte reprise après refus (token e-mail ou identify). Ticket #112.
|
||||
class RepriseSession {
|
||||
RepriseSession._();
|
||||
|
||||
static String? _token;
|
||||
static String? _role;
|
||||
static String? _photoUrl;
|
||||
|
||||
static bool get isActive =>
|
||||
_token != null && _token!.trim().isNotEmpty;
|
||||
|
||||
static String? get token => _token;
|
||||
|
||||
static bool get isParent => _role == 'parent';
|
||||
|
||||
static bool get isAm => _role == 'assistante_maternelle';
|
||||
|
||||
static String? get photoUrl => _photoUrl;
|
||||
|
||||
static void start({
|
||||
required String token,
|
||||
required RepriseDossier dossier,
|
||||
}) {
|
||||
_token = token.trim();
|
||||
_role = dossier.role;
|
||||
_photoUrl = dossier.photoUrl?.trim().isNotEmpty == true
|
||||
? ApiConfig.absoluteMediaUrl(dossier.photoUrl)
|
||||
: null;
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
_token = null;
|
||||
_role = null;
|
||||
_photoUrl = null;
|
||||
}
|
||||
|
||||
static void applyToParent(UserRegistrationData data, RepriseDossier dossier) {
|
||||
data.resetForReprise(
|
||||
firstName: dossier.prenom ?? '',
|
||||
lastName: dossier.nom ?? '',
|
||||
phone: dossier.telephone ?? '',
|
||||
email: dossier.email,
|
||||
address: dossier.adresse ?? '',
|
||||
postalCode: dossier.codePostal ?? '',
|
||||
city: dossier.ville ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
static void applyToAm(AmRegistrationData data, RepriseDossier dossier) {
|
||||
final photo = _photoUrl;
|
||||
data.resetForReprise(
|
||||
firstName: dossier.prenom ?? '',
|
||||
lastName: dossier.nom ?? '',
|
||||
phone: dossier.telephone ?? '',
|
||||
email: dossier.email,
|
||||
streetAddress: dossier.adresse ?? '',
|
||||
postalCode: dossier.codePostal ?? '',
|
||||
city: dossier.ville ?? '',
|
||||
existingPhotoUrl: photo,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user