Authentification stable - WIP
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)',
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user