Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de carte d'action rapide réutilisable
///
/// Affiche une action cliquable avec:
/// - Icône colorée dans un conteneur arrondi
/// - Titre principal
/// - Sous-titre descriptif
/// - Interaction tactile avec feedback visuel
/// - Callback personnalisable pour l'action
class ActionCardWidget extends StatelessWidget {
/// Titre de l'action
final String title;
/// Description de l'action
final String subtitle;
/// Icône représentative
final IconData icon;
/// Couleur thématique de l'action
final Color color;
/// Callback exécuté lors du tap
final VoidCallback? onTap;
const ActionCardWidget({
super.key,
required this.title,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: 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(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 8,
offset: const Offset(0, 1),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 18,
),
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View File

@@ -1,165 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'action_card_widget.dart';
/// Widget de section des actions rapides et de gestion
///
/// Affiche une grille d'actions rapides organisées par catégories:
/// - Actions principales (nouveau membre, créer événement)
/// - Gestion financière (encaisser cotisation, relances)
/// - Communication (messages, convocations)
/// - Rapports et conformité (OHADA, exports)
/// - Urgences et support (alertes, assistance)
class QuickActionsWidget extends StatelessWidget {
const QuickActionsWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions rapides & Gestion',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
// Grille compacte 3x4 - Actions organisées par priorité
// Première ligne - Actions principales (3 colonnes)
Row(
children: [
Expanded(
child: ActionCardWidget(
title: 'Nouveau membre',
subtitle: 'Inscription',
icon: Icons.person_add,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Créer événement',
subtitle: 'Organiser',
icon: Icons.event_available,
color: AppTheme.secondaryColor,
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Encaisser',
subtitle: 'Cotisation',
icon: Icons.payment,
color: AppTheme.successColor,
),
),
],
),
const SizedBox(height: 8),
// Deuxième ligne - Gestion et communication
Row(
children: [
Expanded(
child: ActionCardWidget(
title: 'Relances',
subtitle: 'SMS/Email',
icon: Icons.notifications_active,
color: AppTheme.warningColor,
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Message groupe',
subtitle: 'WhatsApp',
icon: Icons.message,
color: const Color(0xFF25D366),
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Convoquer AG',
subtitle: 'Assemblée',
icon: Icons.groups,
color: const Color(0xFF9C27B0),
),
),
],
),
const SizedBox(height: 8),
// Troisième ligne - Rapports et conformité
Row(
children: [
Expanded(
child: ActionCardWidget(
title: 'Rapport OHADA',
subtitle: 'Conformité',
icon: Icons.gavel,
color: const Color(0xFF795548),
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Export données',
subtitle: 'Excel/PDF',
icon: Icons.file_download,
color: AppTheme.infoColor,
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Statistiques',
subtitle: 'Analyses',
icon: Icons.analytics,
color: const Color(0xFF00BCD4),
),
),
],
),
const SizedBox(height: 8),
// Quatrième ligne - Support et urgences
Row(
children: [
Expanded(
child: ActionCardWidget(
title: 'Alerte urgente',
subtitle: 'Critique',
icon: Icons.emergency,
color: AppTheme.errorColor,
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Support tech',
subtitle: 'Assistance',
icon: Icons.support_agent,
color: const Color(0xFF607D8B),
),
),
const SizedBox(width: 8),
Expanded(
child: ActionCardWidget(
title: 'Paramètres',
subtitle: 'Configuration',
icon: Icons.settings,
color: const Color(0xFF9E9E9E),
),
),
],
),
],
);
}
}

View File

@@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget d'élément d'activité récente réutilisable
///
/// Affiche une activité avec:
/// - Icône colorée avec indicateur "nouveau" optionnel
/// - Titre et description
/// - Horodatage avec mise en évidence pour les nouveaux éléments
/// - Badge "NOUVEAU" pour les activités récentes
/// - Indicateur visuel pour les nouvelles activités
class ActivityItemWidget extends StatelessWidget {
/// Titre de l'activité
final String title;
/// Description détaillée de l'activité
final String description;
/// Icône représentative
final IconData icon;
/// Couleur thématique
final Color color;
/// Horodatage de l'activité
final String time;
/// Indique si l'activité est nouvelle
final bool isNew;
const ActivityItemWidget({
super.key,
required this.title,
required this.description,
required this.icon,
required this.color,
required this.time,
this.isNew = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Stack(
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,
),
),
if (isNew)
Positioned(
top: -2,
right: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppTheme.errorColor,
shape: BoxShape.circle,
),
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: isNew ? FontWeight.w700 : FontWeight.w600,
color: isNew ? AppTheme.textPrimary : AppTheme.textPrimary,
),
),
),
if (isNew)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.errorColor,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'NOUVEAU',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 12,
color: isNew ? AppTheme.textPrimary : AppTheme.textSecondary,
fontWeight: isNew ? FontWeight.w500 : FontWeight.normal,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
time,
style: TextStyle(
fontSize: 12,
color: isNew ? AppTheme.primaryColor : AppTheme.textHint,
fontWeight: isNew ? FontWeight.w600 : FontWeight.normal,
),
),
if (isNew)
const SizedBox(height: 2),
if (isNew)
const Icon(
Icons.fiber_new,
size: 12,
color: AppTheme.errorColor,
),
],
),
],
),
);
}
}

