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:
parent
c226c2fcdf
commit
671da71752
@ -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(),
|
||||||
|
|||||||
@ -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('
|
||||||
|
|||||||
53
frontend/lib/models/reprise_dossier.dart
Normal file
53
frontend/lib/models/reprise_dossier.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 là (simplifié)
|
// Méthode pour vérifier si toutes les données requises sont là (simplifié)
|
||||||
bool isRegistrationComplete() {
|
bool isRegistrationComplete() {
|
||||||
// Ajouter ici les validations nécessaires
|
// Ajouter ici les validations nécessaires
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
154
frontend/lib/screens/auth/reprise_entry_screen.dart
Normal file
154
frontend/lib/screens/auth/reprise_entry_screen.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
67
frontend/lib/services/reprise_session.dart
Normal file
67
frontend/lib/services/reprise_session.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user