- 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
454 lines
16 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|