View File

@@ -1,162 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'activity_item_widget.dart';
/// Widget de section des activités récentes en temps réel
///
/// Affiche un flux d'activités en temps réel avec:
/// - En-tête avec indicateur "Live" et bouton "Tout voir"
/// - Liste d'activités avec indicateurs visuels pour les nouveaux éléments
/// - Séparateurs entre les éléments
/// - Horodatage précis pour chaque activité
/// - Icônes et couleurs thématiques par type d'activité
class RecentActivitiesWidget extends StatelessWidget {
const RecentActivitiesWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Flux d\'activités en temps réel',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 4,
height: 4,
decoration: const BoxDecoration(
color: AppTheme.successColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 3),
const Text(
'Live',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: AppTheme.successColor,
),
),
],
),
),
const SizedBox(width: 6),
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'Tout',
style: TextStyle(fontSize: 12),
),
),
],
),
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: const Column(
children: [
ActivityItemWidget(
title: 'Paiement Mobile Money reçu',
description: 'Kouassi Yao - 25,000 FCFA via Orange Money',
icon: Icons.phone_android,
color: Color(0xFFFF9800),
time: 'Il y a 3 min',
isNew: true,
),
Divider(height: 1),
ActivityItemWidget(
title: 'Nouveau membre validé',
description: 'Adjoua Marie inscrite depuis Abidjan',
icon: Icons.person_add,
color: AppTheme.successColor,
time: 'Il y a 15 min',
isNew: true,
),
Divider(height: 1),
ActivityItemWidget(
title: 'Relance automatique envoyée',
description: '12 SMS de rappel cotisations expédiés',
icon: Icons.sms,
color: AppTheme.infoColor,
time: 'Il y a 1h',
),
Divider(height: 1),
ActivityItemWidget(
title: 'Rapport OHADA généré',
description: 'Bilan financier T4 2024 exporté',
icon: Icons.description,
color: Color(0xFF795548),
time: 'Il y a 2h',
),
Divider(height: 1),
ActivityItemWidget(
title: 'Événement: Forte participation',
description: 'AG Extraordinaire - 89% de présence',
icon: Icons.trending_up,
color: AppTheme.successColor,
time: 'Il y a 3h',
),
Divider(height: 1),
ActivityItemWidget(
title: 'Alerte: Cotisations en retard',
description: '23 membres avec +30 jours de retard',
icon: Icons.warning,
color: AppTheme.warningColor,
time: 'Il y a 4h',
),
Divider(height: 1),
ActivityItemWidget(
title: 'Synchronisation réussie',
description: 'Données sauvegardées sur le cloud',
icon: Icons.cloud_done,
color: AppTheme.successColor,
time: 'Il y a 6h',
),
Divider(height: 1),
ActivityItemWidget(
title: 'Message diffusé',
description: 'Info COVID-19 envoyée à 1,247 membres',
icon: Icons.campaign,
color: Color(0xFF9C27B0),
time: 'Hier 18:30',
),
],
),
),
],
);
}
}

View File

@@ -1,218 +0,0 @@
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,
});
}

View File

@@ -1,335 +0,0 @@
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),
),
),
],
);
}
}

View File

@@ -1,252 +0,0 @@
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();
}
}

View File

@@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget d'en-tête de section réutilisable
///
/// Affiche un titre de section avec style cohérent
/// utilisé dans toutes les sections du dashboard.
class SectionHeaderWidget extends StatelessWidget {
/// Titre de la section
final String title;
/// Style de texte personnalisé (optionnel)
final TextStyle? textStyle;
const SectionHeaderWidget({
super.key,
required this.title,
this.textStyle,
});
@override
Widget build(BuildContext context) {
return Text(
title,
style: textStyle ?? Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
);
}
}

View File

