From 6752dc97b4291fc2834427b5886056ce8a4721c2 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sun, 15 Feb 2026 23:02:12 +0100 Subject: [PATCH] =?UTF-8?q?feat(#14):=20redirection=20premi=C3=A8re=20conn?= =?UTF-8?q?exion=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redirection vers /login après première config réussie - Gestion défensive des réponses API (200/201, bool/string) - Force l'onglet Paramètres si setup non terminé Co-authored-by: Cursor --- frontend/lib/config/env.dart | 2 +- .../admin_dashboardScreen.dart | 16 ++++----- .../lib/services/configuration_service.dart | 36 +++++++++++-------- .../lib/widgets/admin/dashboard_admin.dart | 12 ++++--- .../lib/widgets/admin/parametres_panel.dart | 19 ++++++---- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/frontend/lib/config/env.dart b/frontend/lib/config/env.dart index 34e1cbd..fb9c0e1 100644 --- a/frontend/lib/config/env.dart +++ b/frontend/lib/config/env.dart @@ -6,7 +6,7 @@ class Env { ); // Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/') - static String apiV1(String path) => "${apiBaseUrl}/api/v1$path"; + static String apiV1(String path) => '$apiBaseUrl/api/v1$path'; } diff --git a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart index ff67e5f..1868c9d 100644 --- a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -15,13 +15,8 @@ class AdminDashboardScreen extends StatefulWidget { } class _AdminDashboardScreenState extends State { - /// null = chargement du statut setup, true/false = connu bool? _setupCompleted; - - /// 0 = Gestion des utilisateurs, 1 = Paramètres int mainTabIndex = 0; - - /// Sous-onglet quand mainTabIndex == 0 : 0=Gestionnaires, 1=Parents, 2=AM, 3=Administrateurs int subIndex = 0; @override @@ -38,8 +33,11 @@ class _AdminDashboardScreenState extends State { _setupCompleted = completed; if (!completed) mainTabIndex = 1; }); - } catch (_) { - if (mounted) setState(() => _setupCompleted = true); + } catch (e) { + if (mounted) setState(() { + _setupCompleted = false; + mainTabIndex = 1; + }); } } @@ -96,9 +94,7 @@ class _AdminDashboardScreenState extends State { Widget _getBody() { if (mainTabIndex == 1) { - return ParametresPanel( - onSetupCompleted: () => setState(() => _setupCompleted = true), - ); + return ParametresPanel(redirectToLoginAfterSave: !_setupCompleted!); } switch (subIndex) { case 0: diff --git a/frontend/lib/services/configuration_service.dart b/frontend/lib/services/configuration_service.dart index 52cd407..21e987e 100644 --- a/frontend/lib/services/configuration_service.dart +++ b/frontend/lib/services/configuration_service.dart @@ -55,6 +55,12 @@ class ConfigurationService { : Map.from(ApiConfig.headers); } + static String? _toStr(dynamic v) { + if (v == null) return null; + if (v is String) return v; + return v.toString(); + } + /// GET /api/v1/configuration/setup/status static Future getSetupStatus() async { final response = await http.get( @@ -63,7 +69,11 @@ class ConfigurationService { ); if (response.statusCode != 200) return true; final data = jsonDecode(response.body); - return data['data']?['setupCompleted'] as bool? ?? true; + final val = data['data']?['setupCompleted']; + if (val is bool) return val; + if (val is String) return val.toLowerCase() == 'true' || val == '1'; + if (val is int) return val == 1; + return true; // Par défaut on considère configuré pour ne pas bloquer } /// GET /api/v1/configuration (toutes les configs) @@ -73,9 +83,8 @@ class ConfigurationService { headers: await _headers(), ); if (response.statusCode != 200) { - throw Exception( - (jsonDecode(response.body) as Map)['message'] ?? 'Erreur chargement configuration', - ); + final err = jsonDecode(response.body) as Map?; + throw Exception(_toStr(err?['message']) ?? 'Erreur chargement configuration'); } final data = jsonDecode(response.body); final list = data['data'] as List? ?? []; @@ -89,9 +98,8 @@ class ConfigurationService { headers: await _headers(), ); if (response.statusCode != 200) { - throw Exception( - (jsonDecode(response.body) as Map)['message'] ?? 'Erreur chargement configuration', - ); + final err = jsonDecode(response.body) as Map?; + throw Exception(_toStr(err?['message']) ?? 'Erreur chargement configuration'); } final data = jsonDecode(response.body); final map = data['data'] as Map? ?? {}; @@ -105,9 +113,9 @@ class ConfigurationService { headers: await _headers(), body: jsonEncode(body), ); - if (response.statusCode != 200) { + if (response.statusCode != 200 && response.statusCode != 201) { final err = jsonDecode(response.body) as Map?; - final msg = err != null ? (err['message'] as String? ?? err['error'] as String?) : null; + final msg = err != null ? (_toStr(err['error']) ?? _toStr(err['message'])) : null; throw Exception(msg ?? 'Erreur lors de la sauvegarde'); } } @@ -120,10 +128,10 @@ class ConfigurationService { body: jsonEncode({'testEmail': testEmail}), ); final data = jsonDecode(response.body) as Map?; - if (response.statusCode == 200 && (data?['success'] == true)) { - return data!['message'] as String? ?? 'Test SMTP réussi.'; + if ((response.statusCode == 200 || response.statusCode == 201) && (data?['success'] == true)) { + return _toStr(data?['message']) ?? 'Test SMTP réussi.'; } - final msg = data != null ? (data['error'] as String? ?? data['message'] as String?) : null; + final msg = data != null ? (_toStr(data['error']) ?? _toStr(data['message'])) : null; throw Exception(msg ?? 'Échec du test SMTP'); } @@ -133,9 +141,9 @@ class ConfigurationService { Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationSetupComplete}'), headers: await _headers(), ); - if (response.statusCode != 200) { + if (response.statusCode != 200 && response.statusCode != 201) { final err = jsonDecode(response.body) as Map?; - final msg = err != null ? (err['message'] as String? ?? err['error'] as String?) : null; + final msg = err != null ? (_toStr(err['error']) ?? _toStr(err['message'])) : null; throw Exception(msg ?? 'Erreur finalisation configuration'); } } diff --git a/frontend/lib/widgets/admin/dashboard_admin.dart b/frontend/lib/widgets/admin/dashboard_admin.dart index c32644c..12fbe8b 100644 --- a/frontend/lib/widgets/admin/dashboard_admin.dart +++ b/frontend/lib/widgets/admin/dashboard_admin.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:p_tits_pas/services/auth_service.dart'; -/// Barre principale du dashboard admin : 2 onglets (Gestion des utilisateurs | Paramètres) + infos utilisateur. +/// Barre du dashboard admin : onglets Gestion des utilisateurs | Paramètres + déconnexion. class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget { final int selectedIndex; final ValueChanged onTabChange; - /// Si false, l'onglet "Gestion des utilisateurs" est grisé et inaccessible. final bool setupCompleted; const DashboardAppBarAdmin({ @@ -115,9 +116,10 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge child: const Text('Annuler'), ), ElevatedButton( - onPressed: () { + onPressed: () async { Navigator.pop(context); - // TODO: Implémenter la logique de déconnexion + await AuthService.logout(); + if (context.mounted) context.go('/login'); }, child: const Text('Déconnecter'), ), @@ -127,7 +129,7 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge } } -/// Sous-barre affichée quand "Gestion des utilisateurs" est actif : 4 onglets sans infos utilisateur. +/// Sous-barre : Gestionnaires | Parents | Assistantes maternelles | Administrateurs. class DashboardUserManagementSubBar extends StatelessWidget { final int selectedSubIndex; final ValueChanged onSubTabChange; diff --git a/frontend/lib/widgets/admin/parametres_panel.dart b/frontend/lib/widgets/admin/parametres_panel.dart index c7c6fe0..22e7b7c 100644 --- a/frontend/lib/widgets/admin/parametres_panel.dart +++ b/frontend/lib/widgets/admin/parametres_panel.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:p_tits_pas/services/configuration_service.dart'; -/// Panneau Paramètres / Configuration (ticket #15) : 3 sections sur une page. +/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé. class ParametresPanel extends StatefulWidget { - /// Appelé après une sauvegarde réussie (pour débloquer le reste du dashboard si config initiale). - final VoidCallback? onSetupCompleted; + /// Si true, après sauvegarde on redirige vers le login (première config). Sinon on reste sur la page. + final bool redirectToLoginAfterSave; - const ParametresPanel({super.key, this.onSetupCompleted}); + const ParametresPanel({super.key, this.redirectToLoginAfterSave = false}); @override State createState() => _ParametresPanelState(); @@ -107,13 +108,14 @@ class _ParametresPanelState extends State { return payload; } - /// Enregistre en base sans marquer la config initiale comme terminée (utilisé avant test SMTP). + /// Sauvegarde en base sans completeSetup (utilisé avant test SMTP). Future _saveBulkOnly() async { await ConfigurationService.updateBulk(_buildPayload()); } - /// Sauvegarde + marque la config initiale comme terminée + débloque les panneaux. + /// Sauvegarde la config, marque le setup comme terminé. Si première config, redirige vers le login. Future _save() async { + final redirectAfter = widget.redirectToLoginAfterSave; setState(() { _message = null; _isSaving = true; @@ -123,11 +125,14 @@ class _ParametresPanelState extends State { if (!mounted) return; await ConfigurationService.completeSetup(); if (!mounted) return; - widget.onSetupCompleted?.call(); setState(() { _isSaving = false; _message = 'Configuration enregistrée.'; }); + if (!mounted) return; + if (redirectAfter) { + GoRouter.of(context).go('/login'); + } } catch (e) { if (mounted) { setState(() {