feat(#112): aligner reprise front sur dossier complet GET/PATCH
Préremplit parents, enfants, motivation et fiche AM depuis reprise-dossier et envoie le body PATCH complet (co-parent, enfants par id, champs pro AM). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
25c10c885a
commit
f300505225
@ -117,6 +117,16 @@ class AmRegistrationData extends ChangeNotifier {
|
|||||||
required String postalCode,
|
required String postalCode,
|
||||||
required String city,
|
required String city,
|
||||||
String? existingPhotoUrl,
|
String? existingPhotoUrl,
|
||||||
|
bool consentementPhoto = false,
|
||||||
|
DateTime? dateOfBirth,
|
||||||
|
String birthCity = '',
|
||||||
|
String birthCountry = '',
|
||||||
|
String nir = '',
|
||||||
|
String agrementNumber = '',
|
||||||
|
DateTime? agreementDate,
|
||||||
|
int? capacity,
|
||||||
|
int? placesAvailable,
|
||||||
|
String presentationText = '',
|
||||||
}) {
|
}) {
|
||||||
this.firstName = firstName;
|
this.firstName = firstName;
|
||||||
this.lastName = lastName;
|
this.lastName = lastName;
|
||||||
@ -130,16 +140,16 @@ class AmRegistrationData extends ChangeNotifier {
|
|||||||
photoPath = existingPhotoUrl;
|
photoPath = existingPhotoUrl;
|
||||||
photoBytes = null;
|
photoBytes = null;
|
||||||
photoFilename = null;
|
photoFilename = null;
|
||||||
photoConsent = false;
|
photoConsent = consentementPhoto;
|
||||||
dateOfBirth = null;
|
dateOfBirth = dateOfBirth;
|
||||||
birthCity = '';
|
this.birthCity = birthCity;
|
||||||
birthCountry = '';
|
this.birthCountry = birthCountry;
|
||||||
nir = '';
|
this.nir = nir;
|
||||||
agrementNumber = '';
|
this.agrementNumber = agrementNumber;
|
||||||
agreementDate = null;
|
agreementDate = agreementDate;
|
||||||
capacity = null;
|
this.capacity = capacity;
|
||||||
placesAvailable = null;
|
placesAvailable = placesAvailable;
|
||||||
presentationText = '';
|
this.presentationText = presentationText;
|
||||||
cguAccepted = false;
|
cguAccepted = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -187,8 +197,8 @@ 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.
|
/// Reprise (#112) : dossier AM complet renvoyé par PATCH reprise-resoumettre.
|
||||||
bool get isRepriseSubmitReady => isStep1Complete && cguAccepted;
|
bool get isRepriseSubmitReady => isRegistrationComplete;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@ -184,6 +184,7 @@ class EnfantDossier {
|
|||||||
final String? dueDate;
|
final String? dueDate;
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
final bool consentPhoto;
|
final bool consentPhoto;
|
||||||
|
final bool estMultiple;
|
||||||
|
|
||||||
EnfantDossier({
|
EnfantDossier({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -195,6 +196,7 @@ class EnfantDossier {
|
|||||||
this.dueDate,
|
this.dueDate,
|
||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
this.consentPhoto = false,
|
this.consentPhoto = false,
|
||||||
|
this.estMultiple = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
|
String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
|
||||||
@ -223,6 +225,8 @@ class EnfantDossier {
|
|||||||
photoUrl: resolvedPhoto,
|
photoUrl: resolvedPhoto,
|
||||||
consentPhoto:
|
consentPhoto:
|
||||||
json['consent_photo'] == true || json['consentPhoto'] == true,
|
json['consent_photo'] == true || json['consentPhoto'] == true,
|
||||||
|
estMultiple:
|
||||||
|
json['est_multiple'] == true || json['estMultiple'] == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
/// Réponse GET /auth/reprise-dossier. Ticket #111, #112.
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
|
|
||||||
|
/// Réponse GET /auth/reprise-dossier. Tickets #111, #112.
|
||||||
class RepriseDossier {
|
class RepriseDossier {
|
||||||
final String id;
|
final String id;
|
||||||
final String email;
|
final String email;
|
||||||
@ -14,6 +16,21 @@ class RepriseDossier {
|
|||||||
final String? genre;
|
final String? genre;
|
||||||
final String? situationFamiliale;
|
final String? situationFamiliale;
|
||||||
|
|
||||||
|
final List<ParentDossier> parents;
|
||||||
|
final List<EnfantDossier> enfants;
|
||||||
|
final String? texteMotivation;
|
||||||
|
|
||||||
|
final bool? consentementPhoto;
|
||||||
|
final String? dateNaissance;
|
||||||
|
final String? lieuNaissanceVille;
|
||||||
|
final String? lieuNaissancePays;
|
||||||
|
final String? numeroAgrement;
|
||||||
|
final String? nir;
|
||||||
|
final String? dateAgrement;
|
||||||
|
final int? nbMaxEnfants;
|
||||||
|
final int? placeDisponible;
|
||||||
|
final String? biographie;
|
||||||
|
|
||||||
const RepriseDossier({
|
const RepriseDossier({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.email,
|
required this.email,
|
||||||
@ -28,12 +45,44 @@ class RepriseDossier {
|
|||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
this.genre,
|
this.genre,
|
||||||
this.situationFamiliale,
|
this.situationFamiliale,
|
||||||
|
this.parents = const [],
|
||||||
|
this.enfants = const [],
|
||||||
|
this.texteMotivation,
|
||||||
|
this.consentementPhoto,
|
||||||
|
this.dateNaissance,
|
||||||
|
this.lieuNaissanceVille,
|
||||||
|
this.lieuNaissancePays,
|
||||||
|
this.numeroAgrement,
|
||||||
|
this.nir,
|
||||||
|
this.dateAgrement,
|
||||||
|
this.nbMaxEnfants,
|
||||||
|
this.placeDisponible,
|
||||||
|
this.biographie,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isParent => role == 'parent';
|
bool get isParent => role == 'parent';
|
||||||
bool get isAm => role == 'assistante_maternelle';
|
bool get isAm => role == 'assistante_maternelle';
|
||||||
|
|
||||||
factory RepriseDossier.fromJson(Map<String, dynamic> json) {
|
factory RepriseDossier.fromJson(Map<String, dynamic> json) {
|
||||||
|
final parentsRaw = json['parents'];
|
||||||
|
final parentsList = parentsRaw is List
|
||||||
|
? parentsRaw
|
||||||
|
.where((e) => e is Map)
|
||||||
|
.map((e) => ParentDossier.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList()
|
||||||
|
: <ParentDossier>[];
|
||||||
|
|
||||||
|
final enfantsRaw = json['enfants'];
|
||||||
|
final enfantsList = enfantsRaw is List
|
||||||
|
? enfantsRaw
|
||||||
|
.where((e) => e is Map)
|
||||||
|
.map((e) => EnfantDossier.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList()
|
||||||
|
: <EnfantDossier>[];
|
||||||
|
|
||||||
|
final nbMax = json['nb_max_enfants'];
|
||||||
|
final places = json['place_disponible'];
|
||||||
|
|
||||||
return RepriseDossier(
|
return RepriseDossier(
|
||||||
id: json['id']?.toString() ?? '',
|
id: json['id']?.toString() ?? '',
|
||||||
email: json['email']?.toString() ?? '',
|
email: json['email']?.toString() ?? '',
|
||||||
@ -48,6 +97,21 @@ class RepriseDossier {
|
|||||||
photoUrl: json['photo_url']?.toString(),
|
photoUrl: json['photo_url']?.toString(),
|
||||||
genre: json['genre']?.toString(),
|
genre: json['genre']?.toString(),
|
||||||
situationFamiliale: json['situation_familiale']?.toString(),
|
situationFamiliale: json['situation_familiale']?.toString(),
|
||||||
|
parents: parentsList,
|
||||||
|
enfants: enfantsList,
|
||||||
|
texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier'])
|
||||||
|
?.toString(),
|
||||||
|
consentementPhoto: json['consentement_photo'] == true,
|
||||||
|
dateNaissance: json['date_naissance']?.toString(),
|
||||||
|
lieuNaissanceVille: json['lieu_naissance_ville']?.toString(),
|
||||||
|
lieuNaissancePays: json['lieu_naissance_pays']?.toString(),
|
||||||
|
numeroAgrement: json['numero_agrement']?.toString(),
|
||||||
|
nir: json['nir']?.toString(),
|
||||||
|
dateAgrement: json['date_agrement']?.toString(),
|
||||||
|
nbMaxEnfants: nbMax is int ? nbMax : (nbMax is num ? nbMax.toInt() : null),
|
||||||
|
placeDisponible:
|
||||||
|
places is int ? places : (places is num ? places.toInt() : null),
|
||||||
|
biographie: json['biographie']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,10 @@ class ChildData {
|
|||||||
/// Octets de la photo (fiable à l’envoi API ; [imageFile] peut être absent sur le web).
|
/// Octets de la photo (fiable à l’envoi API ; [imageFile] peut être absent sur le web).
|
||||||
Uint8List? imageBytes;
|
Uint8List? imageBytes;
|
||||||
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
|
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
|
||||||
|
/// UUID enfant en base (reprise #112) — requis pour PATCH reprise-resoumettre.
|
||||||
|
String? repriseChildId;
|
||||||
|
/// Photo déjà stockée (affichage reprise sans re-upload).
|
||||||
|
String? existingPhotoUrl;
|
||||||
|
|
||||||
ChildData({
|
ChildData({
|
||||||
this.firstName = '',
|
this.firstName = '',
|
||||||
@ -55,6 +59,8 @@ class ChildData {
|
|||||||
this.imageFile,
|
this.imageFile,
|
||||||
this.imageBytes,
|
this.imageBytes,
|
||||||
required this.cardColor, // Rendre requis dans le constructeur
|
required this.cardColor, // Rendre requis dans le constructeur
|
||||||
|
this.repriseChildId,
|
||||||
|
this.existingPhotoUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
ChildData copyWith({
|
ChildData copyWith({
|
||||||
@ -68,6 +74,8 @@ class ChildData {
|
|||||||
Object? imageFile = _unsetImage,
|
Object? imageFile = _unsetImage,
|
||||||
Object? imageBytes = _unsetImageBytes,
|
Object? imageBytes = _unsetImageBytes,
|
||||||
CardColorVertical? cardColor,
|
CardColorVertical? cardColor,
|
||||||
|
String? repriseChildId,
|
||||||
|
String? existingPhotoUrl,
|
||||||
}) {
|
}) {
|
||||||
return ChildData(
|
return ChildData(
|
||||||
firstName: firstName ?? this.firstName,
|
firstName: firstName ?? this.firstName,
|
||||||
@ -81,6 +89,8 @@ class ChildData {
|
|||||||
imageBytes:
|
imageBytes:
|
||||||
identical(imageBytes, _unsetImageBytes) ? this.imageBytes : imageBytes as Uint8List?,
|
identical(imageBytes, _unsetImageBytes) ? this.imageBytes : imageBytes as Uint8List?,
|
||||||
cardColor: cardColor ?? this.cardColor,
|
cardColor: cardColor ?? this.cardColor,
|
||||||
|
repriseChildId: repriseChildId ?? this.repriseChildId,
|
||||||
|
existingPhotoUrl: existingPhotoUrl ?? this.existingPhotoUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,27 +190,17 @@ class UserRegistrationData extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Reprise après refus (#112) : réinitialise le flux parent avec les données dossier.
|
/// Reprise après refus (#112) : réinitialise le flux parent avec les données dossier.
|
||||||
void resetForReprise({
|
void resetForReprise({
|
||||||
required String firstName,
|
required ParentData parent1Data,
|
||||||
required String lastName,
|
ParentData? parent2Data,
|
||||||
required String phone,
|
List<ChildData>? childrenData,
|
||||||
required String email,
|
String motivation = '',
|
||||||
required String address,
|
|
||||||
required String postalCode,
|
|
||||||
required String city,
|
|
||||||
}) {
|
}) {
|
||||||
parent1 = ParentData(
|
parent1 = parent1Data;
|
||||||
firstName: firstName,
|
parent2 = parent2Data;
|
||||||
lastName: lastName,
|
children
|
||||||
phone: phone,
|
..clear()
|
||||||
email: email,
|
..addAll(childrenData ?? const []);
|
||||||
address: address,
|
motivationText = motivation;
|
||||||
postalCode: postalCode,
|
|
||||||
city: city,
|
|
||||||
password: '',
|
|
||||||
);
|
|
||||||
parent2 = null;
|
|
||||||
children.clear();
|
|
||||||
motivationText = '';
|
|
||||||
cguAccepted = false;
|
cguAccepted = false;
|
||||||
bankDetails = null;
|
bankDetails = null;
|
||||||
attestationCafNumber = '';
|
attestationCafNumber = '';
|
||||||
@ -208,11 +208,16 @@ class UserRegistrationData extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reprise (#112) : coordonnées parent principal + CGU.
|
/// Reprise (#112) : coordonnées + enfants connus + CGU.
|
||||||
bool get isRepriseSubmitReady =>
|
bool get isRepriseSubmitReady =>
|
||||||
parent1.firstName.isNotEmpty &&
|
parent1.firstName.isNotEmpty &&
|
||||||
parent1.lastName.isNotEmpty &&
|
parent1.lastName.isNotEmpty &&
|
||||||
parent1.email.isNotEmpty &&
|
parent1.email.isNotEmpty &&
|
||||||
|
children.isNotEmpty &&
|
||||||
|
children.every(
|
||||||
|
(c) =>
|
||||||
|
c.repriseChildId != null && c.repriseChildId!.trim().isNotEmpty,
|
||||||
|
) &&
|
||||||
cguAccepted;
|
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é)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ 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 '../../services/reprise_session.dart';
|
||||||
|
import '../../utils/reprise_payload.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';
|
||||||
@ -59,14 +60,11 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
try {
|
try {
|
||||||
if (RepriseSession.isActive) {
|
if (RepriseSession.isActive) {
|
||||||
await AuthService.resoumettreReprise(
|
await AuthService.resoumettreReprise(
|
||||||
token: RepriseSession.token!,
|
await ReprisePayload.amPatch(
|
||||||
prenom: registrationData.firstName,
|
registrationData,
|
||||||
nom: registrationData.lastName,
|
RepriseSession.token!,
|
||||||
telephone: registrationData.phone,
|
existingPhotoUrl: RepriseSession.photoUrlForApi,
|
||||||
adresse: registrationData.streetAddress,
|
),
|
||||||
ville: registrationData.city,
|
|
||||||
codePostal: registrationData.postalCode,
|
|
||||||
photoUrl: RepriseSession.photoUrl,
|
|
||||||
);
|
);
|
||||||
RepriseSession.clear();
|
RepriseSession.clear();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ 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';
|
import '../../services/reprise_session.dart';
|
||||||
|
import '../../utils/reprise_payload.dart';
|
||||||
|
|
||||||
class ParentRegisterStep5Screen extends StatefulWidget {
|
class ParentRegisterStep5Screen extends StatefulWidget {
|
||||||
const ParentRegisterStep5Screen({super.key});
|
const ParentRegisterStep5Screen({super.key});
|
||||||
@ -33,7 +34,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Vérifiez vos coordonnées et acceptez les conditions.',
|
'Vérifiez vos coordonnées, les enfants et acceptez les conditions.',
|
||||||
style: GoogleFonts.merienda(fontSize: 14),
|
style: GoogleFonts.merienda(fontSize: 14),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red.shade700,
|
backgroundColor: Colors.red.shade700,
|
||||||
@ -46,15 +47,8 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
try {
|
try {
|
||||||
if (RepriseSession.isActive) {
|
if (RepriseSession.isActive) {
|
||||||
final token = RepriseSession.token!;
|
final token = RepriseSession.token!;
|
||||||
final p = data.parent1;
|
|
||||||
await AuthService.resoumettreReprise(
|
await AuthService.resoumettreReprise(
|
||||||
token: token,
|
ReprisePayload.parentPatch(data, token),
|
||||||
prenom: p.firstName,
|
|
||||||
nom: p.lastName,
|
|
||||||
telephone: p.phone,
|
|
||||||
adresse: p.address,
|
|
||||||
ville: p.city,
|
|
||||||
codePostal: p.postalCode,
|
|
||||||
);
|
);
|
||||||
RepriseSession.clear();
|
RepriseSession.clear();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|||||||
@ -291,43 +291,20 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resoumission après refus. PATCH /auth/reprise-resoumettre. Ticket #112.
|
/// Resoumission après refus. PATCH /auth/reprise-resoumettre. Ticket #112.
|
||||||
static Future<void> resoumettreReprise({
|
static Future<void> resoumettreReprise(Map<String, dynamic> body) async {
|
||||||
required String token,
|
final cleaned = body['token']?.toString().trim() ?? '';
|
||||||
String? prenom,
|
|
||||||
String? nom,
|
|
||||||
String? telephone,
|
|
||||||
String? adresse,
|
|
||||||
String? ville,
|
|
||||||
String? codePostal,
|
|
||||||
String? photoUrl,
|
|
||||||
}) async {
|
|
||||||
final cleaned = token.trim();
|
|
||||||
if (cleaned.isEmpty) {
|
if (cleaned.isEmpty) {
|
||||||
throw Exception('Lien invalide ou expiré.');
|
throw Exception('Lien invalide ou expiré.');
|
||||||
}
|
}
|
||||||
final body = <String, dynamic>{'token': cleaned};
|
final payload = Map<String, dynamic>.from(body);
|
||||||
if (prenom != null && prenom.trim().isNotEmpty) body['prenom'] = prenom.trim();
|
payload['token'] = cleaned;
|
||||||
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;
|
late final http.Response response;
|
||||||
try {
|
try {
|
||||||
response = await http.patch(
|
response = await http.patch(
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseResoumettre}'),
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseResoumettre}'),
|
||||||
headers: ApiConfig.headers,
|
headers: ApiConfig.headers,
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(payload),
|
||||||
);
|
);
|
||||||
} on http.ClientException {
|
} on http.ClientException {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
|
|||||||
@ -2,6 +2,7 @@ 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/reprise_dossier.dart';
|
||||||
import 'package:p_tits_pas/models/user_registration_data.dart';
|
import 'package:p_tits_pas/models/user_registration_data.dart';
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
|
import 'package:p_tits_pas/utils/reprise_mapper.dart';
|
||||||
|
|
||||||
/// Contexte reprise après refus (token e-mail ou identify). Ticket #112.
|
/// Contexte reprise après refus (token e-mail ou identify). Ticket #112.
|
||||||
class RepriseSession {
|
class RepriseSession {
|
||||||
@ -10,6 +11,7 @@ class RepriseSession {
|
|||||||
static String? _token;
|
static String? _token;
|
||||||
static String? _role;
|
static String? _role;
|
||||||
static String? _photoUrl;
|
static String? _photoUrl;
|
||||||
|
static String? _photoUrlForApi;
|
||||||
|
|
||||||
static bool get isActive =>
|
static bool get isActive =>
|
||||||
_token != null && _token!.trim().isNotEmpty;
|
_token != null && _token!.trim().isNotEmpty;
|
||||||
@ -20,16 +22,22 @@ class RepriseSession {
|
|||||||
|
|
||||||
static bool get isAm => _role == 'assistante_maternelle';
|
static bool get isAm => _role == 'assistante_maternelle';
|
||||||
|
|
||||||
|
/// URL absolue pour l'affichage.
|
||||||
static String? get photoUrl => _photoUrl;
|
static String? get photoUrl => _photoUrl;
|
||||||
|
|
||||||
|
/// Chemin relatif API (`/uploads/…`) pour PATCH sans re-upload.
|
||||||
|
static String? get photoUrlForApi => _photoUrlForApi;
|
||||||
|
|
||||||
static void start({
|
static void start({
|
||||||
required String token,
|
required String token,
|
||||||
required RepriseDossier dossier,
|
required RepriseDossier dossier,
|
||||||
}) {
|
}) {
|
||||||
_token = token.trim();
|
_token = token.trim();
|
||||||
_role = dossier.role;
|
_role = dossier.role;
|
||||||
_photoUrl = dossier.photoUrl?.trim().isNotEmpty == true
|
final raw = dossier.photoUrl?.trim();
|
||||||
? ApiConfig.absoluteMediaUrl(dossier.photoUrl)
|
_photoUrlForApi = raw != null && raw.isNotEmpty ? raw : null;
|
||||||
|
_photoUrl = _photoUrlForApi != null
|
||||||
|
? ApiConfig.absoluteMediaUrl(_photoUrlForApi)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,22 +45,15 @@ class RepriseSession {
|
|||||||
_token = null;
|
_token = null;
|
||||||
_role = null;
|
_role = null;
|
||||||
_photoUrl = null;
|
_photoUrl = null;
|
||||||
|
_photoUrlForApi = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void applyToParent(UserRegistrationData data, RepriseDossier dossier) {
|
static void applyToParent(UserRegistrationData data, RepriseDossier dossier) {
|
||||||
data.resetForReprise(
|
RepriseMapper.applyParentDossier(data, dossier);
|
||||||
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) {
|
static void applyToAm(AmRegistrationData data, RepriseDossier dossier) {
|
||||||
final photo = _photoUrl;
|
final displayPhoto = _photoUrl;
|
||||||
data.resetForReprise(
|
data.resetForReprise(
|
||||||
firstName: dossier.prenom ?? '',
|
firstName: dossier.prenom ?? '',
|
||||||
lastName: dossier.nom ?? '',
|
lastName: dossier.nom ?? '',
|
||||||
@ -61,7 +62,17 @@ class RepriseSession {
|
|||||||
streetAddress: dossier.adresse ?? '',
|
streetAddress: dossier.adresse ?? '',
|
||||||
postalCode: dossier.codePostal ?? '',
|
postalCode: dossier.codePostal ?? '',
|
||||||
city: dossier.ville ?? '',
|
city: dossier.ville ?? '',
|
||||||
existingPhotoUrl: photo,
|
existingPhotoUrl: displayPhoto,
|
||||||
|
consentementPhoto: dossier.consentementPhoto ?? false,
|
||||||
|
dateOfBirth: RepriseMapper.parseIsoDate(dossier.dateNaissance),
|
||||||
|
birthCity: dossier.lieuNaissanceVille ?? '',
|
||||||
|
birthCountry: dossier.lieuNaissancePays ?? '',
|
||||||
|
nir: dossier.nir ?? '',
|
||||||
|
agrementNumber: dossier.numeroAgrement ?? '',
|
||||||
|
agreementDate: RepriseMapper.parseIsoDate(dossier.dateAgrement),
|
||||||
|
capacity: dossier.nbMaxEnfants,
|
||||||
|
placesAvailable: dossier.placeDisponible,
|
||||||
|
presentationText: dossier.biographie ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,6 +149,20 @@ class ParentRegistrationPayload {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enfant pour PATCH reprise (inclut `id` si connu).
|
||||||
|
static Map<String, dynamic> childToRepriseJson(
|
||||||
|
ChildData c,
|
||||||
|
int index,
|
||||||
|
String parentNom,
|
||||||
|
) {
|
||||||
|
final map = _childToJson(c, index, parentNom);
|
||||||
|
final id = c.repriseChildId?.trim();
|
||||||
|
if (id != null && id.isNotEmpty) {
|
||||||
|
map['id'] = id;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
|
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
|
||||||
final map = <String, dynamic>{
|
final map = <String, dynamic>{
|
||||||
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
|
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
|
||||||
|
|||||||
120
frontend/lib/utils/reprise_mapper.dart
Normal file
120
frontend/lib/utils/reprise_mapper.dart
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import 'package:p_tits_pas/models/card_assets.dart';
|
||||||
|
import 'package:p_tits_pas/models/dossier_unifie.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';
|
||||||
|
|
||||||
|
/// Mapping GET reprise-dossier → modèles wizard. Ticket #112.
|
||||||
|
class RepriseMapper {
|
||||||
|
RepriseMapper._();
|
||||||
|
|
||||||
|
static const List<CardColorVertical> _childCardColors = [
|
||||||
|
CardColorVertical.lavender,
|
||||||
|
CardColorVertical.pink,
|
||||||
|
CardColorVertical.peach,
|
||||||
|
CardColorVertical.lime,
|
||||||
|
CardColorVertical.red,
|
||||||
|
CardColorVertical.green,
|
||||||
|
CardColorVertical.blue,
|
||||||
|
];
|
||||||
|
|
||||||
|
static ParentData parentFromDossier(ParentDossier p) {
|
||||||
|
return ParentData(
|
||||||
|
firstName: p.prenom ?? '',
|
||||||
|
lastName: p.nom ?? '',
|
||||||
|
phone: p.telephone ?? '',
|
||||||
|
email: p.email,
|
||||||
|
address: p.adresse ?? '',
|
||||||
|
postalCode: p.codePostal ?? '',
|
||||||
|
city: p.ville ?? '',
|
||||||
|
password: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ChildData childFromEnfant(EnfantDossier e, int index) {
|
||||||
|
final isUnborn = e.status == 'a_naitre';
|
||||||
|
final dob = isUnborn
|
||||||
|
? isoToDdMmYyyy(e.dueDate)
|
||||||
|
: isoToDdMmYyyy(e.birthDate);
|
||||||
|
final photo = e.photoUrl?.trim();
|
||||||
|
return ChildData(
|
||||||
|
firstName: e.firstName ?? '',
|
||||||
|
lastName: e.lastName ?? '',
|
||||||
|
dob: dob,
|
||||||
|
genre: e.gender ?? '',
|
||||||
|
photoConsent: e.consentPhoto,
|
||||||
|
multipleBirth: e.estMultiple,
|
||||||
|
isUnbornChild: isUnborn,
|
||||||
|
cardColor: _childCardColors[index % _childCardColors.length],
|
||||||
|
repriseChildId: e.id,
|
||||||
|
existingPhotoUrl: photo != null && photo.isNotEmpty
|
||||||
|
? ApiConfig.absoluteMediaUrl(photo)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyParentDossier(UserRegistrationData data, RepriseDossier dossier) {
|
||||||
|
ParentDossier? titulaire;
|
||||||
|
ParentDossier? coParent;
|
||||||
|
|
||||||
|
if (dossier.parents.isNotEmpty) {
|
||||||
|
for (final p in dossier.parents) {
|
||||||
|
if (p.id == dossier.id) {
|
||||||
|
titulaire = p;
|
||||||
|
} else {
|
||||||
|
coParent = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
titulaire ??= dossier.parents.first;
|
||||||
|
if (coParent == null && dossier.parents.length > 1) {
|
||||||
|
coParent = dossier.parents.firstWhere(
|
||||||
|
(p) => p.id != titulaire!.id,
|
||||||
|
orElse: () => dossier.parents.last,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final p1 = titulaire != null
|
||||||
|
? parentFromDossier(titulaire)
|
||||||
|
: ParentData(
|
||||||
|
firstName: dossier.prenom ?? '',
|
||||||
|
lastName: dossier.nom ?? '',
|
||||||
|
phone: dossier.telephone ?? '',
|
||||||
|
email: dossier.email,
|
||||||
|
address: dossier.adresse ?? '',
|
||||||
|
postalCode: dossier.codePostal ?? '',
|
||||||
|
city: dossier.ville ?? '',
|
||||||
|
password: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
final children = dossier.enfants
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => childFromEnfant(e.value, e.key))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
data.resetForReprise(
|
||||||
|
parent1Data: p1,
|
||||||
|
parent2Data: coParent != null ? parentFromDossier(coParent) : null,
|
||||||
|
childrenData: children,
|
||||||
|
motivation: dossier.texteMotivation ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateTime? parseIsoDate(String? raw) {
|
||||||
|
if (raw == null || raw.trim().isEmpty) return null;
|
||||||
|
try {
|
||||||
|
return DateTime.parse(raw.trim());
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String isoToDdMmYyyy(String? iso) {
|
||||||
|
final dt = parseIsoDate(iso);
|
||||||
|
if (dt == null) return '';
|
||||||
|
return '${dt.day.toString().padLeft(2, '0')}/'
|
||||||
|
'${dt.month.toString().padLeft(2, '0')}/'
|
||||||
|
'${dt.year}';
|
||||||
|
}
|
||||||
|
}
|
||||||
150
frontend/lib/utils/reprise_payload.dart
Normal file
150
frontend/lib/utils/reprise_payload.dart
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../models/am_registration_data.dart';
|
||||||
|
import '../models/user_registration_data.dart';
|
||||||
|
import 'nir_utils.dart';
|
||||||
|
import 'parent_registration_payload.dart';
|
||||||
|
|
||||||
|
/// Body PATCH /auth/reprise-resoumettre. Ticket #112.
|
||||||
|
class ReprisePayload {
|
||||||
|
ReprisePayload._();
|
||||||
|
|
||||||
|
static Map<String, dynamic> parentPatch(
|
||||||
|
UserRegistrationData data,
|
||||||
|
String token,
|
||||||
|
) {
|
||||||
|
final base = ParentRegistrationPayload.toJson(data);
|
||||||
|
base.remove('email');
|
||||||
|
base.remove('acceptation_cgu');
|
||||||
|
base.remove('acceptation_privacy');
|
||||||
|
base['token'] = token.trim();
|
||||||
|
|
||||||
|
final enfants = data.children.asMap().entries.map((e) {
|
||||||
|
final childMap = ParentRegistrationPayload.childToRepriseJson(
|
||||||
|
e.value,
|
||||||
|
e.key,
|
||||||
|
data.parent1.lastName.trim(),
|
||||||
|
);
|
||||||
|
return childMap;
|
||||||
|
}).toList();
|
||||||
|
base['enfants'] = enfants;
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> amPatch(
|
||||||
|
AmRegistrationData data,
|
||||||
|
String token, {
|
||||||
|
String? existingPhotoUrl,
|
||||||
|
}) async {
|
||||||
|
final body = <String, dynamic>{'token': token.trim()};
|
||||||
|
|
||||||
|
_put(body, 'prenom', data.firstName.trim());
|
||||||
|
_put(body, 'nom', data.lastName.trim());
|
||||||
|
_put(body, 'telephone', data.phone.trim());
|
||||||
|
_put(body, 'adresse', data.streetAddress.trim());
|
||||||
|
_put(body, 'code_postal', data.postalCode.trim());
|
||||||
|
_put(body, 'ville', data.city.trim());
|
||||||
|
|
||||||
|
body['consentement_photo'] = data.photoConsent;
|
||||||
|
|
||||||
|
if (data.dateOfBirth != null) {
|
||||||
|
final d = data.dateOfBirth!;
|
||||||
|
body['date_naissance'] =
|
||||||
|
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
_put(body, 'lieu_naissance_ville', data.birthCity.trim());
|
||||||
|
_put(body, 'lieu_naissance_pays', data.birthCountry.trim());
|
||||||
|
_put(body, 'numero_agrement', data.agrementNumber.trim());
|
||||||
|
if (data.nir.trim().isNotEmpty) {
|
||||||
|
body['nir'] = normalizeNir(data.nir);
|
||||||
|
}
|
||||||
|
if (data.agreementDate != null) {
|
||||||
|
final d = data.agreementDate!;
|
||||||
|
body['date_agrement'] =
|
||||||
|
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
if (data.capacity != null) {
|
||||||
|
body['capacite_accueil'] = data.capacity;
|
||||||
|
}
|
||||||
|
if (data.placesAvailable != null) {
|
||||||
|
body['places_disponibles'] = data.placesAvailable;
|
||||||
|
}
|
||||||
|
if (data.presentationText.trim().isNotEmpty) {
|
||||||
|
body['biographie'] = data.presentationText.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
final photo = await _amPhotoPayload(data, existingPhotoUrl);
|
||||||
|
if (photo != null) {
|
||||||
|
body.addAll(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, String>?> _amPhotoPayload(
|
||||||
|
AmRegistrationData data,
|
||||||
|
String? existingPhotoUrl,
|
||||||
|
) async {
|
||||||
|
if (data.photoBytes != null && data.photoBytes!.isNotEmpty) {
|
||||||
|
final mime = _imageMimeForBytes(data.photoBytes!);
|
||||||
|
final fn = (data.photoFilename ?? '').trim();
|
||||||
|
return {
|
||||||
|
'photo_base64':
|
||||||
|
'data:$mime;base64,${base64Encode(data.photoBytes!)}',
|
||||||
|
'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kIsWeb &&
|
||||||
|
data.photoPath != null &&
|
||||||
|
data.photoPath!.isNotEmpty &&
|
||||||
|
!data.photoPath!.startsWith('assets/') &&
|
||||||
|
!data.photoPath!.startsWith('http')) {
|
||||||
|
try {
|
||||||
|
final file = File(data.photoPath!);
|
||||||
|
if (await file.exists()) {
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final mime = _imageMimeForBytes(bytes);
|
||||||
|
return {
|
||||||
|
'photo_base64': 'data:$mime;base64,${base64Encode(bytes)}',
|
||||||
|
'photo_filename':
|
||||||
|
_basenameFromPath(data.photoPath!) ?? 'photo_am.jpg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final url = (existingPhotoUrl ?? data.repriseExistingPhotoUrl ?? '').trim();
|
||||||
|
if (url.isNotEmpty) {
|
||||||
|
return {'photo_url': url};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _imageMimeForBytes(Uint8List bytes) {
|
||||||
|
if (bytes.length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8) {
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
if (bytes.length >= 8 &&
|
||||||
|
bytes[0] == 0x89 &&
|
||||||
|
bytes[1] == 0x50 &&
|
||||||
|
bytes[2] == 0x4E &&
|
||||||
|
bytes[3] == 0x47) {
|
||||||
|
return 'image/png';
|
||||||
|
}
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _basenameFromPath(String path) {
|
||||||
|
final parts = path.replaceAll('\\', '/').split('/');
|
||||||
|
return parts.isEmpty ? null : parts.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _put(Map<String, dynamic> m, String key, String value) {
|
||||||
|
if (value.isNotEmpty) m[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user