@@ -0,0 +1,98 @@
/// Widget de tuile d'activité individuelle
/// Affiche une activité récente avec icône, titre et timestamp
library dashboard_activity_tile;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une activité récente
class DashboardActivity {
/// Titre principal de l'activité
final String title;
/// Description détaillée de l'activité
final String subtitle;
/// Icône représentative de l'activité
final IconData icon;
/// Couleur thématique de l'activité
final Color color;
/// Timestamp de l'activité
final String time;
/// Callback optionnel lors du tap sur l'activité
final VoidCallback? onTap;
/// Constructeur du modèle d'activité
const DashboardActivity({
required this.title,
required this.subtitle,
required this.icon,
required this.color,
required this.time,
this.onTap,
});
}
/// Widget de tuile d'activité
///
/// Affiche une activité récente avec :
/// - Avatar coloré avec icône thématique
/// - Titre et description de l'activité
/// - Timestamp relatif
/// - Design compact et lisible
/// - Support du tap pour détails
class DashboardActivityTile extends StatelessWidget {
/// Données de l'activité à afficher
final DashboardActivity activity;
/// Constructeur de la tuile d'activité
const DashboardActivityTile({
super.key,
required this.activity,
});
@override
Widget build(BuildContext context) {
return ListTile(
onTap: activity.onTap,
contentPadding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
leading: CircleAvatar(
radius: 16,
backgroundColor: activity.color.withOpacity(0.1),
child: Icon(
activity.icon,
color: activity.color,
size: 16,
),
),
title: Text(
activity.title,
style: TypographyTokens.bodySmall.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
activity.subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
fontSize: 12,
),
),
trailing: Text(
activity.time,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
fontSize: 11,
),
),
);
}
}

View File

@@ -0,0 +1,191 @@
/// Widget de menu latéral (drawer) du dashboard
/// Navigation principale de l'application
library dashboard_drawer;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour un élément de menu
class DrawerMenuItem {
/// Icône de l'élément de menu
final IconData icon;
/// Titre de l'élément de menu
final String title;
/// Callback lors du tap sur l'élément
final VoidCallback? onTap;
/// Constructeur du modèle d'élément de menu
const DrawerMenuItem({
required this.icon,
required this.title,
this.onTap,
});
}
/// Widget de menu latéral
///
/// Affiche la navigation principale avec :
/// - Header avec profil utilisateur
/// - Menu de navigation structuré
/// - Actions secondaires
/// - Design Material avec gradient
class DashboardDrawer extends StatelessWidget {
/// Callback pour les actions de navigation
final Function(String route)? onNavigate;
/// Callback pour la déconnexion
final VoidCallback? onLogout;
/// Constructeur du menu latéral
const DashboardDrawer({
super.key,
this.onNavigate,
this.onLogout,
});
/// Génère la liste des éléments de menu principaux
List<DrawerMenuItem> _getMainMenuItems() {
return [
DrawerMenuItem(
icon: Icons.dashboard,
title: 'Dashboard',
onTap: () => onNavigate?.call('/dashboard'),
),
DrawerMenuItem(
icon: Icons.people,
title: 'Membres',
onTap: () => onNavigate?.call('/members'),
),
DrawerMenuItem(
icon: Icons.account_balance_wallet,
title: 'Cotisations',
onTap: () => onNavigate?.call('/cotisations'),
),
DrawerMenuItem(
icon: Icons.event,
title: 'Événements',
onTap: () => onNavigate?.call('/events'),
),
DrawerMenuItem(
icon: Icons.favorite,
title: 'Solidarité',
onTap: () => onNavigate?.call('/solidarity'),
),
];
}
/// Génère la liste des éléments de menu secondaires
List<DrawerMenuItem> _getSecondaryMenuItems() {
return [
DrawerMenuItem(
icon: Icons.analytics,
title: 'Rapports',
onTap: () => onNavigate?.call('/reports'),
),
DrawerMenuItem(
icon: Icons.settings,
title: 'Paramètres',
onTap: () => onNavigate?.call('/settings'),
),
DrawerMenuItem(
icon: Icons.help,
title: 'Aide',
onTap: () => onNavigate?.call('/help'),
),
];
}
@override
Widget build(BuildContext context) {
final mainItems = _getMainMenuItems();
final secondaryItems = _getSecondaryMenuItems();
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildDrawerHeader(),
...mainItems.map((item) => _buildMenuItem(item)),
const Divider(),
...secondaryItems.map((item) => _buildMenuItem(item)),
const Divider(),
_buildLogoutItem(),
],
),
);
}
/// Construit l'en-tête du drawer avec profil utilisateur
Widget _buildDrawerHeader() {
return DrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [ColorTokens.primary, ColorTokens.secondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
size: 35,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.md),
Text(
'Utilisateur UnionFlow',
style: TypographyTokens.titleMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
Text(
'admin@unionflow.dev',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
),
);
}
/// Construit un élément de menu
Widget _buildMenuItem(DrawerMenuItem item) {
return ListTile(
leading: Icon(item.icon),
title: Text(
item.title,
style: TypographyTokens.bodyMedium,
),
onTap: item.onTap,
);
}
/// Construit l'élément de déconnexion
Widget _buildLogoutItem() {
return ListTile(
leading: const Icon(
Icons.logout,
color: ColorTokens.error,
),
title: Text(
'Déconnexion',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.error,
),
),
onTap: onLogout,
);
}
}

