first commit
This commit is contained in:
@@ -0,0 +1,675 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class DashboardPage extends StatelessWidget {
|
||||
const DashboardPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text('Tableau de bord'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Message de bienvenue
|
||||
_buildWelcomeSection(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Cartes KPI principales
|
||||
_buildKPICards(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Graphiques et statistiques
|
||||
_buildChartsSection(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions rapides
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activités récentes
|
||||
_buildRecentActivities(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeSection(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppTheme.primaryColor, AppTheme.primaryLight],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Bonjour !',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Voici un aperçu de votre association',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.dashboard,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPICards(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Indicateurs clés',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildKPICard(
|
||||
context,
|
||||
'Membres',
|
||||
'1,247',
|
||||
'+5.2%',
|
||||
Icons.people,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildKPICard(
|
||||
context,
|
||||
'Revenus',
|
||||
'€45,890',
|
||||
'+12.8%',
|
||||
Icons.euro,
|
||||
AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildKPICard(
|
||||
context,
|
||||
'Événements',
|
||||
'23',
|
||||
'+3',
|
||||
Icons.event,
|
||||
AppTheme.accentColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildKPICard(
|
||||
context,
|
||||
'Cotisations',
|
||||
'89.5%',
|
||||
'+2.1%',
|
||||
Icons.payments,
|
||||
AppTheme.infoColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPICard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String value,
|
||||
String change,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
change,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.successColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartsSection(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Analyses',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildLineChart(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildPieChart(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLineChart(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
padding: const EdgeInsets.all(16),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Évolution des membres',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 1000),
|
||||
FlSpot(1, 1050),
|
||||
FlSpot(2, 1100),
|
||||
FlSpot(3, 1180),
|
||||
FlSpot(4, 1247),
|
||||
],
|
||||
color: AppTheme.primaryColor,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPieChart(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
padding: const EdgeInsets.all(16),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Répartition des membres',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: AppTheme.primaryColor,
|
||||
value: 45,
|
||||
title: '45%',
|
||||
radius: 50,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.secondaryColor,
|
||||
value: 30,
|
||||
title: '30%',
|
||||
radius: 50,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.accentColor,
|
||||
value: 25,
|
||||
title: '25%',
|
||||
radius: 50,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Actions rapides',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
context,
|
||||
'Nouveau membre',
|
||||
'Ajouter un membre',
|
||||
Icons.person_add,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
context,
|
||||
'Créer événement',
|
||||
'Organiser un événement',
|
||||
Icons.event_available,
|
||||
AppTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
context,
|
||||
'Suivi cotisations',
|
||||
'Gérer les cotisations',
|
||||
Icons.payment,
|
||||
AppTheme.accentColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
context,
|
||||
'Rapports',
|
||||
'Générer des rapports',
|
||||
Icons.analytics,
|
||||
AppTheme.infoColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$title - En cours de développement'),
|
||||
backgroundColor: color,
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentActivities(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Activités récentes',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Voir tout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
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: [
|
||||
_buildActivityItem(
|
||||
'Nouveau membre inscrit',
|
||||
'Marie Dupont a rejoint l\'association',
|
||||
Icons.person_add,
|
||||
AppTheme.successColor,
|
||||
'Il y a 2h',
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildActivityItem(
|
||||
'Cotisation reçue',
|
||||
'Jean Martin a payé sa cotisation annuelle',
|
||||
Icons.payment,
|
||||
AppTheme.primaryColor,
|
||||
'Il y a 4h',
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildActivityItem(
|
||||
'Événement créé',
|
||||
'Assemblée générale 2024 programmée',
|
||||
Icons.event,
|
||||
AppTheme.accentColor,
|
||||
'Hier',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityItem(
|
||||
String title,
|
||||
String description,
|
||||
IconData icon,
|
||||
Color color,
|
||||
String time,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
time,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../widgets/kpi_card.dart';
|
||||
import '../widgets/clickable_kpi_card.dart';
|
||||
import '../widgets/chart_card.dart';
|
||||
import '../widgets/activity_feed.dart';
|
||||
import '../widgets/quick_actions_grid.dart';
|
||||
import '../widgets/navigation_cards.dart';
|
||||
|
||||
class EnhancedDashboard extends StatefulWidget {
|
||||
final Function(int)? onNavigateToTab;
|
||||
|
||||
const EnhancedDashboard({
|
||||
super.key,
|
||||
this.onNavigateToTab,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EnhancedDashboard> createState() => _EnhancedDashboardState();
|
||||
}
|
||||
|
||||
class _EnhancedDashboardState extends State<EnhancedDashboard> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
_buildAppBar(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildKPISection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildChartsSection(),
|
||||
const SizedBox(height: 24),
|
||||
NavigationCards(
|
||||
onNavigateToTab: widget.onNavigateToTab,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const QuickActionsGrid(),
|
||||
const SizedBox(height: 24),
|
||||
const ActivityFeed(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
return SliverAppBar(
|
||||
expandedHeight: 120,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: const Text(
|
||||
'Tableau de bord',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppTheme.primaryColor, AppTheme.primaryDark],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () => _showNotifications(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => _refreshData(),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: _handleMenuSelection,
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'settings',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.settings),
|
||||
SizedBox(width: 8),
|
||||
Text('Paramètres'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.download),
|
||||
SizedBox(width: 8),
|
||||
Text('Exporter'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'help',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.help),
|
||||
SizedBox(width: 8),
|
||||
Text('Aide'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppTheme.primaryColor, AppTheme.primaryLight],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Bonjour !',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Découvrez les dernières statistiques de votre association',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.trending_up,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'+12% ce mois',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.dashboard_rounded,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPISection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Indicateurs clés',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.analytics, size: 16),
|
||||
label: const Text('Analyse détaillée'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
_buildKPIPage1(),
|
||||
_buildKPIPage2(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildPageIndicator(0),
|
||||
const SizedBox(width: 8),
|
||||
_buildPageIndicator(1),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPIPage1() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClickableKPICard(
|
||||
title: 'Membres actifs',
|
||||
value: '1,247',
|
||||
change: '+5.2%',
|
||||
icon: Icons.people,
|
||||
color: AppTheme.secondaryColor,
|
||||
actionText: 'Gérer',
|
||||
onTap: () => widget.onNavigateToTab?.call(1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ClickableKPICard(
|
||||
title: 'Revenus mensuel',
|
||||
value: '€45,890',
|
||||
change: '+12.8%',
|
||||
icon: Icons.euro,
|
||||
color: AppTheme.successColor,
|
||||
actionText: 'Finances',
|
||||
onTap: () => _showFinancesMessage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPIPage2() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClickableKPICard(
|
||||
title: 'Événements',
|
||||
value: '23',
|
||||
change: '+3',
|
||||
icon: Icons.event,
|
||||
color: AppTheme.warningColor,
|
||||
actionText: 'Planifier',
|
||||
onTap: () => widget.onNavigateToTab?.call(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ClickableKPICard(
|
||||
title: 'Taux cotisation',
|
||||
value: '89.5%',
|
||||
change: '+2.1%',
|
||||
icon: Icons.payments,
|
||||
color: AppTheme.accentColor,
|
||||
actionText: 'Gérer',
|
||||
onTap: () => widget.onNavigateToTab?.call(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageIndicator(int index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: _currentPage == index ? 20 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _currentPage == index
|
||||
? AppTheme.primaryColor
|
||||
: AppTheme.primaryColor.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartsSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Analyses et tendances',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ChartCard(
|
||||
title: 'Évolution des membres',
|
||||
subtitle: 'Croissance sur 6 mois',
|
||||
chart: const MembershipChart(),
|
||||
onTap: () => widget.onNavigateToTab?.call(1),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ChartCard(
|
||||
title: 'Répartition',
|
||||
subtitle: 'Par catégorie',
|
||||
chart: const CategoryChart(),
|
||||
onTap: () => widget.onNavigateToTab?.call(1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ChartCard(
|
||||
title: 'Revenus',
|
||||
subtitle: 'Évolution mensuelle',
|
||||
chart: const RevenueChart(),
|
||||
onTap: () => _showFinancesMessage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showNotifications() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Notifications',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.warning, color: AppTheme.warningColor),
|
||||
title: const Text('3 cotisations en retard'),
|
||||
subtitle: const Text('Nécessite votre attention'),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.event, color: AppTheme.accentColor),
|
||||
title: const Text('Assemblée générale'),
|
||||
subtitle: const Text('Dans 5 jours'),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.check_circle, color: AppTheme.successColor),
|
||||
title: const Text('Rapport mensuel'),
|
||||
subtitle: const Text('Prêt à être envoyé'),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshData() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Données actualisées'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuSelection(String value) {
|
||||
switch (value) {
|
||||
case 'settings':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Paramètres - En développement')),
|
||||
);
|
||||
break;
|
||||
case 'export':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Export - En développement')),
|
||||
);
|
||||
break;
|
||||
case 'help':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Aide - En développement')),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _showFinancesMessage() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Module Finances - Prochainement disponible'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class ActivityFeed extends StatelessWidget {
|
||||
const ActivityFeed({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Activités récentes',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Voir tout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
..._getActivities().map((activity) => _buildActivityItem(activity)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityItem(ActivityItem activity) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppTheme.borderColor, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: activity.color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
activity.icon,
|
||||
color: activity.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
activity.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 14,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatTime(activity.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (activity.actionRequired)
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.errorColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<ActivityItem> _getActivities() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
ActivityItem(
|
||||
title: 'Nouveau membre inscrit',
|
||||
description: 'Marie Dupont a rejoint l\'association',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.successColor,
|
||||
timestamp: now.subtract(const Duration(hours: 2)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Cotisation en retard',
|
||||
description: 'Pierre Martin - Cotisation échue depuis 5 jours',
|
||||
icon: Icons.warning,
|
||||
color: AppTheme.warningColor,
|
||||
timestamp: now.subtract(const Duration(hours: 4)),
|
||||
actionRequired: true,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Paiement reçu',
|
||||
description: 'Jean Dubois - Cotisation annuelle 2024',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.primaryColor,
|
||||
timestamp: now.subtract(const Duration(hours: 6)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Événement créé',
|
||||
description: 'Assemblée générale 2024 - 15 mars 2024',
|
||||
icon: Icons.event,
|
||||
color: AppTheme.accentColor,
|
||||
timestamp: now.subtract(const Duration(days: 1)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Mise à jour profil',
|
||||
description: 'Sophie Bernard a modifié ses informations',
|
||||
icon: Icons.edit,
|
||||
color: AppTheme.infoColor,
|
||||
timestamp: now.subtract(const Duration(days: 1, hours: 3)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Nouveau document',
|
||||
description: 'Procès-verbal ajouté aux archives',
|
||||
icon: Icons.file_upload,
|
||||
color: AppTheme.secondaryColor,
|
||||
timestamp: now.subtract(const Duration(days: 2)),
|
||||
actionRequired: false,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
String _formatTime(DateTime timestamp) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(timestamp);
|
||||
|
||||
if (difference.inMinutes < 60) {
|
||||
return 'Il y a ${difference.inMinutes} min';
|
||||
} else if (difference.inHours < 24) {
|
||||
return 'Il y a ${difference.inHours}h';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Hier';
|
||||
} else if (difference.inDays < 7) {
|
||||
return 'Il y a ${difference.inDays} jours';
|
||||
} else {
|
||||
return DateFormat('dd/MM/yyyy').format(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityItem {
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final DateTime timestamp;
|
||||
final bool actionRequired;
|
||||
|
||||
ActivityItem({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.timestamp,
|
||||
this.actionRequired = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class ChartCard extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget chart;
|
||||
final String? subtitle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ChartCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.chart,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: chart,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MembershipChart extends StatelessWidget {
|
||||
const MembershipChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 200,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: AppTheme.borderColor.withOpacity(0.5),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 200,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'];
|
||||
if (value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: 5,
|
||||
minY: 800,
|
||||
maxY: 1400,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 1000),
|
||||
FlSpot(1, 1050),
|
||||
FlSpot(2, 1100),
|
||||
FlSpot(3, 1180),
|
||||
FlSpot(4, 1220),
|
||||
FlSpot(5, 1247),
|
||||
],
|
||||
color: AppTheme.primaryColor,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: AppTheme.primaryColor,
|
||||
strokeWidth: 2,
|
||||
strokeColor: Colors.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppTheme.primaryColor.withOpacity(0.3),
|
||||
AppTheme.primaryColor.withOpacity(0.1),
|
||||
AppTheme.primaryColor.withOpacity(0.0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryChart extends StatelessWidget {
|
||||
const CategoryChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 4,
|
||||
centerSpaceRadius: 50,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: AppTheme.primaryColor,
|
||||
value: 45,
|
||||
title: 'Actifs\n45%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.secondaryColor,
|
||||
value: 30,
|
||||
title: 'Retraités\n30%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.accentColor,
|
||||
value: 25,
|
||||
title: 'Étudiants\n25%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RevenueChart extends StatelessWidget {
|
||||
const RevenueChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: 15000,
|
||||
barTouchData: BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 5000,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toInt()}k€',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const months = ['J', 'F', 'M', 'A', 'M', 'J'];
|
||||
if (value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 5000,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: AppTheme.borderColor.withOpacity(0.5),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
barGroups: [
|
||||
_buildBarGroup(0, 8000, AppTheme.primaryColor),
|
||||
_buildBarGroup(1, 9500, AppTheme.primaryColor),
|
||||
_buildBarGroup(2, 7800, AppTheme.primaryColor),
|
||||
_buildBarGroup(3, 11200, AppTheme.primaryColor),
|
||||
_buildBarGroup(4, 13500, AppTheme.primaryColor),
|
||||
_buildBarGroup(5, 12800, AppTheme.primaryColor),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BarChartGroupData _buildBarGroup(int x, double y, Color color) {
|
||||
return BarChartGroupData(
|
||||
x: x,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: y,
|
||||
color: color,
|
||||
width: 16,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class ClickableKPICard extends StatefulWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final String change;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isPositiveChange;
|
||||
final VoidCallback? onTap;
|
||||
final String? actionText;
|
||||
|
||||
const ClickableKPICard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.change,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.isPositiveChange = true,
|
||||
this.onTap,
|
||||
this.actionText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ClickableKPICard> createState() => _ClickableKPICardState();
|
||||
}
|
||||
|
||||
class _ClickableKPICardState extends State<ClickableKPICard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialiser ResponsiveUtils
|
||||
ResponsiveUtils.init(context);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap != null ? _handleTap : null,
|
||||
onTapDown: widget.onTap != null ? (_) => _animationController.forward() : null,
|
||||
onTapUp: widget.onTap != null ? (_) => _animationController.reverse() : null,
|
||||
onTapCancel: widget.onTap != null ? () => _animationController.reverse() : null,
|
||||
borderRadius: ResponsiveUtils.borderRadius(4),
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingAll(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: ResponsiveUtils.borderRadius(4),
|
||||
border: widget.onTap != null
|
||||
? Border.all(
|
||||
color: widget.color.withOpacity(0.2),
|
||||
width: ResponsiveUtils.adaptive(
|
||||
small: 1,
|
||||
medium: 1.5,
|
||||
large: 2,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 3.5.sp,
|
||||
offset: Offset(0, 1.hp),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icône et indicateur de changement
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: ResponsiveUtils.paddingAll(2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(0.15),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2.5),
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: widget.color,
|
||||
size: ResponsiveUtils.iconSize(5),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.hp),
|
||||
// Valeur principale
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.value,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 4.5.fs,
|
||||
medium: 4.2.fs,
|
||||
large: 4.fs,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.5.hp),
|
||||
// Titre et action
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.fs,
|
||||
medium: 2.8.fs,
|
||||
large: 2.6.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.onTap != null) ...[
|
||||
SizedBox(width: 1.5.wp),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingSymmetric(
|
||||
horizontal: 1.5,
|
||||
vertical: 0.3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(0.1),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2.5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.actionText ?? 'Voir',
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 2.5.fs,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 0.5.wp),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: ResponsiveUtils.iconSize(2),
|
||||
color: widget.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChangeIndicator() {
|
||||
final changeColor = widget.isPositiveChange
|
||||
? AppTheme.successColor
|
||||
: AppTheme.errorColor;
|
||||
final changeIcon = widget.isPositiveChange
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: changeColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
changeIcon,
|
||||
size: 16,
|
||||
color: changeColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.change,
|
||||
style: TextStyle(
|
||||
color: changeColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
HapticFeedback.lightImpact();
|
||||
widget.onTap?.call();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class KPICard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final String change;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isPositiveChange;
|
||||
|
||||
const KPICard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.change,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.isPositiveChange = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChangeIndicator() {
|
||||
final changeColor = isPositiveChange
|
||||
? AppTheme.successColor
|
||||
: AppTheme.errorColor;
|
||||
final changeIcon = isPositiveChange
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: changeColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
changeIcon,
|
||||
size: 16,
|
||||
color: changeColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
change,
|
||||
style: TextStyle(
|
||||
color: changeColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class NavigationCards extends StatelessWidget {
|
||||
final Function(int)? onNavigateToTab;
|
||||
|
||||
const NavigationCards({
|
||||
super.key,
|
||||
this.onNavigateToTab,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ResponsiveUtils.init(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dashboard_customize,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Accès rapide aux modules',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.1,
|
||||
children: [
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Membres',
|
||||
subtitle: '1,247 membres',
|
||||
icon: Icons.people_rounded,
|
||||
color: AppTheme.secondaryColor,
|
||||
onTap: () => _navigateToModule(context, 1, 'Membres'),
|
||||
badge: '+5 cette semaine',
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Cotisations',
|
||||
subtitle: '89.5% à jour',
|
||||
icon: Icons.payment_rounded,
|
||||
color: AppTheme.accentColor,
|
||||
onTap: () => _navigateToModule(context, 2, 'Cotisations'),
|
||||
badge: '15 en retard',
|
||||
badgeColor: AppTheme.warningColor,
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Événements',
|
||||
subtitle: '3 à venir',
|
||||
icon: Icons.event_rounded,
|
||||
color: AppTheme.warningColor,
|
||||
onTap: () => _navigateToModule(context, 3, 'Événements'),
|
||||
badge: 'AG dans 5 jours',
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Finances',
|
||||
subtitle: '€45,890',
|
||||
icon: Icons.account_balance_rounded,
|
||||
color: AppTheme.primaryColor,
|
||||
onTap: () => _navigateToModule(context, 4, 'Finances'),
|
||||
badge: '+12.8% ce mois',
|
||||
badgeColor: AppTheme.successColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
String? badge,
|
||||
Color? badgeColor,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
color.withOpacity(0.05),
|
||||
color.withOpacity(0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec icône et badge
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
width: ResponsiveUtils.iconSize(8),
|
||||
height: ResponsiveUtils.iconSize(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(4)),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: ResponsiveUtils.iconSize(4.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (badge != null)
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingSymmetric(
|
||||
horizontal: 1.5,
|
||||
vertical: 0.3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.1),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2),
|
||||
border: Border.all(
|
||||
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
badge,
|
||||
style: TextStyle(
|
||||
color: badgeColor ?? AppTheme.successColor,
|
||||
fontSize: 2.5.fs,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Contenu principal
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 4.fs,
|
||||
medium: 3.8.fs,
|
||||
large: 3.6.fs,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 1.hp),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.2.fs,
|
||||
medium: 3.fs,
|
||||
large: 2.8.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Flèche d'action
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Gérer',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToModule(BuildContext context, int tabIndex, String moduleName) {
|
||||
// Si onNavigateToTab est fourni, l'utiliser pour naviguer vers l'onglet
|
||||
if (onNavigateToTab != null) {
|
||||
onNavigateToTab!(tabIndex);
|
||||
} else {
|
||||
// Sinon, afficher un message temporaire
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Navigation vers $moduleName'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class QuickActionsGrid extends StatelessWidget {
|
||||
const QuickActionsGrid({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ResponsiveUtils.init(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Actions rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.2,
|
||||
children: _getQuickActions(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getQuickActions(BuildContext context) {
|
||||
final actions = [
|
||||
QuickAction(
|
||||
title: 'Nouveau membre',
|
||||
description: 'Ajouter un membre',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.primaryColor,
|
||||
onTap: () => _showAction(context, 'Nouveau membre'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Créer événement',
|
||||
description: 'Organiser un événement',
|
||||
icon: Icons.event_available,
|
||||
color: AppTheme.secondaryColor,
|
||||
onTap: () => _showAction(context, 'Créer événement'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Suivi cotisations',
|
||||
description: 'Gérer les cotisations',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.accentColor,
|
||||
onTap: () => _showAction(context, 'Suivi cotisations'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Rapports',
|
||||
description: 'Générer des rapports',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.infoColor,
|
||||
onTap: () => _showAction(context, 'Rapports'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Messages',
|
||||
description: 'Envoyer des notifications',
|
||||
icon: Icons.message,
|
||||
color: AppTheme.warningColor,
|
||||
onTap: () => _showAction(context, 'Messages'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Documents',
|
||||
description: 'Gérer les documents',
|
||||
icon: Icons.folder,
|
||||
color: Color(0xFF9C27B0),
|
||||
onTap: () => _showAction(context, 'Documents'),
|
||||
),
|
||||
];
|
||||
|
||||
return actions.map((action) => _buildActionCard(action)).toList();
|
||||
}
|
||||
|
||||
Widget _buildActionCard(QuickAction action) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: action.onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: action.color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
width: ResponsiveUtils.iconSize(12),
|
||||
height: ResponsiveUtils.iconSize(12),
|
||||
decoration: BoxDecoration(
|
||||
color: action.color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(6)),
|
||||
),
|
||||
child: Icon(
|
||||
action.icon,
|
||||
color: action.color,
|
||||
size: ResponsiveUtils.iconSize(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.hp),
|
||||
Flexible(
|
||||
child: Text(
|
||||
action.title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.5.fs,
|
||||
medium: 3.2.fs,
|
||||
large: 3.fs,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.5.hp),
|
||||
Flexible(
|
||||
child: Text(
|
||||
action.description,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 2.8.fs,
|
||||
medium: 2.6.fs,
|
||||
large: 2.4.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAction(BuildContext context, String actionName) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$actionName - En cours de développement'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickAction {
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
QuickAction({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user