first commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class ActivityFeed extends StatelessWidget {
|
||||
const ActivityFeed({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Activités récentes',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Voir tout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
..._getActivities().map((activity) => _buildActivityItem(activity)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityItem(ActivityItem activity) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppTheme.borderColor, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: activity.color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
activity.icon,
|
||||
color: activity.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
activity.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 14,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatTime(activity.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (activity.actionRequired)
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.errorColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<ActivityItem> _getActivities() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
ActivityItem(
|
||||
title: 'Nouveau membre inscrit',
|
||||
description: 'Marie Dupont a rejoint l\'association',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.successColor,
|
||||
timestamp: now.subtract(const Duration(hours: 2)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Cotisation en retard',
|
||||
description: 'Pierre Martin - Cotisation échue depuis 5 jours',
|
||||
icon: Icons.warning,
|
||||
color: AppTheme.warningColor,
|
||||
timestamp: now.subtract(const Duration(hours: 4)),
|
||||
actionRequired: true,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Paiement reçu',
|
||||
description: 'Jean Dubois - Cotisation annuelle 2024',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.primaryColor,
|
||||
timestamp: now.subtract(const Duration(hours: 6)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Événement créé',
|
||||
description: 'Assemblée générale 2024 - 15 mars 2024',
|
||||
icon: Icons.event,
|
||||
color: AppTheme.accentColor,
|
||||
timestamp: now.subtract(const Duration(days: 1)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Mise à jour profil',
|
||||
description: 'Sophie Bernard a modifié ses informations',
|
||||
icon: Icons.edit,
|
||||
color: AppTheme.infoColor,
|
||||
timestamp: now.subtract(const Duration(days: 1, hours: 3)),
|
||||
actionRequired: false,
|
||||
),
|
||||
ActivityItem(
|
||||
title: 'Nouveau document',
|
||||
description: 'Procès-verbal ajouté aux archives',
|
||||
icon: Icons.file_upload,
|
||||
color: AppTheme.secondaryColor,
|
||||
timestamp: now.subtract(const Duration(days: 2)),
|
||||
actionRequired: false,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
String _formatTime(DateTime timestamp) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(timestamp);
|
||||
|
||||
if (difference.inMinutes < 60) {
|
||||
return 'Il y a ${difference.inMinutes} min';
|
||||
} else if (difference.inHours < 24) {
|
||||
return 'Il y a ${difference.inHours}h';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Hier';
|
||||
} else if (difference.inDays < 7) {
|
||||
return 'Il y a ${difference.inDays} jours';
|
||||
} else {
|
||||
return DateFormat('dd/MM/yyyy').format(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityItem {
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final DateTime timestamp;
|
||||
final bool actionRequired;
|
||||
|
||||
ActivityItem({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.timestamp,
|
||||
this.actionRequired = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class ChartCard extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget chart;
|
||||
final String? subtitle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ChartCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.chart,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: chart,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MembershipChart extends StatelessWidget {
|
||||
const MembershipChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 200,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: AppTheme.borderColor.withOpacity(0.5),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 200,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'];
|
||||
if (value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: 5,
|
||||
minY: 800,
|
||||
maxY: 1400,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 1000),
|
||||
FlSpot(1, 1050),
|
||||
FlSpot(2, 1100),
|
||||
FlSpot(3, 1180),
|
||||
FlSpot(4, 1220),
|
||||
FlSpot(5, 1247),
|
||||
],
|
||||
color: AppTheme.primaryColor,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: AppTheme.primaryColor,
|
||||
strokeWidth: 2,
|
||||
strokeColor: Colors.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppTheme.primaryColor.withOpacity(0.3),
|
||||
AppTheme.primaryColor.withOpacity(0.1),
|
||||
AppTheme.primaryColor.withOpacity(0.0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryChart extends StatelessWidget {
|
||||
const CategoryChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 4,
|
||||
centerSpaceRadius: 50,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: AppTheme.primaryColor,
|
||||
value: 45,
|
||||
title: 'Actifs\n45%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.secondaryColor,
|
||||
value: 30,
|
||||
title: 'Retraités\n30%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: AppTheme.accentColor,
|
||||
value: 25,
|
||||
title: 'Étudiants\n25%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RevenueChart extends StatelessWidget {
|
||||
const RevenueChart({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: 15000,
|
||||
barTouchData: BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 5000,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toInt()}k€',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const months = ['J', 'F', 'M', 'A', 'M', 'J'];
|
||||
if (value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 5000,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: AppTheme.borderColor.withOpacity(0.5),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
barGroups: [
|
||||
_buildBarGroup(0, 8000, AppTheme.primaryColor),
|
||||
_buildBarGroup(1, 9500, AppTheme.primaryColor),
|
||||
_buildBarGroup(2, 7800, AppTheme.primaryColor),
|
||||
_buildBarGroup(3, 11200, AppTheme.primaryColor),
|
||||
_buildBarGroup(4, 13500, AppTheme.primaryColor),
|
||||
_buildBarGroup(5, 12800, AppTheme.primaryColor),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BarChartGroupData _buildBarGroup(int x, double y, Color color) {
|
||||
return BarChartGroupData(
|
||||
x: x,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: y,
|
||||
color: color,
|
||||
width: 16,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class ClickableKPICard extends StatefulWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final String change;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isPositiveChange;
|
||||
final VoidCallback? onTap;
|
||||
final String? actionText;
|
||||
|
||||
const ClickableKPICard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.change,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.isPositiveChange = true,
|
||||
this.onTap,
|
||||
this.actionText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ClickableKPICard> createState() => _ClickableKPICardState();
|
||||
}
|
||||
|
||||
class _ClickableKPICardState extends State<ClickableKPICard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialiser ResponsiveUtils
|
||||
ResponsiveUtils.init(context);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap != null ? _handleTap : null,
|
||||
onTapDown: widget.onTap != null ? (_) => _animationController.forward() : null,
|
||||
onTapUp: widget.onTap != null ? (_) => _animationController.reverse() : null,
|
||||
onTapCancel: widget.onTap != null ? () => _animationController.reverse() : null,
|
||||
borderRadius: ResponsiveUtils.borderRadius(4),
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingAll(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: ResponsiveUtils.borderRadius(4),
|
||||
border: widget.onTap != null
|
||||
? Border.all(
|
||||
color: widget.color.withOpacity(0.2),
|
||||
width: ResponsiveUtils.adaptive(
|
||||
small: 1,
|
||||
medium: 1.5,
|
||||
large: 2,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 3.5.sp,
|
||||
offset: Offset(0, 1.hp),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icône et indicateur de changement
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: ResponsiveUtils.paddingAll(2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(0.15),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2.5),
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: widget.color,
|
||||
size: ResponsiveUtils.iconSize(5),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.hp),
|
||||
// Valeur principale
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.value,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 4.5.fs,
|
||||
medium: 4.2.fs,
|
||||
large: 4.fs,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.5.hp),
|
||||
// Titre et action
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.fs,
|
||||
medium: 2.8.fs,
|
||||
large: 2.6.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.onTap != null) ...[
|
||||
SizedBox(width: 1.5.wp),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingSymmetric(
|
||||
horizontal: 1.5,
|
||||
vertical: 0.3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(0.1),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2.5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.actionText ?? 'Voir',
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 2.5.fs,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 0.5.wp),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: ResponsiveUtils.iconSize(2),
|
||||
color: widget.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChangeIndicator() {
|
||||
final changeColor = widget.isPositiveChange
|
||||
? AppTheme.successColor
|
||||
: AppTheme.errorColor;
|
||||
final changeIcon = widget.isPositiveChange
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: changeColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
changeIcon,
|
||||
size: 16,
|
||||
color: changeColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.change,
|
||||
style: TextStyle(
|
||||
color: changeColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
HapticFeedback.lightImpact();
|
||||
widget.onTap?.call();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
class KPICard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final String change;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isPositiveChange;
|
||||
|
||||
const KPICard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.change,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.isPositiveChange = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChangeIndicator() {
|
||||
final changeColor = isPositiveChange
|
||||
? AppTheme.successColor
|
||||
: AppTheme.errorColor;
|
||||
final changeIcon = isPositiveChange
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: changeColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
changeIcon,
|
||||
size: 16,
|
||||
color: changeColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
change,
|
||||
style: TextStyle(
|
||||
color: changeColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class NavigationCards extends StatelessWidget {
|
||||
final Function(int)? onNavigateToTab;
|
||||
|
||||
const NavigationCards({
|
||||
super.key,
|
||||
this.onNavigateToTab,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ResponsiveUtils.init(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dashboard_customize,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Accès rapide aux modules',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.1,
|
||||
children: [
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Membres',
|
||||
subtitle: '1,247 membres',
|
||||
icon: Icons.people_rounded,
|
||||
color: AppTheme.secondaryColor,
|
||||
onTap: () => _navigateToModule(context, 1, 'Membres'),
|
||||
badge: '+5 cette semaine',
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Cotisations',
|
||||
subtitle: '89.5% à jour',
|
||||
icon: Icons.payment_rounded,
|
||||
color: AppTheme.accentColor,
|
||||
onTap: () => _navigateToModule(context, 2, 'Cotisations'),
|
||||
badge: '15 en retard',
|
||||
badgeColor: AppTheme.warningColor,
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Événements',
|
||||
subtitle: '3 à venir',
|
||||
icon: Icons.event_rounded,
|
||||
color: AppTheme.warningColor,
|
||||
onTap: () => _navigateToModule(context, 3, 'Événements'),
|
||||
badge: 'AG dans 5 jours',
|
||||
),
|
||||
_buildNavigationCard(
|
||||
context,
|
||||
title: 'Finances',
|
||||
subtitle: '€45,890',
|
||||
icon: Icons.account_balance_rounded,
|
||||
color: AppTheme.primaryColor,
|
||||
onTap: () => _navigateToModule(context, 4, 'Finances'),
|
||||
badge: '+12.8% ce mois',
|
||||
badgeColor: AppTheme.successColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
String? badge,
|
||||
Color? badgeColor,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
color.withOpacity(0.05),
|
||||
color.withOpacity(0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec icône et badge
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
width: ResponsiveUtils.iconSize(8),
|
||||
height: ResponsiveUtils.iconSize(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(4)),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: ResponsiveUtils.iconSize(4.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (badge != null)
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: ResponsiveUtils.paddingSymmetric(
|
||||
horizontal: 1.5,
|
||||
vertical: 0.3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.1),
|
||||
borderRadius: ResponsiveUtils.borderRadius(2),
|
||||
border: Border.all(
|
||||
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
badge,
|
||||
style: TextStyle(
|
||||
color: badgeColor ?? AppTheme.successColor,
|
||||
fontSize: 2.5.fs,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Contenu principal
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 4.fs,
|
||||
medium: 3.8.fs,
|
||||
large: 3.6.fs,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 1.hp),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.2.fs,
|
||||
medium: 3.fs,
|
||||
large: 2.8.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Flèche d'action
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Gérer',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToModule(BuildContext context, int tabIndex, String moduleName) {
|
||||
// Si onNavigateToTab est fourni, l'utiliser pour naviguer vers l'onglet
|
||||
if (onNavigateToTab != null) {
|
||||
onNavigateToTab!(tabIndex);
|
||||
} else {
|
||||
// Sinon, afficher un message temporaire
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Navigation vers $moduleName'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
|
||||
class QuickActionsGrid extends StatelessWidget {
|
||||
const QuickActionsGrid({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ResponsiveUtils.init(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Actions rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.2,
|
||||
children: _getQuickActions(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getQuickActions(BuildContext context) {
|
||||
final actions = [
|
||||
QuickAction(
|
||||
title: 'Nouveau membre',
|
||||
description: 'Ajouter un membre',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.primaryColor,
|
||||
onTap: () => _showAction(context, 'Nouveau membre'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Créer événement',
|
||||
description: 'Organiser un événement',
|
||||
icon: Icons.event_available,
|
||||
color: AppTheme.secondaryColor,
|
||||
onTap: () => _showAction(context, 'Créer événement'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Suivi cotisations',
|
||||
description: 'Gérer les cotisations',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.accentColor,
|
||||
onTap: () => _showAction(context, 'Suivi cotisations'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Rapports',
|
||||
description: 'Générer des rapports',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.infoColor,
|
||||
onTap: () => _showAction(context, 'Rapports'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Messages',
|
||||
description: 'Envoyer des notifications',
|
||||
icon: Icons.message,
|
||||
color: AppTheme.warningColor,
|
||||
onTap: () => _showAction(context, 'Messages'),
|
||||
),
|
||||
QuickAction(
|
||||
title: 'Documents',
|
||||
description: 'Gérer les documents',
|
||||
icon: Icons.folder,
|
||||
color: Color(0xFF9C27B0),
|
||||
onTap: () => _showAction(context, 'Documents'),
|
||||
),
|
||||
];
|
||||
|
||||
return actions.map((action) => _buildActionCard(action)).toList();
|
||||
}
|
||||
|
||||
Widget _buildActionCard(QuickAction action) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: action.onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: action.color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
width: ResponsiveUtils.iconSize(12),
|
||||
height: ResponsiveUtils.iconSize(12),
|
||||
decoration: BoxDecoration(
|
||||
color: action.color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(6)),
|
||||
),
|
||||
child: Icon(
|
||||
action.icon,
|
||||
color: action.color,
|
||||
size: ResponsiveUtils.iconSize(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.hp),
|
||||
Flexible(
|
||||
child: Text(
|
||||
action.title,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.5.fs,
|
||||
medium: 3.2.fs,
|
||||
large: 3.fs,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.5.hp),
|
||||
Flexible(
|
||||
child: Text(
|
||||
action.description,
|
||||
style: TextStyle(
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 2.8.fs,
|
||||
medium: 2.6.fs,
|
||||
large: 2.4.fs,
|
||||
),
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAction(BuildContext context, String actionName) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$actionName - En cours de développement'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickAction {
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
QuickAction({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user