feat(front): câbler PATCH refus utilisateur depuis wizards validation (#110)
- UserService.refuseUser → /users/:id/refuser - AM + famille : envoi motifs, onSuccess + refresh liste - Famille : refus séquentiel pour chaque parent en_attente - ValidationRefusForm : état isSubmitting (UX) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
43dabd1885
commit
7ba8d51668
@ -215,6 +215,30 @@ class UserService {
|
||||
return AppUser.fromJson(Map<String, dynamic>.from(data is Map ? data : {}));
|
||||
}
|
||||
|
||||
/// Refuser un compte en attente (AM ou parent). PATCH /users/:id/refuser. Ticket #110.
|
||||
static Future<AppUser> 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<String, dynamic>.from(data is Map ? data : {}));
|
||||
}
|
||||
|
||||
/// Valider tout le dossier famille. POST /parents/:parentId/valider-dossier. Ticket #108.
|
||||
static Future<List<AppUser>> validerDossierFamille(String parentId, {String? comment}) async {
|
||||
final response = await http.post(
|
||||
|
||||
@ -480,15 +480,12 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
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<ValidationAmWizard> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -697,16 +697,12 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
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<ValidationFamilyWizard> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Refuse chaque parent encore `en_attente` (même commentaire, e-mail reprise par compte). Ticket #110.
|
||||
Future<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String?> 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<ValidationRefusForm> {
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
readOnly: widget.isSubmitting,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
validator: _validateMotifs,
|
||||
@ -91,7 +95,7 @@ class _ValidationRefusFormState extends State<ValidationRefusForm> {
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: widget.onCancel,
|
||||
onPressed: widget.isSubmitting ? null : widget.onCancel,
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const Spacer(),
|
||||
@ -99,10 +103,20 @@ class _ValidationRefusFormState extends State<ValidationRefusForm> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: widget.onPrevious,
|
||||
onPressed: widget.isSubmitting ? null : widget.onPrevious,
|
||||
child: const Text('Précédent'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
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: () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user