import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:p_tits_pas/models/am_registration_data.dart'; import 'package:p_tits_pas/models/dossier_unifie.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/api/api_config.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/utils/date_display_utils.dart'; import 'package:p_tits_pas/utils/email_utils.dart'; import 'package:p_tits_pas/utils/name_format_utils.dart'; import 'package:p_tits_pas/utils/nir_utils.dart'; import 'package:p_tits_pas/utils/phone_utils.dart'; import 'package:p_tits_pas/utils/postal_utils.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart'; import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart'; import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart'; import 'package:p_tits_pas/widgets/admin/validation_refus_form.dart'; 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 } /// Wizard dossier AM — modes [review] (validation pending) et [create] (staff #156). class AmDossierWizard extends StatefulWidget { final AmDossierWizardMode mode; final DossierAM? dossier; final VoidCallback onClose; final VoidCallback onSuccess; final void Function(int step, int total)? onStepChanged; const AmDossierWizard._({ super.key, required this.mode, this.dossier, required this.onClose, required this.onSuccess, this.onStepChanged, }); factory AmDossierWizard.review({ Key? key, required DossierAM dossier, required VoidCallback onClose, required VoidCallback onSuccess, void Function(int step, int total)? onStepChanged, }) { return AmDossierWizard._( key: key, mode: AmDossierWizardMode.review, dossier: dossier, onClose: onClose, onSuccess: onSuccess, onStepChanged: onStepChanged, ); } factory AmDossierWizard.create({ Key? key, required VoidCallback onClose, required VoidCallback onSuccess, void Function(int step, int total)? onStepChanged, }) { return AmDossierWizard._( key: key, mode: AmDossierWizardMode.create, onClose: onClose, onSuccess: onSuccess, onStepChanged: onStepChanged, ); } bool get isCreate => mode == AmDossierWizardMode.create; /// Hauteur corps modale AM — dérivée de [ValidationFormMetrics] (4 lignes). static double get shellBodyHeight => ValidationFormMetrics.shellBodyHeightForRows(4); @override State createState() => _AmDossierWizardState(); } class _AmDossierWizardState extends State { int _step = 0; bool _showRefusForm = false; bool _submitting = false; static const int _stepCount = 3; static const List _photoProRowLayout = [2, 2, 2, 2]; static const double _idPhotoAspectRatio = 35 / 45; static const double _photoProGap = 24; static const double _proColumnMinWidth = 260; static const double _photoColumnMinWidth = 160; // --- Create mode controllers / state --- late final TextEditingController _nomCtrl; late final TextEditingController _prenomCtrl; late final TextEditingController _telCtrl; late final TextEditingController _emailCtrl; late final TextEditingController _adresseCtrl; late final TextEditingController _cpCtrl; late final TextEditingController _villeCtrl; late final TextEditingController _nirCtrl; late final TextEditingController _dateNaissanceCtrl; late final TextEditingController _lieuNaissanceVilleCtrl; late final TextEditingController _lieuNaissancePaysCtrl; late final TextEditingController _agrementCtrl; late final TextEditingController _dateAgrementCtrl; late final TextEditingController _capaciteCtrl; late final TextEditingController _placesCtrl; late final TextEditingController _presentationCtrl; Uint8List? _photoBytes; String? _photoFilename; bool get _isCreate => widget.isCreate; DossierAM get _dossier => widget.dossier!; bool get _isEnAttente => !_isCreate && _dossier.user.statut == 'en_attente'; static String _v(String? s) => (s != null && s.trim().isNotEmpty) ? s.trim() : '–'; static String _formatNirForDisplay(String? nir) { final v = _v(nir); if (v == '–') return v; final raw = nirToRaw(v).toUpperCase(); return raw.length == 15 ? formatNir(raw) : v; } @override void initState() { super.initState(); _nomCtrl = TextEditingController(); _prenomCtrl = TextEditingController(); _telCtrl = TextEditingController(); _emailCtrl = TextEditingController(); _adresseCtrl = TextEditingController(); _cpCtrl = TextEditingController(); _villeCtrl = TextEditingController(); _nirCtrl = TextEditingController(); _dateNaissanceCtrl = TextEditingController(); _lieuNaissanceVilleCtrl = TextEditingController(); _lieuNaissancePaysCtrl = TextEditingController(text: 'France'); _agrementCtrl = TextEditingController(); _dateAgrementCtrl = TextEditingController(); _capaciteCtrl = TextEditingController(text: '1'); _placesCtrl = TextEditingController(text: '1'); _presentationCtrl = TextEditingController(); WidgetsBinding.instance.addPostFrameCallback((_) => _emitStep()); } @override void dispose() { _nomCtrl.dispose(); _prenomCtrl.dispose(); _telCtrl.dispose(); _emailCtrl.dispose(); _adresseCtrl.dispose(); _cpCtrl.dispose(); _villeCtrl.dispose(); _nirCtrl.dispose(); _dateNaissanceCtrl.dispose(); _lieuNaissanceVilleCtrl.dispose(); _lieuNaissancePaysCtrl.dispose(); _agrementCtrl.dispose(); _dateAgrementCtrl.dispose(); _capaciteCtrl.dispose(); _placesCtrl.dispose(); _presentationCtrl.dispose(); super.dispose(); } void _emitStep() => widget.onStepChanged?.call(_step, _stepCount); List _photoProFields(DossierAM d) { final u = d.user; return [ AdminDetailField(label: 'NIR', value: _formatNirForDisplay(d.nir)), AdminDetailField( label: 'Date de naissance', value: formatIsoDateFr(u.dateNaissance), ), AdminDetailField( label: 'Ville de naissance', value: _v(u.lieuNaissanceVille), ), AdminDetailField( label: 'Pays de naissance', value: _v(u.lieuNaissancePays), ), AdminDetailField(label: 'N° Agrément', value: _v(d.numeroAgrement)), AdminDetailField( label: 'Date d’agrément', value: formatIsoDateFr(d.dateAgrement), ), AdminDetailField( label: 'Capacité max (enfants)', value: d.nbMaxEnfants != null ? d.nbMaxEnfants.toString() : '–', ), AdminDetailField( label: 'Places disponibles', value: d.placesDisponibles != null ? d.placesDisponibles.toString() : '–', ), ]; } static String _fullPhotoUrl(String? url) => ApiConfig.absoluteMediaUrl(url); Widget _buildPhotoSectionReview(AppUser u) { final photoUrl = _fullPhotoUrl(u.photoUrl); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 8), child: LayoutBuilder( builder: (context, c) { const uniformFrame = 8.0; final maxPhotoW = (c.maxWidth - 2 * uniformFrame).clamp(0.0, double.infinity); final maxPhotoH = (c.maxHeight - 2 * uniformFrame).clamp(0.0, double.infinity); const ar = _idPhotoAspectRatio; double ph = maxPhotoH; double pw = ph * ar; if (pw > maxPhotoW) { pw = maxPhotoW; ph = pw / ar; } return Align( alignment: Alignment.center, child: Container( decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(uniformFrame), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: SizedBox( width: pw, height: ph, child: photoUrl.isEmpty ? ColoredBox( color: Colors.grey.shade200, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.person_off_outlined, size: 40, color: Colors.grey.shade400), const SizedBox(height: 8), Text( 'Aucune photo fournie', style: TextStyle( color: Colors.grey.shade600, fontSize: 12), ), ], ), ) : AuthNetworkImage( url: photoUrl, fit: BoxFit.cover, width: pw, height: ph, loadingBuilder: (_, child, progress) { if (progress == null) return child; return ColoredBox( color: Colors.grey.shade200, child: Center( child: CircularProgressIndicator( value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / (progress.expectedTotalBytes!) : null, ), ), ); }, errorBuilder: (_, __, ___) => ColoredBox( color: Colors.grey.shade200, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.broken_image_outlined, size: 40, color: Colors.grey.shade400), const SizedBox(height: 8), Text( 'Impossible de charger la photo', style: TextStyle( color: Colors.grey.shade600, fontSize: 12), ), ], ), ), ), ), ), ), ), ); }, ), ), ), ], ); } Future _pickPhoto() async { try { final picked = await ImagePicker().pickImage( source: ImageSource.gallery, maxWidth: 1200, imageQuality: 85, ); if (picked == null) return; final bytes = await picked.readAsBytes(); if (!mounted) return; setState(() { _photoBytes = bytes; _photoFilename = picked.name; }); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Impossible de charger la photo'), backgroundColor: Colors.red.shade700, ), ); } } void _clearPhoto() => setState(() { _photoBytes = null; _photoFilename = null; }); String? _validateStep0() { final nom = _nomCtrl.text.trim(); final prenom = _prenomCtrl.text.trim(); if (nom.length < 2) return 'Le nom doit contenir au moins 2 caractères.'; if (prenom.length < 2) { return 'Le prénom doit contenir au moins 2 caractères.'; } final phoneErr = validateFrenchNationalPhone(_telCtrl.text); if (phoneErr != null) return phoneErr; final emailErr = validateEmail(_emailCtrl.text); if (emailErr != null) return emailErr; if (_adresseCtrl.text.trim().isEmpty) { return 'L’adresse est requise.'; } final cpErr = validateFrenchPostalCode(_cpCtrl.text); if (cpErr != null) return cpErr; if (_villeCtrl.text.trim().isEmpty) return 'La ville est requise.'; return null; } String? _validateStep1() { if (_photoBytes == null || _photoBytes!.isEmpty) { return 'Une photo de profil est requise.'; } final birthIso = parseFrDateToIso(_dateNaissanceCtrl.text); if (birthIso == null) { return 'La date de naissance est invalide (jj/mm/aaaa).'; } final ville = _lieuNaissanceVilleCtrl.text.trim(); final pays = _lieuNaissancePaysCtrl.text.trim(); if (ville.length < 2 || pays.length < 2) { return 'La ville et le pays de naissance sont requis (au moins 2 caractères).'; } final nirErr = validateNir(_nirCtrl.text); if (nirErr != null) return nirErr; if (_agrementCtrl.text.trim().isEmpty) { return 'Le numéro d’agrément est requis.'; } final agrementIso = parseFrDateToIso(_dateAgrementCtrl.text); if (agrementIso == null) { return 'La date d’agrément est invalide (jj/mm/aaaa).'; } final capa = int.tryParse(_capaciteCtrl.text.trim()); if (capa == null || capa < 1 || capa > kAmCapaciteAccueilMax) { return 'La capacité doit être entre 1 et $kAmCapaciteAccueilMax.'; } final places = int.tryParse(_placesCtrl.text.trim()); if (places == null || places < 0 || places > capa) { return 'Les places disponibles doivent être entre 0 et la capacité.'; } return null; } String? _validateCurrentStep() { if (!_isCreate) return null; switch (_step) { case 0: return _validateStep0(); case 1: return _validateStep1(); default: return null; } } void _goNext() { final err = _validateCurrentStep(); if (err != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(err), backgroundColor: Colors.red.shade700), ); return; } setState(() => _step++); _emitStep(); } static String _imageMimeForBytes(Uint8List bytes) { if (bytes.length >= 8 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) { return 'image/png'; } if (bytes.length >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) { return 'image/jpeg'; } if (bytes.length >= 6 && bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) { return 'image/gif'; } if (bytes.length >= 12 && bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46) { return 'image/webp'; } return 'image/jpeg'; } Map _buildCreateBody() { final bytes = _photoBytes!; final mime = _imageMimeForBytes(bytes); final photoBase64 = 'data:$mime;base64,${base64Encode(bytes)}'; final fn = (_photoFilename ?? '').trim(); final birthIso = parseFrDateToIso(_dateNaissanceCtrl.text)!; final agrementIso = parseFrDateToIso(_dateAgrementCtrl.text)!; final capa = int.parse(_capaciteCtrl.text.trim()); final places = int.parse(_placesCtrl.text.trim()); final presentation = _presentationCtrl.text.trim(); return { 'email': normalizeEmailText(_emailCtrl.text), 'prenom': formatPersonNameCase(_prenomCtrl.text), 'nom': formatPersonNameCase(_nomCtrl.text), 'telephone': normalizePhone(_telCtrl.text), 'adresse': _adresseCtrl.text.trim().isNotEmpty ? _adresseCtrl.text.trim() : null, 'code_postal': _cpCtrl.text.trim().isNotEmpty ? _cpCtrl.text.trim() : null, 'ville': _villeCtrl.text.trim().isNotEmpty ? formatPersonNameCase(_villeCtrl.text) : null, 'photo_base64': photoBase64, 'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg', 'consentement_photo': true, 'date_naissance': birthIso, 'lieu_naissance_ville': formatPersonNameCase(_lieuNaissanceVilleCtrl.text), 'lieu_naissance_pays': formatPersonNameCase(_lieuNaissancePaysCtrl.text), 'nir': normalizeNir(_nirCtrl.text), 'numero_agrement': _agrementCtrl.text.trim(), 'date_agrement': agrementIso, 'capacite_accueil': capa, 'places_disponibles': places, 'biographie': presentation.isNotEmpty ? presentation : null, 'acceptation_cgu': true, 'acceptation_privacy': true, }; } Future _createAndValidate() async { if (_submitting) return; final err0 = _validateStep0(); if (err0 != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(err0), backgroundColor: Colors.red.shade700), ); return; } final err1 = _validateStep1(); if (err1 != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(err1), backgroundColor: Colors.red.shade700), ); return; } final ok = await showValidationValiderConfirmDialog( context, body: 'Créer et activer le dossier de cette assistante maternelle ? Un e-mail de création de mot de passe sera envoyé.', ); if (!mounted || !ok) return; setState(() => _submitting = true); try { await UserService.createAmDossier(_buildCreateBody()); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Dossier créé et validé. L’assistante maternelle est active.', ), 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) { return _buildRefusPage(); } return Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), Expanded(child: _buildStepContent()), const SizedBox(height: 24), _buildNavigation(), ], ), ); } Widget _buildStepContent() { switch (_step) { case 0: return _buildStep0(); case 1: return _buildStep1(); case 2: return _buildStep2(); default: return const SizedBox(); } } Widget _buildStep0() { if (_isCreate) { return IdentityBlock.editable( title: 'Identité et coordonnées', nomController: _nomCtrl, prenomController: _prenomCtrl, telephoneController: _telCtrl, emailController: _emailCtrl, adresseController: _adresseCtrl, codePostalController: _cpCtrl, villeController: _villeCtrl, ); } final u = _dossier.user; return IdentityBlock.readOnlyFromUser( u, title: 'Identité et coordonnées', emptyLabel: '–', ); } Widget _buildStep1() { // Modale calée sur les TF ; photo étirée sur toute la hauteur utile // (largeur = [AdminAmPhotoFrame.columnWidthForHeight], cadre inclus). return LayoutBuilder( builder: (context, c) { final maxRowW = c.maxWidth; final maxRowH = c.maxHeight.clamp(0.0, double.infinity); final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth) .clamp(0.0, double.infinity); var photoW = AdminAmPhotoFrame.columnWidthForHeight(maxRowH) .clamp(_photoColumnMinWidth, 360.0); if (photoW > maxPhotoW) photoW = maxPhotoW; final form = _isCreate ? _buildCreateProFields() : ValidationDetailSection( title: 'Dossier professionnel', fields: _photoProFields(_dossier), rowLayout: _photoProRowLayout, ); 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), ), const SizedBox(width: _photoProGap), Expanded( child: Align( alignment: Alignment.topCenter, child: form, ), ), ], ); }, ); } Widget _buildCreateProFields() { return ValidationFormGrid( title: 'Dossier professionnel', rowLayout: _photoProRowLayout, fields: [ ValidationLabeledField( label: 'NIR', field: ValidationNirField(controller: _nirCtrl), ), ValidationLabeledField( label: 'Date de naissance', field: ValidationEditableField( controller: _dateNaissanceCtrl, hintText: 'jj / mm / aaaa', keyboardType: TextInputType.number, inputFormatters: const [FrenchDateInputFormatter()], ), ), ValidationLabeledField( label: 'Ville de naissance', field: ValidationEditableField( controller: _lieuNaissanceVilleCtrl, inputFormatters: const [PersonNameInputFormatter()], ), ), ValidationLabeledField( label: 'Pays de naissance', field: ValidationEditableField( controller: _lieuNaissancePaysCtrl, inputFormatters: const [PersonNameInputFormatter()], ), ), ValidationLabeledField( label: 'N° Agrément', field: ValidationEditableField(controller: _agrementCtrl), ), ValidationLabeledField( label: 'Date d’agrément', field: ValidationEditableField( controller: _dateAgrementCtrl, hintText: 'jj / mm / aaaa', keyboardType: TextInputType.number, inputFormatters: const [FrenchDateInputFormatter()], ), ), ValidationLabeledField( label: 'Capacité max (enfants)', field: ValidationEditableField( controller: _capaciteCtrl, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), ), ValidationLabeledField( label: 'Places disponibles', field: ValidationEditableField( controller: _placesCtrl, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), ), ], ); } Widget _buildStep2() { if (_isCreate) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Présentation', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 12), Expanded( child: TextField( controller: _presentationCtrl, maxLines: null, expands: true, maxLength: 2000, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( hintText: 'Présentation (optionnel, max 2000 caractères)', filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: Colors.grey.shade300), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), ), ), ), ], ); } final presentation = (_dossier.presentation != null && _dossier.presentation!.trim().isNotEmpty) ? _dossier.presentation! : '–'; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Présentation', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 12), Expanded( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.grey.shade300), ), child: SelectableText( presentation, style: const TextStyle(color: Colors.black87, fontSize: 14), ), ), ), ); }, ), ), ], ); } Widget _buildNavigation() { if (_step == 2) { return Row( children: [ TextButton(onPressed: widget.onClose, child: const Text('Annuler')), const Spacer(), Row( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { setState(() => _step = 1); _emitStep(); }, child: const Text('Précédent'), ), const SizedBox(width: 8), if (_isCreate) ...[ ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: _submitting ? null : _createAndValidate, child: Text(_submitting ? 'Envoi...' : 'Créer et valider'), ), ] else if (_isEnAttente) ...[ OutlinedButton( onPressed: _submitting ? null : _refuser, child: const Text('Refuser')), const SizedBox(width: 12), ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: _submitting ? null : _onValiderPressed, child: Text(_submitting ? 'Envoi...' : 'Valider'), ), ] else ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: widget.onClose, child: const Text('Fermer'), ), ], ), ], ); } return Row( children: [ TextButton(onPressed: widget.onClose, child: const Text('Annuler')), const Spacer(), if (_step > 0) ...[ TextButton( onPressed: () { setState(() => _step--); _emitStep(); }, child: const Text('Précédent'), ), const SizedBox(width: 8), ], ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: _goNext, child: const Text('Suivant'), ), ], ); } Future _onValiderPressed() async { if (_submitting) return; final ok = await showValidationValiderConfirmDialog( context, body: 'Voulez-vous valider le dossier de cette assistante maternelle ? Cette action confirme le compte.', ); if (!mounted || !ok) return; await _valider(); } Future _valider() async { if (_submitting) return; setState(() => _submitting = true); try { await UserService.validateUser(_dossier.user.id); if (!mounted) return; 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); } } void _refuser() => setState(() => _showRefusForm = true); Widget _buildRefusPage() { return Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: ValidationRefusForm( isSubmitting: _submitting, onCancel: widget.onClose, onPrevious: () => setState(() => _showRefusForm = false), onSubmit: (comment) { if (comment == null || comment.trim().isEmpty) return; _refuserEnvoyer(comment.trim()); }, ), ), ], ), ); } Future _refuserEnvoyer(String comment) async { if (_submitting) return; setState(() => _submitting = true); try { await UserService.refuseUser(_dossier.user.id, comment: comment); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Refus enregistré. Un e-mail avec le lien de reprise a été envoyé.', ), 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); } } }