feat(#112): reprise après refus — dossier complet, email resoumission
- GET/PATCH reprise-dossier enrichis (parents, enfants, motivation, fiche AM) - Front: lien mail, modale identify login, wizards préremplis, PATCH complet - Email accusé resoumission aux parents avec n° de dossier - Fixes préremplissage AM (dates, places, ValueKey étape 2) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,6 +33,9 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
/// Places libres actuellement (0 ≤ valeur ≤ capacité) — API `places_disponibles`.
|
||||
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
|
||||
String presentationText = '';
|
||||
bool cguAccepted = false;
|
||||
@@ -104,6 +107,53 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
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,
|
||||
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.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 = consentementPhoto;
|
||||
this.dateOfBirth = dateOfBirth;
|
||||
this.birthCity = birthCity;
|
||||
this.birthCountry = birthCountry;
|
||||
this.nir = nir;
|
||||
this.agrementNumber = agrementNumber;
|
||||
this.agreementDate = agreementDate;
|
||||
this.capacity = capacity;
|
||||
this.placesAvailable = placesAvailable;
|
||||
this.presentationText = presentationText;
|
||||
cguAccepted = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// --- Getters for validation or display ---
|
||||
bool get isStep1Complete =>
|
||||
firstName.trim().length >= 2 &&
|
||||
@@ -118,6 +168,8 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
/// Photo réelle (pas seulement un placeholder asset).
|
||||
bool get _hasUserPhoto =>
|
||||
(photoBytes != null && photoBytes!.isNotEmpty) ||
|
||||
(repriseExistingPhotoUrl != null &&
|
||||
repriseExistingPhotoUrl!.trim().isNotEmpty) ||
|
||||
(photoPath != null &&
|
||||
photoPath!.isNotEmpty &&
|
||||
!photoPath!.startsWith('assets/'));
|
||||
@@ -145,6 +197,9 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
bool get isRegistrationComplete =>
|
||||
isStep1Complete && isStep2Complete && isStep3Complete;
|
||||
|
||||
/// Reprise (#112) : dossier AM complet renvoyé par PATCH reprise-resoumettre.
|
||||
bool get isRepriseSubmitReady => isRegistrationComplete;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AmRegistrationData('
|
||||
|
||||
@@ -184,6 +184,7 @@ class EnfantDossier {
|
||||
final String? dueDate;
|
||||
final String? photoUrl;
|
||||
final bool consentPhoto;
|
||||
final bool estMultiple;
|
||||
|
||||
EnfantDossier({
|
||||
required this.id,
|
||||
@@ -195,6 +196,7 @@ class EnfantDossier {
|
||||
this.dueDate,
|
||||
this.photoUrl,
|
||||
this.consentPhoto = false,
|
||||
this.estMultiple = false,
|
||||
});
|
||||
|
||||
String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
|
||||
@@ -223,6 +225,8 @@ class EnfantDossier {
|
||||
photoUrl: resolvedPhoto,
|
||||
consentPhoto:
|
||||
json['consent_photo'] == true || json['consentPhoto'] == true,
|
||||
estMultiple:
|
||||
json['est_multiple'] == true || json['estMultiple'] == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||
import 'package:p_tits_pas/utils/reprise_mapper.dart';
|
||||
|
||||
/// Réponse GET /auth/reprise-dossier. Tickets #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;
|
||||
|
||||
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({
|
||||
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,
|
||||
this.parents = const [],
|
||||
this.enfants = const [],
|
||||
this.texteMotivation,
|
||||
this.consentementPhoto = false,
|
||||
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 isAm => role == 'assistante_maternelle';
|
||||
|
||||
static Map<String, dynamic>? _nestedMap(dynamic value) {
|
||||
if (value is Map) return Map<String, dynamic>.from(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
static dynamic _firstNonNull(List<dynamic> values) {
|
||||
for (final v in values) {
|
||||
if (v != null) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
factory RepriseDossier.fromJson(Map<String, dynamic> json) {
|
||||
final nestedUser = _nestedMap(json['user']);
|
||||
final nestedDossier = _nestedMap(json['dossier']);
|
||||
|
||||
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>[];
|
||||
|
||||
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(),
|
||||
parents: parentsList,
|
||||
enfants: enfantsList,
|
||||
texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier'])
|
||||
?.toString(),
|
||||
consentementPhoto: RepriseMapper.optionalBool(json['consentement_photo']) ||
|
||||
RepriseMapper.optionalBool(json['consent_photo']) ||
|
||||
RepriseMapper.optionalBool(nestedUser?['consentement_photo']),
|
||||
dateNaissance: RepriseMapper.optionalDateString(
|
||||
_firstNonNull([
|
||||
json['date_naissance'],
|
||||
json['dateNaissance'],
|
||||
nestedUser?['date_naissance'],
|
||||
nestedUser?['dateNaissance'],
|
||||
]),
|
||||
),
|
||||
lieuNaissanceVille: _firstNonNull([
|
||||
json['lieu_naissance_ville'],
|
||||
json['lieuNaissanceVille'],
|
||||
nestedUser?['lieu_naissance_ville'],
|
||||
nestedUser?['lieuNaissanceVille'],
|
||||
])?.toString(),
|
||||
lieuNaissancePays: _firstNonNull([
|
||||
json['lieu_naissance_pays'],
|
||||
json['lieuNaissancePays'],
|
||||
nestedUser?['lieu_naissance_pays'],
|
||||
nestedUser?['lieuNaissancePays'],
|
||||
])?.toString(),
|
||||
numeroAgrement: _firstNonNull([
|
||||
json['numero_agrement'],
|
||||
json['numero_agrement_am'],
|
||||
json['numeroAgrement'],
|
||||
nestedDossier?['numero_agrement'],
|
||||
nestedDossier?['numeroAgrement'],
|
||||
])?.toString(),
|
||||
nir: _firstNonNull([
|
||||
json['nir'],
|
||||
nestedDossier?['nir'],
|
||||
])?.toString(),
|
||||
dateAgrement: RepriseMapper.optionalDateString(
|
||||
_firstNonNull([
|
||||
json['date_agrement'],
|
||||
json['dateAgrement'],
|
||||
nestedDossier?['date_agrement'],
|
||||
nestedDossier?['dateAgrement'],
|
||||
]),
|
||||
),
|
||||
nbMaxEnfants: RepriseMapper.optionalInt(
|
||||
_firstNonNull([
|
||||
json['nb_max_enfants'],
|
||||
json['capacite_accueil'],
|
||||
json['nbMaxEnfants'],
|
||||
nestedDossier?['nb_max_enfants'],
|
||||
nestedDossier?['capacite_accueil'],
|
||||
]),
|
||||
),
|
||||
placeDisponible: RepriseMapper.optionalInt(
|
||||
_firstNonNull([
|
||||
json['place_disponible'],
|
||||
json['places_disponibles'],
|
||||
json['placesDisponibles'],
|
||||
nestedDossier?['place_disponible'],
|
||||
nestedDossier?['places_disponibles'],
|
||||
]),
|
||||
),
|
||||
biographie: (json['biographie'] ?? json['presentation'])?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class ParentData {
|
||||
class ChildData {
|
||||
static const Object _unsetImage = Object();
|
||||
static const Object _unsetImageBytes = Object();
|
||||
static const Object _unsetExistingPhotoUrl = Object();
|
||||
|
||||
String firstName;
|
||||
String lastName;
|
||||
@@ -43,6 +44,10 @@ class ChildData {
|
||||
/// Octets de la photo (fiable à l’envoi API ; [imageFile] peut être absent sur le web).
|
||||
Uint8List? imageBytes;
|
||||
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({
|
||||
this.firstName = '',
|
||||
@@ -55,6 +60,8 @@ class ChildData {
|
||||
this.imageFile,
|
||||
this.imageBytes,
|
||||
required this.cardColor, // Rendre requis dans le constructeur
|
||||
this.repriseChildId,
|
||||
this.existingPhotoUrl,
|
||||
});
|
||||
|
||||
ChildData copyWith({
|
||||
@@ -68,6 +75,8 @@ class ChildData {
|
||||
Object? imageFile = _unsetImage,
|
||||
Object? imageBytes = _unsetImageBytes,
|
||||
CardColorVertical? cardColor,
|
||||
String? repriseChildId,
|
||||
Object? existingPhotoUrl = _unsetExistingPhotoUrl,
|
||||
}) {
|
||||
return ChildData(
|
||||
firstName: firstName ?? this.firstName,
|
||||
@@ -81,6 +90,10 @@ class ChildData {
|
||||
imageBytes:
|
||||
identical(imageBytes, _unsetImageBytes) ? this.imageBytes : imageBytes as Uint8List?,
|
||||
cardColor: cardColor ?? this.cardColor,
|
||||
repriseChildId: repriseChildId ?? this.repriseChildId,
|
||||
existingPhotoUrl: identical(existingPhotoUrl, _unsetExistingPhotoUrl)
|
||||
? this.existingPhotoUrl
|
||||
: existingPhotoUrl as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -178,6 +191,38 @@ class UserRegistrationData extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reprise après refus (#112) : réinitialise le flux parent avec les données dossier.
|
||||
void resetForReprise({
|
||||
required ParentData parent1Data,
|
||||
ParentData? parent2Data,
|
||||
List<ChildData>? childrenData,
|
||||
String motivation = '',
|
||||
}) {
|
||||
parent1 = parent1Data;
|
||||
parent2 = parent2Data;
|
||||
children
|
||||
..clear()
|
||||
..addAll(childrenData ?? const []);
|
||||
motivationText = motivation;
|
||||
cguAccepted = false;
|
||||
bankDetails = null;
|
||||
attestationCafNumber = '';
|
||||
consentQuotientFamilial = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reprise (#112) : coordonnées + enfants connus + CGU.
|
||||
bool get isRepriseSubmitReady =>
|
||||
parent1.firstName.isNotEmpty &&
|
||||
parent1.lastName.isNotEmpty &&
|
||||
parent1.email.isNotEmpty &&
|
||||
children.isNotEmpty &&
|
||||
children.every(
|
||||
(c) =>
|
||||
c.repriseChildId != null && c.repriseChildId!.trim().isNotEmpty,
|
||||
) &&
|
||||
cguAccepted;
|
||||
|
||||
// Méthode pour vérifier si toutes les données requises sont là (simplifié)
|
||||
bool isRegistrationComplete() {
|
||||
// Ajouter ici les validations nécessaires
|
||||
|
||||
Reference in New Issue
Block a user