feat(admin): panneau Paramètres - sauvegarde config + test SMTP

- Onglet Paramètres dans l'admin avec 3 sections (Email, Personnalisation, Avancé)
- Service ConfigurationService (GET config, PATCH bulk, POST test-smtp)
- Bouton Sauvegarder et bouton Tester SMTP (sauvegarde avant test)
- Endpoints api_config pour configuration

Closes #15

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-13 11:16:37 +01:00
co-authored by Cursor
parent c43f55bed6
commit 1834eb8c79
5 changed files with 688 additions and 74 deletions
+102 -64
View File
@@ -1,47 +1,47 @@
import 'package:flutter/material.dart';
/// Barre principale du dashboard admin : 2 onglets (Gestion des utilisateurs | Paramètres) + infos utilisateur.
class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget {
final int selectedIndex;
final ValueChanged<int> onTabChange;
const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
const DashboardAppBarAdmin({
Key? key,
required this.selectedIndex,
required this.onTabChange,
}) : super(key: key);
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 768;
return AppBar(
elevation: 0,
automaticallyImplyLeading: false,
title: Row(
children: [
SizedBox(width: MediaQuery.of(context).size.width * 0.19),
const Text(
"P'tit Pas",
style: TextStyle(
color: Color(0xFF9CC5C0),
fontWeight: FontWeight.bold,
fontSize: 18,
const SizedBox(width: 24),
Image.asset(
'assets/images/logo.png',
height: 40,
fit: BoxFit.contain,
),
Expanded(
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildNavItem(context, 'Gestion des utilisateurs', 0),
const SizedBox(width: 24),
_buildNavItem(context, 'Paramètres', 1),
],
),
),
),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
// Navigation principale
_buildNavItem(context, 'Gestionnaires', 0),
const SizedBox(width: 24),
_buildNavItem(context, 'Parents', 1),
const SizedBox(width: 24),
_buildNavItem(context, 'Assistantes maternelles', 2),
const SizedBox(width: 24),
_buildNavItem(context, 'Administrateurs', 3),
],
),
actions: isMobile
? [_buildMobileMenu(context)]
: [
// Nom de l'utilisateur
actions: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Center(
@@ -55,8 +55,6 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
),
),
),
// Bouton déconnexion
Padding(
padding: const EdgeInsets.only(right: 16),
child: TextButton(
@@ -72,51 +70,30 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
child: const Text('Se déconnecter'),
),
),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
],
);
}
Widget _buildNavItem(BuildContext context, String title, int index) {
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,
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,
),
),
),
),
);
}
Widget _buildMobileMenu(BuildContext context) {
return PopupMenuButton<int>(
icon: const Icon(Icons.menu, color: Colors.white),
onSelected: (value) {
if (value == 4) {
_handleLogout(context);
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 0, child: Text("Gestionnaires")),
const PopupMenuItem(value: 1, child: Text("Parents")),
const PopupMenuItem(value: 2, child: Text("Assistantes maternelles")),
const PopupMenuItem(value: 3, child: Text("Administrateurs")),
const PopupMenuDivider(),
const PopupMenuItem(value: 4, child: Text("Se déconnecter")),
],
);
}
@@ -142,4 +119,65 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
),
);
}
}
}
/// Sous-barre affichée quand "Gestion des utilisateurs" est actif : 4 onglets sans infos utilisateur.
class DashboardUserManagementSubBar extends StatelessWidget {
final int selectedSubIndex;
final ValueChanged<int> onSubTabChange;
const DashboardUserManagementSubBar({
Key? key,
required this.selectedSubIndex,
required this.onSubTabChange,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 48,
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildSubNavItem(context, 'Gestionnaires', 0),
const SizedBox(width: 16),
_buildSubNavItem(context, 'Parents', 1),
const SizedBox(width: 16),
_buildSubNavItem(context, 'Assistantes maternelles', 2),
const SizedBox(width: 16),
_buildSubNavItem(context, 'Administrateurs', 3),
],
),
),
);
}
Widget _buildSubNavItem(BuildContext context, String title, int index) {
final bool isActive = index == selectedSubIndex;
return InkWell(
onTap: () => onSubTabChange(index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
borderRadius: BorderRadius.circular(16),
border: isActive ? null : Border.all(color: Colors.black26),
),
child: Text(
title,
style: TextStyle(
color: isActive ? Colors.white : Colors.black87,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
fontSize: 13,
),
),
),
);
}
}