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;
|
photoBytes = null;
|
||||||
photoFilename = null;
|
photoFilename = null;
|
||||||
photoConsent = consentementPhoto;
|
photoConsent = consentementPhoto;
|
||||||
dateOfBirth = dateOfBirth;
|
this.dateOfBirth = dateOfBirth;
|
||||||
this.birthCity = birthCity;
|
this.birthCity = birthCity;
|
||||||
this.birthCountry = birthCountry;
|
this.birthCountry = birthCountry;
|
||||||
this.nir = nir;
|
this.nir = nir;
|
||||||
this.agrementNumber = agrementNumber;
|
this.agrementNumber = agrementNumber;
|
||||||
agreementDate = agreementDate;
|
this.agreementDate = agreementDate;
|
||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
placesAvailable = placesAvailable;
|
this.placesAvailable = placesAvailable;
|
||||||
this.presentationText = presentationText;
|
this.presentationText = presentationText;
|
||||||
cguAccepted = false;
|
cguAccepted = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@ -64,7 +64,22 @@ class RepriseDossier {
|
|||||||
bool get isParent => role == 'parent';
|
bool get isParent => role == 'parent';
|
||||||
bool get isAm => role == 'assistante_maternelle';
|
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) {
|
factory RepriseDossier.fromJson(Map<String, dynamic> json) {
|
||||||
|
final nestedUser = _nestedMap(json['user']);
|
||||||
|
final nestedDossier = _nestedMap(json['dossier']);
|
||||||
|
|
||||||
final parentsRaw = json['parents'];
|
final parentsRaw = json['parents'];
|
||||||
final parentsList = parentsRaw is List
|
final parentsList = parentsRaw is List
|
||||||
? parentsRaw
|
? parentsRaw
|
||||||
@ -100,19 +115,64 @@ class RepriseDossier {
|
|||||||
texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier'])
|
texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier'])
|
||||||
?.toString(),
|
?.toString(),
|
||||||
consentementPhoto: RepriseMapper.optionalBool(json['consentement_photo']) ||
|
consentementPhoto: RepriseMapper.optionalBool(json['consentement_photo']) ||
|
||||||
RepriseMapper.optionalBool(json['consent_photo']),
|
RepriseMapper.optionalBool(json['consent_photo']) ||
|
||||||
dateNaissance: RepriseMapper.optionalDateString(json['date_naissance']),
|
RepriseMapper.optionalBool(nestedUser?['consentement_photo']),
|
||||||
lieuNaissanceVille: json['lieu_naissance_ville']?.toString(),
|
dateNaissance: RepriseMapper.optionalDateString(
|
||||||
lieuNaissancePays: json['lieu_naissance_pays']?.toString(),
|
_firstNonNull([
|
||||||
numeroAgrement: (json['numero_agrement'] ?? json['numero_agrement_am'])
|
json['date_naissance'],
|
||||||
?.toString(),
|
json['dateNaissance'],
|
||||||
nir: json['nir']?.toString(),
|
nestedUser?['date_naissance'],
|
||||||
dateAgrement: RepriseMapper.optionalDateString(json['date_agrement']),
|
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(
|
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(
|
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(),
|
biographie: (json['biographie'] ?? json['presentation'])?.toString(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,51 +11,53 @@ class AmRegisterStep2Screen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final registrationData = Provider.of<AmRegistrationData>(context, listen: false);
|
return Consumer<AmRegistrationData>(
|
||||||
|
builder: (context, registrationData, _) {
|
||||||
final initialData = ProfessionalInfoData(
|
final initialData = ProfessionalInfoData(
|
||||||
photoPath: registrationData.photoPath,
|
photoPath: registrationData.photoPath,
|
||||||
photoBytes: registrationData.photoBytes,
|
photoBytes: registrationData.photoBytes,
|
||||||
photoFilename: registrationData.photoFilename,
|
photoFilename: registrationData.photoFilename,
|
||||||
photoConsent: registrationData.photoConsent,
|
photoConsent: registrationData.photoConsent,
|
||||||
dateOfBirth: registrationData.dateOfBirth,
|
dateOfBirth: registrationData.dateOfBirth,
|
||||||
birthCity: registrationData.birthCity,
|
birthCity: registrationData.birthCity,
|
||||||
birthCountry: registrationData.birthCountry,
|
birthCountry: registrationData.birthCountry,
|
||||||
nir: registrationData.nir,
|
nir: registrationData.nir,
|
||||||
agrementNumber: registrationData.agrementNumber,
|
agrementNumber: registrationData.agrementNumber,
|
||||||
agreementDate: registrationData.agreementDate,
|
agreementDate: registrationData.agreementDate,
|
||||||
capacity: registrationData.capacity,
|
capacity: registrationData.capacity,
|
||||||
placesAvailable: registrationData.placesAvailable,
|
placesAvailable: registrationData.placesAvailable,
|
||||||
);
|
);
|
||||||
|
|
||||||
return ProfessionalInfoFormScreen(
|
return ProfessionalInfoFormScreen(
|
||||||
key: ValueKey(
|
key: ValueKey(
|
||||||
'am-pro-${registrationData.dateOfBirth?.millisecondsSinceEpoch}'
|
'am-pro-${registrationData.dateOfBirth?.millisecondsSinceEpoch}'
|
||||||
'-${registrationData.agreementDate?.millisecondsSinceEpoch}'
|
'-${registrationData.agreementDate?.millisecondsSinceEpoch}'
|
||||||
'-${registrationData.placesAvailable}'
|
'-${registrationData.placesAvailable}'
|
||||||
'-${registrationData.nir}',
|
'-${registrationData.nir}',
|
||||||
),
|
),
|
||||||
stepText: 'Étape 2/4',
|
stepText: 'Étape 2/4',
|
||||||
title: 'Vos informations professionnelles',
|
title: 'Vos informations professionnelles',
|
||||||
cardColor: CardColorHorizontal.green,
|
cardColor: CardColorHorizontal.green,
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
previousRoute: '/am-register-step1',
|
previousRoute: '/am-register-step1',
|
||||||
onSubmit: (data) {
|
onSubmit: (data) {
|
||||||
registrationData.updateProfessionalInfo(
|
registrationData.updateProfessionalInfo(
|
||||||
photoPath: data.photoPath,
|
photoPath: data.photoPath,
|
||||||
photoBytes: data.photoBytes,
|
photoBytes: data.photoBytes,
|
||||||
photoFilename: data.photoFilename,
|
photoFilename: data.photoFilename,
|
||||||
photoConsent: data.photoConsent,
|
photoConsent: data.photoConsent,
|
||||||
dateOfBirth: data.dateOfBirth,
|
dateOfBirth: data.dateOfBirth,
|
||||||
birthCity: data.birthCity,
|
birthCity: data.birthCity,
|
||||||
birthCountry: data.birthCountry,
|
birthCountry: data.birthCountry,
|
||||||
nir: data.nir,
|
nir: data.nir,
|
||||||
agrementNumber: data.agrementNumber,
|
agrementNumber: data.agrementNumber,
|
||||||
agreementDate: data.agreementDate,
|
agreementDate: data.agreementDate,
|
||||||
capacity: data.capacity,
|
capacity: data.capacity,
|
||||||
placesAvailable: data.placesAvailable,
|
placesAvailable: data.placesAvailable,
|
||||||
|
);
|
||||||
|
context.go('/am-register-step3');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
context.go('/am-register-step3');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -287,7 +287,10 @@ class AuthService {
|
|||||||
if (decoded is! Map<String, dynamic>) {
|
if (decoded is! Map<String, dynamic>) {
|
||||||
throw Exception('Réponse invalide du serveur.');
|
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.
|
/// Modale login : numéro + e-mail → token reprise. POST /auth/reprise-identify. #112.
|
||||||
|
|||||||
@ -108,6 +108,15 @@ class RepriseMapper {
|
|||||||
static DateTime? parseIsoDate(String? raw) {
|
static DateTime? parseIsoDate(String? raw) {
|
||||||
if (raw == null || raw.trim().isEmpty) return null;
|
if (raw == null || raw.trim().isEmpty) return null;
|
||||||
final s = raw.trim();
|
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 {
|
try {
|
||||||
return DateTime.parse(s);
|
return DateTime.parse(s);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -128,10 +137,34 @@ class RepriseMapper {
|
|||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is String) {
|
if (value is String) {
|
||||||
final s = value.trim();
|
final s = value.trim();
|
||||||
return s.isEmpty ? null : s;
|
return s.isEmpty || s == 'null' ? null : s;
|
||||||
}
|
}
|
||||||
if (value is DateTime) return value.toIso8601String();
|
if (value is DateTime) {
|
||||||
return null;
|
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) {
|
static int? optionalInt(dynamic value) {
|
||||||
|
|||||||
@ -134,30 +134,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_applyInitialData(widget.initialData);
|
||||||
final data = widget.initialData;
|
|
||||||
if (data != null) {
|
|
||||||
_selectedDate = data.dateOfBirth;
|
|
||||||
_dateOfBirthController.text = data.dateOfBirth != null
|
|
||||||
? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!)
|
|
||||||
: '';
|
|
||||||
_birthCityController.text = data.birthCity;
|
|
||||||
_birthCountryController.text = data.birthCountry;
|
|
||||||
final nirRaw = nirToRaw(data.nir);
|
|
||||||
_nirController.text = nirRaw.length == 15 ? formatNir(nirRaw) : data.nir;
|
|
||||||
_agrementController.text = data.agrementNumber;
|
|
||||||
_selectedAgreementDate = data.agreementDate;
|
|
||||||
_agreementDateController.text = data.agreementDate != null
|
|
||||||
? DateFormat('dd/MM/yyyy').format(data.agreementDate!)
|
|
||||||
: '';
|
|
||||||
_capacityController.text = data.capacity?.toString() ?? '';
|
|
||||||
_placesAvailableController.text = data.placesAvailable?.toString() ?? '';
|
|
||||||
_photoPathFramework = data.photoPath;
|
|
||||||
_photoFile = data.photoFile;
|
|
||||||
_photoBytes = data.photoBytes;
|
|
||||||
_photoFilename = data.photoFilename;
|
|
||||||
_photoConsent = data.photoConsent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.mode == DisplayMode.editable) {
|
if (widget.mode == DisplayMode.editable) {
|
||||||
_birthCityFocus = FocusNode();
|
_birthCityFocus = FocusNode();
|
||||||
@ -168,6 +145,51 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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!)
|
||||||
|
: '';
|
||||||
|
_birthCityController.text = data.birthCity;
|
||||||
|
_birthCountryController.text = data.birthCountry;
|
||||||
|
final nirRaw = nirToRaw(data.nir);
|
||||||
|
_nirController.text = nirRaw.length == 15 ? formatNir(nirRaw) : data.nir;
|
||||||
|
_agrementController.text = data.agrementNumber;
|
||||||
|
_selectedAgreementDate = data.agreementDate;
|
||||||
|
_agreementDateController.text = data.agreementDate != null
|
||||||
|
? DateFormat('dd/MM/yyyy').format(data.agreementDate!)
|
||||||
|
: '';
|
||||||
|
_capacityController.text = data.capacity?.toString() ?? '';
|
||||||
|
_placesAvailableController.text = data.placesAvailable?.toString() ?? '';
|
||||||
|
_photoPathFramework = data.photoPath;
|
||||||
|
_photoFile = data.photoFile;
|
||||||
|
_photoBytes = data.photoBytes;
|
||||||
|
_photoFilename = data.photoFilename;
|
||||||
|
_photoConsent = data.photoConsent;
|
||||||
|
}
|
||||||
|
|
||||||
void _onBirthCityFocusChange() {
|
void _onBirthCityFocusChange() {
|
||||||
if (_birthCityFocus == null || _birthCityFocus!.hasFocus) return;
|
if (_birthCityFocus == null || _birthCityFocus!.hasFocus) return;
|
||||||
_applyPlaceNameFormat(_birthCityController);
|
_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