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 : {}));
|
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.
|
/// Valider tout le dossier famille. POST /parents/:parentId/valider-dossier. Ticket #108.
|
||||||
static Future<List<AppUser>> validerDossierFamille(String parentId, {String? comment}) async {
|
static Future<List<AppUser>> validerDossierFamille(String parentId, {String? comment}) async {
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
|
|||||||
@ -480,15 +480,12 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValidationRefusForm(
|
child: ValidationRefusForm(
|
||||||
|
isSubmitting: _submitting,
|
||||||
onCancel: widget.onClose,
|
onCancel: widget.onClose,
|
||||||
onPrevious: () => setState(() => _showRefusForm = false),
|
onPrevious: () => setState(() => _showRefusForm = false),
|
||||||
onSubmit: (comment) {
|
onSubmit: (comment) {
|
||||||
if (!mounted) return;
|
if (comment == null || comment.trim().isEmpty) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
_refuserEnvoyer(comment.trim());
|
||||||
const SnackBar(
|
|
||||||
content: Text('Refus (à brancher sur l’API refus)')),
|
|
||||||
);
|
|
||||||
widget.onClose();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValidationRefusForm(
|
child: ValidationRefusForm(
|
||||||
|
isSubmitting: _submitting,
|
||||||
onCancel: widget.onClose,
|
onCancel: widget.onClose,
|
||||||
onPrevious: () => setState(() => _showRefusForm = false),
|
onPrevious: () => setState(() => _showRefusForm = false),
|
||||||
onSubmit: (comment) {
|
onSubmit: (comment) {
|
||||||
if (!mounted) return;
|
if (comment == null || comment.trim().isEmpty) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
_refuserEnvoyer(comment.trim());
|
||||||
const SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Refus (à brancher sur l’API refus – ticket #110)')),
|
|
||||||
);
|
|
||||||
widget.onClose();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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).
|
/// Retour à l’étape précédente du wizard (écran avec Valider / Refuser).
|
||||||
final VoidCallback onPrevious;
|
final VoidCallback onPrevious;
|
||||||
final ValueChanged<String?> onSubmit;
|
final ValueChanged<String?> onSubmit;
|
||||||
|
/// Pendant l'appel API : désactive les actions (ticket #110).
|
||||||
|
final bool isSubmitting;
|
||||||
|
|
||||||
const ValidationRefusForm({
|
const ValidationRefusForm({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onCancel,
|
required this.onCancel,
|
||||||
required this.onPrevious,
|
required this.onPrevious,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
|
this.isSubmitting = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -67,6 +70,7 @@ class _ValidationRefusFormState extends State<ValidationRefusForm> {
|
|||||||
),
|
),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
readOnly: widget.isSubmitting,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
validator: _validateMotifs,
|
validator: _validateMotifs,
|
||||||
@ -91,7 +95,7 @@ class _ValidationRefusFormState extends State<ValidationRefusForm> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: widget.onCancel,
|
onPressed: widget.isSubmitting ? null : widget.onCancel,
|
||||||
child: const Text('Annuler'),
|
child: const Text('Annuler'),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@ -99,19 +103,29 @@ class _ValidationRefusFormState extends State<ValidationRefusForm> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: widget.onPrevious,
|
onPressed: widget.isSubmitting ? null : widget.onPrevious,
|
||||||
child: const Text('Précédent'),
|
child: const Text('Précédent'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
if (widget.isSubmitting)
|
||||||
style: ValidationModalTheme.primaryElevatedStyle,
|
const Padding(
|
||||||
onPressed: () {
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
if (_formKey.currentState!.validate()) {
|
child: SizedBox(
|
||||||
widget.onSubmit(_controller.text.trim());
|
width: 22,
|
||||||
}
|
height: 22,
|
||||||
},
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
child: const Text('Envoyer'),
|
),
|
||||||
),
|
)
|
||||||
|
else
|
||||||
|
ElevatedButton(
|
||||||
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
widget.onSubmit(_controller.text.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Envoyer'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user