refactoring
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
library adaptive_navigation;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../shared/design_system/tokens/color_tokens.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../features/authentication/data/models/user_role.dart';
|
||||
@@ -125,7 +127,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Super Administrateur',
|
||||
const Color(0xFF6C5CE7),
|
||||
AppColors.brandGreen,
|
||||
Icons.admin_panel_settings,
|
||||
items,
|
||||
);
|
||||
@@ -181,7 +183,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Administrateur',
|
||||
const Color(0xFF0984E3),
|
||||
AppColors.primaryGreen,
|
||||
Icons.business_center,
|
||||
items,
|
||||
);
|
||||
@@ -225,7 +227,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Modérateur',
|
||||
const Color(0xFFE17055),
|
||||
ColorTokens.secondaryDark,
|
||||
Icons.manage_accounts,
|
||||
items,
|
||||
);
|
||||
@@ -275,7 +277,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Membre Actif',
|
||||
const Color(0xFF00B894),
|
||||
AppColors.brandGreenLight,
|
||||
Icons.groups,
|
||||
items,
|
||||
);
|
||||
@@ -319,7 +321,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Membre',
|
||||
const Color(0xFF00CEC9),
|
||||
ColorTokens.secondary,
|
||||
Icons.person,
|
||||
items,
|
||||
);
|
||||
@@ -363,7 +365,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'Visiteur',
|
||||
const Color(0xFF6C5CE7),
|
||||
AppColors.brandMint,
|
||||
Icons.waving_hand,
|
||||
items,
|
||||
);
|
||||
@@ -374,7 +376,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
return _buildDrawer(
|
||||
context,
|
||||
'UnionFlow',
|
||||
Colors.grey,
|
||||
AppColors.textSecondaryLight,
|
||||
Icons.dashboard,
|
||||
[
|
||||
const AdaptiveNavigationItem(
|
||||
@@ -394,7 +396,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
@@ -523,7 +525,7 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
color: AppColors.error,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
@@ -547,10 +549,10 @@ class AdaptiveNavigationDrawer extends StatelessWidget {
|
||||
/// Construit l'élément de déconnexion
|
||||
Widget _buildLogoutItem(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
leading: Icon(Icons.logout, color: AppColors.error),
|
||||
title: const Text(
|
||||
'Déconnexion',
|
||||
style: TextStyle(color: Colors.red),
|
||||
style: TextStyle(color: AppColors.error),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'more_page.dart';
|
||||
|
||||
@@ -13,8 +14,8 @@ import '../../features/events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../features/dashboard/presentation/bloc/dashboard_bloc.dart';
|
||||
import '../di/injection.dart';
|
||||
|
||||
/// Layout principal avec navigation hybride
|
||||
/// Bottom Navigation pour les sections principales + Drawer pour fonctions avancées
|
||||
/// Layout principal — Navigation hybride
|
||||
/// Pill navigation (style Material 3) au bas de l'écran
|
||||
class MainNavigationLayout extends StatefulWidget {
|
||||
const MainNavigationLayout({super.key});
|
||||
|
||||
@@ -27,18 +28,20 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
List<Widget>? _cachedPages;
|
||||
UserRole? _lastRole;
|
||||
String? _lastUserId;
|
||||
String? _lastOrgId;
|
||||
|
||||
/// Obtient le dashboard approprié selon le rôle de l'utilisateur
|
||||
Widget _getDashboardForRole(UserRole role, String userId, String? orgId) {
|
||||
// Admin d'organisation sans orgId (organizationContexts vide) : charger /mes puis dashboard
|
||||
if (role == UserRole.orgAdmin && (orgId == null || orgId.isEmpty)) {
|
||||
return OrgAdminDashboardLoader(userId: userId);
|
||||
}
|
||||
// Pour SUPER_ADMIN sans org: forcer les stats globales (toutes organisations)
|
||||
final isGlobalAdmin = role == UserRole.superAdmin && (orgId == null || orgId.isEmpty);
|
||||
return BlocProvider<DashboardBloc>(
|
||||
create: (context) => getIt<DashboardBloc>()
|
||||
..add(LoadDashboardData(
|
||||
organizationId: orgId ?? '',
|
||||
userId: userId,
|
||||
useGlobalDashboard: isGlobalAdmin,
|
||||
)),
|
||||
child: _buildDashboardView(role),
|
||||
);
|
||||
@@ -65,21 +68,21 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient les pages et les met en cache pour éviter les rebuilds inutiles
|
||||
List<Widget> _getPages(UserRole role, String userId, String? orgId) {
|
||||
if (_cachedPages != null && _lastRole == role && _lastUserId == userId) {
|
||||
if (_cachedPages != null && _lastRole == role && _lastUserId == userId && _lastOrgId == orgId) {
|
||||
return _cachedPages!;
|
||||
}
|
||||
|
||||
debugPrint('🔄 [MainNavigationLayout] Initialisation des pages (Role: $role, User: $userId)');
|
||||
debugPrint('🔄 [MainNavigationLayout] Init pages (Role: $role, User: $userId)');
|
||||
_lastRole = role;
|
||||
_lastUserId = userId;
|
||||
|
||||
final canManageMembers = role.hasLevelOrAbove(UserRole.hrManager);
|
||||
_lastOrgId = orgId;
|
||||
|
||||
_cachedPages = [
|
||||
_getDashboardForRole(role, userId, orgId),
|
||||
if (canManageMembers) const MembersPageWrapper(),
|
||||
if (role.hasLevelOrAbove(UserRole.hrManager))
|
||||
MembersPageWrapper(
|
||||
organisationId: role == UserRole.orgAdmin ? orgId : null,
|
||||
),
|
||||
const EventsPageWrapper(),
|
||||
const MorePage(),
|
||||
];
|
||||
@@ -96,74 +99,36 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
);
|
||||
}
|
||||
|
||||
final orgId = state.user.organizationContexts.isNotEmpty
|
||||
? state.user.organizationContexts.first.organizationId
|
||||
final orgId = state.user.organizationContexts.isNotEmpty
|
||||
? state.user.organizationContexts.first.organizationId
|
||||
: null;
|
||||
final pages = _getPages(state.effectiveRole, state.user.id, orgId);
|
||||
final safeIndex = _selectedIndex >= pages.length ? 0 : _selectedIndex;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
body: SafeArea(
|
||||
top: true, // Respecte le StatusBar
|
||||
bottom: false, // Le BottomNavigationBar gère son propre SafeArea
|
||||
child: IndexedStack(
|
||||
index: safeIndex,
|
||||
children: pages,
|
||||
),
|
||||
// Construire la liste des items de navigation selon le rôle
|
||||
final navItems = _buildNavItems(state.effectiveRole);
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorTokens.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorTokens.shadow,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: IndexedStack(
|
||||
index: safeIndex,
|
||||
children: pages,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
currentIndex: safeIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
},
|
||||
backgroundColor: ColorTokens.surface,
|
||||
selectedItemColor: ColorTokens.primary,
|
||||
unselectedItemColor: ColorTokens.onSurfaceVariant,
|
||||
selectedLabelStyle: TypographyTokens.labelSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: TypographyTokens.labelSmall,
|
||||
elevation: 0, // Géré par le Container
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
if (state.effectiveRole.hasLevelOrAbove(UserRole.hrManager))
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.people_outline),
|
||||
activeIcon: Icon(Icons.people),
|
||||
label: 'Membres',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.event_outlined),
|
||||
activeIcon: Icon(Icons.event),
|
||||
label: 'Événements',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.more_horiz_outlined),
|
||||
activeIcon: Icon(Icons.more_horiz),
|
||||
label: 'Plus',
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: _PillNavigationBar(
|
||||
items: navItems,
|
||||
selectedIndex: safeIndex,
|
||||
onItemTap: (i) => setState(() => _selectedIndex = i),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -171,4 +136,146 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<_NavItem> _buildNavItems(UserRole role) {
|
||||
return [
|
||||
const _NavItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard_rounded,
|
||||
label: 'Dashboard',
|
||||
),
|
||||
if (role.hasLevelOrAbove(UserRole.hrManager))
|
||||
const _NavItem(
|
||||
icon: Icons.people_outline_rounded,
|
||||
activeIcon: Icons.people_rounded,
|
||||
label: 'Membres',
|
||||
),
|
||||
const _NavItem(
|
||||
icon: Icons.event_outlined,
|
||||
activeIcon: Icons.event_rounded,
|
||||
label: 'Événements',
|
||||
),
|
||||
const _NavItem(
|
||||
icon: Icons.more_horiz_rounded,
|
||||
activeIcon: Icons.more_horiz_rounded,
|
||||
label: 'Plus',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Pill Navigation Bar — Material 3 style
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class _NavItem {
|
||||
const _NavItem({
|
||||
required this.icon,
|
||||
required this.activeIcon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final String label;
|
||||
}
|
||||
|
||||
class _PillNavigationBar extends StatelessWidget {
|
||||
const _PillNavigationBar({
|
||||
required this.items,
|
||||
required this.selectedIndex,
|
||||
required this.onItemTap,
|
||||
});
|
||||
|
||||
final List<_NavItem> items;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onItemTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorTokens.shadow,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: List.generate(items.length, (i) {
|
||||
return _PillNavItem(
|
||||
item: items[i],
|
||||
isSelected: i == selectedIndex,
|
||||
onTap: () => onItemTap(i),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PillNavItem extends StatelessWidget {
|
||||
const _PillNavItem({
|
||||
required this.item,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _NavItem item;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isSelected ? 16 : 14,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorTokens.primary.withOpacity(0.12)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: isSelected
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
item.activeIcon,
|
||||
size: 22,
|
||||
color: ColorTokens.primary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
item.label,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorTokens.primary,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Icon(
|
||||
item.icon,
|
||||
size: 22,
|
||||
color: ColorTokens.navigationUnselected,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper.
|
||||
import '../../features/organizations/presentation/pages/organizations_page_wrapper.dart';
|
||||
import '../../features/profile/presentation/pages/profile_page_wrapper.dart';
|
||||
|
||||
/// Page "Plus" avec les fonctions avancées selon le rôle (Menu Principal Extensif)
|
||||
/// Page "Plus" avec les fonctions avancées selon le rôle
|
||||
class MorePage extends StatelessWidget {
|
||||
const MorePage({super.key});
|
||||
|
||||
@@ -33,26 +33,20 @@ class MorePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
appBar: const UFAppBar(
|
||||
title: 'PLUS',
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Profil utilisateur
|
||||
_buildUserProfile(context, state),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Options selon le rôle
|
||||
..._buildRoleBasedOptions(context, state),
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Options communes
|
||||
..._buildCommonOptions(context),
|
||||
],
|
||||
),
|
||||
@@ -63,14 +57,20 @@ class MorePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildUserProfile(BuildContext context, AuthAuthenticated state) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final roleColor = isDark ? AppColors.brandGreenLight : AppColors.primaryGreen;
|
||||
|
||||
return CoreCard(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ProfilePageWrapper()),
|
||||
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
MiniAvatar(
|
||||
fallbackText: state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
|
||||
fallbackText: state.user.firstName.isNotEmpty
|
||||
? state.user.firstName[0].toUpperCase()
|
||||
: 'U',
|
||||
size: 40,
|
||||
imageUrl: state.user.avatar,
|
||||
),
|
||||
@@ -81,19 +81,21 @@ class MorePage extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'${state.user.firstName} ${state.user.lastName}',
|
||||
style: AppTypography.actionText,
|
||||
style: AppTypography.actionText.copyWith(color: textColor),
|
||||
),
|
||||
Text(
|
||||
state.effectiveRole.displayName.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
color: roleColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppColors.textSecondaryLight, size: 16),
|
||||
Icon(Icons.chevron_right,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
size: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -102,118 +104,90 @@ class MorePage extends StatelessWidget {
|
||||
List<Widget> _buildRoleBasedOptions(BuildContext context, AuthAuthenticated state) {
|
||||
final options = <Widget>[];
|
||||
|
||||
// Options Super Admin uniquement
|
||||
if (state.effectiveRole == UserRole.superAdmin) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Administration Système'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Administration Système'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.people,
|
||||
title: 'Gestion des utilisateurs',
|
||||
subtitle: 'Utilisateurs Keycloak et rôles',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const UserManagementPage()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UserManagementPage())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.settings,
|
||||
title: 'Paramètres Système',
|
||||
subtitle: 'Configuration globale',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const SystemSettingsPage()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.backup,
|
||||
title: 'Sauvegarde & Restauration',
|
||||
subtitle: 'Gestion des sauvegardes',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const BackupPage()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const BackupPage())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.article,
|
||||
title: 'Logs & Monitoring',
|
||||
subtitle: 'Surveillance et journaux',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const LogsPage()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const LogsPage())),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Options Admin+ (Admin Organisation et Super Admin)
|
||||
if (state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin) {
|
||||
if (state.effectiveRole == UserRole.orgAdmin ||
|
||||
state.effectiveRole == UserRole.superAdmin) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Administration'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Administration'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.business,
|
||||
title: 'Gestion des Organisations',
|
||||
subtitle: 'Créer et gérer les organisations',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const OrganizationsPageWrapper()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())),
|
||||
),
|
||||
_buildSectionTitle('Workflow Financier'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Workflow Financier'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Approbations en attente',
|
||||
subtitle: 'Valider les transactions financières',
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/approvals');
|
||||
},
|
||||
onTap: () => Navigator.pushNamed(context, '/approvals'),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.account_balance_wallet,
|
||||
title: 'Gestion des Budgets',
|
||||
subtitle: 'Créer et suivre les budgets',
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/budgets');
|
||||
},
|
||||
onTap: () => Navigator.pushNamed(context, '/budgets'),
|
||||
),
|
||||
_buildSectionTitle('Communication'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Communication'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.message,
|
||||
title: 'Messages & Broadcast',
|
||||
subtitle: 'Communiquer avec les membres',
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/messages');
|
||||
},
|
||||
onTap: () => Navigator.pushNamed(context, '/messages'),
|
||||
),
|
||||
_buildSectionTitle('Rapports & Analytics'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Rapports & Analytics'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.assessment,
|
||||
title: 'Rapports & Analytics',
|
||||
subtitle: 'Statistiques détaillées',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ReportsPageWrapper()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const ReportsPageWrapper())),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Options Modérateur (Communication limitée)
|
||||
if (state.effectiveRole == UserRole.moderator) {
|
||||
options.addAll([
|
||||
_buildSectionTitle('Communication'),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Communication'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.message,
|
||||
title: 'Messages aux membres',
|
||||
subtitle: 'Communiquer avec les membres',
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/messages');
|
||||
},
|
||||
onTap: () => Navigator.pushNamed(context, '/messages'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -223,129 +197,83 @@ class MorePage extends StatelessWidget {
|
||||
|
||||
List<Widget> _buildCommonOptions(BuildContext context) {
|
||||
return [
|
||||
_buildSectionTitle('Général'),
|
||||
_buildOptionTile(
|
||||
icon: Icons.person_outline,
|
||||
title: 'Mon profil',
|
||||
subtitle: 'Voir et modifier mon profil',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ProfilePageWrapper()),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildSectionTitle(context, 'Général'),
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.payment,
|
||||
title: 'Cotisations',
|
||||
subtitle: 'Gérer les cotisations',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const CotisationsPageWrapper()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const CotisationsPageWrapper())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.how_to_reg,
|
||||
title: 'Demandes d\'adhésion',
|
||||
subtitle: 'Demandes d\'adhésion à une organisation',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const AdhesionsPageWrapper()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const AdhesionsPageWrapper())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.volunteer_activism,
|
||||
title: 'Demandes d\'aide',
|
||||
subtitle: 'Solidarité – demandes d\'aide',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const DemandesAidePageWrapper()),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper())),
|
||||
),
|
||||
_buildOptionTile(
|
||||
_buildOptionTile(context,
|
||||
icon: Icons.savings_outlined,
|
||||
title: 'Comptes épargne',
|
||||
subtitle: 'Mutuelle épargne – dépôts (LCB-FT)',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const EpargnePage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildOptionTile(
|
||||
icon: Icons.logout,
|
||||
title: 'Déconnexion',
|
||||
subtitle: 'Se déconnecter de l\'application',
|
||||
color: AppColors.error,
|
||||
onTap: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Déconnexion'),
|
||||
content: const Text('Voulez-vous vraiment vous déconnecter ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AuthBloc>().add(AuthLogoutRequested());
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: AppColors.error),
|
||||
child: const Text('Déconnecter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const EpargnePage())),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildSectionTitle(BuildContext context, String title) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 8, left: 4),
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 6, left: 4),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionTile({
|
||||
Widget _buildOptionTile(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
Color? color,
|
||||
Color? accentColor,
|
||||
}) {
|
||||
final effectiveColor = color ?? AppColors.primaryGreen;
|
||||
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final accent = accentColor ?? AppColors.primaryGreen;
|
||||
final titleColor = accentColor != null
|
||||
? accentColor
|
||||
: (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight);
|
||||
final subtitleColor =
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final chevronColor =
|
||||
isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveColor.withOpacity(0.1),
|
||||
color: accent.withOpacity(isDark ? 0.2 : 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveColor,
|
||||
size: 20,
|
||||
),
|
||||
child: Icon(icon, color: accent, size: 18),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -354,22 +282,16 @@ class MorePage extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: color ?? AppColors.textPrimaryLight,
|
||||
),
|
||||
style: AppTypography.actionText.copyWith(color: titleColor),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.subtitleSmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(color: subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
Icon(Icons.chevron_right, color: chevronColor, size: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../app/app.dart';
|
||||
import '../config/environment.dart';
|
||||
import '../di/injection.dart';
|
||||
import '../error/error_handler.dart';
|
||||
@@ -80,6 +82,10 @@ class ApiClient {
|
||||
if (retryError.response?.statusCode == 401) {
|
||||
debugPrint('🚪 [API] Persistent 401. Force Logout.');
|
||||
_forceLogout();
|
||||
return handler.reject(DioException(
|
||||
requestOptions: retryError.requestOptions,
|
||||
type: DioExceptionType.cancel,
|
||||
));
|
||||
}
|
||||
return handler.next(retryError);
|
||||
} catch (retryError) {
|
||||
@@ -90,6 +96,10 @@ class ApiClient {
|
||||
} else {
|
||||
debugPrint('🚪 [API] Refresh failed. Force Logout.');
|
||||
_forceLogout();
|
||||
return handler.reject(DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
type: DioExceptionType.cancel,
|
||||
));
|
||||
}
|
||||
}
|
||||
return handler.next(e);
|
||||
@@ -100,6 +110,27 @@ class ApiClient {
|
||||
|
||||
void _forceLogout() {
|
||||
try {
|
||||
UnionFlowApp.scaffoldMessengerKey.currentState
|
||||
?..clearSnackBars()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: const Row(
|
||||
children: [
|
||||
Icon(Icons.lock_clock, color: Colors.white, size: 20),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Session expirée. Vous avez été déconnecté automatiquement.',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
duration: const Duration(seconds: 4),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
final authBloc = getIt<AuthBloc>();
|
||||
authBloc.add(const AuthLogoutRequested());
|
||||
} catch (e, st) {
|
||||
|
||||
42
lib/core/theme/theme_provider.dart
Normal file
42
lib/core/theme/theme_provider.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Provider de thème — light / dark avec persistance
|
||||
class ThemeProvider extends ChangeNotifier {
|
||||
static const String _key = 'uf_theme_mode';
|
||||
|
||||
ThemeMode _mode;
|
||||
|
||||
ThemeProvider({ThemeMode initial = ThemeMode.system}) : _mode = initial;
|
||||
|
||||
ThemeMode get mode => _mode;
|
||||
|
||||
bool get isDark => _mode == ThemeMode.dark;
|
||||
|
||||
Future<void> toggle() async {
|
||||
_mode = isDark ? ThemeMode.light : ThemeMode.dark;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_key, _mode.name);
|
||||
}
|
||||
|
||||
Future<void> setMode(ThemeMode mode) async {
|
||||
if (_mode == mode) return;
|
||||
_mode = mode;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_key, mode.name);
|
||||
}
|
||||
|
||||
/// Charge le thème persisté depuis les préférences
|
||||
static Future<ThemeProvider> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final stored = prefs.getString(_key);
|
||||
final mode = switch (stored) {
|
||||
'light' => ThemeMode.light,
|
||||
'dark' => ThemeMode.dark,
|
||||
_ => ThemeMode.system,
|
||||
};
|
||||
return ThemeProvider(initial: mode);
|
||||
}
|
||||
}
|
||||
@@ -284,14 +284,18 @@ class WebSocketService {
|
||||
void _onError(dynamic error) {
|
||||
AppLogger.error('WebSocket error', error: error);
|
||||
_isConnected = false;
|
||||
_stopHeartbeat();
|
||||
_channel = null;
|
||||
_connectionStatusController.add(false);
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
/// Gestion de la fermeture de connexion
|
||||
void _onDone() {
|
||||
if (!_isConnected) return; // Déjà traité par _onError
|
||||
AppLogger.info('WebSocket connexion fermée');
|
||||
_isConnected = false;
|
||||
_channel = null;
|
||||
_connectionStatusController.add(false);
|
||||
_stopHeartbeat();
|
||||
_scheduleReconnect();
|
||||
@@ -303,6 +307,10 @@ class WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reconnectTimer != null) {
|
||||
return; // Reconnexion déjà planifiée
|
||||
}
|
||||
|
||||
_stopReconnectTimer();
|
||||
|
||||
// Backoff exponentiel : 2^attempts secondes (max 60s)
|
||||
@@ -312,6 +320,7 @@ class WebSocketService {
|
||||
AppLogger.info('⏳ Reconnexion WebSocket dans ${delaySeconds}s (tentative $_reconnectAttempts)');
|
||||
|
||||
_reconnectTimer = Timer(Duration(seconds: delaySeconds), () {
|
||||
_channel = null;
|
||||
AppLogger.info('🔄 Tentative de reconnexion WebSocket...');
|
||||
connect();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user