View File

@@ -0,0 +1,104 @@
/// Widget de section d'insights du dashboard
/// Affiche les métriques de performance dans une carte
library dashboard_insights_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_metric_row.dart';
/// Widget de section d'insights
///
/// Affiche les métriques de performance :
/// - Taux de cotisation
/// - Participation aux événements
/// - Demandes traitées
///
/// Chaque métrique peut être tapée pour plus de détails
class DashboardInsightsSection extends StatelessWidget {
/// Callback pour les actions sur les métriques
final Function(String metricType)? onMetricTap;
/// Liste des métriques à afficher
final List<DashboardMetric>? metrics;
/// Constructeur de la section d'insights
const DashboardInsightsSection({
super.key,
this.onMetricTap,
this.metrics,
});
/// Génère la liste des métriques par défaut
List<DashboardMetric> _getDefaultMetrics() {
return [
DashboardMetric(
label: 'Taux de cotisation',
value: '85%',
progress: 0.85,
color: ColorTokens.success,
onTap: () => onMetricTap?.call('cotisation_rate'),
),
DashboardMetric(
label: 'Participation événements',
value: '72%',
progress: 0.72,
color: ColorTokens.primary,
onTap: () => onMetricTap?.call('event_participation'),
),
DashboardMetric(
label: 'Demandes traitées',
value: '95%',
progress: 0.95,
color: ColorTokens.tertiary,
onTap: () => onMetricTap?.call('requests_processed'),
),
];
}
@override
Widget build(BuildContext context) {
final metricsToShow = metrics ?? _getDefaultMetrics();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Insights',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance ce mois-ci',
style: TypographyTokens.titleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.md),
...metricsToShow.map((metric) {
final isLast = metric == metricsToShow.last;
return Column(
children: [
DashboardMetricRow(metric: metric),
if (!isLast) const SizedBox(height: SpacingTokens.sm),
],
);
}).toList(),
],
),
),
),
],
);
}
}

View File

