/// 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 { final VoidCallback? onLogout; const DashboardDrawer({super.key, this.onLogout}); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return BlocBuilder( builder: (context, authState) { if (authState is! AuthAuthenticated) return const Drawer(); return Drawer( backgroundColor: isDark ? AppColors.surfaceDark : AppColors.background, child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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, icon: Icons.person, title: 'Mon Profil', subtitle: 'Modifier mes informations', onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ProfilePageWrapper()), ), ), _buildOptionTile( context: context, icon: Icons.notifications, title: 'Notifications', subtitle: 'Gérer les notifications', onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const NotificationsPageWrapper()), ), ), _buildOptionTile( context: context, icon: Icons.help, title: 'Aide & Support', subtitle: 'Documentation et support', onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const HelpSupportPage()), ), ), _buildOptionTile( context: context, icon: Icons.info, title: 'À propos', subtitle: 'Version et informations', onTap: () => Navigator.of(context).push( 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, icon: Icons.logout, title: 'Déconnexion', subtitle: 'Se déconnecter de l\'application', accentColor: AppColors.error, onTap: () { Navigator.pop(context); context.read().add(const AuthLogoutRequested()); }, ), ], ), ), ), ); }, ); } Widget _buildUserProfile(BuildContext context, AuthAuthenticated state) { final isDark = Theme.of(context).brightness == Brightness.dark; final nameColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; final emailColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; final roleColor = isDark ? AppColors.primaryLight : AppColors.primary; return CoreCard( child: Row( children: [ MiniAvatar( fallbackText: state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U', size: 32, imageUrl: state.user.avatar, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${state.user.firstName} ${state.user.lastName}', style: AppTypography.actionText.copyWith(color: nameColor), ), Text( state.effectiveRole.displayName.toUpperCase(), style: AppTypography.badgeText.copyWith( color: roleColor, fontWeight: FontWeight.bold, ), ), Text( state.user.email, style: AppTypography.subtitleSmall.copyWith(color: emailColor), ), ], ), ), ], ), ); } Widget _buildSectionTitle(BuildContext context, String title) { final isDark = Theme.of(context).brightness == Brightness.dark; return Padding( padding: const EdgeInsets.only(top: 10, bottom: 6, left: 4), child: Text( title.toUpperCase(), style: AppTypography.subtitleSmall.copyWith( fontWeight: FontWeight.bold, letterSpacing: 1.1, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, ), ), ); } Widget _buildOptionTile({ required BuildContext context, required IconData icon, required String title, required String subtitle, required VoidCallback onTap, Color? accentColor, }) { final isDark = Theme.of(context).brightness == Brightness.dark; final accent = accentColor ?? AppColors.primary; final titleColor = accentColor != null ? accentColor : (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary); final subtitleColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; final chevronColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; return CoreCard( margin: const EdgeInsets.only(bottom: 8), onTap: onTap, child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: accent.withOpacity(isDark ? 0.2 : 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: accent, size: 16), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppTypography.actionText.copyWith(color: titleColor), ), Text( subtitle, style: AppTypography.subtitleSmall.copyWith(color: subtitleColor), ), ], ), ), Icon(Icons.chevron_right, color: chevronColor, size: 16), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // 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 _rotation; // Scale : 1→0 sur la première moitié, 0→1 sur la seconde late final Animation _scale; bool _isAnimating = false; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 480), ); _rotation = Tween(begin: 0, end: 2 * math.pi).animate( CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut), ); _scale = TweenSequence([ 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 _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(); 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(); 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), ), ], ), ), ), ), ), ], ), ); } }