Files
dahoud 120434aba0 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
2026-04-15 20:26:48 +00:00

454 lines
16 KiB
Dart

/// 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<AuthBloc, AuthState>(
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<AuthBloc>().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<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),
),
],
),
),
),
),
),
],
),
);
}
}