@@ -0,0 +1,94 @@
/// Widget de ligne de métrique avec barre de progression
/// Affiche une métrique avec label, valeur et indicateur visuel
library dashboard_metric_row;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une métrique
class DashboardMetric {
/// Label descriptif de la métrique
final String label;
/// Valeur formatée à afficher
final String value;
/// Progression entre 0.0 et 1.0
final double progress;
/// Couleur thématique de la métrique
final Color color;
/// Callback optionnel lors du tap sur la métrique
final VoidCallback? onTap;
/// Constructeur du modèle de métrique
const DashboardMetric({
required this.label,
required this.value,
required this.progress,
required this.color,
this.onTap,
});
}
/// Widget de ligne de métrique
///
/// Affiche une métrique avec :
/// - Label et valeur alignés horizontalement
/// - Barre de progression colorée
/// - Design compact et lisible
/// - Support du tap pour détails
class DashboardMetricRow extends StatelessWidget {
/// Données de la métrique à afficher
final DashboardMetric metric;
/// Constructeur de la ligne de métrique
const DashboardMetricRow({
super.key,
required this.metric,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: metric.onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xs),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
metric.label,
style: TypographyTokens.bodySmall.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
metric.value,
style: TypographyTokens.labelLarge.copyWith(
fontWeight: FontWeight.w600,
color: metric.color,
),
),
],
),
const SizedBox(height: SpacingTokens.xs),
LinearProgressIndicator(
value: metric.progress,
backgroundColor: metric.color.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(metric.color),
minHeight: 4,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,102 @@
/// Widget de bouton d'action rapide individuel
/// Bouton stylisé pour les actions principales du dashboard
library dashboard_quick_action_button;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une action rapide
class DashboardQuickAction {
/// Icône représentative de l'action
final IconData icon;
/// Titre de l'action
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Couleur thématique du bouton
final Color color;
/// Callback lors du tap sur le bouton
final VoidCallback? onTap;
/// Constructeur du modèle d'action rapide
const DashboardQuickAction({
required this.icon,
required this.title,
this.subtitle,
required this.color,
this.onTap,
});
}
/// Widget de bouton d'action rapide
///
/// Affiche un bouton stylisé avec :
/// - Icône thématique
/// - Titre descriptif
/// - Couleur de fond subtile
/// - Design Material avec bordures arrondies
/// - Support du tap pour actions
class DashboardQuickActionButton extends StatelessWidget {
/// Données de l'action à afficher
final DashboardQuickAction action;
/// Constructeur du bouton d'action rapide
const DashboardQuickActionButton({
super.key,
required this.action,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: action.onTap,
style: ElevatedButton.styleFrom(
backgroundColor: action.color.withOpacity(0.1),
foregroundColor: action.color,
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
action.icon,
size: 18,
),
const SizedBox(height: 4),
Text(
action.title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
if (action.subtitle != null) ...[
const SizedBox(height: 2),
Text(
action.subtitle!,
style: TextStyle(
fontSize: 10,
color: action.color.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}

View File

@@ -0,0 +1,95 @@
/// Widget de grille d'actions rapides du dashboard
/// Affiche les actions principales dans une grille responsive
library dashboard_quick_actions_grid;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_quick_action_button.dart';
/// Widget de grille d'actions rapides
///
/// Affiche les actions principales dans une grille 2x2 :
/// - Ajouter un membre
/// - Enregistrer une cotisation
/// - Créer un événement
/// - Demande de solidarité
///
/// Chaque bouton déclenche une action spécifique
class DashboardQuickActionsGrid extends StatelessWidget {
/// Callback pour les actions rapides
final Function(String actionType)? onActionTap;
/// Liste des actions à afficher
final List<DashboardQuickAction>? actions;
/// Constructeur de la grille d'actions rapides
const DashboardQuickActionsGrid({
super.key,
this.onActionTap,
this.actions,
});
/// Génère la liste des actions rapides par défaut
List<DashboardQuickAction> _getDefaultActions() {
return [
DashboardQuickAction(
icon: Icons.person_add,
title: 'Ajouter Membre',
color: ColorTokens.primary,
onTap: () => onActionTap?.call('add_member'),
),
DashboardQuickAction(
icon: Icons.payment,
title: 'Cotisation',
color: ColorTokens.success,
onTap: () => onActionTap?.call('add_cotisation'),
),
DashboardQuickAction(
icon: Icons.event_note,
title: 'Événement',
color: ColorTokens.tertiary,
onTap: () => onActionTap?.call('create_event'),
),
DashboardQuickAction(
icon: Icons.volunteer_activism,
title: 'Solidarité',
color: ColorTokens.error,
onTap: () => onActionTap?.call('solidarity_request'),
),
];
}
@override
Widget build(BuildContext context) {
final actionsToShow = actions ?? _getDefaultActions();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions rapides',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
childAspectRatio: 2.2,
),
itemCount: actionsToShow.length,
itemBuilder: (context, index) {
return DashboardQuickActionButton(action: actionsToShow[index]);
},
),
],
);
}
}

View File

@@ -0,0 +1,98 @@
/// Widget de section d'activité récente du dashboard
/// Affiche les dernières activités dans une liste compacte
library dashboard_recent_activity_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_activity_tile.dart';
/// Widget de section d'activité récente
///
/// Affiche les dernières activités de l'union :
/// - Nouveaux membres
/// - Cotisations reçues
/// - Événements créés
/// - Demandes de solidarité
///
/// Chaque activité peut être tapée pour plus de détails
class DashboardRecentActivitySection extends StatelessWidget {
/// Callback pour les actions sur les activités
final Function(String activityId)? onActivityTap;
/// Liste des activités à afficher
final List<DashboardActivity>? activities;
/// Constructeur de la section d'activité récente
const DashboardRecentActivitySection({
super.key,
this.onActivityTap,
this.activities,
});
/// Génère la liste des activités récentes par défaut
List<DashboardActivity> _getDefaultActivities() {
return [
DashboardActivity(
title: 'Nouveau membre ajouté',
subtitle: 'Marie Dupont a rejoint l\'union',
icon: Icons.person_add,
color: ColorTokens.primary,
time: 'Il y a 2h',
onTap: () => onActivityTap?.call('member_added_001'),
),
DashboardActivity(
title: 'Cotisation reçue',
subtitle: 'Paiement de 50€ de Jean Martin',
icon: Icons.payment,
color: ColorTokens.success,
time: 'Il y a 4h',
onTap: () => onActivityTap?.call('cotisation_002'),
),
DashboardActivity(
title: 'Événement créé',
subtitle: 'Assemblée générale programmée',
icon: Icons.event,
color: ColorTokens.tertiary,
time: 'Hier',
onTap: () => onActivityTap?.call('event_003'),
),
];
}
@override
Widget build(BuildContext context) {
final activitiesToShow = activities ?? _getDefaultActivities();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activité récente',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
child: Column(
children: activitiesToShow.map((activity) {
final isLast = activity == activitiesToShow.last;
return Column(
children: [
DashboardActivityTile(activity: activity),
if (!isLast) const Divider(height: 1),
],
);
}).toList(),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,94 @@
/// Widget de carte de statistique individuelle
/// Affiche une métrique avec icône, valeur et titre
library dashboard_stats_card;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une statistique
class DashboardStat {
/// Icône représentative de la statistique
final IconData icon;
/// Valeur numérique à afficher
final String value;
/// Titre descriptif de la statistique
final String title;
/// Couleur thématique de la carte
final Color color;
/// Callback optionnel lors du tap sur la carte
final VoidCallback? onTap;
/// Constructeur du modèle de statistique
const DashboardStat({
required this.icon,
required this.value,
required this.title,
required this.color,
this.onTap,
});
}
/// Widget de carte de statistique
///
/// Affiche une métrique individuelle avec :
/// - Icône colorée thématique
/// - Valeur numérique mise en évidence
/// - Titre descriptif
/// - Design Material avec élévation subtile
/// - Support du tap pour navigation
class DashboardStatsCard extends StatelessWidget {
/// Données de la statistique à afficher
final DashboardStat stat;
/// Constructeur de la carte de statistique
const DashboardStatsCard({
super.key,
required this.stat,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 1,
child: InkWell(
onTap: stat.onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
stat.icon,
size: 28,
color: stat.color,
),
const SizedBox(height: SpacingTokens.sm),
Text(
stat.value,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
color: stat.color,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
stat.title,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,99 @@
/// Widget de grille de statistiques du dashboard
/// Affiche les métriques principales dans une grille responsive
library dashboard_stats_grid;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_stats_card.dart';
/// Widget de grille de statistiques
///
/// Affiche les statistiques principales dans une grille 2x2 :
/// - Membres actifs
/// - Cotisations du mois
/// - Événements programmés
/// - Demandes de solidarité
///
/// Chaque carte est interactive et peut déclencher une navigation
class DashboardStatsGrid extends StatelessWidget {
/// Callback pour les actions sur les statistiques
final Function(String statType)? onStatTap;
/// Liste des statistiques à afficher
final List<DashboardStat>? stats;
/// Constructeur de la grille de statistiques
const DashboardStatsGrid({
super.key,
this.onStatTap,
this.stats,
});
/// Génère la liste des statistiques par défaut
List<DashboardStat> _getDefaultStats() {
return [
DashboardStat(
icon: Icons.people,
value: '25',
title: 'Membres',
color: ColorTokens.primary,
onTap: () => onStatTap?.call('members'),
),
DashboardStat(
icon: Icons.account_balance_wallet,
value: '15',
title: 'Cotisations',
color: ColorTokens.success,
onTap: () => onStatTap?.call('cotisations'),
),
DashboardStat(
icon: Icons.event,
value: '8',
title: 'Événements',
color: ColorTokens.tertiary,
onTap: () => onStatTap?.call('events'),
),
DashboardStat(
icon: Icons.favorite,
value: '3',
title: 'Solidarité',
color: ColorTokens.error,
onTap: () => onStatTap?.call('solidarity'),
),
];
}
@override
Widget build(BuildContext context) {
final statsToShow = stats ?? _getDefaultStats();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistiques',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
childAspectRatio: 1.4,
),
itemCount: statsToShow.length,
itemBuilder: (context, index) {
return DashboardStatsCard(stat: statsToShow[index]);
},
),
],
);
}
}

View File

@@ -0,0 +1,70 @@
/// Widget de section de bienvenue du dashboard
/// Affiche un message d'accueil avec gradient et design moderne
library dashboard_welcome_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Widget de section de bienvenue
///
/// Affiche un message d'accueil personnalisé avec :
/// - Gradient de fond élégant
/// - Typographie hiérarchisée
/// - Design responsive et moderne
class DashboardWelcomeSection extends StatelessWidget {
/// Titre principal de la section
final String title;
/// Sous-titre descriptif
final String subtitle;
/// Constructeur du widget de bienvenue
const DashboardWelcomeSection({
super.key,
this.title = 'Bienvenue sur UnionFlow',
this.subtitle = 'Votre plateforme de gestion d\'union familiale',
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorTokens.primary.withOpacity(0.1),
ColorTokens.secondary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(
color: ColorTokens.outline.withOpacity(0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle,
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
);
}
}

View File

@@ -1,289 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de carte KPI réutilisable avec détails enrichis
///
/// Affiche un indicateur de performance clé avec:
/// - Icône et badge de tendance coloré
/// - Valeur principale avec objectif optionnel
/// - Titre avec période
/// - Description détaillée
/// - Points de détail sous forme de puces
/// - Horodatage de dernière mise à jour
class KPICardWidget extends StatelessWidget {
/// Titre de l'indicateur
final String title;
/// Valeur principale affichée
final String value;
/// Changement/tendance (ex: "+5.2%", "-3.1%")
final String change;
/// Icône représentative
final IconData icon;
/// Couleur thématique de la carte
final Color color;
/// Description détaillée optionnelle
final String? subtitle;
/// Période de référence (ex: "30j", "Mois")
final String? period;
/// Objectif cible optionnel
final String? target;
/// Horodatage de dernière mise à jour
final String? lastUpdate;
/// Liste de détails supplémentaires (max 3)
final List<String>? details;
const KPICardWidget({
super.key,
required this.title,
required this.value,
required this.change,
required this.icon,
required this.color,
this.subtitle,
this.period,
this.target,
this.lastUpdate,
this.details,
});
@override
Widget build(BuildContext context) {
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: [
// En-tête avec icône et badge de tendance
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: _getChangeColor(change).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getChangeIcon(change),
color: _getChangeColor(change),
size: 12,
),
const SizedBox(width: 4),
Text(
change,
style: TextStyle(
color: _getChangeColor(change),
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Valeur principale
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
if (target != null)
Text(
'/ $target',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
// Titre et période
Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
if (period != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
period!,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: color,
),
),
),
],
),
// Description détaillée
if (subtitle != null) ...[
const SizedBox(height: 6),
Text(
subtitle!,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
height: 1.3,
),
),
],
// Détails supplémentaires sous forme de puces
if (details != null && details!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: color.withOpacity(0.1),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: details!.take(3).map((detail) => Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(top: 4),
width: 4,
height: 4,
decoration: BoxDecoration(
color: color.withOpacity(0.6),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
detail,
style: TextStyle(
fontSize: 10,
color: AppTheme.textSecondary.withOpacity(0.8),
height: 1.2,
),
),
),
],
),
)).toList(),
),
),
],
// Dernière mise à jour
if (lastUpdate != null) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.access_time,
size: 10,
color: AppTheme.textSecondary.withOpacity(0.5),
),
const SizedBox(width: 4),
Text(
'Mis à jour: $lastUpdate',
style: TextStyle(
fontSize: 9,
color: AppTheme.textSecondary.withOpacity(0.5),
fontStyle: FontStyle.italic,
),
),
],
),
],
],
),
);
}
/// Détermine la couleur du badge de changement selon la valeur
Color _getChangeColor(String change) {
if (change.startsWith('+')) {
return AppTheme.successColor;
} else if (change.startsWith('-')) {
return AppTheme.errorColor;
} else {
return AppTheme.textSecondary;
}
}
/// Détermine l'icône du badge de changement selon la valeur
IconData _getChangeIcon(String change) {
if (change.startsWith('+')) {
return Icons.trending_up;
} else if (change.startsWith('-')) {
return Icons.trending_down;
} else {
return Icons.trending_flat;
}
}
}

