diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index eb67a67..533af83 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -215,6 +215,30 @@ class UserService { return AppUser.fromJson(Map.from(data is Map ? data : {})); } + /// Refuser un compte en attente (AM ou parent). PATCH /users/:id/refuser. Ticket #110. + static Future refuseUser(String userId, {String? comment}) async { + final response = await http.patch( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId/refuser'), + headers: await _headers(), + body: jsonEncode( + comment != null && comment.trim().isNotEmpty + ? {'comment': comment.trim()} + : {}, + ), + ); + if (response.statusCode != 200) { + try { + final err = jsonDecode(response.body); + throw Exception(_errMessage(err is Map ? err['message'] : err)); + } catch (e) { + if (e is Exception) rethrow; + throw Exception('Erreur refus (${response.statusCode})'); + } + } + final data = jsonDecode(response.body); + return AppUser.fromJson(Map.from(data is Map ? data : {})); + } + /// Valider tout le dossier famille. POST /parents/:parentId/valider-dossier. Ticket #108. static Future> validerDossierFamille(String parentId, {String? comment}) async { final response = await http.post( diff --git a/frontend/lib/widgets/admin/validation_am_wizard.dart b/frontend/lib/widgets/admin/validation_am_wizard.dart index d910db5..991ef24 100644 --- a/frontend/lib/widgets/admin/validation_am_wizard.dart +++ b/frontend/lib/widgets/admin/validation_am_wizard.dart @@ -480,15 +480,12 @@ class _ValidationAmWizardState extends State { children: [ Expanded( child: ValidationRefusForm( + isSubmitting: _submitting, onCancel: widget.onClose, onPrevious: () => setState(() => _showRefusForm = false), onSubmit: (comment) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Refus (à brancher sur l’API refus)')), - ); - widget.onClose(); + if (comment == null || comment.trim().isEmpty) return; + _refuserEnvoyer(comment.trim()); }, ), ), @@ -496,4 +493,26 @@ class _ValidationAmWizardState extends State { ), ); } + + Future _refuserEnvoyer(String comment) async { + if (_submitting) return; + setState(() => _submitting = true); + try { + await UserService.refuseUser(widget.dossier.user.id, comment: comment); + 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); + } + } } diff --git a/frontend/lib/widgets/admin/validation_family_wizard.dart b/frontend/lib/widgets/admin/validation_family_wizard.dart index 2178bf5..27972e2 100644 --- a/frontend/lib/widgets/admin/validation_family_wizard.dart +++ b/frontend/lib/widgets/admin/validation_family_wizard.dart @@ -697,16 +697,12 @@ class _ValidationFamilyWizardState extends State { children: [ Expanded( child: ValidationRefusForm( + isSubmitting: _submitting, onCancel: widget.onClose, onPrevious: () => setState(() => _showRefusForm = false), onSubmit: (comment) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Refus (à brancher sur l’API refus – ticket #110)')), - ); - widget.onClose(); + if (comment == null || comment.trim().isEmpty) return; + _refuserEnvoyer(comment.trim()); }, ), ), @@ -714,4 +710,42 @@ class _ValidationFamilyWizardState extends State { ), ); } + + /// Refuse chaque parent encore `en_attente` (même commentaire, e-mail reprise par compte). Ticket #110. + Future _refuserEnvoyer(String comment) async { + if (_submitting) return; + final pending = widget.dossier.parents + .where((p) => p.statut == 'en_attente' && p.id.trim().isNotEmpty) + .toList(); + if (pending.isEmpty) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Aucun compte en attente à refuser pour ce dossier.'), + backgroundColor: Colors.red.shade700, + ), + ); + return; + } + setState(() => _submitting = true); + try { + for (final p in pending) { + await UserService.refuseUser(p.id, comment: comment); + } + 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); + } + } } diff --git a/frontend/lib/widgets/admin/validation_refus_form.dart b/frontend/lib/widgets/admin/validation_refus_form.dart index e2d6285..3eb9d3a 100644 --- a/frontend/lib/widgets/admin/validation_refus_form.dart +++ b/frontend/lib/widgets/admin/validation_refus_form.dart @@ -8,12 +8,15 @@ class ValidationRefusForm extends StatefulWidget { /// Retour à l’étape précédente du wizard (écran avec Valider / Refuser). final VoidCallback onPrevious; final ValueChanged onSubmit; + /// Pendant l'appel API : désactive les actions (ticket #110). + final bool isSubmitting; const ValidationRefusForm({ super.key, required this.onCancel, required this.onPrevious, required this.onSubmit, + this.isSubmitting = false, }); @override @@ -67,6 +70,7 @@ class _ValidationRefusFormState extends State { ), child: TextFormField( controller: _controller, + readOnly: widget.isSubmitting, maxLines: null, minLines: 1, validator: _validateMotifs, @@ -91,7 +95,7 @@ class _ValidationRefusFormState extends State { Row( children: [ TextButton( - onPressed: widget.onCancel, + onPressed: widget.isSubmitting ? null : widget.onCancel, child: const Text('Annuler'), ), const Spacer(), @@ -99,19 +103,29 @@ class _ValidationRefusFormState extends State { mainAxisSize: MainAxisSize.min, children: [ TextButton( - onPressed: widget.onPrevious, + onPressed: widget.isSubmitting ? null : widget.onPrevious, child: const Text('Précédent'), ), const SizedBox(width: 8), - ElevatedButton( - style: ValidationModalTheme.primaryElevatedStyle, - onPressed: () { - if (_formKey.currentState!.validate()) { - widget.onSubmit(_controller.text.trim()); - } - }, - child: const Text('Envoyer'), - ), + if (widget.isSubmitting) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + else + ElevatedButton( + style: ValidationModalTheme.primaryElevatedStyle, + onPressed: () { + if (_formKey.currentState!.validate()) { + widget.onSubmit(_controller.text.trim()); + } + }, + child: const Text('Envoyer'), + ), ], ), ],