feat(features): refontes adhesions/admin/auth/backup/contributions/dashboard/epargne/events
- adhesions : bloc complet avec events/states/model, dialogs paiement/rejet - admin : users bloc, user management list/detail pages - authentication : bloc + keycloak auth service + webview - backup : bloc complet, repository, models - contributions : bloc + widgets + export - dashboard : widgets connectés (activities, events, notifications, search) + charts + monitoring + shortcuts - epargne : repository, transactions, dialogs - events : bloc complet, pages (detail, connected, wrapper), models
This commit is contained in:
@@ -1,18 +1,28 @@
|
||||
/// Widget de menu latéral (drawer) du dashboard
|
||||
library dashboard_drawer;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../core/theme/theme_provider.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../authentication/data/models/user_role.dart';
|
||||
import '../../../profile/presentation/pages/profile_page_wrapper.dart';
|
||||
import '../../../notifications/presentation/pages/notifications_page_wrapper.dart';
|
||||
import '../../../help/presentation/pages/help_support_page.dart';
|
||||
import '../../../about/presentation/pages/about_page.dart';
|
||||
import '../../../admin/presentation/pages/user_management_page.dart';
|
||||
import '../../../settings/presentation/pages/system_settings_page.dart';
|
||||
import '../../../backup/presentation/pages/backup_page.dart';
|
||||
import '../../../logs/presentation/pages/logs_page.dart';
|
||||
|
||||
/// Drawer principal — Mon Espace
|
||||
/// Profil · Notifications · Aide · À propos · Déconnexion
|
||||
class DashboardDrawer extends StatelessWidget {
|
||||
@@ -30,7 +40,7 @@ class DashboardDrawer extends StatelessWidget {
|
||||
|
||||
return Drawer(
|
||||
backgroundColor:
|
||||
isDark ? AppColors.darkSurface : AppColors.lightBackground,
|
||||
isDark ? AppColors.surfaceDark : AppColors.background,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(10),
|
||||
@@ -39,6 +49,9 @@ class DashboardDrawer extends StatelessWidget {
|
||||
children: [
|
||||
_buildUserProfile(context, authState),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildSectionTitle(context, 'Apparence'),
|
||||
const _ThemeToggleTile(),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildSectionTitle(context, 'Mon Espace'),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
@@ -77,6 +90,53 @@ class DashboardDrawer extends StatelessWidget {
|
||||
MaterialPageRoute(builder: (_) => const AboutPage()),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Section SYSTÈME (super admin uniquement) ──
|
||||
if (authState.effectiveRole == UserRole.superAdmin) ...[
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildSectionTitle(context, 'Système'),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.people,
|
||||
title: 'Gestion des utilisateurs',
|
||||
subtitle: 'Utilisateurs Keycloak et rôles',
|
||||
accentColor: ModuleColors.membres,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UserManagementPage()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.settings,
|
||||
title: 'Paramètres Système',
|
||||
subtitle: 'Configuration globale',
|
||||
accentColor: ModuleColors.parametres,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SystemSettingsPage()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.backup,
|
||||
title: 'Sauvegarde & Restauration',
|
||||
subtitle: 'Gestion des sauvegardes',
|
||||
accentColor: ModuleColors.backup,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const BackupPage()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.article,
|
||||
title: 'Logs & Monitoring',
|
||||
subtitle: 'Surveillance et journaux',
|
||||
accentColor: ModuleColors.logs,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const LogsPage()),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
@@ -101,11 +161,11 @@ class DashboardDrawer extends StatelessWidget {
|
||||
Widget _buildUserProfile(BuildContext context, AuthAuthenticated state) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final nameColor =
|
||||
isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final emailColor =
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
final roleColor =
|
||||
isDark ? AppColors.brandGreenLight : AppColors.primaryGreen;
|
||||
isDark ? AppColors.primaryLight : AppColors.primary;
|
||||
|
||||
return CoreCard(
|
||||
child: Row(
|
||||
@@ -154,7 +214,7 @@ class DashboardDrawer extends StatelessWidget {
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -169,14 +229,14 @@ class DashboardDrawer extends StatelessWidget {
|
||||
Color? accentColor,
|
||||
}) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final accent = accentColor ?? AppColors.primaryGreen;
|
||||
final accent = accentColor ?? AppColors.primary;
|
||||
final titleColor = accentColor != null
|
||||
? accentColor
|
||||
: (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight);
|
||||
: (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary);
|
||||
final subtitleColor =
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
final chevronColor =
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -213,3 +273,181 @@ class DashboardDrawer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Toggle thème jour / nuit avec animation soleil ↔ lune
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class _ThemeToggleTile extends StatefulWidget {
|
||||
const _ThemeToggleTile();
|
||||
|
||||
@override
|
||||
State<_ThemeToggleTile> createState() => _ThemeToggleTileState();
|
||||
}
|
||||
|
||||
class _ThemeToggleTileState extends State<_ThemeToggleTile>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
// Rotation complète sur toute la durée
|
||||
late final Animation<double> _rotation;
|
||||
// Scale : 1→0 sur la première moitié, 0→1 sur la seconde
|
||||
late final Animation<double> _scale;
|
||||
|
||||
bool _isAnimating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 480),
|
||||
);
|
||||
_rotation = Tween<double>(begin: 0, end: 2 * math.pi).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut),
|
||||
);
|
||||
_scale = TweenSequence<double>([
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.0, end: 0.0)
|
||||
.chain(CurveTween(curve: Curves.easeIn)),
|
||||
weight: 50,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.0, end: 1.0)
|
||||
.chain(CurveTween(curve: Curves.elasticOut)),
|
||||
weight: 50,
|
||||
),
|
||||
]).animate(_ctrl);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _toggle() async {
|
||||
if (_isAnimating) return;
|
||||
_isAnimating = true;
|
||||
|
||||
// Première moitié : rotation + disparition de l'icône
|
||||
await _ctrl.animateTo(0.5);
|
||||
|
||||
// Bascule le thème au moment où l'icône est invisible (scale ≈ 0)
|
||||
if (mounted) {
|
||||
final tp = context.read<ThemeProvider>();
|
||||
tp.setMode(tp.mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
|
||||
}
|
||||
|
||||
// Seconde moitié : réapparition avec la nouvelle icône + fin de rotation
|
||||
if (mounted) await _ctrl.animateTo(1.0);
|
||||
|
||||
_ctrl.reset();
|
||||
_isAnimating = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tp = context.watch<ThemeProvider>();
|
||||
final isDark = tp.mode == ThemeMode.dark;
|
||||
final isDarkBrightness = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Soleil = jaune ambré / Lune = indigo
|
||||
final accent =
|
||||
isDark ? const Color(0xFFFBBF24) : const Color(0xFF6366F1);
|
||||
final titleColor =
|
||||
isDarkBrightness ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final subtitleColor = isDarkBrightness
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondary;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
onTap: _toggle,
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône animée
|
||||
AnimatedBuilder(
|
||||
animation: _ctrl,
|
||||
builder: (_, __) => Transform.rotate(
|
||||
angle: _rotation.value,
|
||||
child: Transform.scale(
|
||||
scale: _scale.value.clamp(0.01, 1.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(isDarkBrightness ? 0.22 : 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded,
|
||||
color: accent,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Thème',
|
||||
style: AppTypography.actionText.copyWith(color: titleColor),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
isDark ? 'Mode sombre' : 'Mode clair',
|
||||
key: ValueKey(isDark),
|
||||
style: AppTypography.subtitleSmall
|
||||
.copyWith(color: subtitleColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pill toggle animée
|
||||
GestureDetector(
|
||||
onTap: _toggle,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: 40,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
color: isDark
|
||||
? const Color(0xFF6366F1)
|
||||
: AppColors.borderStrong,
|
||||
),
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
alignment:
|
||||
isDark ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user