View File

@@ -1,171 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'kpi_card_widget.dart';
/// Widget de section des cartes KPI principales
///
/// Affiche les 8 indicateurs clés de performance principaux
/// en une seule colonne pour optimiser l'utilisation de l'espace écran.
/// Chaque KPI contient des détails enrichis et des informations contextuelles.
class KPICardsWidget extends StatelessWidget {
const KPICardsWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Indicateurs clés de performance',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
// Indicateurs principaux - Une seule colonne pour exploiter toute la largeur
KPICardWidget(
title: 'Membres Actifs',
value: '1,247',
change: '+5.2%',
icon: Icons.people,
color: AppTheme.primaryColor,
subtitle: 'Base de cotisants actifs avec droits de vote et participation aux décisions',
period: '30j',
target: '1,300',
lastUpdate: 'il y a 2h',
details: const [
'892 membres à jour de cotisation (71.5%)',
'355 nouveaux membres cette année',
'23 membres en période d\'essai de 3 mois',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Revenus Totaux',
value: '2,845,000 FCFA',
change: '+12.8%',
icon: Icons.account_balance_wallet,
color: AppTheme.successColor,
subtitle: 'Ensemble des revenus générés incluant cotisations, événements et subventions',
period: 'Mois',
target: '3,200,000 FCFA',
lastUpdate: 'il y a 1h',
details: const [
'1,950,000 FCFA de cotisations mensuelles (68.5%)',
'645,000 FCFA d\'activités et événements (22.7%)',
'250,000 FCFA de dons et subventions (8.8%)',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Événements Actifs',
value: '23',
change: '+3',
icon: Icons.event,
color: AppTheme.accentColor,
subtitle: 'Événements planifiés, formations professionnelles et activités sociales',
period: 'Mois',
target: '25',
lastUpdate: 'il y a 3h',
details: const [
'8 formations professionnelles et techniques',
'9 événements sociaux et culturels',
'6 assemblées générales et réunions',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Taux de Participation',
value: '78.3%',
change: '+2.1%',
icon: Icons.groups,
color: const Color(0xFF2196F3), // Blue
subtitle: 'Pourcentage de membres participant activement aux événements et décisions',
period: 'Trim.',
target: '85%',
lastUpdate: 'il y a 4h',
details: const [
'158 membres en retard de paiement',
'45,000 FCFA de frais de relance économisés',
'Amélioration de 12% par rapport au trimestre précédent',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Nouveaux Membres (30j)',
value: '47',
change: '+18.5%',
icon: Icons.person_add,
color: const Color(0xFF9C27B0), // Purple
subtitle: 'Nouvelles adhésions validées par le comité d\'admission',
period: '30j',
target: '50',
lastUpdate: 'il y a 30min',
details: const [
'28 adhésions individuelles (59.6%)',
'12 adhésions familiales (25.5%)',
'7 adhésions d\'entreprises partenaires (14.9%)',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Montant en Attente',
value: '785,000 FCFA',
change: '-5.2%',
icon: Icons.schedule,
color: AppTheme.warningColor,
subtitle: 'Montants promis en attente d\'encaissement ou de validation administrative',
period: 'Total',
lastUpdate: 'il y a 1h',
details: const [
'450,000 FCFA de promesses de dons (57.3%)',
'235,000 FCFA de cotisations promises (29.9%)',
'100,000 FCFA de subventions en cours (12.8%)',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Cotisations en Retard',
value: '156',
change: '+8.3%',
icon: Icons.access_time,
color: AppTheme.errorColor,
subtitle: 'Membres en situation d\'impayé nécessitant un suivi personnalisé',
period: '+30j',
lastUpdate: 'il y a 2h',
details: const [
'89 retards de 1-3 mois (57.1%)',
'45 retards de 3-6 mois (28.8%)',
'22 retards de plus de 6 mois (14.1%)',
],
),
const SizedBox(height: 12),
KPICardWidget(
title: 'Score Global de Performance',
value: '85/100',
change: '+3 pts',
icon: Icons.assessment,
color: const Color(0xFF00BCD4), // Cyan
subtitle: 'Évaluation globale basée sur 15 indicateurs de santé organisationnelle',
period: 'Mois',
target: '90/100',
lastUpdate: 'il y a 6h',
details: const [
'Finances: 92/100 (Excellent)',
'Participation: 78/100 (Bon)',
'Gouvernance: 85/100 (Très bon)',
],
),
],
);
}
}

View File

@@ -1,116 +0,0 @@
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,
),
),
],
),
);
}
}

