From dfe7daed14000bb488ac21fda76b4211935eb127 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sun, 15 Feb 2026 23:20:15 +0100 Subject: [PATCH] =?UTF-8?q?Merge=20squash=20develop=20into=20master=20(inc?= =?UTF-8?q?l.=20#14=20premi=C3=A8re=20config=20setup/complete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../src/modules/config/config.controller.ts | 3 +- backend/src/modules/config/config.service.ts | 6 +-- docs/14_NOTE-BACKEND-CONFIG-SETUP.md | 37 ++++++++++++++ frontend/lib/config/env.dart | 2 +- .../admin_dashboardScreen.dart | 35 +++++++++++-- .../lib/services/configuration_service.dart | 45 ++++++++++------- .../lib/widgets/admin/dashboard_admin.dart | 50 +++++++++++-------- .../lib/widgets/admin/parametres_panel.dart | 23 +++++++-- 8 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 docs/14_NOTE-BACKEND-CONFIG-SETUP.md diff --git a/backend/src/modules/config/config.controller.ts b/backend/src/modules/config/config.controller.ts index ee1c9ba..701bb48 100644 --- a/backend/src/modules/config/config.controller.ts +++ b/backend/src/modules/config/config.controller.ts @@ -53,8 +53,7 @@ export class ConfigController { // @Roles('super_admin') async completeSetup(@Request() req: any) { try { - // TODO: Récupérer l'ID utilisateur depuis le JWT - const userId = req.user?.id || 'system'; + const userId = req.user?.id ?? null; await this.configService.markSetupCompleted(userId); diff --git a/backend/src/modules/config/config.service.ts b/backend/src/modules/config/config.service.ts index 421ccae..973546b 100644 --- a/backend/src/modules/config/config.service.ts +++ b/backend/src/modules/config/config.service.ts @@ -259,10 +259,10 @@ export class AppConfigService implements OnModuleInit { /** * Marquer la configuration initiale comme terminée - * @param userId ID de l'utilisateur qui termine la configuration + * @param userId ID de l'utilisateur qui termine la configuration (null si non authentifié) */ - async markSetupCompleted(userId: string): Promise { - await this.set('setup_completed', 'true', userId); + async markSetupCompleted(userId: string | null): Promise { + await this.set('setup_completed', 'true', userId ?? undefined); this.logger.log('✅ Configuration initiale marquée comme terminée'); } diff --git a/docs/14_NOTE-BACKEND-CONFIG-SETUP.md b/docs/14_NOTE-BACKEND-CONFIG-SETUP.md new file mode 100644 index 0000000..f1716d9 --- /dev/null +++ b/docs/14_NOTE-BACKEND-CONFIG-SETUP.md @@ -0,0 +1,37 @@ +# Ticket #14 – Note pour modifications backend + +**Contexte :** Première connexion admin → panneau Paramètres, déblocage après clic sur « Sauvegarder ». Le front appelle `POST /api/v1/configuration/setup/complete` au clic sur Sauvegarder. + +## Problème + +Erreur renvoyée par le back : +`invalid input syntax for type uuid: "system"` + +- Le controller fait `const userId = req.user?.id || 'system'` puis `markSetupCompleted(userId)`. +- Le service `set()` fait `config.modifiePar = { id: userId }` ; la colonne `modifie_par` est une FK UUID vers `users`. +- La chaîne `"system"` n’est pas un UUID valide → erreur PostgreSQL. + +## Modifications à apporter au backend + +**Option A – Accepter l’absence d’utilisateur (recommandé si la route peut être appelée sans JWT)** + +1. **`config.controller.ts`** (route `completeSetup`) + - Remplacer : + `const userId = req.user?.id || 'system';` + - Par : + `const userId = req.user?.id ?? null;` + +2. **`config.service.ts`** (`markSetupCompleted`) + - Changer la signature : + `async markSetupCompleted(userId: string | null): Promise` + - Et appeler : + `await this.set('setup_completed', 'true', userId ?? undefined);` + - Dans `set()`, ne pas remplir `modifiePar` quand `userId` est absent (déjà le cas si `if (userId)`). + +**Option B – Imposer un utilisateur authentifié** + +- Activer le guard JWT (et éventuellement RolesGuard) sur `POST /configuration/setup/complete` pour que `req.user` soit toujours défini, et garder `userId = req.user.id` (plus de fallback `'system'`). + +--- + +Une fois le back modifié, le flux « Sauvegarder » → déblocage des panneaux fonctionne sans erreur. 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 be288bd..1868c9d 100644 --- a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/services/configuration_service.dart'; import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; @@ -14,12 +15,32 @@ class AdminDashboardScreen extends StatefulWidget { } class _AdminDashboardScreenState extends State { - /// 0 = Gestion des utilisateurs, 1 = Paramètres + bool? _setupCompleted; int mainTabIndex = 0; - - /// Sous-onglet quand mainTabIndex == 0 : 0=Gestionnaires, 1=Parents, 2=AM, 3=Administrateurs int subIndex = 0; + @override + void initState() { + super.initState(); + _loadSetupStatus(); + } + + Future _loadSetupStatus() async { + try { + final completed = await ConfigurationService.getSetupStatus(); + if (!mounted) return; + setState(() { + _setupCompleted = completed; + if (!completed) mainTabIndex = 1; + }); + } catch (e) { + if (mounted) setState(() { + _setupCompleted = false; + mainTabIndex = 1; + }); + } + } + void onMainTabChange(int index) { setState(() { mainTabIndex = index; @@ -34,6 +55,11 @@ class _AdminDashboardScreenState extends State { @override Widget build(BuildContext context) { + if (_setupCompleted == null) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(60.0), @@ -46,6 +72,7 @@ class _AdminDashboardScreenState extends State { child: DashboardAppBarAdmin( selectedIndex: mainTabIndex, onTabChange: onMainTabChange, + setupCompleted: _setupCompleted!, ), ), ), @@ -67,7 +94,7 @@ class _AdminDashboardScreenState extends State { Widget _getBody() { if (mainTabIndex == 1) { - return const ParametresPanel(); + 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 8f1c905..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,10 @@ class ConfigurationService { headers: await _headers(), body: jsonEncode(body), ); - if (response.statusCode != 200) { - final err = jsonDecode(response.body) as Map; - throw Exception(err['message'] ?? 'Erreur lors de la sauvegarde'); + if (response.statusCode != 200 && response.statusCode != 201) { + final err = jsonDecode(response.body) as Map?; + final msg = err != null ? (_toStr(err['error']) ?? _toStr(err['message'])) : null; + throw Exception(msg ?? 'Erreur lors de la sauvegarde'); } } @@ -118,11 +127,12 @@ class ConfigurationService { headers: await _headers(), 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.'; + final data = jsonDecode(response.body) as Map?; + if ((response.statusCode == 200 || response.statusCode == 201) && (data?['success'] == true)) { + return _toStr(data?['message']) ?? 'Test SMTP réussi.'; } - throw Exception(data['error'] ?? data['message'] ?? 'Échec du test SMTP'); + final msg = data != null ? (_toStr(data['error']) ?? _toStr(data['message'])) : null; + throw Exception(msg ?? 'Échec du test SMTP'); } /// POST /api/v1/configuration/setup/complete (après première config) @@ -131,9 +141,10 @@ class ConfigurationService { Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationSetupComplete}'), headers: await _headers(), ); - if (response.statusCode != 200) { - final err = jsonDecode(response.body) as Map; - throw Exception(err['message'] ?? 'Erreur finalisation configuration'); + if (response.statusCode != 200 && response.statusCode != 201) { + final err = jsonDecode(response.body) as Map?; + 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 d19030a..12fbe8b 100644 --- a/frontend/lib/widgets/admin/dashboard_admin.dart +++ b/frontend/lib/widgets/admin/dashboard_admin.dart @@ -1,14 +1,18 @@ 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; + final bool setupCompleted; const DashboardAppBarAdmin({ Key? key, required this.selectedIndex, required this.onTabChange, + this.setupCompleted = true, }) : super(key: key); @override @@ -32,9 +36,9 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge child: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildNavItem(context, 'Gestion des utilisateurs', 0), + _buildNavItem(context, 'Gestion des utilisateurs', 0, enabled: setupCompleted), const SizedBox(width: 24), - _buildNavItem(context, 'Paramètres', 1), + _buildNavItem(context, 'Paramètres', 1, enabled: true), ], ), ), @@ -74,23 +78,26 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge ); } - Widget _buildNavItem(BuildContext context, String title, int index) { + Widget _buildNavItem(BuildContext context, String title, int index, {bool enabled = true}) { final bool isActive = index == selectedIndex; return InkWell( - onTap: () => onTabChange(index), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent, - borderRadius: BorderRadius.circular(20), - border: isActive ? null : Border.all(color: Colors.black26), - ), - child: Text( - title, - style: TextStyle( - color: isActive ? Colors.white : Colors.black, - fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, - fontSize: 14, + onTap: enabled ? () => onTabChange(index) : null, + child: Opacity( + opacity: enabled ? 1.0 : 0.5, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: isActive ? null : Border.all(color: Colors.black26), + ), + child: Text( + title, + style: TextStyle( + color: isActive ? Colors.white : Colors.black, + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + fontSize: 14, + ), ), ), ), @@ -109,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'), ), @@ -121,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 c50ecdd..22e7b7c 100644 --- a/frontend/lib/widgets/admin/parametres_panel.dart +++ b/frontend/lib/widgets/admin/parametres_panel.dart @@ -1,9 +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 { - const ParametresPanel({super.key}); + /// 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.redirectToLoginAfterSave = false}); @override State createState() => _ParametresPanelState(); @@ -104,7 +108,14 @@ class _ParametresPanelState extends State { return payload; } + /// Sauvegarde en base sans completeSetup (utilisé avant test SMTP). + Future _saveBulkOnly() async { + await ConfigurationService.updateBulk(_buildPayload()); + } + + /// 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; @@ -112,10 +123,16 @@ class _ParametresPanelState extends State { try { await ConfigurationService.updateBulk(_buildPayload()); if (!mounted) return; + await ConfigurationService.completeSetup(); + if (!mounted) return; setState(() { _isSaving = false; _message = 'Configuration enregistrée.'; }); + if (!mounted) return; + if (redirectAfter) { + GoRouter.of(context).go('/login'); + } } catch (e) { if (mounted) { setState(() { @@ -160,7 +177,7 @@ class _ParametresPanelState extends State { if (email == null || !mounted) return; setState(() => _message = null); try { - await _save(); + await _saveBulkOnly(); if (!mounted) return; final msg = await ConfigurationService.testSmtp(email); if (!mounted) return;