feat(#135): mode édition dossier (PATCH fiche, co-parent, enfants).
Wizard edit famille/AM depuis la liste Dossiers ; save parents + co-parent ; enfants existants en PATCH (photo multipart) sans POST doublon. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,9 +23,9 @@ import 'package:p_tits_pas/widgets/admin/validation_valider_confirm_dialog.dart'
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||
|
||||
enum AmDossierWizardMode { review, create }
|
||||
enum AmDossierWizardMode { review, create, edit }
|
||||
|
||||
/// Wizard dossier AM — modes [review] (validation pending) et [create] (staff #156).
|
||||
/// Wizard dossier AM — [review] (#107), [create] (#156), [edit] (#135).
|
||||
class AmDossierWizard extends StatefulWidget {
|
||||
final AmDossierWizardMode mode;
|
||||
final DossierAM? dossier;
|
||||
@@ -74,7 +74,25 @@ class AmDossierWizard extends StatefulWidget {
|
||||
);
|
||||
}
|
||||
|
||||
factory AmDossierWizard.edit({
|
||||
Key? key,
|
||||
required DossierAM dossier,
|
||||
required VoidCallback onClose,
|
||||
required VoidCallback onSuccess,
|
||||
void Function(int step, int total)? onStepChanged,
|
||||
}) {
|
||||
return AmDossierWizard._(
|
||||
key: key,
|
||||
mode: AmDossierWizardMode.edit,
|
||||
dossier: dossier,
|
||||
onClose: onClose,
|
||||
onSuccess: onSuccess,
|
||||
onStepChanged: onStepChanged,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isCreate => mode == AmDossierWizardMode.create;
|
||||
bool get isEdit => mode == AmDossierWizardMode.edit;
|
||||
|
||||
/// Hauteur corps modale AM — dérivée de [ValidationFormMetrics] (4 lignes).
|
||||
static double get shellBodyHeight =>
|
||||
@@ -118,9 +136,11 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
String? _photoFilename;
|
||||
|
||||
bool get _isCreate => widget.isCreate;
|
||||
bool get _isEdit => widget.isEdit;
|
||||
bool get _isEditable => _isCreate || _isEdit;
|
||||
DossierAM get _dossier => widget.dossier!;
|
||||
bool get _isEnAttente =>
|
||||
!_isCreate && _dossier.user.statut == 'en_attente';
|
||||
!_isCreate && !_isEdit && _dossier.user.statut == 'en_attente';
|
||||
|
||||
static String _v(String? s) =>
|
||||
(s != null && s.trim().isNotEmpty) ? s.trim() : '–';
|
||||
@@ -151,9 +171,33 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
_capaciteCtrl = TextEditingController(text: '1');
|
||||
_placesCtrl = TextEditingController(text: '1');
|
||||
_presentationCtrl = TextEditingController();
|
||||
if (_isEdit) {
|
||||
_prefillFromDossier();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitStep());
|
||||
}
|
||||
|
||||
void _prefillFromDossier() {
|
||||
final u = _dossier.user;
|
||||
_nomCtrl.text = (u.nom ?? '').trim();
|
||||
_prenomCtrl.text = (u.prenom ?? '').trim();
|
||||
_telCtrl.text = (u.telephone ?? '').trim();
|
||||
_emailCtrl.text = u.email.trim();
|
||||
_adresseCtrl.text = (u.adresse ?? '').trim();
|
||||
_cpCtrl.text = (u.codePostal ?? '').trim();
|
||||
_villeCtrl.text = (u.ville ?? '').trim();
|
||||
_nirCtrl.text = (_dossier.nir ?? '').trim();
|
||||
_dateNaissanceCtrl.text = formatIsoDateFrInput(u.dateNaissance);
|
||||
_lieuNaissanceVilleCtrl.text = (u.lieuNaissanceVille ?? '').trim();
|
||||
final pays = (u.lieuNaissancePays ?? '').trim();
|
||||
_lieuNaissancePaysCtrl.text = pays.isEmpty ? 'France' : pays;
|
||||
_agrementCtrl.text = (_dossier.numeroAgrement ?? '').trim();
|
||||
_dateAgrementCtrl.text = formatIsoDateFrInput(_dossier.dateAgrement);
|
||||
_capaciteCtrl.text = '${_dossier.nbMaxEnfants ?? 1}';
|
||||
_placesCtrl.text = '${_dossier.placesDisponibles ?? 0}';
|
||||
_presentationCtrl.text = (_dossier.presentation ?? '').trim();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomCtrl.dispose();
|
||||
@@ -373,8 +417,8 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateStep1() {
|
||||
if (_photoBytes == null || _photoBytes!.isEmpty) {
|
||||
String? _validateStep1({required bool requirePhoto}) {
|
||||
if (requirePhoto && (_photoBytes == null || _photoBytes!.isEmpty)) {
|
||||
return 'Une photo de profil est requise.';
|
||||
}
|
||||
final birthIso = parseFrDateToIso(_dateNaissanceCtrl.text);
|
||||
@@ -407,12 +451,12 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
}
|
||||
|
||||
String? _validateCurrentStep() {
|
||||
if (!_isCreate) return null;
|
||||
if (!_isEditable) return null;
|
||||
switch (_step) {
|
||||
case 0:
|
||||
return _validateStep0();
|
||||
case 1:
|
||||
return _validateStep1();
|
||||
return _validateStep1(requirePhoto: _isCreate);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -511,7 +555,7 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
final err1 = _validateStep1();
|
||||
final err1 = _validateStep1(requirePhoto: true);
|
||||
if (err1 != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(err1), backgroundColor: Colors.red.shade700),
|
||||
@@ -554,6 +598,94 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveEdit() async {
|
||||
if (_submitting || !_isEdit) return;
|
||||
final err0 = _validateStep0();
|
||||
if (err0 != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(err0), backgroundColor: Colors.red.shade700),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final err1 = _validateStep1(requirePhoto: false);
|
||||
if (err1 != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(err1), backgroundColor: Colors.red.shade700),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final amId = _dossier.user.id.trim();
|
||||
if (amId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Identifiant AM manquant.'),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final ok = await showValidationValiderConfirmDialog(
|
||||
context,
|
||||
body: 'Enregistrer les modifications de la fiche assistante maternelle ?',
|
||||
);
|
||||
if (!mounted || !ok) return;
|
||||
|
||||
final birthIso = parseFrDateToIso(_dateNaissanceCtrl.text);
|
||||
final agrementIso = parseFrDateToIso(_dateAgrementCtrl.text);
|
||||
final capa = int.tryParse(_capaciteCtrl.text.trim());
|
||||
final places = int.tryParse(_placesCtrl.text.trim());
|
||||
final biography = _presentationCtrl.text.trim();
|
||||
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
await UserService.updateAmFiche(
|
||||
amUserId: amId,
|
||||
body: {
|
||||
'nom': formatPersonNameCase(_nomCtrl.text),
|
||||
'prenom': formatPersonNameCase(_prenomCtrl.text),
|
||||
'email': normalizeEmailText(_emailCtrl.text),
|
||||
'telephone': normalizePhone(_telCtrl.text),
|
||||
'adresse': _adresseCtrl.text.trim(),
|
||||
'ville': formatPersonNameCase(_villeCtrl.text),
|
||||
'code_postal': _cpCtrl.text.trim(),
|
||||
'approval_number': _agrementCtrl.text.trim(),
|
||||
'nir': nirToRaw(_nirCtrl.text),
|
||||
if (birthIso != null) 'date_naissance': birthIso,
|
||||
'lieu_naissance_ville':
|
||||
formatPersonNameCase(_lieuNaissanceVilleCtrl.text),
|
||||
'lieu_naissance_pays':
|
||||
formatPersonNameCase(_lieuNaissancePaysCtrl.text),
|
||||
if (agrementIso != null) 'agreement_date': agrementIso,
|
||||
if (capa != null) 'max_children': capa,
|
||||
if (places != null) 'places_available': places,
|
||||
'biography': biography,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fiche AM enregistrée.'),
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
widget.onSuccess();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e is Exception
|
||||
? e.toString().replaceFirst('Exception: ', '')
|
||||
: 'Erreur'),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_showRefusForm) {
|
||||
@@ -587,7 +719,7 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
}
|
||||
|
||||
Widget _buildStep0() {
|
||||
if (_isCreate) {
|
||||
if (_isEditable) {
|
||||
return IdentityBlock.editable(
|
||||
title: 'Identité et coordonnées',
|
||||
nomController: _nomCtrl,
|
||||
@@ -620,7 +752,7 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
.clamp(_photoColumnMinWidth, 360.0);
|
||||
if (photoW > maxPhotoW) photoW = maxPhotoW;
|
||||
|
||||
final form = _isCreate
|
||||
final form = _isEditable
|
||||
? _buildCreateProFields()
|
||||
: ValidationDetailSection(
|
||||
title: 'Dossier professionnel',
|
||||
@@ -628,20 +760,25 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
rowLayout: _photoProRowLayout,
|
||||
);
|
||||
|
||||
final Widget photo;
|
||||
if (_isCreate) {
|
||||
photo = AdminAmPhotoFrame(
|
||||
imageBytes: _photoBytes,
|
||||
onTap: _pickPhoto,
|
||||
onClear: _photoBytes != null ? _clearPhoto : null,
|
||||
emptyLabel: 'Ajouter une photo',
|
||||
);
|
||||
} else if (_isEdit) {
|
||||
// Édition : photo existante en lecture ; remplacement hors scope PATCH fiche.
|
||||
photo = _buildPhotoSectionReview(_dossier.user);
|
||||
} else {
|
||||
photo = _buildPhotoSectionReview(_dossier.user);
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: photoW,
|
||||
child: _isCreate
|
||||
? AdminAmPhotoFrame(
|
||||
imageBytes: _photoBytes,
|
||||
onTap: _pickPhoto,
|
||||
onClear: _photoBytes != null ? _clearPhoto : null,
|
||||
emptyLabel: 'Ajouter une photo',
|
||||
)
|
||||
: _buildPhotoSectionReview(_dossier.user),
|
||||
),
|
||||
SizedBox(width: photoW, child: photo),
|
||||
const SizedBox(width: _photoProGap),
|
||||
Expanded(
|
||||
child: Align(
|
||||
@@ -721,7 +858,7 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
}
|
||||
|
||||
Widget _buildStep2() {
|
||||
if (_isCreate) {
|
||||
if (_isEditable) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -832,6 +969,12 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
onPressed: _submitting ? null : _createAndValidate,
|
||||
child: Text(_submitting ? 'Envoi...' : 'Créer et valider'),
|
||||
),
|
||||
] else if (_isEdit) ...[
|
||||
ElevatedButton(
|
||||
style: ValidationModalTheme.primaryElevatedStyle,
|
||||
onPressed: _submitting ? null : _saveEdit,
|
||||
child: Text(_submitting ? 'Envoi...' : 'Enregistrer'),
|
||||
),
|
||||
] else if (_isEnAttente) ...[
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : _refuser,
|
||||
|
||||
Reference in New Issue
Block a user