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:
MARTIN Julien 2026-06-16 16:28:41 +02:00
parent c226c2fcdf
commit 671da71752
12 changed files with 575 additions and 3 deletions

View File

@ -21,6 +21,7 @@ import '../screens/auth/am_register_step4_screen.dart';
import '../screens/auth/create_password_screen.dart'; import '../screens/auth/create_password_screen.dart';
import '../screens/auth/forgot_password_screen.dart'; import '../screens/auth/forgot_password_screen.dart';
import '../screens/auth/reset_password_screen.dart'; import '../screens/auth/reset_password_screen.dart';
import '../screens/auth/reprise_entry_screen.dart';
import '../screens/home/home_screen.dart'; import '../screens/home/home_screen.dart';
import '../screens/administrateurs/admin_dashboardScreen.dart'; import '../screens/administrateurs/admin_dashboardScreen.dart';
import '../screens/gestionnaire/gestionnaire_dashboard_screen.dart'; import '../screens/gestionnaire/gestionnaire_dashboard_screen.dart';
@ -66,6 +67,11 @@ class AppRouter {
builder: (BuildContext context, GoRouterState state) => builder: (BuildContext context, GoRouterState state) =>
ResetPasswordScreen(token: state.uri.queryParameters['token']), ResetPasswordScreen(token: state.uri.queryParameters['token']),
), ),
GoRoute(
path: '/reprise',
builder: (BuildContext context, GoRouterState state) =>
RepriseEntryScreen(token: state.uri.queryParameters['token']),
),
GoRoute( GoRoute(
path: '/home', path: '/home',
builder: (BuildContext context, GoRouterState state) => const HomeScreen(), builder: (BuildContext context, GoRouterState state) => const HomeScreen(),

View File

@ -33,6 +33,9 @@ class AmRegistrationData extends ChangeNotifier {
/// Places libres actuellement (0 valeur capacité) API `places_disponibles`. /// Places libres actuellement (0 valeur capacité) API `places_disponibles`.
int? placesAvailable; int? placesAvailable;
/// Photo déjà en base lors d'une reprise (#112) — sert pour l'affichage et `photo_url` API.
String? repriseExistingPhotoUrl;
// Step 3: Presentation & CGU // Step 3: Presentation & CGU
String presentationText = ''; String presentationText = '';
bool cguAccepted = false; bool cguAccepted = false;
@ -104,6 +107,43 @@ class AmRegistrationData extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Reprise après refus (#112).
void resetForReprise({
required String firstName,
required String lastName,
required String phone,
required String email,
required String streetAddress,
required String postalCode,
required String city,
String? existingPhotoUrl,
}) {
this.firstName = firstName;
this.lastName = lastName;
this.phone = phone;
this.email = email;
this.streetAddress = streetAddress;
this.postalCode = postalCode;
this.city = city;
password = '';
repriseExistingPhotoUrl = existingPhotoUrl;
photoPath = existingPhotoUrl;
photoBytes = null;
photoFilename = null;
photoConsent = false;
dateOfBirth = null;
birthCity = '';
birthCountry = '';
nir = '';
agrementNumber = '';
agreementDate = null;
capacity = null;
placesAvailable = null;
presentationText = '';
cguAccepted = false;
notifyListeners();
}
// --- Getters for validation or display --- // --- Getters for validation or display ---
bool get isStep1Complete => bool get isStep1Complete =>
firstName.trim().length >= 2 && firstName.trim().length >= 2 &&
@ -118,6 +158,8 @@ class AmRegistrationData extends ChangeNotifier {
/// Photo réelle (pas seulement un placeholder asset). /// Photo réelle (pas seulement un placeholder asset).
bool get _hasUserPhoto => bool get _hasUserPhoto =>
(photoBytes != null && photoBytes!.isNotEmpty) || (photoBytes != null && photoBytes!.isNotEmpty) ||
(repriseExistingPhotoUrl != null &&
repriseExistingPhotoUrl!.trim().isNotEmpty) ||
(photoPath != null && (photoPath != null &&
photoPath!.isNotEmpty && photoPath!.isNotEmpty &&
!photoPath!.startsWith('assets/')); !photoPath!.startsWith('assets/'));
@ -145,6 +187,9 @@ class AmRegistrationData extends ChangeNotifier {
bool get isRegistrationComplete => bool get isRegistrationComplete =>
isStep1Complete && isStep2Complete && isStep3Complete; isStep1Complete && isStep2Complete && isStep3Complete;
/// Reprise (#112) : champs renvoyés par PATCH reprise-resoumettre.
bool get isRepriseSubmitReady => isStep1Complete && cguAccepted;
@override @override
String toString() { String toString() {
return 'AmRegistrationData(' return 'AmRegistrationData('

View File

@ -0,0 +1,53 @@
/// Réponse GET /auth/reprise-dossier. Ticket #111, #112.
class RepriseDossier {
final String id;
final String email;
final String? prenom;
final String? nom;
final String? telephone;
final String? adresse;
final String? ville;
final String? codePostal;
final String? numeroDossier;
final String role;
final String? photoUrl;
final String? genre;
final String? situationFamiliale;
const RepriseDossier({
required this.id,
required this.email,
this.prenom,
this.nom,
this.telephone,
this.adresse,
this.ville,
this.codePostal,
this.numeroDossier,
required this.role,
this.photoUrl,
this.genre,
this.situationFamiliale,
});
bool get isParent => role == 'parent';
bool get isAm => role == 'assistante_maternelle';
factory RepriseDossier.fromJson(Map<String, dynamic> json) {
return RepriseDossier(
id: json['id']?.toString() ?? '',
email: json['email']?.toString() ?? '',
prenom: json['prenom']?.toString(),
nom: json['nom']?.toString(),
telephone: json['telephone']?.toString(),
adresse: json['adresse']?.toString(),
ville: json['ville']?.toString(),
codePostal: json['code_postal']?.toString(),
numeroDossier: json['numero_dossier']?.toString(),
role: json['role']?.toString() ?? '',
photoUrl: json['photo_url']?.toString(),
genre: json['genre']?.toString(),
situationFamiliale: json['situation_familiale']?.toString(),
);
}
}

View File

@ -178,6 +178,43 @@ class UserRegistrationData extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Reprise après refus (#112) : réinitialise le flux parent avec les données dossier.
void resetForReprise({
required String firstName,
required String lastName,
required String phone,
required String email,
required String address,
required String postalCode,
required String city,
}) {
parent1 = ParentData(
firstName: firstName,
lastName: lastName,
phone: phone,
email: email,
address: address,
postalCode: postalCode,
city: city,
password: '',
);
parent2 = null;
children.clear();
motivationText = '';
cguAccepted = false;
bankDetails = null;
attestationCafNumber = '';
consentQuotientFamilial = false;
notifyListeners();
}
/// Reprise (#112) : coordonnées parent principal + CGU.
bool get isRepriseSubmitReady =>
parent1.firstName.isNotEmpty &&
parent1.lastName.isNotEmpty &&
parent1.email.isNotEmpty &&
cguAccepted;
// Méthode pour vérifier si toutes les données requises sont (simplifié) // Méthode pour vérifier si toutes les données requises sont (simplifié)
bool isRegistrationComplete() { bool isRegistrationComplete() {
// Ajouter ici les validations nécessaires // Ajouter ici les validations nécessaires

View File

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../models/am_registration_data.dart'; import '../../models/am_registration_data.dart';
import '../../widgets/personal_info_form_screen.dart'; import '../../widgets/personal_info_form_screen.dart';
import '../../services/reprise_session.dart';
import '../../models/card_assets.dart'; import '../../models/card_assets.dart';
class AmRegisterStep1Screen extends StatelessWidget { class AmRegisterStep1Screen extends StatelessWidget {
@ -29,7 +30,7 @@ class AmRegisterStep1Screen extends StatelessWidget {
cardColor: CardColorHorizontal.blue, cardColor: CardColorHorizontal.blue,
initialData: initialData, initialData: initialData,
minPersonNameLength: 2, minPersonNameLength: 2,
previousRoute: '/register-choice', previousRoute: RepriseSession.isActive ? '/login' : '/register-choice',
onSubmit: (data, {hasSecondPerson, sameAddress}) { onSubmit: (data, {hasSecondPerson, sameAddress}) {
registrationData.updateIdentityInfo( registrationData.updateIdentityInfo(
firstName: data.firstName, firstName: data.firstName,

View File

@ -8,6 +8,7 @@ import '../../models/am_registration_data.dart';
import '../../models/card_assets.dart'; import '../../models/card_assets.dart';
import '../../config/display_config.dart'; import '../../config/display_config.dart';
import '../../services/auth_service.dart'; import '../../services/auth_service.dart';
import '../../services/reprise_session.dart';
import '../../widgets/hover_relief_widget.dart'; import '../../widgets/hover_relief_widget.dart';
import '../../widgets/image_button.dart'; import '../../widgets/image_button.dart';
import '../../widgets/custom_navigation_button.dart'; import '../../widgets/custom_navigation_button.dart';
@ -27,7 +28,21 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
Future<void> _submitAMRegistration(AmRegistrationData registrationData) async { Future<void> _submitAMRegistration(AmRegistrationData registrationData) async {
if (_isSubmitting) return; if (_isSubmitting) return;
if (!registrationData.isRegistrationComplete) { if (RepriseSession.isActive) {
if (!registrationData.isRepriseSubmitReady) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Vérifiez vos coordonnées et acceptez les conditions.',
style: GoogleFonts.merienda(fontSize: 14),
),
backgroundColor: Colors.red.shade700,
),
);
return;
}
} else if (!registrationData.isRegistrationComplete) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -42,6 +57,22 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
} }
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
try { try {
if (RepriseSession.isActive) {
await AuthService.resoumettreReprise(
token: RepriseSession.token!,
prenom: registrationData.firstName,
nom: registrationData.lastName,
telephone: registrationData.phone,
adresse: registrationData.streetAddress,
ville: registrationData.city,
codePostal: registrationData.postalCode,
photoUrl: RepriseSession.photoUrl,
);
RepriseSession.clear();
if (!mounted) return;
_showRepriseConfirmationModal(context);
return;
}
await AuthService.registerAM(registrationData); await AuthService.registerAM(registrationData);
if (!mounted) return; if (!mounted) return;
_showConfirmationModal(context); _showConfirmationModal(context);
@ -244,6 +275,34 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
); );
} }
void _showRepriseConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Dossier resoumis',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Vos modifications ont été enregistrées. Votre dossier est de nouveau en attente de validation.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop();
context.go('/login');
},
),
],
);
},
);
}
void _showConfirmationModal(BuildContext context) { void _showConfirmationModal(BuildContext context) {
showDialog<void>( showDialog<void>(
context: context, context: context,

View File

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../models/user_registration_data.dart'; import '../../models/user_registration_data.dart';
import '../../widgets/personal_info_form_screen.dart'; import '../../widgets/personal_info_form_screen.dart';
import '../../services/reprise_session.dart';
import '../../models/card_assets.dart'; import '../../models/card_assets.dart';
class ParentRegisterStep1Screen extends StatelessWidget { class ParentRegisterStep1Screen extends StatelessWidget {
@ -29,7 +30,7 @@ class ParentRegisterStep1Screen extends StatelessWidget {
title: 'Informations du Parent Principal', title: 'Informations du Parent Principal',
cardColor: CardColorHorizontal.peach, cardColor: CardColorHorizontal.peach,
initialData: initialData, initialData: initialData,
previousRoute: '/register-choice', previousRoute: RepriseSession.isActive ? '/login' : '/register-choice',
onSubmit: (data, {hasSecondPerson, sameAddress}) { onSubmit: (data, {hasSecondPerson, sameAddress}) {
registrationData.updateParent1(ParentData( registrationData.updateParent1(ParentData(
firstName: data.firstName, firstName: data.firstName,

View File

@ -14,6 +14,7 @@ import '../../widgets/personal_info_form_screen.dart';
import '../../widgets/child_card_widget.dart'; import '../../widgets/child_card_widget.dart';
import '../../widgets/presentation_form_screen.dart'; import '../../widgets/presentation_form_screen.dart';
import '../../services/auth_service.dart'; import '../../services/auth_service.dart';
import '../../services/reprise_session.dart';
class ParentRegisterStep5Screen extends StatefulWidget { class ParentRegisterStep5Screen extends StatefulWidget {
const ParentRegisterStep5Screen({super.key}); const ParentRegisterStep5Screen({super.key});
@ -27,8 +28,39 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
Future<void> _submitRegistration(BuildContext context, UserRegistrationData data) async { Future<void> _submitRegistration(BuildContext context, UserRegistrationData data) async {
if (_isSubmitting) return; if (_isSubmitting) return;
if (RepriseSession.isActive) {
if (!data.isRepriseSubmitReady) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Vérifiez vos coordonnées et acceptez les conditions.',
style: GoogleFonts.merienda(fontSize: 14),
),
backgroundColor: Colors.red.shade700,
),
);
return;
}
}
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
try { try {
if (RepriseSession.isActive) {
final token = RepriseSession.token!;
final p = data.parent1;
await AuthService.resoumettreReprise(
token: token,
prenom: p.firstName,
nom: p.lastName,
telephone: p.phone,
adresse: p.address,
ville: p.city,
codePostal: p.postalCode,
);
RepriseSession.clear();
if (!context.mounted) return;
_showRepriseSuccessModal(context);
return;
}
await AuthService.registerParent(data); await AuthService.registerParent(data);
if (!context.mounted) return; if (!context.mounted) return;
_showSuccessModal(context); _showSuccessModal(context);
@ -279,6 +311,34 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
); );
} }
void _showRepriseSuccessModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Dossier resoumis',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Vos modifications ont été enregistrées. Votre dossier est de nouveau en attente de validation.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop();
context.go('/login');
},
),
],
);
},
);
}
void _showSuccessModal(BuildContext context) { void _showSuccessModal(BuildContext context) {
showDialog<void>( showDialog<void>(
context: context, context: context,

View File

@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../config/app_router.dart';
import '../../services/auth_service.dart';
import '../../services/reprise_session.dart';
/// Point d'entrée `/reprise?token=` — charge le dossier et ouvre le wizard (#112).
class RepriseEntryScreen extends StatefulWidget {
final String? token;
const RepriseEntryScreen({super.key, this.token});
@override
State<RepriseEntryScreen> createState() => _RepriseEntryScreenState();
}
class _RepriseEntryScreenState extends State<RepriseEntryScreen> {
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
}
Future<void> _load() async {
final token = widget.token?.trim() ?? '';
if (token.isEmpty) {
setState(() {
_loading = false;
_error = 'Lien invalide ou expiré.';
});
return;
}
setState(() {
_loading = true;
_error = null;
});
try {
final dossier = await AuthService.getRepriseDossier(token);
if (!mounted) return;
RepriseSession.clear();
RepriseSession.start(token: token, dossier: dossier);
if (dossier.isParent) {
RepriseSession.applyToParent(userRegistrationDataNotifier, dossier);
context.go('/parent-register-step1');
return;
}
if (dossier.isAm) {
RepriseSession.applyToAm(amRegistrationDataNotifier, dossier);
context.go('/am-register-step1');
return;
}
RepriseSession.clear();
setState(() {
_loading = false;
_error = 'Type de dossier non pris en charge pour la reprise.';
});
} catch (e) {
if (!mounted) return;
RepriseSession.clear();
setState(() {
_loading = false;
_error = e is Exception
? e.toString().replaceFirst('Exception: ', '')
: 'Impossible de charger votre dossier.';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Padding(
padding: const EdgeInsets.all(24),
child: _buildBody(),
),
),
),
],
),
);
}
Widget _buildBody() {
if (_loading) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text(
'Chargement de votre dossier…',
style: GoogleFonts.merienda(fontSize: 16),
textAlign: TextAlign.center,
),
],
);
}
final err = _error ?? 'Lien invalide ou expiré.';
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red.shade700),
const SizedBox(height: 16),
Text(
'Reprise du dossier',
style: GoogleFonts.merienda(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
err,
style: GoogleFonts.merienda(fontSize: 14, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextButton(
onPressed: () => context.go('/login'),
child: Text(
'Retour à la connexion',
style: GoogleFonts.merienda(
decoration: TextDecoration.underline,
color: const Color(0xFF2D6A4F),
),
),
),
],
);
}
}

View File

@ -49,6 +49,9 @@ class ApiConfig {
/// Ticket #127 mot de passe oublié (back à livrer en parallèle). /// Ticket #127 mot de passe oublié (back à livrer en parallèle).
static const String forgotPassword = '/auth/forgot-password'; static const String forgotPassword = '/auth/forgot-password';
static const String resetPassword = '/auth/reset-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 // Users endpoints
static const String users = '/users'; static const String users = '/users';

View File

@ -8,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart'; import '../models/user.dart';
import '../models/am_registration_data.dart'; import '../models/am_registration_data.dart';
import '../models/user_registration_data.dart'; import '../models/user_registration_data.dart';
import '../models/reprise_dossier.dart';
import '../utils/parent_registration_payload.dart'; import '../utils/parent_registration_payload.dart';
import 'api/api_config.dart'; import 'api/api_config.dart';
import 'api/tokenService.dart'; import 'api/tokenService.dart';
@ -258,6 +259,91 @@ class AuthService {
throw Exception(msg); 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 /// Déconnexion de l'utilisateur
static Future<void> logout() async { static Future<void> logout() async {
await TokenService.clearAll(); await TokenService.clearAll();

View File

@ -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,
);
}
}