first commit
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/coming_soon_page.dart';
|
||||
import '../../../../shared/widgets/buttons/buttons.dart';
|
||||
import '../../../dashboard/presentation/pages/enhanced_dashboard.dart';
|
||||
import '../../../members/presentation/pages/members_list_page.dart';
|
||||
import '../widgets/custom_bottom_nav_bar.dart';
|
||||
|
||||
class MainNavigation extends StatefulWidget {
|
||||
const MainNavigation({super.key});
|
||||
|
||||
@override
|
||||
State<MainNavigation> createState() => _MainNavigationState();
|
||||
}
|
||||
|
||||
class _MainNavigationState extends State<MainNavigation>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
late PageController _pageController;
|
||||
late AnimationController _fabAnimationController;
|
||||
late Animation<double> _fabAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController();
|
||||
|
||||
_fabAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fabAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _fabAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_fabAnimationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
_fabAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final List<NavigationTab> _tabs = [
|
||||
NavigationTab(
|
||||
title: 'Tableau de bord',
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
NavigationTab(
|
||||
title: 'Membres',
|
||||
icon: Icons.people_outline,
|
||||
activeIcon: Icons.people,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
NavigationTab(
|
||||
title: 'Cotisations',
|
||||
icon: Icons.payment_outlined,
|
||||
activeIcon: Icons.payment,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
NavigationTab(
|
||||
title: 'Événements',
|
||||
icon: Icons.event_outlined,
|
||||
activeIcon: Icons.event,
|
||||
color: AppTheme.warningColor,
|
||||
),
|
||||
NavigationTab(
|
||||
title: 'Plus',
|
||||
icon: Icons.more_horiz_outlined,
|
||||
activeIcon: Icons.menu,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: _onPageChanged,
|
||||
children: [
|
||||
EnhancedDashboard(
|
||||
onNavigateToTab: _onTabTapped,
|
||||
),
|
||||
_buildMembresPage(),
|
||||
_buildCotisationsPage(),
|
||||
_buildEventsPage(),
|
||||
_buildMorePage(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: CustomBottomNavBar(
|
||||
currentIndex: _currentIndex,
|
||||
tabs: _tabs,
|
||||
onTap: _onTabTapped,
|
||||
),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton() {
|
||||
// Afficher le FAB seulement sur certains onglets
|
||||
if (_currentIndex == 1 || _currentIndex == 2 || _currentIndex == 3) {
|
||||
return ScaleTransition(
|
||||
scale: _fabAnimation,
|
||||
child: QuickButtons.fab(
|
||||
onPressed: _onFabPressed,
|
||||
icon: _getFabIcon(),
|
||||
variant: FABVariant.gradient,
|
||||
size: FABSize.regular,
|
||||
tooltip: _getFabTooltip(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
IconData _getFabIcon() {
|
||||
switch (_currentIndex) {
|
||||
case 1: // Membres
|
||||
return Icons.person_add;
|
||||
case 2: // Cotisations
|
||||
return Icons.add_card;
|
||||
case 3: // Événements
|
||||
return Icons.add_circle_outline;
|
||||
default:
|
||||
return Icons.add;
|
||||
}
|
||||
}
|
||||
|
||||
String _getFabTooltip() {
|
||||
switch (_currentIndex) {
|
||||
case 1: // Membres
|
||||
return 'Ajouter un membre';
|
||||
case 2: // Cotisations
|
||||
return 'Nouvelle cotisation';
|
||||
case 3: // Événements
|
||||
return 'Créer un événement';
|
||||
default:
|
||||
return 'Ajouter';
|
||||
}
|
||||
}
|
||||
|
||||
void _onPageChanged(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
// Animation du FAB
|
||||
if (index == 1 || index == 2 || index == 3) {
|
||||
_fabAnimationController.forward();
|
||||
} else {
|
||||
_fabAnimationController.reverse();
|
||||
}
|
||||
|
||||
// Vibration légère
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
void _onTabTapped(int index) {
|
||||
if (_currentIndex != index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFabPressed() {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
String action;
|
||||
switch (_currentIndex) {
|
||||
case 1:
|
||||
action = 'Ajouter un membre';
|
||||
break;
|
||||
case 2:
|
||||
action = 'Nouvelle cotisation';
|
||||
break;
|
||||
case 3:
|
||||
action = 'Créer un événement';
|
||||
break;
|
||||
default:
|
||||
action = 'Action';
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$action - En cours de développement'),
|
||||
backgroundColor: _tabs[_currentIndex].color,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembresPage() {
|
||||
return MembersListPage();
|
||||
}
|
||||
|
||||
Widget _buildCotisationsPage() {
|
||||
return ComingSoonPage(
|
||||
title: 'Module Cotisations',
|
||||
description: 'Suivi et gestion des cotisations avec paiements automatiques',
|
||||
icon: Icons.payment_rounded,
|
||||
color: AppTheme.accentColor,
|
||||
features: [
|
||||
'Tableau de bord des cotisations',
|
||||
'Relances automatiques par email/SMS',
|
||||
'Paiements en ligne sécurisés',
|
||||
'Génération de reçus automatique',
|
||||
'Suivi des retards de paiement',
|
||||
'Rapports financiers détaillés',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsPage() {
|
||||
return ComingSoonPage(
|
||||
title: 'Module Événements',
|
||||
description: 'Organisation et gestion d\'événements avec calendrier intégré',
|
||||
icon: Icons.event_rounded,
|
||||
color: AppTheme.warningColor,
|
||||
features: [
|
||||
'Calendrier interactif des événements',
|
||||
'Gestion des inscriptions en ligne',
|
||||
'Envoi d\'invitations automatiques',
|
||||
'Suivi de la participation',
|
||||
'Gestion des lieux et ressources',
|
||||
'Sondages et feedback post-événement',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMorePage() {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text('Plus'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildMoreSection(
|
||||
'Gestion',
|
||||
[
|
||||
_buildMoreItem(Icons.analytics, 'Rapports', 'Génération de rapports'),
|
||||
_buildMoreItem(Icons.account_balance, 'Finances', 'Tableau de bord financier'),
|
||||
_buildMoreItem(Icons.message, 'Communications', 'Messages et notifications'),
|
||||
_buildMoreItem(Icons.folder, 'Documents', 'Gestion documentaire'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildMoreSection(
|
||||
'Paramètres',
|
||||
[
|
||||
_buildMoreItem(Icons.person, 'Mon profil', 'Informations personnelles'),
|
||||
_buildMoreItem(Icons.notifications, 'Notifications', 'Préférences de notification'),
|
||||
_buildMoreItem(Icons.security, 'Sécurité', 'Mot de passe et sécurité'),
|
||||
_buildMoreItem(Icons.language, 'Langue', 'Changer la langue'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildMoreSection(
|
||||
'Support',
|
||||
[
|
||||
_buildMoreItem(Icons.help, 'Aide', 'Centre d\'aide et FAQ'),
|
||||
_buildMoreItem(Icons.contact_support, 'Contact', 'Nous contacter'),
|
||||
_buildMoreItem(Icons.info, 'À propos', 'Informations sur l\'application'),
|
||||
_buildMoreItem(Icons.logout, 'Déconnexion', 'Se déconnecter', isDestructive: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreSection(String title, List<Widget> items) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 12),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: items,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreItem(IconData icon, String title, String subtitle, {bool isDestructive = false}) {
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: (isDestructive ? AppTheme.errorColor : AppTheme.primaryColor).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: isDestructive ? AppTheme.errorColor : AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDestructive ? AppTheme.errorColor : AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$title - En cours de développement'),
|
||||
backgroundColor: isDestructive ? AppTheme.errorColor : AppTheme.primaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationTab {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final Color color;
|
||||
|
||||
NavigationTab({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.activeIcon,
|
||||
required this.color,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../pages/main_navigation.dart';
|
||||
|
||||
class CustomBottomNavBar extends StatefulWidget {
|
||||
final int currentIndex;
|
||||
final List<NavigationTab> tabs;
|
||||
final Function(int) onTap;
|
||||
|
||||
const CustomBottomNavBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.tabs,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomBottomNavBar> createState() => _CustomBottomNavBarState();
|
||||
}
|
||||
|
||||
class _CustomBottomNavBarState extends State<CustomBottomNavBar>
|
||||
with TickerProviderStateMixin {
|
||||
late List<AnimationController> _animationControllers;
|
||||
late List<Animation<double>> _scaleAnimations;
|
||||
late List<Animation<Color?>> _colorAnimations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationControllers = List.generate(
|
||||
widget.tabs.length,
|
||||
(index) => AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
),
|
||||
);
|
||||
|
||||
_scaleAnimations = _animationControllers
|
||||
.map((controller) => Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
)))
|
||||
.toList();
|
||||
|
||||
_colorAnimations = _animationControllers
|
||||
.map((controller) => ColorTween(
|
||||
begin: AppTheme.textHint,
|
||||
end: AppTheme.primaryColor,
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
)))
|
||||
.toList();
|
||||
|
||||
// Animation initiale pour l'onglet sélectionné
|
||||
if (widget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[widget.currentIndex].forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomBottomNavBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.currentIndex != widget.currentIndex) {
|
||||
// Reverse animation for old tab
|
||||
if (oldWidget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[oldWidget.currentIndex].reverse();
|
||||
}
|
||||
|
||||
// Forward animation for new tab
|
||||
if (widget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[widget.currentIndex].forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _animationControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: List.generate(
|
||||
widget.tabs.length,
|
||||
(index) => _buildNavItem(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(int index) {
|
||||
final tab = widget.tabs[index];
|
||||
final isSelected = index == widget.currentIndex;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _handleTap(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationControllers[index],
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icône avec animation
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? tab.color.withOpacity(0.15)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: _scaleAnimations[index].value,
|
||||
child: Icon(
|
||||
isSelected ? tab.activeIcon : tab.icon,
|
||||
size: 20,
|
||||
color: isSelected ? tab.color : AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// Label avec animation
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? tab.color : AppTheme.textHint,
|
||||
),
|
||||
child: Text(
|
||||
tab.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Indicateur de sélection
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: isSelected ? 16 : 0,
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: tab.color,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap(int index) {
|
||||
// Vibration tactile
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
// Animation de pression
|
||||
_animationControllers[index].forward().then((_) {
|
||||
if (mounted && index != widget.currentIndex) {
|
||||
_animationControllers[index].reverse();
|
||||
}
|
||||
});
|
||||
|
||||
// Callback
|
||||
widget.onTap(index);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user