View File

@@ -1,281 +0,0 @@
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();
},
),
),
);
}
}
}

View File

@@ -1,214 +0,0 @@
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,
});
}

View File

@@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de section d'accueil personnalisé pour le dashboard
///
/// Affiche un message de bienvenue avec un gradient coloré et une icône.
/// Conçu pour donner une impression chaleureuse et professionnelle à l'utilisateur.
class WelcomeSectionWidget extends StatelessWidget {
/// Titre principal affiché (par défaut "Bonjour !")
final String title;
/// Sous-titre descriptif (par défaut "Voici un aperçu de votre association")
final String subtitle;
/// Icône affichée à droite (par défaut Icons.dashboard)
final IconData icon;
/// Couleurs du gradient (par défaut primaryColor vers primaryLight)
final List<Color>? gradientColors;
const WelcomeSectionWidget({
super.key,
this.title = 'Bonjour !',
this.subtitle = 'Voici un aperçu de votre association',
this.icon = Icons.dashboard,
this.gradientColors,
});
@override
Widget build(BuildContext context) {
final colors = gradientColors ?? [AppTheme.primaryColor, AppTheme.primaryLight];
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: colors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
subtitle,
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: Icon(
icon,
color: Colors.white,
size: 30,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,17 @@
/// Fichier d'index pour tous les widgets du dashboard
/// Facilite les imports et maintient une API propre
library dashboard_widgets;
// === WIDGETS DE SECTION ===
export 'dashboard_welcome_section.dart';
export 'dashboard_stats_grid.dart';
export 'dashboard_quick_actions_grid.dart';
export 'dashboard_recent_activity_section.dart';
export 'dashboard_insights_section.dart';
export 'dashboard_drawer.dart';
// === WIDGETS ATOMIQUES ===
export 'dashboard_stats_card.dart';
export 'dashboard_quick_action_button.dart';
export 'dashboard_activity_tile.dart';
export 'dashboard_metric_row.dart';