fix(#112): dates et places AM en reprise (resetForReprise)
Corrige l’ombre des paramètres dateOfBirth, agreementDate et placesAvailable dans resetForReprise ; renforce le parsing reprise-dossier et ajoute un test. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c26ed00374
commit
df776d8200
@ -141,14 +141,14 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
photoBytes = null;
|
||||
photoFilename = null;
|
||||
photoConsent = consentementPhoto;
|
||||
dateOfBirth = dateOfBirth;
|
||||
this.dateOfBirth = dateOfBirth;
|
||||
this.birthCity = birthCity;
|
||||
this.birthCountry = birthCountry;
|
||||
this.nir = nir;
|
||||
this.agrementNumber = agrementNumber;
|
||||
agreementDate = agreementDate;
|
||||
this.agreementDate = agreementDate;
|
||||
this.capacity = capacity;
|
||||
placesAvailable = placesAvailable;
|
||||
this.placesAvailable = placesAvailable;
|
||||
this.presentationText = presentationText;
|
||||
cguAccepted = false;
|
||||
notifyListeners();
|
||||
|
||||
@ -64,7 +64,22 @@ class RepriseDossier {
|
||||
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
|
||||
@ -100,19 +115,64 @@ class RepriseDossier {
|
||||
texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier'])
|
||||
?.toString(),
|
||||
consentementPhoto: RepriseMapper.optionalBool(json['consentement_photo']) ||
|
||||
RepriseMapper.optionalBool(json['consent_photo']),
|
||||
dateNaissance: RepriseMapper.optionalDateString(json['date_naissance']),
|
||||
lieuNaissanceVille: json['lieu_naissance_ville']?.toString(),
|
||||
lieuNaissancePays: json['lieu_naissance_pays']?.toString(),
|
||||
numeroAgrement: (json['numero_agrement'] ?? json['numero_agrement_am'])
|
||||
?.toString(),
|
||||
nir: json['nir']?.toString(),
|
||||
dateAgrement: RepriseMapper.optionalDateString(json['date_agrement']),
|
||||
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(
|
||||
json['nb_max_enfants'] ?? json['capacite_accueil'],
|
||||
_firstNonNull([
|
||||
json['nb_max_enfants'],
|
||||
json['capacite_accueil'],
|
||||
json['nbMaxEnfants'],
|
||||
nestedDossier?['nb_max_enfants'],
|
||||
nestedDossier?['capacite_accueil'],
|
||||
]),
|
||||
),
|
||||
placeDisponible: RepriseMapper.optionalInt(
|
||||
json['place_disponible'] ?? json['places_disponibles'],
|
||||
_firstNonNull([
|
||||
json['place_disponible'],
|
||||
json['places_disponibles'],
|
||||
json['placesDisponibles'],
|
||||
nestedDossier?['place_disponible'],
|
||||
nestedDossier?['places_disponibles'],
|
||||
]),
|
||||
),
|
||||
biographie: (json['biographie'] ?? json['presentation'])?.toString(),
|
||||
);
|
||||
|
||||
@ -11,8 +11,8 @@ class AmRegisterStep2Screen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final registrationData = Provider.of<AmRegistrationData>(context, listen: false);
|
||||
|
||||
return Consumer<AmRegistrationData>(
|
||||
builder: (context, registrationData, _) {
|
||||
final initialData = ProfessionalInfoData(
|
||||
photoPath: registrationData.photoPath,
|
||||
photoBytes: registrationData.photoBytes,
|
||||
@ -58,5 +58,7 @@ class AmRegisterStep2Screen extends StatelessWidget {
|
||||
context.go('/am-register-step3');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +287,10 @@ class AuthService {
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw Exception('Réponse invalide du serveur.');
|
||||
}
|
||||
return RepriseDossier.fromJson(decoded);
|
||||
final payload = decoded['data'] is Map
|
||||
? Map<String, dynamic>.from(decoded['data'] as Map)
|
||||
: decoded;
|
||||
return RepriseDossier.fromJson(payload);
|
||||
}
|
||||
|
||||
/// Modale login : numéro + e-mail → token reprise. POST /auth/reprise-identify. #112.
|
||||
|
||||
@ -108,6 +108,15 @@ class RepriseMapper {
|
||||
static DateTime? parseIsoDate(String? raw) {
|
||||
if (raw == null || raw.trim().isEmpty) return null;
|
||||
final s = raw.trim();
|
||||
final iso = RegExp(r'^(\d{4})-(\d{2})-(\d{2})');
|
||||
final isoMatch = iso.firstMatch(s);
|
||||
if (isoMatch != null) {
|
||||
return DateTime(
|
||||
int.parse(isoMatch.group(1)!),
|
||||
int.parse(isoMatch.group(2)!),
|
||||
int.parse(isoMatch.group(3)!),
|
||||
);
|
||||
}
|
||||
try {
|
||||
return DateTime.parse(s);
|
||||
} catch (_) {
|
||||
@ -128,10 +137,34 @@ class RepriseMapper {
|
||||
if (value == null) return null;
|
||||
if (value is String) {
|
||||
final s = value.trim();
|
||||
return s.isEmpty ? null : s;
|
||||
return s.isEmpty || s == 'null' ? null : s;
|
||||
}
|
||||
if (value is DateTime) return value.toIso8601String();
|
||||
return null;
|
||||
if (value is DateTime) {
|
||||
return '${value.year.toString().padLeft(4, '0')}-'
|
||||
'${value.month.toString().padLeft(2, '0')}-'
|
||||
'${value.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
if (value is num) {
|
||||
final ms = value.abs() > 9999999999
|
||||
? value.toInt()
|
||||
: value.toInt() * 1000;
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true);
|
||||
return '${dt.year.toString().padLeft(4, '0')}-'
|
||||
'${dt.month.toString().padLeft(2, '0')}-'
|
||||
'${dt.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
if (value is Map) {
|
||||
final y = value['year'];
|
||||
final m = value['month'] ?? value['monthValue'];
|
||||
final d = value['day'] ?? value['dayOfMonth'];
|
||||
if (y is num && m is num && d is num) {
|
||||
return '${y.toInt().toString().padLeft(4, '0')}-'
|
||||
'${m.toInt().toString().padLeft(2, '0')}-'
|
||||
'${d.toInt().toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
final s = value.toString().trim();
|
||||
return s.isEmpty || s == 'null' ? null : s;
|
||||
}
|
||||
|
||||
static int? optionalInt(dynamic value) {
|
||||
|
||||
@ -134,9 +134,40 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_applyInitialData(widget.initialData);
|
||||
|
||||
final data = widget.initialData;
|
||||
if (data != null) {
|
||||
if (widget.mode == DisplayMode.editable) {
|
||||
_birthCityFocus = FocusNode();
|
||||
_birthCountryFocus = FocusNode();
|
||||
_nirFocus = FocusNode();
|
||||
_birthCityFocus!.addListener(_onBirthCityFocusChange);
|
||||
_birthCountryFocus!.addListener(_onBirthCountryFocusChange);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ProfessionalInfoFormScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final next = widget.initialData;
|
||||
final prev = oldWidget.initialData;
|
||||
if (next == null) return;
|
||||
if (prev == null ||
|
||||
prev.dateOfBirth != next.dateOfBirth ||
|
||||
prev.agreementDate != next.agreementDate ||
|
||||
prev.placesAvailable != next.placesAvailable ||
|
||||
prev.capacity != next.capacity ||
|
||||
prev.nir != next.nir ||
|
||||
prev.birthCity != next.birthCity ||
|
||||
prev.birthCountry != next.birthCountry ||
|
||||
prev.agrementNumber != next.agrementNumber ||
|
||||
prev.photoPath != next.photoPath ||
|
||||
prev.photoConsent != next.photoConsent) {
|
||||
_applyInitialData(next);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyInitialData(ProfessionalInfoData? data) {
|
||||
if (data == null) return;
|
||||
_selectedDate = data.dateOfBirth;
|
||||
_dateOfBirthController.text = data.dateOfBirth != null
|
||||
? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!)
|
||||
@ -159,15 +190,6 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
_photoConsent = data.photoConsent;
|
||||
}
|
||||
|
||||
if (widget.mode == DisplayMode.editable) {
|
||||
_birthCityFocus = FocusNode();
|
||||
_birthCountryFocus = FocusNode();
|
||||
_nirFocus = FocusNode();
|
||||
_birthCityFocus!.addListener(_onBirthCityFocusChange);
|
||||
_birthCountryFocus!.addListener(_onBirthCountryFocusChange);
|
||||
}
|
||||
}
|
||||
|
||||
void _onBirthCityFocusChange() {
|
||||
if (_birthCityFocus == null || _birthCityFocus!.hasFocus) return;
|
||||
_applyPlaceNameFormat(_birthCityController);
|
||||
|
||||
52
frontend/test/reprise_am_parse_test.dart
Normal file
52
frontend/test/reprise_am_parse_test.dart
Normal file
@ -0,0 +1,52 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:p_tits_pas/models/am_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/reprise_dossier.dart';
|
||||
import 'package:p_tits_pas/utils/reprise_mapper.dart';
|
||||
|
||||
void main() {
|
||||
test('RepriseDossier parse champs AM plats', () {
|
||||
final d = RepriseDossier.fromJson({
|
||||
'id': 'am1',
|
||||
'email': 'marie@test.fr',
|
||||
'role': 'assistante_maternelle',
|
||||
'date_naissance': '1980-06-08',
|
||||
'date_agrement': '2019-09-01',
|
||||
'place_disponible': 2,
|
||||
'nb_max_enfants': 4,
|
||||
'lieu_naissance_ville': 'Ajaccio',
|
||||
'nir': '280062A00100191',
|
||||
'numero_agrement': 'AGR-2019-095001',
|
||||
});
|
||||
|
||||
expect(d.dateNaissance, '1980-06-08');
|
||||
expect(d.dateAgrement, '2019-09-01');
|
||||
expect(d.placeDisponible, 2);
|
||||
|
||||
final data = AmRegistrationData();
|
||||
RepriseMapper.applyAmDossier(data, d);
|
||||
expect(data.dateOfBirth, DateTime(1980, 6, 8));
|
||||
expect(data.agreementDate, DateTime(2019, 9, 1));
|
||||
expect(data.placesAvailable, 2);
|
||||
});
|
||||
|
||||
test('RepriseDossier parse champs AM imbriqués + wrapper data', () {
|
||||
final d = RepriseDossier.fromJson({
|
||||
'id': 'am1',
|
||||
'email': 'marie@test.fr',
|
||||
'role': 'assistante_maternelle',
|
||||
'nir': '280062A00100191',
|
||||
'numero_agrement': 'AGR-2019-095001',
|
||||
'nb_max_enfants': 4,
|
||||
'lieu_naissance_ville': 'Ajaccio',
|
||||
'user': {'date_naissance': '1980-06-08T00:00:00.000Z'},
|
||||
'dossier': {
|
||||
'date_agrement': '2019-09-01',
|
||||
'places_disponibles': 2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(d.dateNaissance, isNotNull);
|
||||
expect(d.dateAgrement, '2019-09-01');
|
||||
expect(d.placeDisponible, 2);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user