Authentification stable - WIP

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

View File

@@ -1,117 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de carte d'action réutilisable pour les membres
class MembersActionCardWidget extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final VoidCallback onTap;
final String? badge;
const MembersActionCardWidget({
super.key,
required this.title,
required this.subtitle,
required this.icon,
required this.color,
required this.onTap,
this.badge,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône avec badge optionnel
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: 18,
),
),
if (badge != null)
Positioned(
right: -2,
top: -2,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: AppTheme.errorColor,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
const SizedBox(height: 8),
// Titre
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),
// Sous-titre
Text(
subtitle,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View File

@@ -1,163 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget d'élément d'activité réutilisable pour les membres
class MembersActivityItemWidget extends StatelessWidget {
final String title;
final String description;
final String time;
final IconData icon;
final Color color;
final String? memberName;
final String? memberAvatar;
final VoidCallback? onTap;
const MembersActivityItemWidget({
super.key,
required this.title,
required this.description,
required this.time,
required this.icon,
required this.color,
this.memberName,
this.memberAvatar,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.1),
width: 1,
),
),
child: Row(
children: [
// Icône d'activité
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 16,
),
),
const SizedBox(width: 12),
// Contenu principal
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
// Description
Text(
description,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// Nom du membre si fourni
if (memberName != null) ...[
const SizedBox(height: 4),
Row(
children: [
// Avatar du membre
if (memberAvatar != null)
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.person,
size: 10,
color: AppTheme.primaryColor,
),
)
else
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.person,
size: 10,
color: color,
),
),
const SizedBox(width: 6),
Text(
memberName!,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
],
],
),
),
// Temps et indicateur
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
time,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
],
),
],
),
),
);
}
}

View File

@@ -1,311 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de filtres avancés pour le dashboard des membres
class MembersAdvancedFiltersWidget extends StatefulWidget {
final Function(Map<String, dynamic>) onFiltersChanged;
final Map<String, dynamic> initialFilters;
const MembersAdvancedFiltersWidget({
super.key,
required this.onFiltersChanged,
this.initialFilters = const {},
});
@override
State<MembersAdvancedFiltersWidget> createState() => _MembersAdvancedFiltersWidgetState();
}
class _MembersAdvancedFiltersWidgetState extends State<MembersAdvancedFiltersWidget>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
Map<String, dynamic> _filters = {};
bool _isExpanded = false;
// Options de filtres
final List<String> _statusOptions = ['Tous', 'Actif', 'Inactif', 'Suspendu'];
final List<String> _ageRanges = ['Tous', '18-30', '31-45', '46-60', '60+'];
final List<String> _genderOptions = ['Tous', 'Homme', 'Femme'];
final List<String> _roleOptions = ['Tous', 'Membre', 'Responsable', 'Bureau'];
final List<String> _timeRanges = ['7 jours', '30 jours', '3 mois', '6 mois', '1 an'];
@override
void initState() {
super.initState();
_filters = Map.from(widget.initialFilters);
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
});
}
void _updateFilter(String key, dynamic value) {
setState(() {
_filters[key] = value;
});
widget.onFiltersChanged(_filters);
}
void _resetFilters() {
setState(() {
_filters.clear();
});
widget.onFiltersChanged(_filters);
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 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(
children: [
// En-tête des filtres
InkWell(
onTap: _toggleExpanded,
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.tune,
color: AppTheme.primaryColor,
size: 16,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Filtres Avancés',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
if (_filters.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_filters.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
AnimatedRotation(
turns: _isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 300),
child: const Icon(
Icons.keyboard_arrow_down,
color: AppTheme.textSecondary,
),
),
],
),
),
),
// Contenu des filtres
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: _isExpanded ? null : 0,
child: _isExpanded
? FadeTransition(
opacity: _fadeAnimation,
child: _buildFiltersContent(),
)
: const SizedBox.shrink(),
),
],
),
);
}
Widget _buildFiltersContent() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1),
const SizedBox(height: 16),
// Période
_buildFilterSection(
'Période',
Icons.date_range,
_buildChipFilter('timeRange', _timeRanges),
),
const SizedBox(height: 16),
// Statut
_buildFilterSection(
'Statut',
Icons.verified_user,
_buildChipFilter('status', _statusOptions),
),
const SizedBox(height: 16),
// Tranche d'âge
_buildFilterSection(
'Âge',
Icons.cake,
_buildChipFilter('ageRange', _ageRanges),
),
const SizedBox(height: 16),
// Genre
_buildFilterSection(
'Genre',
Icons.people_outline,
_buildChipFilter('gender', _genderOptions),
),
const SizedBox(height: 16),
// Rôle
_buildFilterSection(
'Rôle',
Icons.admin_panel_settings,
_buildChipFilter('role', _roleOptions),
),
const SizedBox(height: 20),
// Boutons d'action
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _resetFilters,
icon: const Icon(Icons.clear_all, size: 16),
label: const Text('Réinitialiser'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
side: BorderSide(color: AppTheme.textSecondary.withOpacity(0.3)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _toggleExpanded(),
icon: const Icon(Icons.check, size: 16),
label: const Text('Appliquer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
),
],
),
],
),
);
}
Widget _buildFilterSection(String title, IconData icon, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 8),
content,
],
);
}
Widget _buildChipFilter(String filterKey, List<String> options) {
return Wrap(
spacing: 8,
runSpacing: 4,
children: options.map((option) {
final isSelected = _filters[filterKey] == option;
return FilterChip(
label: Text(
option,
style: TextStyle(
fontSize: 12,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
_updateFilter(filterKey, option);
} else {
_updateFilter(filterKey, null);
}
},
backgroundColor: Colors.grey[100],
selectedColor: AppTheme.primaryColor,
checkmarkColor: Colors.white,
side: BorderSide(
color: isSelected ? AppTheme.primaryColor : Colors.grey[300]!,
width: 1,
),
);
}).toList(),
);
}
}

View File

@@ -1,564 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de section d'analyses pour les membres
class MembersAnalyticsWidget extends StatelessWidget {
const MembersAnalyticsWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de section
const Row(
children: [
Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Analyses & Tendances',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
// Grille de graphiques
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 1,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.4,
children: [
// Évolution des inscriptions
_buildMemberGrowthChart(),
// Répartition par âge
_buildAgeDistributionChart(),
// Activité mensuelle
_buildMonthlyActivityChart(),
],
),
],
);
}
/// Graphique d'évolution des inscriptions
Widget _buildMemberGrowthChart() {
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
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.trending_up,
color: AppTheme.primaryColor,
size: 16,
),
),
const SizedBox(width: 8),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Évolution des Inscriptions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 2),
Text(
'Croissance sur 6 mois • +24.7%',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Graphique linéaire
Expanded(
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 50,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.primaryColor.withOpacity(0.1),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (double value, TitleMeta meta) {
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin'];
if (value.toInt() >= 0 && value.toInt() < months.length) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
months[value.toInt()],
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 10,
),
),
);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 50,
reservedSize: 40,
getTitlesWidget: (double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
'${value.toInt()}',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 10,
),
),
);
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 5,
minY: 0,
maxY: 300,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 180), // Janvier: 180 nouveaux
FlSpot(1, 195), // Février: 195 nouveaux
FlSpot(2, 210), // Mars: 210 nouveaux
FlSpot(3, 235), // Avril: 235 nouveaux
FlSpot(4, 265), // Mai: 265 nouveaux
FlSpot(5, 285), // Juin: 285 nouveaux
],
isCurved: true,
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.7),
],
),
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(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.primaryColor.withOpacity(0.2),
AppTheme.primaryColor.withOpacity(0.05),
],
),
),
),
],
),
),
),
],
),
);
}
/// Graphique de répartition par âge
Widget _buildAgeDistributionChart() {
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
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.cake,
color: AppTheme.successColor,
size: 16,
),
),
const SizedBox(width: 8),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Répartition par Âge',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 2),
Text(
'Distribution par tranches d\'âge',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Graphique en camembert
Expanded(
child: Row(
children: [
// Graphique
Expanded(
flex: 2,
child: PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: AppTheme.primaryColor,
value: 42,
title: '42%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.successColor,
value: 38,
title: '38%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.warningColor,
value: 15,
title: '15%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.errorColor,
value: 5,
title: '5%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
// Légende
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAgeLegend('18-30 ans', '524', AppTheme.primaryColor),
const SizedBox(height: 8),
_buildAgeLegend('31-45 ans', '474', AppTheme.successColor),
const SizedBox(height: 8),
_buildAgeLegend('46-60 ans', '187', AppTheme.warningColor),
const SizedBox(height: 8),
_buildAgeLegend('60+ ans', '62', AppTheme.errorColor),
],
),
),
],
),
),
],
),
);
}
/// Widget de légende pour les âges
Widget _buildAgeLegend(String label, String count, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
Text(
count,
style: const TextStyle(
fontSize: 9,
color: AppTheme.textSecondary,
),
),
],
),
),
],
);
}
/// Graphique d'activité mensuelle
Widget _buildMonthlyActivityChart() {
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
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.timeline,
color: AppTheme.infoColor,
size: 16,
),
),
const SizedBox(width: 8),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activité Mensuelle',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 2),
Text(
'Connexions et interactions',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Graphique en barres
Expanded(
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 1200,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (double value, TitleMeta meta) {
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin'];
if (value.toInt() >= 0 && value.toInt() < months.length) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
months[value.toInt()],
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 10,
),
),
);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 200,
getTitlesWidget: (double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
'${value.toInt()}',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 10,
),
),
);
},
),
),
),
borderData: FlBorderData(show: false),
barGroups: [
BarChartGroupData(x: 0, barRods: [BarChartRodData(toY: 850, color: AppTheme.infoColor, width: 16)]),
BarChartGroupData(x: 1, barRods: [BarChartRodData(toY: 920, color: AppTheme.infoColor, width: 16)]),
BarChartGroupData(x: 2, barRods: [BarChartRodData(toY: 1050, color: AppTheme.infoColor, width: 16)]),
BarChartGroupData(x: 3, barRods: [BarChartRodData(toY: 980, color: AppTheme.infoColor, width: 16)]),
BarChartGroupData(x: 4, barRods: [BarChartRodData(toY: 1120, color: AppTheme.infoColor, width: 16)]),
BarChartGroupData(x: 5, barRods: [BarChartRodData(toY: 1089, color: AppTheme.infoColor, width: 16)]),
],
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 200,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.infoColor.withOpacity(0.1),
strokeWidth: 1,
);
},
),
),
),
),
],
),
);
}
}

View File

@@ -1,828 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../core/models/membre_model.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'members_interactive_card_widget.dart';
import 'members_stats_widget.dart';
/// Widget de liste de membres améliorée avec animations
class MembersEnhancedListWidget extends StatefulWidget {
final List<MembreModel> members;
final Function(MembreModel) onMemberTap;
final Function(MembreModel)? onMemberCall;
final Function(MembreModel)? onMemberMessage;
final Function(MembreModel)? onMemberEdit;
final bool isLoading;
final String? searchQuery;
final Map<String, dynamic> filters;
const MembersEnhancedListWidget({
super.key,
required this.members,
required this.onMemberTap,
this.onMemberCall,
this.onMemberMessage,
this.onMemberEdit,
this.isLoading = false,
this.searchQuery,
this.filters = const {},
});
@override
State<MembersEnhancedListWidget> createState() => _MembersEnhancedListWidgetState();
}
class _MembersEnhancedListWidgetState extends State<MembersEnhancedListWidget>
with TickerProviderStateMixin {
late AnimationController _listController;
late Animation<double> _listAnimation;
List<String> _selectedMembers = [];
String _sortBy = 'name';
bool _sortAscending = true;
String _viewMode = 'card'; // 'card', 'list', 'grid'
@override
void initState() {
super.initState();
_listController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_listAnimation = CurvedAnimation(
parent: _listController,
curve: Curves.easeOutQuart,
);
_listController.forward();
}
@override
void dispose() {
_listController.dispose();
super.dispose();
}
List<MembreModel> get _filteredAndSortedMembers {
List<MembreModel> filtered = List.from(widget.members);
// Appliquer les filtres
if (widget.filters.isNotEmpty) {
filtered = filtered.where((member) {
bool matches = true;
if (widget.filters['status'] != null && widget.filters['status'] != 'Tous') {
matches = matches && member.statut.toUpperCase() == widget.filters['status'].toUpperCase();
}
if (widget.filters['ageRange'] != null && widget.filters['ageRange'] != 'Tous') {
final ageRange = widget.filters['ageRange'] as String;
final age = member.age;
switch (ageRange) {
case '18-30':
matches = matches && age >= 18 && age <= 30;
break;
case '31-45':
matches = matches && age >= 31 && age <= 45;
break;
case '46-60':
matches = matches && age >= 46 && age <= 60;
break;
case '60+':
matches = matches && age > 60;
break;
}
}
return matches;
}).toList();
}
// Appliquer la recherche
if (widget.searchQuery != null && widget.searchQuery!.isNotEmpty) {
final query = widget.searchQuery!.toLowerCase();
filtered = filtered.where((member) {
return member.nomComplet.toLowerCase().contains(query) ||
member.numeroMembre.toLowerCase().contains(query) ||
member.email.toLowerCase().contains(query) ||
member.telephone.contains(query);
}).toList();
}
// Trier
filtered.sort((a, b) {
int comparison = 0;
switch (_sortBy) {
case 'name':
comparison = a.nomComplet.compareTo(b.nomComplet);
break;
case 'date':
comparison = a.dateAdhesion.compareTo(b.dateAdhesion);
break;
case 'age':
comparison = a.age.compareTo(b.age);
break;
case 'status':
comparison = a.statut.compareTo(b.statut);
break;
}
return _sortAscending ? comparison : -comparison;
});
return filtered;
}
void _toggleMemberSelection(String memberId) {
setState(() {
if (_selectedMembers.contains(memberId)) {
_selectedMembers.remove(memberId);
} else {
_selectedMembers.add(memberId);
}
});
}
void _clearSelection() {
setState(() {
_selectedMembers.clear();
});
}
void _changeSortBy(String sortBy) {
setState(() {
if (_sortBy == sortBy) {
_sortAscending = !_sortAscending;
} else {
_sortBy = sortBy;
_sortAscending = true;
}
});
}
void _changeViewMode(String viewMode) {
setState(() {
_viewMode = viewMode;
});
}
@override
Widget build(BuildContext context) {
final filteredMembers = _filteredAndSortedMembers;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec contrôles
_buildHeader(filteredMembers.length),
const SizedBox(height: 16),
// Statistiques des membres
if (!widget.isLoading && filteredMembers.isNotEmpty)
MembersStatsWidget(
members: filteredMembers,
searchQuery: widget.searchQuery ?? '',
filters: widget.filters,
),
// Barre de sélection (si des membres sont sélectionnés)
if (_selectedMembers.isNotEmpty)
_buildSelectionBar(),
// Liste des membres
if (widget.isLoading)
_buildLoadingState()
else if (filteredMembers.isEmpty)
_buildEmptyState()
else
_buildMembersList(filteredMembers),
],
);
}
Widget _buildHeader(int memberCount) {
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(
children: [
// Titre et compteur
Row(
children: [
const Icon(
Icons.people,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'Membres ($memberCount)',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const Spacer(),
// Modes d'affichage
_buildViewModeToggle(),
],
),
const SizedBox(height: 12),
// Contrôles de tri
Row(
children: [
const Text(
'Trier par:',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(width: 8),
_buildSortChip('name', 'Nom'),
const SizedBox(width: 4),
_buildSortChip('date', 'Date'),
const SizedBox(width: 4),
_buildSortChip('age', 'Âge'),
const SizedBox(width: 4),
_buildSortChip('status', 'Statut'),
],
),
],
),
);
}
Widget _buildViewModeToggle() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildViewModeButton(Icons.view_agenda, 'card'),
_buildViewModeButton(Icons.view_list, 'list'),
_buildViewModeButton(Icons.grid_view, 'grid'),
],
),
);
}
Widget _buildViewModeButton(IconData icon, String mode) {
final isSelected = _viewMode == mode;
return InkWell(
onTap: () => _changeViewMode(mode),
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
icon,
size: 16,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
),
);
}
Widget _buildSortChip(String sortKey, String label) {
final isSelected = _sortBy == sortKey;
return InkWell(
onTap: () => _changeSortBy(sortKey),
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? AppTheme.primaryColor : Colors.grey[300]!,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: isSelected ? AppTheme.primaryColor : AppTheme.textSecondary,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
),
),
if (isSelected) ...[
const SizedBox(width: 4),
Icon(
_sortAscending ? Icons.arrow_upward : Icons.arrow_downward,
size: 12,
color: AppTheme.primaryColor,
),
],
],
),
),
);
}
Widget _buildSelectionBar() {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
Icons.check_circle,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'${_selectedMembers.length} membre(s) sélectionné(s)',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.primaryColor,
),
),
const Spacer(),
TextButton(
onPressed: _clearSelection,
child: const Text('Désélectionner'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
// TODO: Actions groupées
},
icon: const Icon(Icons.more_horiz, size: 16),
label: const Text('Actions'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
);
}
Widget _buildLoadingState() {
return const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
widget.searchQuery?.isNotEmpty == true ? Icons.search_off : Icons.people_outline,
size: 64,
color: AppTheme.textHint,
),
const SizedBox(height: 16),
Text(
widget.searchQuery?.isNotEmpty == true
? 'Aucun membre trouvé'
: 'Aucun membre',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
widget.searchQuery?.isNotEmpty == true
? 'Essayez avec d\'autres termes de recherche'
: 'Commencez par ajouter des membres',
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildMembersList(List<MembreModel> members) {
if (_viewMode == 'grid') {
return _buildGridView(members);
} else if (_viewMode == 'list') {
return _buildListView(members);
} else {
return _buildCardView(members);
}
}
Widget _buildCardView(List<MembreModel> members) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: MembersInteractiveCardWidget(
member: member,
isSelected: _selectedMembers.contains(member.id),
onTap: () {
if (_selectedMembers.isNotEmpty) {
_toggleMemberSelection(member.id!);
} else {
widget.onMemberTap(member);
}
},
onCall: widget.onMemberCall != null
? () => widget.onMemberCall!(member)
: null,
onMessage: widget.onMemberMessage != null
? () => widget.onMemberMessage!(member)
: null,
onEdit: widget.onMemberEdit != null
? () => widget.onMemberEdit!(member)
: null,
),
);
},
);
}
Widget _buildListView(List<MembreModel> members) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: _buildCompactMemberTile(member),
);
},
);
}
Widget _buildGridView(List<MembreModel> members) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.85,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return _buildGridMemberCard(member);
},
);
}
Widget _buildCompactMemberTile(MembreModel member) {
final isSelected = _selectedMembers.contains(member.id);
return InkWell(
onTap: () {
if (_selectedMembers.isNotEmpty) {
_toggleMemberSelection(member.id!);
} else {
widget.onMemberTap(member);
}
},
onLongPress: () => _toggleMemberSelection(member.id!),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppTheme.primaryColor : Colors.grey[200]!,
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// Avatar
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.7),
],
),
),
child: Center(
child: Text(
member.nomComplet.split(' ').map((e) => e[0]).take(2).join(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
const SizedBox(width: 12),
// Informations
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
member.nomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
member.numeroMembre,
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.phone,
size: 14,
color: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
member.telephone,
style: TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
],
),
),
// Badge de statut
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
member.statutLibelle,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.successColor,
),
),
),
// Actions rapides
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: AppTheme.textSecondary,
size: 20,
),
onSelected: (value) {
switch (value) {
case 'call':
widget.onMemberCall?.call(member);
break;
case 'message':
widget.onMemberMessage?.call(member);
break;
case 'edit':
widget.onMemberEdit?.call(member);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'call',
child: Row(
children: [
Icon(Icons.phone, size: 16),
SizedBox(width: 8),
Text('Appeler'),
],
),
),
const PopupMenuItem(
value: 'message',
child: Row(
children: [
Icon(Icons.message, size: 16),
SizedBox(width: 8),
Text('Message'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 16),
SizedBox(width: 8),
Text('Modifier'),
],
),
),
],
),
],
),
),
);
}
Widget _buildGridMemberCard(MembreModel member) {
final isSelected = _selectedMembers.contains(member.id);
return InkWell(
onTap: () {
if (_selectedMembers.isNotEmpty) {
_toggleMemberSelection(member.id!);
} else {
widget.onMemberTap(member);
}
},
onLongPress: () => _toggleMemberSelection(member.id!),
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? AppTheme.primaryColor : Colors.grey[200]!,
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// Avatar
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.7),
],
),
),
child: Center(
child: Text(
member.nomComplet.split(' ').map((e) => e[0]).take(2).join(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
),
const SizedBox(height: 12),
// Nom
Text(
member.nomComplet,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Numéro membre
Text(
member.numeroMembre,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
// Badge de statut
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
member.statutLibelle,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppTheme.successColor,
),
),
),
const Spacer(),
// Actions rapides
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () => widget.onMemberCall?.call(member),
icon: const Icon(Icons.phone, size: 18),
style: IconButton.styleFrom(
backgroundColor: AppTheme.successColor.withOpacity(0.1),
foregroundColor: AppTheme.successColor,
minimumSize: const Size(32, 32),
),
),
IconButton(
onPressed: () => widget.onMemberMessage?.call(member),
icon: const Icon(Icons.message, size: 18),
style: IconButton.styleFrom(
backgroundColor: AppTheme.infoColor.withOpacity(0.1),
foregroundColor: AppTheme.infoColor,
minimumSize: const Size(32, 32),
),
),
IconButton(
onPressed: () => widget.onMemberEdit?.call(member),
icon: const Icon(Icons.edit, size: 18),
style: IconButton.styleFrom(
backgroundColor: AppTheme.warningColor.withOpacity(0.1),
foregroundColor: AppTheme.warningColor,
minimumSize: const Size(32, 32),
),
),
],
),
],
),
),
);
}
}

View File

@@ -1,471 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../../core/models/membre_model.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Carte membre interactive avec animations avancées
class MembersInteractiveCardWidget extends StatefulWidget {
final MembreModel member;
final VoidCallback? onTap;
final VoidCallback? onCall;
final VoidCallback? onMessage;
final VoidCallback? onEdit;
final bool isSelected;
final bool showActions;
const MembersInteractiveCardWidget({
super.key,
required this.member,
this.onTap,
this.onCall,
this.onMessage,
this.onEdit,
this.isSelected = false,
this.showActions = true,
});
@override
State<MembersInteractiveCardWidget> createState() => _MembersInteractiveCardWidgetState();
}
class _MembersInteractiveCardWidgetState extends State<MembersInteractiveCardWidget>
with TickerProviderStateMixin {
late AnimationController _hoverController;
late AnimationController _tapController;
late AnimationController _actionsController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
late Animation<double> _actionsAnimation;
late Animation<Offset> _slideAnimation;
bool _isHovered = false;
bool _showActions = false;
@override
void initState() {
super.initState();
_hoverController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_tapController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_actionsController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
CurvedAnimation(parent: _hoverController, curve: Curves.easeOut),
);
_elevationAnimation = Tween<double>(begin: 2.0, end: 8.0).animate(
CurvedAnimation(parent: _hoverController, curve: Curves.easeOut),
);
_actionsAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _actionsController, curve: Curves.elasticOut),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _actionsController, curve: Curves.easeOut));
}
@override
void dispose() {
_hoverController.dispose();
_tapController.dispose();
_actionsController.dispose();
super.dispose();
}
void _onHover(bool isHovered) {
setState(() {
_isHovered = isHovered;
});
if (isHovered) {
_hoverController.forward();
if (widget.showActions) {
_showActions = true;
_actionsController.forward();
}
} else {
_hoverController.reverse();
_showActions = false;
_actionsController.reverse();
}
}
void _onTapDown(TapDownDetails details) {
_tapController.forward();
HapticFeedback.lightImpact();
}
void _onTapUp(TapUpDetails details) {
_tapController.reverse();
}
void _onTapCancel() {
_tapController.reverse();
}
Color _getStatusColor() {
switch (widget.member.statut.toUpperCase()) {
case 'ACTIF':
return AppTheme.successColor;
case 'INACTIF':
return AppTheme.warningColor;
case 'SUSPENDU':
return AppTheme.errorColor;
default:
return AppTheme.textSecondary;
}
}
String _getInitials() {
final names = '${widget.member.prenom} ${widget.member.nom}'.split(' ');
return names.take(2).map((name) => name.isNotEmpty ? name[0].toUpperCase() : '').join();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => _onHover(true),
onExit: (_) => _onHover(false),
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: widget.onTap,
child: AnimatedBuilder(
animation: Listenable.merge([_hoverController, _tapController]),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value * (1.0 - _tapController.value * 0.02),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: widget.isSelected
? Border.all(color: AppTheme.primaryColor, width: 2)
: null,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: _elevationAnimation.value,
offset: Offset(0, _elevationAnimation.value / 2),
),
],
),
child: Stack(
children: [
// Contenu principal
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec avatar et statut
Row(
children: [
_buildAnimatedAvatar(),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.member.nomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Row(
children: [
Text(
widget.member.numeroMembre,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(width: 8),
_buildStatusBadge(),
],
),
],
),
),
],
),
const SizedBox(height: 12),
// Informations de contact
_buildContactInfo(),
const SizedBox(height: 12),
// Informations supplémentaires
_buildAdditionalInfo(),
],
),
),
// Actions flottantes
if (_showActions && widget.showActions)
Positioned(
top: 8,
right: 8,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _actionsAnimation,
child: _buildFloatingActions(),
),
),
),
// Indicateur de sélection
if (widget.isSelected)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: AppTheme.primaryColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 12,
),
),
),
],
),
),
);
},
),
),
);
}
Widget _buildAnimatedAvatar() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: _isHovered ? 52 : 48,
height: _isHovered ? 52 : 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.7),
],
),
borderRadius: BorderRadius.circular(_isHovered ? 16 : 14),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: _isHovered ? 8 : 4,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Text(
_getInitials(),
style: TextStyle(
color: Colors.white,
fontSize: _isHovered ? 18 : 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildStatusBadge() {
final statusColor = _getStatusColor();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: statusColor.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
widget.member.statutLibelle,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: statusColor,
),
),
],
),
);
}
Widget _buildContactInfo() {
return Column(
children: [
_buildInfoRow(Icons.email_outlined, widget.member.email),
const SizedBox(height: 4),
_buildInfoRow(Icons.phone_outlined, widget.member.telephone),
],
);
}
Widget _buildInfoRow(IconData icon, String text) {
return Row(
children: [
Icon(
icon,
size: 14,
color: AppTheme.textSecondary,
),
const SizedBox(width: 6),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildAdditionalInfo() {
return Row(
children: [
_buildInfoChip(
Icons.cake_outlined,
'${widget.member.age} ans',
AppTheme.infoColor,
),
const SizedBox(width: 8),
_buildInfoChip(
Icons.calendar_today_outlined,
'Depuis ${widget.member.dateAdhesion?.year ?? 'N/A'}',
AppTheme.successColor,
),
],
);
}
Widget _buildInfoChip(IconData icon, String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 12,
color: color,
),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
);
}
Widget _buildFloatingActions() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildActionButton(
Icons.phone,
AppTheme.successColor,
widget.onCall,
),
_buildActionButton(
Icons.message,
AppTheme.infoColor,
widget.onMessage,
),
_buildActionButton(
Icons.edit,
AppTheme.warningColor,
widget.onEdit,
),
],
),
);
}
Widget _buildActionButton(IconData icon, Color color, VoidCallback? onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
icon,
size: 16,
color: color,
),
),
);
}
}

View File

@@ -1,169 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de carte KPI réutilisable pour les membres
class MembersKPICardWidget extends StatelessWidget {
final String title;
final String value;
final String subtitle;
final IconData icon;
final Color color;
final String? trend;
final bool? isPositiveTrend;
final List<String>? details;
final VoidCallback? onTap;
const MembersKPICardWidget({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.trend,
this.isPositiveTrend,
this.details,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: 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 titre
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 SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
),
if (trend != null) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: (isPositiveTrend ?? true)
? AppTheme.successColor.withOpacity(0.1)
: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
(isPositiveTrend ?? true)
? Icons.trending_up
: Icons.trending_down,
size: 12,
color: (isPositiveTrend ?? true)
? AppTheme.successColor
: AppTheme.errorColor,
),
const SizedBox(width: 2),
Text(
trend!,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: (isPositiveTrend ?? true)
? AppTheme.successColor
: AppTheme.errorColor,
),
),
],
),
),
],
],
),
const SizedBox(height: 12),
// Valeur principale
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
// Sous-titre
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
// Détails optionnels
if (details != null && details!.isNotEmpty) ...[
const SizedBox(height: 8),
...details!.take(2).map((detail) => Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: color.withOpacity(0.6),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
detail,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
),
),
],
),
)),
],
],
),
),
);
}
}

View File

@@ -1,200 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'members_kpi_card_widget.dart';
/// Widget de section KPI pour le dashboard des membres
class MembersKPISectionWidget extends StatelessWidget {
const MembersKPISectionWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de section
const Row(
children: [
Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Indicateurs Clés',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
// Grille de KPI
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.1,
children: [
// Total des membres
MembersKPICardWidget(
title: 'Total Membres',
value: '1,247',
subtitle: 'Membres enregistrés',
icon: Icons.people,
color: AppTheme.primaryColor,
trend: '+24.7%',
isPositiveTrend: true,
details: const [
'1,089 Actifs (87.3%)',
'158 Inactifs (12.7%)',
],
onTap: () => _showMemberDetails(context, 'total'),
),
// Nouveaux membres
MembersKPICardWidget(
title: 'Nouveaux Membres',
value: '47',
subtitle: 'Ce mois-ci',
icon: Icons.person_add,
color: AppTheme.successColor,
trend: '+15.2%',
isPositiveTrend: true,
details: const [
'28 Particuliers',
'19 Professionnels',
],
onTap: () => _showMemberDetails(context, 'nouveaux'),
),
// Membres actifs
MembersKPICardWidget(
title: 'Membres Actifs',
value: '1,089',
subtitle: 'Derniers 30 jours',
icon: Icons.trending_up,
color: AppTheme.infoColor,
trend: '+8.3%',
isPositiveTrend: true,
details: const [
'892 Très actifs',
'197 Modérément actifs',
],
onTap: () => _showMemberDetails(context, 'actifs'),
),
// Taux de rétention
MembersKPICardWidget(
title: 'Taux de Rétention',
value: '94.2%',
subtitle: 'Sur 12 mois',
icon: Icons.favorite,
color: AppTheme.warningColor,
trend: '+2.1%',
isPositiveTrend: true,
details: const [
'1,175 Fidèles',
'72 Nouveaux',
],
onTap: () => _showMemberDetails(context, 'retention'),
),
// Âge moyen
MembersKPICardWidget(
title: 'Âge Moyen',
value: '34.5',
subtitle: 'Années',
icon: Icons.cake,
color: AppTheme.errorColor,
trend: '+0.8',
isPositiveTrend: true,
details: const [
'18-30 ans: 42%',
'31-50 ans: 38%',
],
onTap: () => _showMemberDetails(context, 'age'),
),
// Répartition genre
MembersKPICardWidget(
title: 'Répartition Genre',
value: '52/48',
subtitle: 'Femmes/Hommes (%)',
icon: Icons.people_outline,
color: const Color(0xFF9C27B0),
details: const [
'649 Femmes (52%)',
'598 Hommes (48%)',
],
onTap: () => _showMemberDetails(context, 'genre'),
),
],
),
],
);
}
/// Affiche les détails d'un KPI spécifique
static void _showMemberDetails(BuildContext context, String type) {
String title = '';
String content = '';
switch (type) {
case 'total':
title = 'Total des Membres';
content = 'Détails de tous les membres enregistrés dans le système.';
break;
case 'nouveaux':
title = 'Nouveaux Membres';
content = 'Liste des membres qui ont rejoint ce mois-ci.';
break;
case 'actifs':
title = 'Membres Actifs';
content = 'Membres ayant une activité récente sur la plateforme.';
break;
case 'retention':
title = 'Taux de Rétention';
content = 'Pourcentage de membres restés actifs sur 12 mois.';
break;
case 'age':
title = 'Répartition par Âge';
content = 'Distribution des membres par tranches d\'âge.';
break;
case 'genre':
title = 'Répartition par Genre';
content = 'Distribution des membres par genre.';
break;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la vue détaillée
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
child: const Text('Voir détails'),
),
],
),
);
}
}

View File

@@ -1,519 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de notifications en temps réel pour les membres
class MembersNotificationsWidget extends StatefulWidget {
const MembersNotificationsWidget({super.key});
@override
State<MembersNotificationsWidget> createState() => _MembersNotificationsWidgetState();
}
class _MembersNotificationsWidgetState extends State<MembersNotificationsWidget>
with TickerProviderStateMixin {
late AnimationController _pulseController;
late AnimationController _slideController;
late Animation<double> _pulseAnimation;
late Animation<Offset> _slideAnimation;
Timer? _notificationTimer;
List<Map<String, dynamic>> _notifications = [];
bool _hasUnreadNotifications = false;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOut));
_startNotificationSimulation();
_loadInitialNotifications();
}
@override
void dispose() {
_notificationTimer?.cancel();
_pulseController.dispose();
_slideController.dispose();
super.dispose();
}
void _loadInitialNotifications() {
_notifications = [
{
'id': '1',
'type': 'new_member',
'title': 'Nouveau membre inscrit',
'message': 'Marie Kouassi a rejoint la communauté',
'timestamp': DateTime.now().subtract(const Duration(minutes: 5)),
'isRead': false,
'icon': Icons.person_add,
'color': AppTheme.successColor,
'priority': 'high',
},
{
'id': '2',
'type': 'payment',
'title': 'Cotisation reçue',
'message': 'Jean Baptiste a payé sa cotisation mensuelle',
'timestamp': DateTime.now().subtract(const Duration(minutes: 15)),
'isRead': false,
'icon': Icons.payment,
'color': AppTheme.primaryColor,
'priority': 'medium',
},
{
'id': '3',
'type': 'reminder',
'title': 'Rappel automatique',
'message': '12 membres ont des cotisations en retard',
'timestamp': DateTime.now().subtract(const Duration(hours: 1)),
'isRead': true,
'icon': Icons.notification_important,
'color': AppTheme.warningColor,
'priority': 'medium',
},
];
_updateNotificationState();
}
void _startNotificationSimulation() {
_notificationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
_addRandomNotification();
});
}
void _addRandomNotification() {
final notifications = [
{
'type': 'new_member',
'title': 'Nouveau membre inscrit',
'message': 'Un nouveau membre a rejoint la communauté',
'icon': Icons.person_add,
'color': AppTheme.successColor,
'priority': 'high',
},
{
'type': 'update',
'title': 'Profil mis à jour',
'message': 'Un membre a modifié ses informations',
'icon': Icons.edit,
'color': AppTheme.infoColor,
'priority': 'low',
},
{
'type': 'activity',
'title': 'Activité détectée',
'message': 'Connexion d\'un membre inactif',
'icon': Icons.trending_up,
'color': AppTheme.successColor,
'priority': 'medium',
},
];
final randomNotification = notifications[DateTime.now().millisecond % notifications.length];
final newNotification = {
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'timestamp': DateTime.now(),
'isRead': false,
...randomNotification,
};
setState(() {
_notifications.insert(0, newNotification);
if (_notifications.length > 20) {
_notifications = _notifications.take(20).toList();
}
});
_updateNotificationState();
_showNotificationAnimation();
}
void _updateNotificationState() {
final hasUnread = _notifications.any((notification) => !notification['isRead']);
if (hasUnread != _hasUnreadNotifications) {
setState(() {
_hasUnreadNotifications = hasUnread;
});
if (hasUnread) {
_pulseController.repeat(reverse: true);
} else {
_pulseController.stop();
_pulseController.reset();
}
}
}
void _showNotificationAnimation() {
_slideController.forward().then((_) {
Timer(const Duration(seconds: 3), () {
if (mounted) {
_slideController.reverse();
}
});
});
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
});
}
void _markAsRead(String notificationId) {
setState(() {
final index = _notifications.indexWhere((n) => n['id'] == notificationId);
if (index != -1) {
_notifications[index]['isRead'] = true;
}
});
_updateNotificationState();
}
void _markAllAsRead() {
setState(() {
for (var notification in _notifications) {
notification['isRead'] = true;
}
});
_updateNotificationState();
}
void _clearNotifications() {
setState(() {
_notifications.clear();
});
_updateNotificationState();
}
String _formatTimestamp(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 1) {
return 'À l\'instant';
} else if (difference.inMinutes < 60) {
return 'Il y a ${difference.inMinutes}min';
} else if (difference.inHours < 24) {
return 'Il y a ${difference.inHours}h';
} else {
return 'Il y a ${difference.inDays}j';
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Notification flottante
if (_slideController.isAnimating || _slideController.isCompleted)
SlideTransition(
position: _slideAnimation,
child: _buildFloatingNotification(),
),
// Widget principal des notifications
Container(
margin: const EdgeInsets.only(bottom: 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(
children: [
// En-tête
InkWell(
onTap: _toggleExpanded,
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _hasUnreadNotifications ? _pulseAnimation.value : 1.0,
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: _hasUnreadNotifications
? AppTheme.errorColor.withOpacity(0.1)
: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.notifications,
color: _hasUnreadNotifications
? AppTheme.errorColor
: AppTheme.primaryColor,
size: 16,
),
),
);
},
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Notifications',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
if (_notifications.where((n) => !n['isRead']).isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.errorColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_notifications.where((n) => !n['isRead']).length}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
AnimatedRotation(
turns: _isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 300),
child: const Icon(
Icons.keyboard_arrow_down,
color: AppTheme.textSecondary,
),
),
],
),
),
),
// Liste des notifications
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: _isExpanded ? null : 0,
child: _isExpanded ? _buildNotificationsList() : const SizedBox.shrink(),
),
],
),
),
],
);
}
Widget _buildFloatingNotification() {
if (_notifications.isEmpty) return const SizedBox.shrink();
final notification = _notifications.first;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: notification['color'].withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: notification['color'].withOpacity(0.3)),
),
child: Row(
children: [
Icon(
notification['icon'],
color: notification['color'],
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification['title'],
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
Text(
notification['message'],
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
);
}
Widget _buildNotificationsList() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
children: [
const Divider(height: 1),
const SizedBox(height: 12),
// Actions
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _markAllAsRead,
icon: const Icon(Icons.done_all, size: 16),
label: const Text('Tout marquer lu'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
side: BorderSide(color: AppTheme.textSecondary.withOpacity(0.3)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _clearNotifications,
icon: const Icon(Icons.clear_all, size: 16),
label: const Text('Effacer tout'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.errorColor,
side: BorderSide(color: AppTheme.errorColor.withOpacity(0.3)),
),
),
),
],
),
const SizedBox(height: 16),
// Liste des notifications
...(_notifications.take(5).map((notification) => _buildNotificationItem(notification))),
if (_notifications.length > 5)
TextButton(
onPressed: () {
// TODO: Naviguer vers la page complète des notifications
},
child: Text(
'Voir toutes les notifications (${_notifications.length})',
style: const TextStyle(
fontSize: 12,
color: AppTheme.primaryColor,
),
),
),
],
),
);
}
Widget _buildNotificationItem(Map<String, dynamic> notification) {
return InkWell(
onTap: () => _markAsRead(notification['id']),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: notification['color'].withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
notification['icon'],
color: notification['color'],
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification['title'],
style: TextStyle(
fontSize: 14,
fontWeight: notification['isRead'] ? FontWeight.w500 : FontWeight.w600,
color: notification['isRead'] ? AppTheme.textSecondary : AppTheme.textPrimary,
),
),
),
if (!notification['isRead'])
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppTheme.errorColor,
shape: BoxShape.circle,
),
),
],
),
const SizedBox(height: 2),
Text(
notification['message'],
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_formatTimestamp(notification['timestamp']),
style: const TextStyle(
fontSize: 10,
color: AppTheme.textHint,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -1,182 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import '../../../../../shared/widgets/coming_soon_page.dart';
import '../../pages/membre_create_page.dart';
import 'members_action_card_widget.dart';
/// Widget de section d'actions rapides pour les membres
class MembersQuickActionsWidget extends StatelessWidget {
const MembersQuickActionsWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de section
const Row(
children: [
Icon(
Icons.flash_on,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Actions Rapides',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
// Grille d'actions
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.0,
children: [
// Ajouter membre
MembersActionCardWidget(
title: 'Nouveau Membre',
subtitle: 'Inscription',
icon: Icons.person_add,
color: AppTheme.successColor,
onTap: () => _handleAction(context, 'add_member'),
),
// Rechercher membre
MembersActionCardWidget(
title: 'Rechercher',
subtitle: 'Trouver membre',
icon: Icons.search,
color: AppTheme.infoColor,
onTap: () => _handleAction(context, 'search_member'),
),
// Import/Export
MembersActionCardWidget(
title: 'Import/Export',
subtitle: 'Données',
icon: Icons.import_export,
color: AppTheme.warningColor,
onTap: () => _handleAction(context, 'import_export'),
),
// Envoyer message
MembersActionCardWidget(
title: 'Message Groupe',
subtitle: 'Communication',
icon: Icons.message,
color: AppTheme.primaryColor,
onTap: () => _handleAction(context, 'group_message'),
badge: '12',
),
// Statistiques
MembersActionCardWidget(
title: 'Statistiques',
subtitle: 'Analyses',
icon: Icons.bar_chart,
color: const Color(0xFF9C27B0),
onTap: () => _handleAction(context, 'statistics'),
),
// Rapports
MembersActionCardWidget(
title: 'Rapports',
subtitle: 'Documents',
icon: Icons.description,
color: AppTheme.errorColor,
onTap: () => _handleAction(context, 'reports'),
),
// Paramètres
MembersActionCardWidget(
title: 'Paramètres',
subtitle: 'Configuration',
icon: Icons.settings,
color: const Color(0xFF607D8B),
onTap: () => _handleAction(context, 'settings'),
),
// Sauvegarde
MembersActionCardWidget(
title: 'Sauvegarde',
subtitle: 'Backup',
icon: Icons.backup,
color: const Color(0xFF795548),
onTap: () => _handleAction(context, 'backup'),
),
// Support
MembersActionCardWidget(
title: 'Support',
subtitle: 'Aide',
icon: Icons.help_outline,
color: const Color(0xFF009688),
onTap: () => _handleAction(context, 'support'),
),
],
),
],
);
}
/// Gère les actions des cartes
static void _handleAction(BuildContext context, String action) {
switch (action) {
case 'add_member':
// Navigation vers la page de création de membre
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembreCreatePage(),
),
);
break;
case 'search_member':
_showComingSoon(context, 'Rechercher Membre', 'Recherche avancée dans la base de membres.', Icons.search, AppTheme.infoColor);
break;
case 'import_export':
_showComingSoon(context, 'Import/Export', 'Importer ou exporter les données des membres.', Icons.import_export, AppTheme.warningColor);
break;
case 'group_message':
_showComingSoon(context, 'Message Groupe', 'Envoyer un message à tous les membres ou à un groupe.', Icons.message, AppTheme.primaryColor);
break;
case 'statistics':
_showComingSoon(context, 'Statistiques', 'Analyses détaillées des données membres.', Icons.bar_chart, const Color(0xFF9C27B0));
break;
case 'reports':
_showComingSoon(context, 'Rapports', 'Génération de rapports personnalisés.', Icons.description, AppTheme.errorColor);
break;
case 'settings':
_showComingSoon(context, 'Paramètres', 'Configuration du module membres.', Icons.settings, const Color(0xFF607D8B));
break;
case 'backup':
_showComingSoon(context, 'Sauvegarde', 'Sauvegarde automatique des données.', Icons.backup, const Color(0xFF795548));
break;
case 'support':
_showComingSoon(context, 'Support', 'Aide et documentation du module.', Icons.help_outline, const Color(0xFF009688));
break;
}
}
static void _showComingSoon(BuildContext context, String title, String description, IconData icon, Color color) {
showDialog(
context: context,
builder: (context) => ComingSoonPage(
title: title,
description: description,
icon: icon,
color: color,
),
);
}
}

View File

@@ -1,339 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
import 'members_activity_item_widget.dart';
/// Widget de section d'activités récentes pour les membres
class MembersRecentActivitiesWidget extends StatelessWidget {
const MembersRecentActivitiesWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de section avec bouton "Voir tout"
Row(
children: [
const Icon(
Icons.history,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Activités Récentes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
TextButton(
onPressed: () => _showAllActivities(context),
child: const Text(
'Voir tout',
style: TextStyle(
fontSize: 12,
color: AppTheme.primaryColor,
),
),
),
],
),
const SizedBox(height: 16),
// Container des activités
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(
children: [
// Nouvelle inscription
MembersActivityItemWidget(
title: 'Nouvelle inscription',
description: 'Un nouveau membre a rejoint la communauté',
time: 'Il y a 2h',
icon: Icons.person_add,
color: AppTheme.successColor,
memberName: 'Marie Kouassi',
onTap: () => _showActivityDetails(context, 'inscription'),
),
// Mise à jour profil
MembersActivityItemWidget(
title: 'Profil mis à jour',
description: 'Informations personnelles modifiées',
time: 'Il y a 4h',
icon: Icons.edit,
color: AppTheme.infoColor,
memberName: 'Jean Baptiste',
onTap: () => _showActivityDetails(context, 'profil'),
),
// Cotisation payée
MembersActivityItemWidget(
title: 'Cotisation payée',
description: 'Paiement de cotisation mensuelle reçu',
time: 'Il y a 6h',
icon: Icons.payment,
color: AppTheme.primaryColor,
memberName: 'Fatou Traoré',
onTap: () => _showActivityDetails(context, 'cotisation'),
),
// Message envoyé
MembersActivityItemWidget(
title: 'Message de groupe',
description: 'Notification envoyée à tous les membres',
time: 'Il y a 8h',
icon: Icons.message,
color: AppTheme.warningColor,
onTap: () => _showActivityDetails(context, 'message'),
),
// Export de données
MembersActivityItemWidget(
title: 'Export de données',
description: 'Liste des membres exportée en Excel',
time: 'Il y a 1j',
icon: Icons.file_download,
color: const Color(0xFF9C27B0),
onTap: () => _showActivityDetails(context, 'export'),
),
// Sauvegarde automatique
MembersActivityItemWidget(
title: 'Sauvegarde automatique',
description: 'Données sauvegardées avec succès',
time: 'Il y a 1j',
icon: Icons.backup,
color: const Color(0xFF607D8B),
onTap: () => _showActivityDetails(context, 'sauvegarde'),
),
],
),
),
],
);
}
/// Affiche toutes les activités
static void _showAllActivities(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Handle
Container(
margin: const EdgeInsets.only(top: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// En-tête
const Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.history,
color: AppTheme.primaryColor,
size: 24,
),
SizedBox(width: 12),
Text(
'Toutes les Activités',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
),
// Liste complète
Expanded(
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 20, // Exemple avec plus d'activités
itemBuilder: (context, index) {
return MembersActivityItemWidget(
title: 'Activité ${index + 1}',
description: 'Description de l\'activité numéro ${index + 1}',
time: 'Il y a ${index + 1}h',
icon: _getActivityIcon(index),
color: _getActivityColor(index),
memberName: 'Membre ${index + 1}',
onTap: () => _showActivityDetails(context, 'activite_$index'),
);
},
),
),
],
),
),
),
);
}
/// Affiche les détails d'une activité
static void _showActivityDetails(BuildContext context, String activityType) {
String title = '';
String description = '';
IconData icon = Icons.info;
Color color = AppTheme.primaryColor;
switch (activityType) {
case 'inscription':
title = 'Nouvelle Inscription';
description = 'Marie Kouassi a rejoint la communauté avec le numéro UF-2024-00001247.';
icon = Icons.person_add;
color = AppTheme.successColor;
break;
case 'profil':
title = 'Mise à Jour Profil';
description = 'Jean Baptiste a modifié ses informations de contact et son adresse.';
icon = Icons.edit;
color = AppTheme.infoColor;
break;
case 'cotisation':
title = 'Cotisation Payée';
description = 'Fatou Traoré a payé sa cotisation mensuelle de 25,000 FCFA.';
icon = Icons.payment;
color = AppTheme.primaryColor;
break;
case 'message':
title = 'Message de Groupe';
description = 'Notification envoyée à 1,247 membres concernant la prochaine assemblée générale.';
icon = Icons.message;
color = AppTheme.warningColor;
break;
case 'export':
title = 'Export de Données';
description = 'Liste complète des membres exportée au format Excel (1,247 entrées).';
icon = Icons.file_download;
color = const Color(0xFF9C27B0);
break;
case 'sauvegarde':
title = 'Sauvegarde Automatique';
description = 'Sauvegarde quotidienne effectuée avec succès. Toutes les données sont sécurisées.';
icon = Icons.backup;
color = const Color(0xFF607D8B);
break;
default:
title = 'Activité';
description = 'Détails de l\'activité sélectionnée.';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: 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 SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(fontSize: 18),
),
),
],
),
content: Text(description),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Action spécifique selon le type
},
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
),
child: const Text('Voir plus'),
),
],
),
);
}
/// Retourne une icône selon l'index
static IconData _getActivityIcon(int index) {
final icons = [
Icons.person_add,
Icons.edit,
Icons.payment,
Icons.message,
Icons.file_download,
Icons.backup,
Icons.notifications,
Icons.security,
Icons.update,
Icons.sync,
];
return icons[index % icons.length];
}
/// Retourne une couleur selon l'index
static Color _getActivityColor(int index) {
final colors = [
AppTheme.successColor,
AppTheme.infoColor,
AppTheme.primaryColor,
AppTheme.warningColor,
const Color(0xFF9C27B0),
const Color(0xFF607D8B),
AppTheme.errorColor,
const Color(0xFF009688),
const Color(0xFF795548),
const Color(0xFFFF5722),
];
return colors[index % colors.length];
}
}

View File

@@ -1,396 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de recherche intelligente pour les membres
class MembersSmartSearchWidget extends StatefulWidget {
final Function(String) onSearch;
final Function(Map<String, dynamic>) onSuggestionSelected;
final List<Map<String, dynamic>> recentSearches;
const MembersSmartSearchWidget({
super.key,
required this.onSearch,
required this.onSuggestionSelected,
this.recentSearches = const [],
});
@override
State<MembersSmartSearchWidget> createState() => _MembersSmartSearchWidgetState();
}
class _MembersSmartSearchWidgetState extends State<MembersSmartSearchWidget>
with TickerProviderStateMixin {
final TextEditingController _searchController = TextEditingController();
final FocusNode _focusNode = FocusNode();
Timer? _debounceTimer;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
bool _isSearching = false;
bool _showSuggestions = false;
List<Map<String, dynamic>> _suggestions = [];
List<Map<String, dynamic>> _searchHistory = [];
// Suggestions prédéfinies
final List<Map<String, dynamic>> _predefinedSuggestions = [
{
'type': 'quick_filter',
'title': 'Nouveaux membres',
'subtitle': 'Inscrits ce mois',
'icon': Icons.person_add,
'color': AppTheme.successColor,
'filter': {'timeRange': '30 jours', 'status': 'Actif'},
},
{
'type': 'quick_filter',
'title': 'Membres inactifs',
'subtitle': 'Sans activité récente',
'icon': Icons.person_off,
'color': AppTheme.warningColor,
'filter': {'status': 'Inactif'},
},
{
'type': 'quick_filter',
'title': 'Bureau exécutif',
'subtitle': 'Responsables',
'icon': Icons.admin_panel_settings,
'color': AppTheme.primaryColor,
'filter': {'role': 'Bureau'},
},
{
'type': 'quick_filter',
'title': 'Jeunes membres',
'subtitle': '18-30 ans',
'icon': Icons.people,
'color': AppTheme.infoColor,
'filter': {'ageRange': '18-30'},
},
];
@override
void initState() {
super.initState();
_searchHistory = List.from(widget.recentSearches);
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.95, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_focusNode.addListener(_onFocusChanged);
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_debounceTimer?.cancel();
_animationController.dispose();
_focusNode.dispose();
_searchController.dispose();
super.dispose();
}
void _onFocusChanged() {
setState(() {
_showSuggestions = _focusNode.hasFocus;
if (_showSuggestions) {
_animationController.forward();
_updateSuggestions();
} else {
_animationController.reverse();
}
});
}
void _onSearchChanged() {
final query = _searchController.text;
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
if (query.isNotEmpty) {
widget.onSearch(query);
_addToSearchHistory(query);
}
_updateSuggestions();
});
}
void _updateSuggestions() {
final query = _searchController.text.toLowerCase();
List<Map<String, dynamic>> suggestions = [];
if (query.isEmpty) {
// Afficher les suggestions rapides et l'historique
suggestions.addAll(_predefinedSuggestions);
if (_searchHistory.isNotEmpty) {
suggestions.add({
'type': 'divider',
'title': 'Recherches récentes',
});
suggestions.addAll(_searchHistory.take(3));
}
} else {
// Filtrer les suggestions basées sur la requête
suggestions.addAll(_predefinedSuggestions.where((suggestion) =>
suggestion['title'].toString().toLowerCase().contains(query) ||
suggestion['subtitle'].toString().toLowerCase().contains(query)));
// Ajouter des suggestions de membres simulées
suggestions.addAll(_generateMemberSuggestions(query));
}
setState(() {
_suggestions = suggestions;
});
}
List<Map<String, dynamic>> _generateMemberSuggestions(String query) {
// Simulation de suggestions de membres basées sur la requête
final memberSuggestions = <Map<String, dynamic>>[];
if (query.length >= 2) {
memberSuggestions.addAll([
{
'type': 'member',
'title': 'Jean-Baptiste Kouassi',
'subtitle': 'MBR001 • Actif',
'icon': Icons.person,
'color': AppTheme.primaryColor,
'memberId': 'c6ccf741-c55f-390e-96a7-531819fed1dd',
},
{
'type': 'member',
'title': 'Aminata Traoré',
'subtitle': 'MBR002 • Actif',
'icon': Icons.person,
'color': AppTheme.successColor,
'memberId': '9f4ea9cb-798b-3b1c-8444-4b313af999bd',
},
].where((member) =>
member['title'].toString().toLowerCase().contains(query)).toList());
}
return memberSuggestions;
}
void _addToSearchHistory(String query) {
final historyItem = {
'type': 'history',
'title': query,
'subtitle': 'Recherche récente',
'icon': Icons.history,
'color': AppTheme.textSecondary,
'timestamp': DateTime.now(),
};
setState(() {
_searchHistory.removeWhere((item) => item['title'] == query);
_searchHistory.insert(0, historyItem);
if (_searchHistory.length > 10) {
_searchHistory = _searchHistory.take(10).toList();
}
});
}
void _onSuggestionTap(Map<String, dynamic> suggestion) {
switch (suggestion['type']) {
case 'quick_filter':
widget.onSuggestionSelected(suggestion);
_searchController.text = suggestion['title'];
break;
case 'member':
widget.onSuggestionSelected(suggestion);
_searchController.text = suggestion['title'];
break;
case 'history':
_searchController.text = suggestion['title'];
widget.onSearch(suggestion['title']);
break;
}
_focusNode.unfocus();
}
void _clearSearch() {
_searchController.clear();
widget.onSearch('');
_focusNode.unfocus();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Barre de recherche
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: TextField(
controller: _searchController,
focusNode: _focusNode,
decoration: InputDecoration(
hintText: 'Rechercher un membre, rôle, statut...',
hintStyle: const TextStyle(
color: AppTheme.textHint,
fontSize: 14,
),
prefixIcon: const Icon(
Icons.search,
color: AppTheme.primaryColor,
size: 20,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(
Icons.clear,
color: AppTheme.textSecondary,
size: 20,
),
onPressed: _clearSearch,
)
: const Icon(
Icons.mic,
color: AppTheme.textHint,
size: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[50],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
),
),
),
// Suggestions
if (_showSuggestions && _suggestions.isNotEmpty)
ScaleTransition(
scale: _scaleAnimation,
child: Container(
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: _suggestions.map((suggestion) {
if (suggestion['type'] == 'divider') {
return _buildDivider(suggestion['title']);
}
return _buildSuggestionItem(suggestion);
}).toList(),
),
),
),
],
);
}
Widget _buildDivider(String title) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 1,
color: Colors.grey[200],
),
),
],
),
);
}
Widget _buildSuggestionItem(Map<String, dynamic> suggestion) {
return InkWell(
onTap: () => _onSuggestionTap(suggestion),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: suggestion['color'].withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
suggestion['icon'],
color: suggestion['color'],
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suggestion['title'],
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
if (suggestion['subtitle'] != null)
Text(
suggestion['subtitle'],
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
const Icon(
Icons.north_west,
color: AppTheme.textHint,
size: 16,
),
],
),
),
);
}
}

View File

@@ -1,380 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../core/models/membre_model.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de statistiques avancées pour les membres
class MembersStatsWidget extends StatelessWidget {
final List<MembreModel> members;
final String searchQuery;
final Map<String, dynamic> filters;
const MembersStatsWidget({
super.key,
required this.members,
this.searchQuery = '',
this.filters = const {},
});
@override
Widget build(BuildContext context) {
final stats = _calculateStats();
return Container(
margin: const EdgeInsets.only(bottom: 16),
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
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 12),
const Text(
'Statistiques des membres',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const Spacer(),
if (searchQuery.isNotEmpty || filters.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Filtré',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 16),
// Statistiques principales
Row(
children: [
Expanded(
child: _buildStatCard(
'Total',
stats['total'].toString(),
Icons.people,
AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Actifs',
stats['actifs'].toString(),
Icons.check_circle,
AppTheme.successColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Âge moyen',
'${stats['ageMoyen']} ans',
Icons.cake,
AppTheme.warningColor,
),
),
],
),
const SizedBox(height: 16),
// Statistiques détaillées
Row(
children: [
Expanded(
child: _buildDetailedStat(
'Nouveaux (30j)',
stats['nouveaux'].toString(),
stats['nouveauxPourcentage'],
AppTheme.infoColor,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildDetailedStat(
'Anciens (>1an)',
stats['anciens'].toString(),
stats['anciensPourcentage'],
AppTheme.secondaryColor,
),
),
],
),
if (stats['repartitionAge'].isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 12),
// Répartition par âge
const Text(
'Répartition par âge',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
_buildAgeDistribution(stats['repartitionAge']),
],
],
),
);
}
Map<String, dynamic> _calculateStats() {
if (members.isEmpty) {
return {
'total': 0,
'actifs': 0,
'ageMoyen': 0,
'nouveaux': 0,
'nouveauxPourcentage': 0.0,
'anciens': 0,
'anciensPourcentage': 0.0,
'repartitionAge': <String, int>{},
};
}
final now = DateTime.now();
final total = members.length;
final actifs = members.where((m) => m.statut.toUpperCase() == 'ACTIF').length;
// Calcul de l'âge moyen
final ages = members.map((m) => m.age).where((age) => age > 0).toList();
final ageMoyen = ages.isNotEmpty ? (ages.reduce((a, b) => a + b) / ages.length).round() : 0;
// Nouveaux membres (moins de 30 jours)
final nouveaux = members.where((m) {
final daysDiff = now.difference(m.dateAdhesion).inDays;
return daysDiff <= 30;
}).length;
final nouveauxPourcentage = total > 0 ? (nouveaux / total * 100) : 0.0;
// Anciens membres (plus d'un an)
final anciens = members.where((m) {
final daysDiff = now.difference(m.dateAdhesion).inDays;
return daysDiff > 365;
}).length;
final anciensPourcentage = total > 0 ? (anciens / total * 100) : 0.0;
// Répartition par tranche d'âge
final repartitionAge = <String, int>{};
for (final member in members) {
final age = member.age;
String tranche;
if (age < 25) {
tranche = '18-24';
} else if (age < 35) {
tranche = '25-34';
} else if (age < 45) {
tranche = '35-44';
} else if (age < 55) {
tranche = '45-54';
} else {
tranche = '55+';
}
repartitionAge[tranche] = (repartitionAge[tranche] ?? 0) + 1;
}
return {
'total': total,
'actifs': actifs,
'ageMoyen': ageMoyen,
'nouveaux': nouveaux,
'nouveauxPourcentage': nouveauxPourcentage,
'anciens': anciens,
'anciensPourcentage': anciensPourcentage,
'repartitionAge': repartitionAge,
};
}
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildDetailedStat(String label, String value, double percentage, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 8),
Text(
'(${percentage.toStringAsFixed(1)}%)',
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildAgeDistribution(Map<String, int> repartition) {
final total = repartition.values.fold(0, (sum, count) => sum + count);
if (total == 0) return const SizedBox.shrink();
return Column(
children: repartition.entries.map((entry) {
final percentage = (entry.value / total * 100);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
SizedBox(
width: 50,
child: Text(
entry.key,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 6,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(3),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: percentage / 100,
child: Container(
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(3),
),
),
),
),
),
const SizedBox(width: 8),
SizedBox(
width: 40,
child: Text(
'${entry.value}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.right,
),
),
],
),
);
}).toList(),
);
}
}

View File

@@ -1,109 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/theme/app_theme.dart';
/// Widget de section d'accueil pour le dashboard des membres
class MembersWelcomeSectionWidget extends StatelessWidget {
const MembersWelcomeSectionWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestion des Membres',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(
'Tableau de bord complet',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: const Row(
children: [
Icon(
Icons.info_outline,
color: Colors.white70,
size: 16,
),
SizedBox(width: 8),
Expanded(
child: Text(
'Suivez l\'évolution de votre communauté en temps réel',
style: TextStyle(
fontSize: 12,
color: Colors.white70,
),
),
),
],
),
),
],
),
);
}
}

View File

@@ -1,211 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Container professionnel pour les graphiques du dashboard avec animations
class DashboardChartCard extends StatefulWidget {
const DashboardChartCard({
super.key,
required this.title,
required this.child,
this.subtitle,
this.actions,
this.height,
this.isLoading = false,
this.onRefresh,
this.showBorder = true,
});
final String title;
final Widget child;
final String? subtitle;
final List<Widget>? actions;
final double? height;
final bool isLoading;
final VoidCallback? onRefresh;
final bool showBorder;
@override
State<DashboardChartCard> createState() => _DashboardChartCardState();
}
class _DashboardChartCardState extends State<DashboardChartCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationMedium,
vsync: this,
);
_slideAnimation = Tween<double>(
begin: 30.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurveEnter,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: FadeTransition(
opacity: _fadeAnimation,
child: _buildCard(),
),
);
},
);
}
Widget _buildCard() {
return Container(
height: widget.height,
padding: EdgeInsets.all(DesignSystem.spacingLg),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: DesignSystem.shadowCard,
border: widget.showBorder ? Border.all(
color: AppTheme.borderColor.withOpacity(0.5),
width: 1,
) : null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: DesignSystem.spacingLg),
Expanded(
child: widget.isLoading ? _buildLoadingState() : widget.child,
),
],
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: DesignSystem.headlineMedium.copyWith(
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
SizedBox(height: DesignSystem.spacingXs),
Text(
widget.subtitle!,
style: DesignSystem.bodyMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
],
),
),
if (widget.actions != null || widget.onRefresh != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.onRefresh != null)
_buildRefreshButton(),
if (widget.actions != null) ...widget.actions!,
],
),
],
);
}
Widget _buildRefreshButton() {
return Container(
margin: EdgeInsets.only(right: DesignSystem.spacingSm),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onRefresh,
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
child: Container(
padding: EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
),
child: const Icon(
Icons.refresh,
size: 18,
color: AppTheme.primaryColor,
),
),
),
),
);
}
Widget _buildLoadingState() {
return Column(
children: [
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
AppTheme.primaryColor.withOpacity(0.7),
),
),
),
SizedBox(height: DesignSystem.spacingMd),
Text(
'Chargement des données...',
style: DesignSystem.bodyMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
),
),
),
],
);
}
}

View File

@@ -1,299 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Card statistique professionnelle avec design basé sur le nombre d'or
class DashboardStatCard extends StatefulWidget {
const DashboardStatCard({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
this.trend,
this.subtitle,
this.onTap,
this.isLoading = false,
});
final String title;
final String value;
final IconData icon;
final Color color;
final String? trend;
final String? subtitle;
final VoidCallback? onTap;
final bool isLoading;
@override
State<DashboardStatCard> createState() => _DashboardStatCardState();
}
class _DashboardStatCardState extends State<DashboardStatCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationMedium,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurveEnter,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: FadeTransition(
opacity: _fadeAnimation,
child: _buildCard(context),
),
);
},
);
}
Widget _buildCard(BuildContext context) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: DesignSystem.animationFast,
curve: DesignSystem.animationCurve,
padding: const EdgeInsets.all(DesignSystem.spacingLg),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: _isHovered ? DesignSystem.shadowCardHover : DesignSystem.shadowCard,
border: Border.all(
color: widget.color.withOpacity(0.1),
width: 1,
),
),
child: widget.isLoading ? _buildLoadingState() : _buildContent(),
),
),
);
}
Widget _buildLoadingState() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildShimmer(40, 40, isCircular: true),
if (widget.trend != null) _buildShimmer(60, 24, radius: 12),
],
),
const SizedBox(height: DesignSystem.spacingMd),
_buildShimmer(80, 32),
const SizedBox(height: DesignSystem.spacingSm),
_buildShimmer(120, 16),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
_buildShimmer(100, 14),
],
],
);
}
Widget _buildShimmer(double width, double height, {double? radius, bool isCircular = false}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppTheme.textHint.withOpacity(0.1),
borderRadius: isCircular
? BorderRadius.circular(height / 2)
: BorderRadius.circular(radius ?? DesignSystem.radiusSm),
),
);
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: DesignSystem.goldenHeight(DesignSystem.spacingLg)),
_buildValue(),
const SizedBox(height: DesignSystem.spacingSm),
_buildTitle(),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
_buildSubtitle(),
],
],
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildIconContainer(),
if (widget.trend != null) _buildTrendBadge(),
],
);
}
Widget _buildIconContainer() {
return Container(
width: DesignSystem.goldenWidth(32),
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
widget.color.withOpacity(0.15),
widget.color.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
border: Border.all(
color: widget.color.withOpacity(0.2),
width: 1,
),
),
child: Icon(
widget.icon,
color: widget.color,
size: 20,
),
);
}
Widget _buildTrendBadge() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingSm,
vertical: DesignSystem.spacingXs,
),
decoration: BoxDecoration(
color: _getTrendColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusXl),
border: Border.all(
color: _getTrendColor().withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getTrendIcon(),
color: _getTrendColor(),
size: 14,
),
const SizedBox(width: DesignSystem.spacing2xs),
Text(
widget.trend!,
style: DesignSystem.labelSmall.copyWith(
color: _getTrendColor(),
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildValue() {
return Text(
widget.value,
style: DesignSystem.displayMedium.copyWith(
color: widget.color,
fontWeight: FontWeight.w800,
fontSize: 28,
),
);
}
Widget _buildTitle() {
return Text(
widget.title,
style: DesignSystem.labelLarge.copyWith(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
);
}
Widget _buildSubtitle() {
return Text(
widget.subtitle!,
style: DesignSystem.labelMedium.copyWith(
color: AppTheme.textHint,
),
);
}
void _setHovered(bool hovered) {
if (mounted) {
setState(() {
_isHovered = hovered;
});
}
}
Color _getTrendColor() {
if (widget.trend == null) return AppTheme.textSecondary;
if (widget.trend!.startsWith('+')) {
return AppTheme.successColor;
} else if (widget.trend!.startsWith('-')) {
return AppTheme.errorColor;
} else {
return AppTheme.warningColor;
}
}
IconData _getTrendIcon() {
if (widget.trend == null) return Icons.trending_flat;
if (widget.trend!.startsWith('+')) {
return Icons.trending_up;
} else if (widget.trend!.startsWith('-')) {
return Icons.trending_down;
} else {
return Icons.trending_flat;
}
}
}

View File

@@ -1,341 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/error/error_handler.dart';
import '../../../../core/validation/form_validator.dart';
import '../../../../core/feedback/user_feedback.dart';
import '../../../../core/animations/loading_animations.dart';
import '../../../../core/animations/page_transitions.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget de démonstration des nouvelles fonctionnalités d'erreur et validation
class ErrorDemoWidget extends StatefulWidget {
const ErrorDemoWidget({super.key});
@override
State<ErrorDemoWidget> createState() => _ErrorDemoWidgetState();
}
class _ErrorDemoWidgetState extends State<ErrorDemoWidget> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Démonstration Gestion d\'Erreurs'),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Test des nouvelles fonctionnalités',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 24),
// Champ nom avec validation
ValidatedTextField(
controller: _nameController,
label: 'Nom complet *',
hintText: 'Entrez votre nom',
prefixIcon: Icons.person,
validators: [
(value) => FormValidator.name(value, fieldName: 'Le nom'),
],
),
const SizedBox(height: 16),
// Champ email avec validation
ValidatedTextField(
controller: _emailController,
label: 'Email *',
hintText: 'exemple@email.com',
prefixIcon: Icons.email,
keyboardType: TextInputType.emailAddress,
validators: [
(value) => FormValidator.email(value),
],
),
const SizedBox(height: 16),
// Champ téléphone avec validation
ValidatedTextField(
controller: _phoneController,
label: 'Téléphone *',
hintText: '+225XXXXXXXX',
prefixIcon: Icons.phone,
keyboardType: TextInputType.phone,
validators: [
(value) => FormValidator.phone(value),
],
),
const SizedBox(height: 32),
// Boutons de test
const Text(
'Tests de feedback utilisateur :',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
// Boutons de test des messages
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: () => UserFeedback.showSuccess(
context,
'Opération réussie !',
),
icon: const Icon(Icons.check_circle),
label: const Text('Succès'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () => UserFeedback.showWarning(
context,
'Attention : vérifiez vos données',
),
icon: const Icon(Icons.warning),
label: const Text('Avertissement'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.warningColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () => UserFeedback.showInfo(
context,
'Information importante',
),
icon: const Icon(Icons.info),
label: const Text('Info'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.infoColor,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Boutons de test des dialogues
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: () => _testConfirmationDialog(),
icon: const Icon(Icons.help_outline),
label: const Text('Confirmation'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () => _testInputDialog(),
icon: const Icon(Icons.edit),
label: const Text('Saisie'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: () => _testErrorDialog(),
icon: const Icon(Icons.error),
label: const Text('Erreur'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Bouton de test du chargement
ElevatedButton.icon(
onPressed: () => _testLoadingDialog(),
icon: const Icon(Icons.hourglass_empty),
label: const Text('Test Chargement'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentColor,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 32),
// Section animations de chargement
const Text(
'Animations de chargement :',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
// Démonstration des animations de chargement
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
LoadingAnimations.dots(),
const SizedBox(height: 8),
const Text('Points', style: TextStyle(fontSize: 12)),
],
),
Column(
children: [
LoadingAnimations.waves(),
const SizedBox(height: 8),
const Text('Vagues', style: TextStyle(fontSize: 12)),
],
),
Column(
children: [
LoadingAnimations.spinner(),
const SizedBox(height: 8),
const Text('Spinner', style: TextStyle(fontSize: 12)),
],
),
Column(
children: [
LoadingAnimations.pulse(),
const SizedBox(height: 8),
const Text('Pulse', style: TextStyle(fontSize: 12)),
],
),
],
),
const SizedBox(height: 16),
LoadingAnimations.skeleton(height: 60),
const SizedBox(height: 8),
const Text('Skeleton Loader', style: TextStyle(fontSize: 12)),
],
),
),
const SizedBox(height: 32),
// Bouton de validation du formulaire
ElevatedButton.icon(
onPressed: () => _validateForm(),
icon: const Icon(Icons.check),
label: const Text('Valider le formulaire'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
),
);
}
Future<void> _testConfirmationDialog() async {
final result = await UserFeedback.showConfirmation(
context,
title: 'Confirmer l\'action',
message: 'Êtes-vous sûr de vouloir continuer cette opération ?',
icon: Icons.help_outline,
);
if (result) {
UserFeedback.showSuccess(context, 'Action confirmée !');
} else {
UserFeedback.showInfo(context, 'Action annulée');
}
}
Future<void> _testInputDialog() async {
final result = await UserFeedback.showInputDialog(
context,
title: 'Saisir une valeur',
label: 'Votre commentaire',
hintText: 'Tapez votre commentaire ici...',
validator: (value) => FormValidator.required(value, fieldName: 'Le commentaire'),
);
if (result != null && result.isNotEmpty) {
UserFeedback.showSuccess(context, 'Commentaire saisi : "$result"');
}
}
Future<void> _testErrorDialog() async {
await ErrorHandler.showErrorDialog(
context,
Exception('Erreur de démonstration'),
title: 'Erreur de test',
customMessage: 'Ceci est une erreur de démonstration pour tester le système de gestion d\'erreurs.',
onRetry: () => UserFeedback.showInfo(context, 'Tentative de nouvelle opération...'),
);
}
Future<void> _testLoadingDialog() async {
UserFeedback.showLoading(context, message: 'Traitement en cours...');
// Simuler une opération longue
await Future.delayed(const Duration(seconds: 3));
UserFeedback.hideLoading(context);
UserFeedback.showSuccess(context, 'Opération terminée !');
}
void _validateForm() {
if (_formKey.currentState?.validate() ?? false) {
UserFeedback.showSuccess(
context,
'Formulaire valide ! Toutes les données sont correctes.',
);
} else {
UserFeedback.showWarning(
context,
'Veuillez corriger les erreurs dans le formulaire',
);
}
}
}

View File

@@ -1,427 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
class MemberCard extends StatefulWidget {
final Map<String, dynamic> member;
final VoidCallback? onTap;
final VoidCallback? onEdit;
const MemberCard({
super.key,
required this.member,
this.onTap,
this.onEdit,
});
@override
State<MemberCard> createState() => _MemberCardState();
}
class _MemberCardState extends State<MemberCard>
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.98,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext 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: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: _getStatusColor().withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Row(
children: [
_buildAvatar(),
const SizedBox(width: 16),
Expanded(
child: _buildMemberInfo(),
),
_buildStatusBadge(),
],
),
const SizedBox(height: 16),
_buildMemberDetails(),
const SizedBox(height: 12),
_buildActionButtons(),
],
),
),
),
),
);
},
);
}
Widget _buildAvatar() {
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getStatusColor(),
_getStatusColor().withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: _getStatusColor().withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: widget.member['avatar'] != null
? ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Image.network(
widget.member['avatar'],
fit: BoxFit.cover,
),
)
: Center(
child: Text(
_getInitials(),
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildMemberInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.member['firstName']} ${widget.member['lastName']}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
widget.member['role'],
style: TextStyle(
fontSize: 14,
color: _getStatusColor(),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
widget.member['email'],
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildStatusBadge() {
final isActive = widget.member['status'] == 'Actif';
final color = isActive ? AppTheme.successColor : AppTheme.textHint;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 6),
Text(
widget.member['status'],
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildMemberDetails() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildDetailRow(
icon: Icons.phone,
label: 'Téléphone',
value: widget.member['phone'],
color: AppTheme.infoColor,
),
const SizedBox(height: 8),
_buildDetailRow(
icon: Icons.calendar_today,
label: 'Adhésion',
value: _formatDate(widget.member['joinDate']),
color: AppTheme.primaryColor,
),
const SizedBox(height: 8),
_buildDetailRow(
icon: Icons.payment,
label: 'Cotisation',
value: widget.member['cotisationStatus'],
color: _getCotisationColor(),
),
],
),
);
}
Widget _buildDetailRow({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
icon,
size: 16,
color: color,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: label == 'Cotisation' ? color : AppTheme.textPrimary,
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _callMember,
icon: const Icon(Icons.phone, size: 16),
label: const Text('Appeler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.infoColor,
side: BorderSide(color: AppTheme.infoColor.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _emailMember,
icon: const Icon(Icons.email, size: 16),
label: const Text('Email'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
side: BorderSide(color: AppTheme.primaryColor.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
onPressed: widget.onEdit,
icon: const Icon(Icons.edit, size: 18),
color: AppTheme.secondaryColor,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
),
),
],
);
}
String _getInitials() {
final firstName = widget.member['firstName'] as String;
final lastName = widget.member['lastName'] as String;
return '${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}'.toUpperCase();
}
Color _getStatusColor() {
switch (widget.member['role']) {
case 'Président':
return AppTheme.primaryColor;
case 'Secrétaire':
return AppTheme.secondaryColor;
case 'Trésorier':
return AppTheme.accentColor;
case 'Responsable événements':
return AppTheme.warningColor;
default:
return AppTheme.infoColor;
}
}
Color _getCotisationColor() {
switch (widget.member['cotisationStatus']) {
case 'À jour':
return AppTheme.successColor;
case 'En retard':
return AppTheme.errorColor;
case 'Exempt':
return AppTheme.infoColor;
default:
return AppTheme.textSecondary;
}
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
final months = [
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
} catch (e) {
return dateString;
}
}
void _handleTap() {
HapticFeedback.selectionClick();
widget.onTap?.call();
}
void _callMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Appel vers ${widget.member['phone']} - En développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _emailMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email vers ${widget.member['email']} - En développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -1,377 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
class MembersFilterSheet extends StatefulWidget {
final String selectedFilter;
final Function(String) onFilterChanged;
const MembersFilterSheet({
super.key,
required this.selectedFilter,
required this.onFilterChanged,
});
@override
State<MembersFilterSheet> createState() => _MembersFilterSheetState();
}
class _MembersFilterSheetState extends State<MembersFilterSheet>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _slideAnimation;
late Animation<double> _fadeAnimation;
String _tempSelectedFilter = '';
final List<Map<String, dynamic>> _filterOptions = [
{
'value': 'Tous',
'label': 'Tous les membres',
'icon': Icons.people,
'color': AppTheme.primaryColor,
'description': 'Afficher tous les membres',
},
{
'value': 'Actifs',
'label': 'Membres actifs',
'icon': Icons.check_circle,
'color': AppTheme.successColor,
'description': 'Membres avec un statut actif',
},
{
'value': 'Inactifs',
'label': 'Membres inactifs',
'icon': Icons.pause_circle,
'color': AppTheme.textHint,
'description': 'Membres avec un statut inactif',
},
{
'value': 'Bureau',
'label': 'Membres du bureau',
'icon': Icons.star,
'color': AppTheme.warningColor,
'description': 'Président, secrétaire, trésorier',
},
{
'value': 'En retard',
'label': 'Cotisations en retard',
'icon': Icons.warning,
'color': AppTheme.errorColor,
'description': 'Membres avec cotisations impayées',
},
];
@override
void initState() {
super.initState();
_tempSelectedFilter = widget.selectedFilter;
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_slideAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 1.0, curve: Curves.easeOut),
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5 * _fadeAnimation.value),
),
child: Align(
alignment: Alignment.bottomCenter,
child: Transform.translate(
offset: Offset(0, MediaQuery.of(context).size.height * _slideAnimation.value),
child: Container(
width: double.infinity,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHandle(),
_buildHeader(),
Flexible(child: _buildFilterOptions()),
_buildActionButtons(),
],
),
),
),
),
);
},
);
}
Widget _buildHandle() {
return Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.textHint.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.filter_list,
color: AppTheme.secondaryColor,
size: 20,
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filtrer les membres',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Text(
'Sélectionnez un critère de filtrage',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
IconButton(
onPressed: _closeSheet,
icon: Icon(
Icons.close,
color: AppTheme.textHint,
),
),
],
),
);
}
Widget _buildFilterOptions() {
return ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
itemCount: _filterOptions.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final option = _filterOptions[index];
final isSelected = _tempSelectedFilter == option['value'];
return _buildFilterOption(
option: option,
isSelected: isSelected,
onTap: () => _selectFilter(option['value']),
);
},
);
}
Widget _buildFilterOption({
required Map<String, dynamic> option,
required bool isSelected,
required VoidCallback onTap,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? option['color'].withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? option['color']
: AppTheme.textHint.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: option['color'].withOpacity(isSelected ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
option['icon'],
color: option['color'],
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option['label'],
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? option['color'] : AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
option['description'],
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
AnimatedOpacity(
opacity: isSelected ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.check_circle,
color: option['color'],
size: 24,
),
),
],
),
),
),
);
}
Widget _buildActionButtons() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _resetFilter,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
side: BorderSide(color: AppTheme.textHint.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Réinitialiser'),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _applyFilter,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0,
),
child: const Text(
'Appliquer',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
),
);
}
void _selectFilter(String filter) {
setState(() {
_tempSelectedFilter = filter;
});
}
void _resetFilter() {
setState(() {
_tempSelectedFilter = 'Tous';
});
}
void _applyFilter() {
widget.onFilterChanged(_tempSelectedFilter);
_closeSheet();
}
void _closeSheet() {
_animationController.reverse().then((_) {
if (mounted) {
Navigator.of(context).pop();
}
});
}
}

View File

@@ -1,133 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
class MembersSearchBar extends StatefulWidget {
final TextEditingController controller;
final Function(String) onChanged;
final VoidCallback onClear;
const MembersSearchBar({
super.key,
required this.controller,
required this.onChanged,
required this.onClear,
});
@override
State<MembersSearchBar> createState() => _MembersSearchBarState();
}
class _MembersSearchBarState extends State<MembersSearchBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
bool _hasText = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
widget.controller.addListener(_onTextChanged);
_animationController.forward();
}
@override
void dispose() {
widget.controller.removeListener(_onTextChanged);
_animationController.dispose();
super.dispose();
}
void _onTextChanged() {
final hasText = widget.controller.text.isNotEmpty;
if (hasText != _hasText) {
setState(() {
_hasText = hasText;
});
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _fadeAnimation.value)),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: widget.controller,
onChanged: widget.onChanged,
style: const TextStyle(
fontSize: 16,
color: AppTheme.textPrimary,
),
decoration: InputDecoration(
hintText: 'Rechercher un membre...',
hintStyle: TextStyle(
color: AppTheme.textHint,
fontSize: 16,
),
prefixIcon: Icon(
Icons.search,
color: AppTheme.secondaryColor,
size: 24,
),
suffixIcon: _hasText
? AnimatedOpacity(
opacity: _hasText ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: IconButton(
icon: Icon(
Icons.clear,
color: AppTheme.textHint,
size: 20,
),
onPressed: widget.onClear,
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
),
),
),
);
},
);
}
}

View File

@@ -1,456 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
import '../pages/membre_edit_page.dart';
/// Section des actions disponibles pour un membre
class MembreActionsSection extends StatelessWidget {
const MembreActionsSection({
super.key,
required this.membre,
this.onEdit,
this.onDelete,
this.onExport,
this.onCall,
this.onMessage,
this.onEmail,
});
final MembreModel membre;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
final VoidCallback? onExport;
final VoidCallback? onCall;
final VoidCallback? onMessage;
final VoidCallback? onEmail;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.settings,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Actions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
_buildActionGrid(context),
],
),
),
);
}
Widget _buildActionGrid(BuildContext context) {
return Column(
children: [
Row(
children: [
Expanded(
child: _buildActionButton(
context,
'Modifier',
Icons.edit,
AppTheme.primaryColor,
onEdit ?? () => _showNotImplemented(context, 'Modification'),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionButton(
context,
'Appeler',
Icons.phone,
AppTheme.successColor,
onCall ?? () => _callMember(context),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildActionButton(
context,
'Message',
Icons.message,
AppTheme.infoColor,
onMessage ?? () => _messageMember(context),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionButton(
context,
'Email',
Icons.email,
AppTheme.warningColor,
onEmail ?? () => _emailMember(context),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildActionButton(
context,
'Exporter',
Icons.download,
AppTheme.textSecondary,
onExport ?? () => _exportMember(context),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionButton(
context,
'Supprimer',
Icons.delete,
AppTheme.errorColor,
onDelete ?? () => _deleteMember(context),
),
),
],
),
const SizedBox(height: 20),
_buildQuickInfoSection(context),
],
);
}
Widget _buildActionButton(
BuildContext context,
String label,
IconData icon,
Color color,
VoidCallback onPressed,
) {
return Material(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildQuickInfoSection(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations rapides',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
_buildQuickInfoRow(
'Numéro de membre',
membre.numeroMembre,
Icons.badge,
() => _copyToClipboard(context, membre.numeroMembre, 'Numéro de membre'),
),
_buildQuickInfoRow(
'Téléphone',
membre.telephone,
Icons.phone,
() => _copyToClipboard(context, membre.telephone, 'Téléphone'),
),
_buildQuickInfoRow(
'Email',
membre.email,
Icons.email,
() => _copyToClipboard(context, membre.email, 'Email'),
),
],
),
);
}
Widget _buildQuickInfoRow(
String label,
String value,
IconData icon,
VoidCallback onTap,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row(
children: [
Icon(icon, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
],
),
),
const Icon(
Icons.copy,
size: 14,
color: AppTheme.textHint,
),
],
),
),
),
);
}
void _callMember(BuildContext context) {
// TODO: Implémenter l'appel téléphonique
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Appeler le membre'),
content: Text('Voulez-vous appeler ${membre.prenom} ${membre.nom} au ${membre.telephone} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplemented(context, 'Appel téléphonique');
},
child: const Text('Appeler'),
),
],
),
);
}
void _messageMember(BuildContext context) {
// TODO: Implémenter l'envoi de SMS
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Envoyer un message'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Envoyer un SMS à ${membre.prenom} ${membre.nom} ?'),
const SizedBox(height: 16),
const TextField(
decoration: InputDecoration(
labelText: 'Message',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplemented(context, 'Envoi de SMS');
},
child: const Text('Envoyer'),
),
],
),
);
}
void _emailMember(BuildContext context) {
// TODO: Implémenter l'envoi d'email
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Envoyer un email'),
content: Text('Ouvrir l\'application email pour envoyer un message à ${membre.email} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplemented(context, 'Envoi d\'email');
},
child: const Text('Ouvrir'),
),
],
),
);
}
void _exportMember(BuildContext context) {
// TODO: Implémenter l'export des données du membre
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exporter les données'),
content: Text('Exporter les données de ${membre.prenom} ${membre.nom} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplemented(context, 'Export des données');
},
child: const Text('Exporter'),
),
],
),
);
}
void _deleteMember(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer le membre'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.warning,
color: AppTheme.errorColor,
size: 48,
),
const SizedBox(height: 16),
Text(
'Êtes-vous sûr de vouloir supprimer ${membre.prenom} ${membre.nom} ?',
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Cette action est irréversible.',
style: TextStyle(
color: AppTheme.errorColor,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplemented(context, 'Suppression du membre');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
child: const Text('Supprimer'),
),
],
),
);
}
void _copyToClipboard(BuildContext context, String text, String label) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label copié dans le presse-papiers'),
duration: const Duration(seconds: 2),
backgroundColor: AppTheme.successColor,
),
);
}
void _showNotImplemented(BuildContext context, String feature) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - Fonctionnalité à implémenter'),
backgroundColor: AppTheme.infoColor,
),
);
}
}

View File

@@ -1,282 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Card pour afficher un membre dans la liste
class MembreCard extends StatelessWidget {
const MembreCard({
super.key,
required this.membre,
this.onTap,
this.onEdit,
this.onDelete,
});
final MembreModel membre;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec avatar et actions
Row(
children: [
// Avatar
CircleAvatar(
radius: 24,
backgroundColor: AppTheme.primaryColor,
child: Text(
membre.initiales,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
const SizedBox(width: 12),
// Informations principales
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.nomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontFamily: 'monospace',
),
),
],
),
),
// Badge de statut
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getStatusColor(membre.statut).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _getStatusColor(membre.statut),
width: 1,
),
),
child: Text(
_getStatusLabel(membre.statut),
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _getStatusColor(membre.statut),
),
),
),
// Menu d'actions
PopupMenuButton<String>(
icon: const Icon(
Icons.more_vert,
color: AppTheme.textSecondary,
),
onSelected: (value) {
switch (value) {
case 'edit':
onEdit?.call();
break;
case 'delete':
onDelete?.call();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 16),
SizedBox(width: 8),
Text('Modifier'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 16, color: AppTheme.errorColor),
SizedBox(width: 8),
Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)),
],
),
),
],
),
],
),
const SizedBox(height: 12),
// Informations de contact
Row(
children: [
Expanded(
child: _buildInfoItem(
icon: Icons.email_outlined,
text: membre.email,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildInfoItem(
icon: Icons.phone_outlined,
text: membre.telephone,
),
),
],
),
// Adresse si disponible
if (membre.adresseComplete.isNotEmpty) ...[
const SizedBox(height: 8),
_buildInfoItem(
icon: Icons.location_on_outlined,
text: membre.adresseComplete,
),
],
// Profession si disponible
if (membre.profession?.isNotEmpty == true) ...[
const SizedBox(height: 8),
_buildInfoItem(
icon: Icons.work_outline,
text: membre.profession!,
),
],
const SizedBox(height: 8),
// Footer avec date d'adhésion
Row(
children: [
Icon(
Icons.calendar_today_outlined,
size: 14,
color: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
'Membre depuis ${_formatDate(membre.dateAdhesion)}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
],
),
),
),
);
}
/// Widget pour afficher une information avec icône
Widget _buildInfoItem({
required IconData icon,
required String text,
}) {
return Row(
children: [
Icon(
icon,
size: 14,
color: AppTheme.textSecondary,
),
const SizedBox(width: 4),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
/// Retourne la couleur associée au statut
Color _getStatusColor(String statut) {
switch (statut.toUpperCase()) {
case 'ACTIF':
return AppTheme.successColor;
case 'INACTIF':
return AppTheme.warningColor;
case 'SUSPENDU':
return AppTheme.errorColor;
default:
return AppTheme.textSecondary;
}
}
/// Retourne le label du statut
String _getStatusLabel(String statut) {
switch (statut.toUpperCase()) {
case 'ACTIF':
return 'ACTIF';
case 'INACTIF':
return 'INACTIF';
case 'SUSPENDU':
return 'SUSPENDU';
default:
return statut.toUpperCase();
}
}
/// Formate une date pour l'affichage
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays < 30) {
return '${difference.inDays} jours';
} else if (difference.inDays < 365) {
final months = (difference.inDays / 30).floor();
return '$months mois';
} else {
final years = (difference.inDays / 365).floor();
return '$years an${years > 1 ? 's' : ''}';
}
}
}

View File

@@ -1,431 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Section des cotisations d'un membre
class MembreCotisationsSection extends StatelessWidget {
const MembreCotisationsSection({
super.key,
required this.membre,
required this.cotisations,
required this.isLoading,
this.onRefresh,
});
final MembreModel membre;
final List<CotisationModel> cotisations;
final bool isLoading;
final VoidCallback? onRefresh;
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des cotisations...'),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
onRefresh?.call();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummaryCard(),
const SizedBox(height: 16),
_buildCotisationsList(),
],
),
),
);
}
Widget _buildSummaryCard() {
final totalDu = cotisations.fold<double>(
0,
(sum, cotisation) => sum + cotisation.montantDu,
);
final totalPaye = cotisations.fold<double>(
0,
(sum, cotisation) => sum + cotisation.montantPaye,
);
final totalRestant = totalDu - totalPaye;
final cotisationsPayees = cotisations.where((c) => c.statut == 'PAYEE').length;
final cotisationsEnRetard = cotisations.where((c) => c.isEnRetard).length;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.account_balance_wallet,
color: AppTheme.primaryColor,
size: 24,
),
SizedBox(width: 8),
Text(
'Résumé des cotisations',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildSummaryItem(
'Total dû',
_formatAmount(totalDu),
AppTheme.infoColor,
Icons.receipt_long,
),
),
Expanded(
child: _buildSummaryItem(
'Payé',
_formatAmount(totalPaye),
AppTheme.successColor,
Icons.check_circle,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSummaryItem(
'Restant',
_formatAmount(totalRestant),
totalRestant > 0 ? AppTheme.warningColor : AppTheme.successColor,
Icons.pending,
),
),
Expanded(
child: _buildSummaryItem(
'En retard',
'$cotisationsEnRetard',
cotisationsEnRetard > 0 ? AppTheme.errorColor : AppTheme.successColor,
Icons.warning,
),
),
],
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: totalDu > 0 ? totalPaye / totalDu : 0,
backgroundColor: AppTheme.backgroundLight,
valueColor: AlwaysStoppedAnimation<Color>(
totalPaye == totalDu ? AppTheme.successColor : AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
'$cotisationsPayees/${cotisations.length} cotisations payées',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
);
}
Widget _buildSummaryItem(String label, String value, Color color, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildCotisationsList() {
if (cotisations.isEmpty) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: const Padding(
padding: EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.receipt_long_outlined,
size: 48,
color: AppTheme.textHint,
),
SizedBox(height: 16),
Text(
'Aucune cotisation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 8),
Text(
'Ce membre n\'a pas encore de cotisations enregistrées.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.list_alt,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Historique des cotisations',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 12),
...cotisations.map((cotisation) => _buildCotisationCard(cotisation)),
],
);
}
Widget _buildCotisationCard(CotisationModel cotisation) {
return Card(
elevation: 1,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cotisation.periode ?? 'Période non définie',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
cotisation.typeCotisation,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
_buildStatusBadge(cotisation),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildCotisationDetail(
'Montant dû',
_formatAmount(cotisation.montantDu),
Icons.receipt,
),
),
Expanded(
child: _buildCotisationDetail(
'Montant payé',
_formatAmount(cotisation.montantPaye),
Icons.payment,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildCotisationDetail(
'Échéance',
DateFormat('dd/MM/yyyy').format(cotisation.dateEcheance),
Icons.schedule,
),
),
if (cotisation.datePaiement != null)
Expanded(
child: _buildCotisationDetail(
'Payé le',
DateFormat('dd/MM/yyyy').format(cotisation.datePaiement!),
Icons.check_circle,
),
),
],
),
],
),
),
);
}
Widget _buildStatusBadge(CotisationModel cotisation) {
Color color;
String label;
switch (cotisation.statut) {
case 'PAYEE':
color = AppTheme.successColor;
label = 'Payée';
break;
case 'EN_ATTENTE':
color = AppTheme.warningColor;
label = 'En attente';
break;
case 'EN_RETARD':
color = AppTheme.errorColor;
label = 'En retard';
break;
case 'PARTIELLEMENT_PAYEE':
color = AppTheme.infoColor;
label = 'Partielle';
break;
default:
color = AppTheme.textSecondary;
label = cotisation.statut;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
),
);
}
Widget _buildCotisationDetail(String label, String value, IconData icon) {
return Row(
children: [
Icon(icon, size: 14, color: AppTheme.textSecondary),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
],
),
),
],
);
}
String _formatAmount(double amount) {
return NumberFormat.currency(
locale: 'fr_FR',
symbol: 'FCFA',
decimalDigits: 0,
).format(amount);
}
}

View File

@@ -1,495 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
import '../bloc/membres_bloc.dart';
import '../bloc/membres_event.dart';
import '../bloc/membres_state.dart';
/// Dialog de confirmation de suppression/désactivation d'un membre
class MembreDeleteDialog extends StatefulWidget {
const MembreDeleteDialog({
super.key,
required this.membre,
});
final MembreModel membre;
@override
State<MembreDeleteDialog> createState() => _MembreDeleteDialogState();
}
class _MembreDeleteDialogState extends State<MembreDeleteDialog> {
late MembresBloc _membresBloc;
bool _isLoading = false;
bool _softDelete = true; // Par défaut, désactivation plutôt que suppression
bool _hasActiveCotisations = false;
bool _hasUnpaidCotisations = false;
int _totalCotisations = 0;
double _unpaidAmount = 0.0;
@override
void initState() {
super.initState();
_membresBloc = getIt<MembresBloc>();
_checkMemberDependencies();
}
void _checkMemberDependencies() {
// TODO: Implémenter la vérification des dépendances via le repository
// Pour l'instant, simulation avec des données fictives
setState(() {
_hasActiveCotisations = true;
_hasUnpaidCotisations = true;
_totalCotisations = 5;
_unpaidAmount = 75000.0;
});
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _membresBloc,
child: BlocConsumer<MembresBloc, MembresState>(
listener: (context, state) {
if (state is MembreDeleted) {
setState(() {
_isLoading = false;
});
Navigator.of(context).pop(true);
} else if (state is MembreUpdated) {
setState(() {
_isLoading = false;
});
Navigator.of(context).pop(true);
} else if (state is MembresError) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppTheme.errorColor,
),
);
}
},
builder: (context, state) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
_softDelete ? Icons.person_off : Icons.delete_forever,
color: _softDelete ? AppTheme.warningColor : AppTheme.errorColor,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_softDelete ? 'Désactiver le membre' : 'Supprimer le membre',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informations du membre
_buildMemberInfo(),
const SizedBox(height: 20),
// Vérifications des dépendances
if (_hasActiveCotisations || _hasUnpaidCotisations)
_buildDependenciesWarning(),
const SizedBox(height: 16),
// Options de suppression
_buildDeleteOptions(),
const SizedBox(height: 20),
// Message de confirmation
_buildConfirmationMessage(),
],
),
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _isLoading ? null : _handleDelete,
style: ElevatedButton.styleFrom(
backgroundColor: _softDelete ? AppTheme.warningColor : AppTheme.errorColor,
foregroundColor: Colors.white,
),
child: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(_softDelete ? 'Désactiver' : 'Supprimer'),
),
],
);
},
),
);
}
Widget _buildMemberInfo() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor,
child: Text(
'${widget.membre.prenom[0]}${widget.membre.nom[0]}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.membre.prenom} ${widget.membre.nom}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
Text(
widget.membre.numeroMembre,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: widget.membre.actif ? AppTheme.successColor : AppTheme.errorColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.membre.actif ? 'Actif' : 'Inactif',
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 8),
Text(
widget.membre.email,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildDependenciesWarning() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.warningColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.warningColor.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber,
color: AppTheme.warningColor,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Attention - Dépendances détectées',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.warningColor,
),
),
],
),
const SizedBox(height: 8),
if (_hasActiveCotisations) ...[
Text(
'$_totalCotisations cotisations associées à ce membre',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
if (_hasUnpaidCotisations) ...[
Text(
'${_unpaidAmount.toStringAsFixed(0)} XOF de cotisations impayées',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
const SizedBox(height: 8),
const Text(
'La désactivation est recommandée pour préserver l\'historique.',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
fontStyle: FontStyle.italic,
),
),
],
),
);
}
Widget _buildDeleteOptions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Options de suppression :',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
// Option désactivation
InkWell(
onTap: () {
setState(() {
_softDelete = true;
});
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _softDelete ? AppTheme.warningColor.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _softDelete ? AppTheme.warningColor : AppTheme.borderColor,
width: _softDelete ? 2 : 1,
),
),
child: Row(
children: [
Radio<bool>(
value: true,
groupValue: _softDelete,
onChanged: (value) {
setState(() {
_softDelete = value!;
});
},
activeColor: AppTheme.warningColor,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Désactiver le membre (Recommandé)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'Le membre sera marqué comme inactif mais ses données et historique seront préservés.',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 8),
// Option suppression définitive
InkWell(
onTap: () {
setState(() {
_softDelete = false;
});
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: !_softDelete ? AppTheme.errorColor.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: !_softDelete ? AppTheme.errorColor : AppTheme.borderColor,
width: !_softDelete ? 2 : 1,
),
),
child: Row(
children: [
Radio<bool>(
value: false,
groupValue: _softDelete,
onChanged: (value) {
setState(() {
_softDelete = value!;
});
},
activeColor: AppTheme.errorColor,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Supprimer définitivement',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'ATTENTION : Cette action est irréversible. Toutes les données du membre seront perdues.',
style: TextStyle(
fontSize: 11,
color: AppTheme.errorColor,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
],
);
}
Widget _buildConfirmationMessage() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _softDelete
? AppTheme.warningColor.withOpacity(0.1)
: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _softDelete
? AppTheme.warningColor.withOpacity(0.3)
: AppTheme.errorColor.withOpacity(0.3),
),
),
child: Text(
_softDelete
? 'Le membre "${widget.membre.prenom} ${widget.membre.nom}" sera désactivé et ne pourra plus accéder aux services, mais son historique sera préservé.'
: 'Le membre "${widget.membre.prenom} ${widget.membre.nom}" sera définitivement supprimé avec toutes ses données. Cette action ne peut pas être annulée.',
style: TextStyle(
fontSize: 12,
color: _softDelete ? AppTheme.warningColor : AppTheme.errorColor,
fontWeight: FontWeight.w500,
),
),
);
}
void _handleDelete() {
setState(() {
_isLoading = true;
});
if (_softDelete) {
// Désactivation du membre
final membreDesactive = widget.membre.copyWith(
actif: false,
version: widget.membre.version + 1,
dateModification: DateTime.now(),
);
final memberId = widget.membre.id;
if (memberId != null && memberId.isNotEmpty) {
_membresBloc.add(UpdateMembre(memberId, membreDesactive));
} else {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur : ID du membre manquant'),
backgroundColor: AppTheme.errorColor,
),
);
}
} else {
// Suppression définitive
final memberId = widget.membre.id;
if (memberId != null && memberId.isNotEmpty) {
_membresBloc.add(DeleteMembre(memberId));
} else {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur : ID du membre manquant'),
backgroundColor: AppTheme.errorColor,
),
);
}
}
}
}

View File

@@ -1,390 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Carte membre améliorée avec différents modes d'affichage
class MembreEnhancedCard extends StatelessWidget {
final MembreModel membre;
final String viewMode;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
final VoidCallback? onCall;
final VoidCallback? onMessage;
const MembreEnhancedCard({
super.key,
required this.membre,
this.viewMode = 'card',
this.onTap,
this.onEdit,
this.onDelete,
this.onCall,
this.onMessage,
});
@override
Widget build(BuildContext context) {
switch (viewMode) {
case 'list':
return _buildListView();
case 'grid':
return _buildGridView();
case 'card':
default:
return _buildCardView();
}
}
Widget _buildCardView() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec avatar et actions
Row(
children: [
_buildAvatar(size: 50),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.nomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
_buildStatusBadge(),
],
),
const SizedBox(height: 12),
// Informations de contact
_buildContactInfo(),
const SizedBox(height: 12),
// Actions
_buildActionButtons(),
],
),
),
),
);
}
Widget _buildListView() {
return Card(
elevation: 1,
margin: const EdgeInsets.symmetric(vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
onTap: onTap,
leading: _buildAvatar(size: 40),
title: Text(
membre.nomComplet,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 2),
Text(
membre.telephone,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildStatusBadge(),
const SizedBox(width: 8),
PopupMenuButton<String>(
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'call',
child: Row(
children: [
Icon(Icons.phone, size: 16),
SizedBox(width: 8),
Text('Appeler'),
],
),
),
const PopupMenuItem(
value: 'message',
child: Row(
children: [
Icon(Icons.message, size: 16),
SizedBox(width: 8),
Text('Message'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 16),
SizedBox(width: 8),
Text('Modifier'),
],
),
),
],
),
],
),
),
);
}
Widget _buildGridView() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_buildAvatar(size: 60),
const SizedBox(height: 8),
Text(
membre.prenom,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
membre.nom,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
_buildStatusBadge(),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildGridAction(Icons.phone, onCall),
_buildGridAction(Icons.message, onMessage),
_buildGridAction(Icons.edit, onEdit),
],
),
],
),
),
),
);
}
Widget _buildAvatar({required double size}) {
return CircleAvatar(
radius: size / 2,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: Text(
membre.initiales,
style: TextStyle(
fontSize: size * 0.4,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
);
}
Widget _buildStatusBadge() {
Color color;
switch (membre.statut.toUpperCase()) {
case 'ACTIF':
color = AppTheme.successColor;
break;
case 'INACTIF':
color = AppTheme.warningColor;
break;
case 'SUSPENDU':
color = AppTheme.errorColor;
break;
default:
color = AppTheme.textSecondary;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text(
membre.statutLibelle,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
);
}
Widget _buildContactInfo() {
return Column(
children: [
Row(
children: [
const Icon(Icons.phone, size: 16, color: AppTheme.textHint),
const SizedBox(width: 8),
Text(
membre.telephone,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.email, size: 16, color: AppTheme.textHint),
const SizedBox(width: 8),
Expanded(
child: Text(
membre.email,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onCall,
icon: const Icon(Icons.phone, size: 16),
label: const Text('Appeler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
side: BorderSide(color: AppTheme.primaryColor.withOpacity(0.3)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: onMessage,
icon: const Icon(Icons.message, size: 16),
label: const Text('Message'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.secondaryColor,
side: BorderSide(color: AppTheme.secondaryColor.withOpacity(0.3)),
),
),
),
],
);
}
Widget _buildGridAction(IconData icon, VoidCallback? onPressed) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onPressed?.call();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 16,
color: AppTheme.primaryColor,
),
),
);
}
void _handleMenuAction(String action) {
HapticFeedback.lightImpact();
switch (action) {
case 'call':
onCall?.call();
break;
case 'message':
onMessage?.call();
break;
case 'edit':
onEdit?.call();
break;
}
}
}

View File

@@ -1,373 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Section d'informations détaillées d'un membre
class MembreInfoSection extends StatelessWidget {
const MembreInfoSection({
super.key,
required this.membre,
this.showActions = false,
this.onEdit,
this.onCall,
this.onMessage,
});
final MembreModel membre;
final bool showActions;
final VoidCallback? onEdit;
final VoidCallback? onCall;
final VoidCallback? onMessage;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 20),
_buildPersonalInfo(),
const SizedBox(height: 16),
_buildContactInfo(),
const SizedBox(height: 16),
_buildMembershipInfo(),
if (showActions) ...[
const SizedBox(height: 20),
_buildActionButtons(),
],
],
),
),
);
}
Widget _buildHeader() {
return Row(
children: [
_buildAvatar(),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.nomComplet,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
_buildStatusBadge(),
],
),
),
],
);
}
Widget _buildAvatar() {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3),
width: 2,
),
),
child: Icon(
Icons.person,
size: 40,
color: AppTheme.primaryColor,
),
);
}
Widget _buildStatusBadge() {
final isActive = membre.actif;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isActive ? AppTheme.successColor : AppTheme.errorColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
isActive ? 'Actif' : 'Inactif',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildPersonalInfo() {
return _buildSection(
title: 'Informations personnelles',
icon: Icons.person_outline,
children: [
_buildInfoRow(
icon: Icons.cake_outlined,
label: 'Date de naissance',
value: membre.dateNaissance != null
? DateFormat('dd/MM/yyyy').format(membre.dateNaissance!)
: 'Non renseignée',
),
_buildInfoRow(
icon: Icons.work_outline,
label: 'Profession',
value: membre.profession ?? 'Non renseignée',
),
_buildInfoRow(
icon: Icons.location_on_outlined,
label: 'Adresse',
value: _buildFullAddress(),
),
],
);
}
Widget _buildContactInfo() {
return _buildSection(
title: 'Contact',
icon: Icons.contact_phone_outlined,
children: [
_buildInfoRow(
icon: Icons.email_outlined,
label: 'Email',
value: membre.email,
isSelectable: true,
),
_buildInfoRow(
icon: Icons.phone_outlined,
label: 'Téléphone',
value: membre.telephone,
isSelectable: true,
),
],
);
}
Widget _buildMembershipInfo() {
return _buildSection(
title: 'Adhésion',
icon: Icons.card_membership_outlined,
children: [
_buildInfoRow(
icon: Icons.calendar_today_outlined,
label: 'Date d\'adhésion',
value: DateFormat('dd/MM/yyyy').format(membre.dateAdhesion),
),
_buildInfoRow(
icon: Icons.access_time_outlined,
label: 'Membre depuis',
value: _calculateMembershipDuration(),
),
_buildInfoRow(
icon: Icons.update_outlined,
label: 'Dernière modification',
value: membre.dateModification != null
? DateFormat('dd/MM/yyyy à HH:mm').format(membre.dateModification!)
: 'Jamais modifié',
),
],
);
}
Widget _buildSection({
required String title,
required IconData icon,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
size: 20,
color: AppTheme.primaryColor,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 12),
...children,
],
);
}
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
bool isSelectable = false,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 16,
color: AppTheme.textSecondary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
isSelectable
? SelectableText(
value,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
),
)
: Text(
value,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
),
),
],
),
),
],
),
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: onEdit,
icon: const Icon(Icons.edit, size: 18),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: onCall,
icon: const Icon(Icons.phone, size: 18),
label: const Text('Appeler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
side: const BorderSide(color: AppTheme.primaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: onMessage,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.infoColor,
side: const BorderSide(color: AppTheme.infoColor),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Icon(Icons.message, size: 18),
),
],
);
}
String _buildFullAddress() {
final parts = <String>[];
if (membre.adresse != null && membre.adresse!.isNotEmpty) {
parts.add(membre.adresse!);
}
if (membre.ville != null && membre.ville!.isNotEmpty) {
parts.add(membre.ville!);
}
if (membre.codePostal != null && membre.codePostal!.isNotEmpty) {
parts.add(membre.codePostal!);
}
if (membre.pays != null && membre.pays!.isNotEmpty) {
parts.add(membre.pays!);
}
return parts.isNotEmpty ? parts.join(', ') : 'Non renseignée';
}
String _calculateMembershipDuration() {
final now = DateTime.now();
final adhesion = membre.dateAdhesion;
final difference = now.difference(adhesion);
final years = (difference.inDays / 365).floor();
final months = ((difference.inDays % 365) / 30).floor();
if (years > 0) {
return months > 0 ? '$years an${years > 1 ? 's' : ''} et $months mois' : '$years an${years > 1 ? 's' : ''}';
} else if (months > 0) {
return '$months mois';
} else {
return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
}
}
}

View File

@@ -1,592 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Section des statistiques d'un membre
class MembreStatsSection extends StatelessWidget {
const MembreStatsSection({
super.key,
required this.membre,
required this.cotisations,
});
final MembreModel membre;
final List<CotisationModel> cotisations;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOverviewCard(),
const SizedBox(height: 16),
_buildPaymentChart(),
const SizedBox(height: 16),
_buildStatusChart(),
const SizedBox(height: 16),
_buildTimelineCard(),
],
),
);
}
Widget _buildOverviewCard() {
final totalCotisations = cotisations.length;
final cotisationsPayees = cotisations.where((c) => c.statut == 'PAYEE').length;
final cotisationsEnRetard = cotisations.where((c) => c.isEnRetard).length;
final tauxPaiement = totalCotisations > 0 ? (cotisationsPayees / totalCotisations * 100) : 0.0;
final totalMontantDu = cotisations.fold<double>(0, (sum, c) => sum + c.montantDu);
final totalMontantPaye = cotisations.fold<double>(0, (sum, c) => sum + c.montantPaye);
final membershipDuration = DateTime.now().difference(membre.dateAdhesion).inDays;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 24,
),
SizedBox(width: 8),
Text(
'Vue d\'ensemble',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildStatItem(
'Cotisations',
'$totalCotisations',
AppTheme.primaryColor,
Icons.receipt_long,
),
),
Expanded(
child: _buildStatItem(
'Taux de paiement',
'${tauxPaiement.toStringAsFixed(1)}%',
tauxPaiement >= 80 ? AppTheme.successColor :
tauxPaiement >= 50 ? AppTheme.warningColor : AppTheme.errorColor,
Icons.trending_up,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatItem(
'En retard',
'$cotisationsEnRetard',
cotisationsEnRetard > 0 ? AppTheme.errorColor : AppTheme.successColor,
Icons.warning,
),
),
Expanded(
child: _buildStatItem(
'Ancienneté',
'${(membershipDuration / 365).floor()} an${(membershipDuration / 365).floor() > 1 ? 's' : ''}',
AppTheme.infoColor,
Icons.schedule,
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Total payé',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
_formatAmount(totalMontantPaye),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.successColor,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'Restant à payer',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
_formatAmount(totalMontantDu - totalMontantPaye),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: totalMontantDu > totalMontantPaye ? AppTheme.warningColor : AppTheme.successColor,
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildStatItem(String label, String value, Color color, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildPaymentChart() {
if (cotisations.isEmpty) {
return _buildEmptyChart('Aucune donnée de paiement');
}
final paymentData = _getPaymentChartData();
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.pie_chart,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Répartition des paiements',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: PieChart(
PieChartData(
sections: paymentData,
centerSpaceRadius: 40,
sectionsSpace: 2,
),
),
),
const SizedBox(height: 16),
_buildChartLegend(),
],
),
),
);
}
Widget _buildStatusChart() {
if (cotisations.isEmpty) {
return _buildEmptyChart('Aucune donnée de statut');
}
final statusData = _getStatusChartData();
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.bar_chart,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Évolution des montants',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
barGroups: statusData,
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60,
getTitlesWidget: (value, meta) {
return Text(
_formatAmount(value),
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < cotisations.length) {
return Text(
(cotisations[index].periode ?? 'N/A').substring(0, 3),
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
);
}
return const Text('');
},
),
),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
gridData: const FlGridData(show: false),
),
),
),
],
),
),
);
}
Widget _buildTimelineCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.timeline,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Chronologie',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
_buildTimelineItem(
'Adhésion',
DateFormat('dd/MM/yyyy').format(membre.dateAdhesion),
AppTheme.primaryColor,
Icons.person_add,
true,
),
if (cotisations.isNotEmpty) ...[
_buildTimelineItem(
'Première cotisation',
DateFormat('dd/MM/yyyy').format(
cotisations.map((c) => c.dateCreation).reduce((a, b) => a.isBefore(b) ? a : b),
),
AppTheme.infoColor,
Icons.payment,
true,
),
_buildTimelineItem(
'Dernière cotisation',
DateFormat('dd/MM/yyyy').format(
cotisations.map((c) => c.dateCreation).reduce((a, b) => a.isAfter(b) ? a : b),
),
AppTheme.successColor,
Icons.receipt,
false,
),
],
],
),
),
);
}
Widget _buildTimelineItem(String title, String date, Color color, IconData icon, bool showLine) {
return Row(
children: [
Column(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 16),
),
if (showLine)
Container(
width: 2,
height: 24,
color: color.withOpacity(0.3),
),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
Text(
date,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
],
);
}
Widget _buildEmptyChart(String message) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
children: [
const Icon(
Icons.bar_chart,
size: 48,
color: AppTheme.textHint,
),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
);
}
Widget _buildChartLegend() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildLegendItem('Payé', AppTheme.successColor),
_buildLegendItem('En attente', AppTheme.warningColor),
_buildLegendItem('En retard', AppTheme.errorColor),
],
);
}
Widget _buildLegendItem(String label, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
List<PieChartSectionData> _getPaymentChartData() {
final payees = cotisations.where((c) => c.statut == 'PAYEE').length;
final enAttente = cotisations.where((c) => c.statut == 'EN_ATTENTE').length;
final enRetard = cotisations.where((c) => c.isEnRetard).length;
final total = cotisations.length;
return [
if (payees > 0)
PieChartSectionData(
color: AppTheme.successColor,
value: payees.toDouble(),
title: '${(payees / total * 100).toStringAsFixed(1)}%',
radius: 50,
),
if (enAttente > 0)
PieChartSectionData(
color: AppTheme.warningColor,
value: enAttente.toDouble(),
title: '${(enAttente / total * 100).toStringAsFixed(1)}%',
radius: 50,
),
if (enRetard > 0)
PieChartSectionData(
color: AppTheme.errorColor,
value: enRetard.toDouble(),
title: '${(enRetard / total * 100).toStringAsFixed(1)}%',
radius: 50,
),
];
}
List<BarChartGroupData> _getStatusChartData() {
return cotisations.asMap().entries.map((entry) {
final index = entry.key;
final cotisation = entry.value;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: cotisation.montantDu,
color: AppTheme.infoColor.withOpacity(0.7),
width: 8,
),
BarChartRodData(
toY: cotisation.montantPaye,
color: AppTheme.successColor,
width: 8,
),
],
);
}).toList();
}
String _formatAmount(double amount) {
return NumberFormat.currency(
locale: 'fr_FR',
symbol: 'FCFA',
decimalDigits: 0,
).format(amount);
}
}

View File

@@ -1,626 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
/// Widget de recherche avancée pour les membres
class MembresAdvancedSearch extends StatefulWidget {
const MembresAdvancedSearch({
super.key,
required this.onSearch,
this.initialFilters,
});
final Function(Map<String, dynamic>) onSearch;
final Map<String, dynamic>? initialFilters;
@override
State<MembresAdvancedSearch> createState() => _MembresAdvancedSearchState();
}
class _MembresAdvancedSearchState extends State<MembresAdvancedSearch> {
final _formKey = GlobalKey<FormState>();
// Contrôleurs de texte
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _numeroMembreController = TextEditingController();
final _professionController = TextEditingController();
final _villeController = TextEditingController();
// Filtres de statut
bool? _actifFilter;
// Filtres de date
DateTime? _dateAdhesionDebut;
DateTime? _dateAdhesionFin;
DateTime? _dateNaissanceDebut;
DateTime? _dateNaissanceFin;
// Filtres d'âge
int? _ageMin;
int? _ageMax;
@override
void initState() {
super.initState();
_initializeFilters();
}
void _initializeFilters() {
if (widget.initialFilters != null) {
final filters = widget.initialFilters!;
_nomController.text = filters['nom'] ?? '';
_prenomController.text = filters['prenom'] ?? '';
_emailController.text = filters['email'] ?? '';
_telephoneController.text = filters['telephone'] ?? '';
_numeroMembreController.text = filters['numeroMembre'] ?? '';
_professionController.text = filters['profession'] ?? '';
_villeController.text = filters['ville'] ?? '';
_actifFilter = filters['actif'];
_dateAdhesionDebut = filters['dateAdhesionDebut'];
_dateAdhesionFin = filters['dateAdhesionFin'];
_dateNaissanceDebut = filters['dateNaissanceDebut'];
_dateNaissanceFin = filters['dateNaissanceFin'];
_ageMin = filters['ageMin'];
_ageMax = filters['ageMax'];
}
}
@override
void dispose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_numeroMembreController.dispose();
_professionController.dispose();
_villeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête
_buildHeader(),
const SizedBox(height: 20),
// Contenu scrollable
Flexible(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informations personnelles
_buildSection(
'Informations personnelles',
Icons.person,
[
Row(
children: [
Expanded(
child: CustomTextField(
controller: _nomController,
label: 'Nom',
prefixIcon: Icons.person_outline,
),
),
const SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: _prenomController,
label: 'Prénom',
prefixIcon: Icons.person_outline,
),
),
],
),
const SizedBox(height: 12),
CustomTextField(
controller: _numeroMembreController,
label: 'Numéro de membre',
prefixIcon: Icons.badge,
),
const SizedBox(height: 12),
CustomTextField(
controller: _professionController,
label: 'Profession',
prefixIcon: Icons.work,
),
],
),
const SizedBox(height: 20),
// Contact et localisation
_buildSection(
'Contact et localisation',
Icons.contact_phone,
[
CustomTextField(
controller: _emailController,
label: 'Email',
prefixIcon: Icons.email,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 12),
CustomTextField(
controller: _telephoneController,
label: 'Téléphone',
prefixIcon: Icons.phone,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 12),
CustomTextField(
controller: _villeController,
label: 'Ville',
prefixIcon: Icons.location_city,
),
],
),
const SizedBox(height: 20),
// Statut et dates
_buildSection(
'Statut et dates',
Icons.calendar_today,
[
_buildStatusFilter(),
const SizedBox(height: 16),
_buildDateRangeFilter(
'Période d\'adhésion',
_dateAdhesionDebut,
_dateAdhesionFin,
(debut, fin) {
setState(() {
_dateAdhesionDebut = debut;
_dateAdhesionFin = fin;
});
},
),
const SizedBox(height: 16),
_buildDateRangeFilter(
'Période de naissance',
_dateNaissanceDebut,
_dateNaissanceFin,
(debut, fin) {
setState(() {
_dateNaissanceDebut = debut;
_dateNaissanceFin = fin;
});
},
),
const SizedBox(height: 16),
_buildAgeRangeFilter(),
],
),
],
),
),
),
const SizedBox(height: 20),
// Boutons d'action
_buildActionButtons(),
],
),
),
);
}
Widget _buildHeader() {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.search,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Recherche avancée',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
color: AppTheme.textSecondary,
),
],
);
}
Widget _buildSection(String title, IconData icon, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 12),
...children,
],
);
}
Widget _buildStatusFilter() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statut du membre',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<bool?>(
title: const Text('Tous', style: TextStyle(fontSize: 14)),
value: null,
groupValue: _actifFilter,
onChanged: (value) {
setState(() {
_actifFilter = value;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
),
Expanded(
child: RadioListTile<bool?>(
title: const Text('Actifs', style: TextStyle(fontSize: 14)),
value: true,
groupValue: _actifFilter,
onChanged: (value) {
setState(() {
_actifFilter = value;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
),
Expanded(
child: RadioListTile<bool?>(
title: const Text('Inactifs', style: TextStyle(fontSize: 14)),
value: false,
groupValue: _actifFilter,
onChanged: (value) {
setState(() {
_actifFilter = value;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
),
],
);
}
Widget _buildDateRangeFilter(
String title,
DateTime? dateDebut,
DateTime? dateFin,
Function(DateTime?, DateTime?) onChanged,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: InkWell(
onTap: () => _selectDate(context, dateDebut, (date) {
onChanged(date, dateFin);
}),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: AppTheme.borderColor),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: AppTheme.textSecondary,
size: 16,
),
const SizedBox(width: 8),
Text(
dateDebut != null
? DateFormat('dd/MM/yyyy').format(dateDebut)
: 'Date début',
style: TextStyle(
fontSize: 14,
color: dateDebut != null
? AppTheme.textPrimary
: AppTheme.textSecondary,
),
),
],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: InkWell(
onTap: () => _selectDate(context, dateFin, (date) {
onChanged(dateDebut, date);
}),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: AppTheme.borderColor),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: AppTheme.textSecondary,
size: 16,
),
const SizedBox(width: 8),
Text(
dateFin != null
? DateFormat('dd/MM/yyyy').format(dateFin)
: 'Date fin',
style: TextStyle(
fontSize: 14,
color: dateFin != null
? AppTheme.textPrimary
: AppTheme.textSecondary,
),
),
],
),
),
),
),
],
),
],
);
}
Widget _buildAgeRangeFilter() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tranche d\'âge',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextFormField(
initialValue: _ageMin?.toString(),
decoration: InputDecoration(
labelText: 'Âge minimum',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
),
keyboardType: TextInputType.number,
onChanged: (value) {
_ageMin = int.tryParse(value);
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
initialValue: _ageMax?.toString(),
decoration: InputDecoration(
labelText: 'Âge maximum',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
),
keyboardType: TextInputType.number,
onChanged: (value) {
_ageMax = int.tryParse(value);
},
),
),
],
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _clearFilters,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: AppTheme.borderColor),
),
child: const Text('Effacer'),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _performSearch,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Rechercher'),
),
),
],
);
}
Future<void> _selectDate(
BuildContext context,
DateTime? initialDate,
Function(DateTime?) onDateSelected,
) async {
final date = await showDatePicker(
context: context,
initialDate: initialDate ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
onDateSelected(date);
}
}
void _clearFilters() {
setState(() {
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_numeroMembreController.clear();
_professionController.clear();
_villeController.clear();
_actifFilter = null;
_dateAdhesionDebut = null;
_dateAdhesionFin = null;
_dateNaissanceDebut = null;
_dateNaissanceFin = null;
_ageMin = null;
_ageMax = null;
});
}
void _performSearch() {
final filters = <String, dynamic>{};
// Ajout des filtres texte
if (_nomController.text.isNotEmpty) {
filters['nom'] = _nomController.text;
}
if (_prenomController.text.isNotEmpty) {
filters['prenom'] = _prenomController.text;
}
if (_emailController.text.isNotEmpty) {
filters['email'] = _emailController.text;
}
if (_telephoneController.text.isNotEmpty) {
filters['telephone'] = _telephoneController.text;
}
if (_numeroMembreController.text.isNotEmpty) {
filters['numeroMembre'] = _numeroMembreController.text;
}
if (_professionController.text.isNotEmpty) {
filters['profession'] = _professionController.text;
}
if (_villeController.text.isNotEmpty) {
filters['ville'] = _villeController.text;
}
// Ajout des filtres de statut
if (_actifFilter != null) {
filters['actif'] = _actifFilter;
}
// Ajout des filtres de date
if (_dateAdhesionDebut != null) {
filters['dateAdhesionDebut'] = _dateAdhesionDebut;
}
if (_dateAdhesionFin != null) {
filters['dateAdhesionFin'] = _dateAdhesionFin;
}
if (_dateNaissanceDebut != null) {
filters['dateNaissanceDebut'] = _dateNaissanceDebut;
}
if (_dateNaissanceFin != null) {
filters['dateNaissanceFin'] = _dateNaissanceFin;
}
// Ajout des filtres d'âge
if (_ageMin != null) {
filters['ageMin'] = _ageMin;
}
if (_ageMax != null) {
filters['ageMax'] = _ageMax;
}
widget.onSearch(filters);
Navigator.of(context).pop();
}
}

View File

@@ -1,421 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../core/services/export_import_service.dart';
/// Dialog d'export des données des membres
class MembresExportDialog extends StatefulWidget {
const MembresExportDialog({
super.key,
required this.membres,
this.selectedMembers,
});
final List<MembreModel> membres;
final List<MembreModel>? selectedMembers;
@override
State<MembresExportDialog> createState() => _MembresExportDialogState();
}
class _MembresExportDialogState extends State<MembresExportDialog> {
String _selectedFormat = 'excel';
bool _includeInactiveMembers = true;
bool _includePersonalInfo = true;
bool _includeContactInfo = true;
bool _includeAdhesionInfo = true;
bool _includeStatistics = false;
final List<String> _availableFormats = [
'excel',
'csv',
'pdf',
'json',
];
@override
Widget build(BuildContext context) {
final membersToExport = widget.selectedMembers ?? widget.membres;
final activeMembers = membersToExport.where((m) => m.actif).length;
final inactiveMembers = membersToExport.length - activeMembers;
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.file_download,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Exporter les données',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Résumé des données à exporter
_buildDataSummary(membersToExport.length, activeMembers, inactiveMembers),
const SizedBox(height: 20),
// Sélection du format
_buildFormatSelection(),
const SizedBox(height: 20),
// Options d'export
_buildExportOptions(),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton.icon(
onPressed: () => _performExport(membersToExport),
icon: const Icon(Icons.download),
label: const Text('Exporter'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildDataSummary(int total, int active, int inactive) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Données à exporter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSummaryItem(
'Total',
total.toString(),
AppTheme.primaryColor,
Icons.people,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryItem(
'Actifs',
active.toString(),
AppTheme.successColor,
Icons.person,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryItem(
'Inactifs',
inactive.toString(),
AppTheme.errorColor,
Icons.person_off,
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(String label, String value, Color color, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(
icon,
color: color,
size: 20,
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildFormatSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Format d\'export',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _availableFormats.map((format) {
final isSelected = _selectedFormat == format;
return InkWell(
onTap: () {
setState(() {
_selectedFormat = format;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? AppTheme.primaryColor : AppTheme.borderColor,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getFormatIcon(format),
color: isSelected ? Colors.white : AppTheme.textSecondary,
size: 20,
),
const SizedBox(width: 8),
Text(
_getFormatLabel(format),
style: TextStyle(
color: isSelected ? Colors.white : AppTheme.textPrimary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}).toList(),
),
],
);
}
Widget _buildExportOptions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Options d\'export',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
// Inclusion des membres inactifs
CheckboxListTile(
title: const Text('Inclure les membres inactifs'),
subtitle: const Text('Exporter aussi les membres désactivés'),
value: _includeInactiveMembers,
onChanged: (value) {
setState(() {
_includeInactiveMembers = value ?? true;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
const Divider(),
// Sections de données à inclure
const Text(
'Sections à inclure',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
CheckboxListTile(
title: const Text('Informations personnelles'),
subtitle: const Text('Nom, prénom, date de naissance, etc.'),
value: _includePersonalInfo,
onChanged: (value) {
setState(() {
_includePersonalInfo = value ?? true;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Informations de contact'),
subtitle: const Text('Email, téléphone, adresse'),
value: _includeContactInfo,
onChanged: (value) {
setState(() {
_includeContactInfo = value ?? true;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Informations d\'adhésion'),
subtitle: const Text('Date d\'adhésion, statut, numéro de membre'),
value: _includeAdhesionInfo,
onChanged: (value) {
setState(() {
_includeAdhesionInfo = value ?? true;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Statistiques'),
subtitle: const Text('Données de cotisations et statistiques'),
value: _includeStatistics,
onChanged: (value) {
setState(() {
_includeStatistics = value ?? false;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
),
],
);
}
IconData _getFormatIcon(String format) {
switch (format) {
case 'excel':
return Icons.table_chart;
case 'csv':
return Icons.text_snippet;
case 'pdf':
return Icons.picture_as_pdf;
case 'json':
return Icons.code;
default:
return Icons.file_download;
}
}
String _getFormatLabel(String format) {
switch (format) {
case 'excel':
return 'Excel (.xlsx)';
case 'csv':
return 'CSV (.csv)';
case 'pdf':
return 'PDF (.pdf)';
case 'json':
return 'JSON (.json)';
default:
return format.toUpperCase();
}
}
Future<void> _performExport(List<MembreModel> membersToExport) async {
// Filtrer les membres selon les options
List<MembreModel> filteredMembers = membersToExport;
if (!_includeInactiveMembers) {
filteredMembers = filteredMembers.where((m) => m.actif).toList();
}
// Créer les options d'export
final exportOptions = ExportOptions(
format: _selectedFormat,
includePersonalInfo: _includePersonalInfo,
includeContactInfo: _includeContactInfo,
includeAdhesionInfo: _includeAdhesionInfo,
includeStatistics: _includeStatistics,
includeInactiveMembers: _includeInactiveMembers,
);
// Fermer le dialog avant l'export
Navigator.of(context).pop();
// Effectuer l'export réel
final exportService = ExportImportService();
await exportService.exportMembers(context, filteredMembers, exportOptions);
}
}

View File

@@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Barre de recherche pour les membres
class MembresSearchBar extends StatefulWidget {
const MembresSearchBar({
super.key,
required this.controller,
required this.onSearch,
required this.onClear,
this.hintText = 'Rechercher un membre...',
});
final TextEditingController controller;
final ValueChanged<String> onSearch;
final VoidCallback onClear;
final String hintText;
@override
State<MembresSearchBar> createState() => _MembresSearchBarState();
}
class _MembresSearchBarState extends State<MembresSearchBar> {
bool _isSearching = false;
@override
void initState() {
super.initState();
widget.controller.addListener(_onTextChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onTextChanged);
super.dispose();
}
void _onTextChanged() {
final hasText = widget.controller.text.isNotEmpty;
if (_isSearching != hasText) {
setState(() {
_isSearching = hasText;
});
}
}
void _onSubmitted(String value) {
if (value.trim().isNotEmpty) {
widget.onSearch(value.trim());
} else {
widget.onClear();
}
}
void _onClearPressed() {
widget.controller.clear();
widget.onClear();
FocusScope.of(context).unfocus();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: widget.controller,
onSubmitted: _onSubmitted,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: const TextStyle(
color: AppTheme.textHint,
fontSize: 16,
),
prefixIcon: const Icon(
Icons.search,
color: AppTheme.textSecondary,
),
suffixIcon: _isSearching
? IconButton(
icon: const Icon(
Icons.clear,
color: AppTheme.textSecondary,
),
onPressed: _onClearPressed,
tooltip: 'Effacer la recherche',
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
style: const TextStyle(
fontSize: 16,
color: AppTheme.textPrimary,
),
),
);
}
}

View File

@@ -1,253 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/theme/app_theme.dart';
/// Card pour afficher les statistiques des membres
class MembresStatsCard extends StatelessWidget {
const MembresStatsCard({
super.key,
required this.stats,
});
final Map<String, dynamic> stats;
@override
Widget build(BuildContext context) {
final nombreMembresActifs = stats['nombreMembresActifs'] as int? ?? 0;
final nombreMembresInactifs = stats['nombreMembresInactifs'] as int? ?? 0;
final nombreMembresSuspendus = stats['nombreMembresSuspendus'] as int? ?? 0;
final total = nombreMembresActifs + nombreMembresInactifs + nombreMembresSuspendus;
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Statistiques des membres',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
],
),
const SizedBox(height: 20),
// Statistiques principales
Row(
children: [
Expanded(
child: _buildStatItem(
title: 'Total',
value: total.toString(),
color: AppTheme.primaryColor,
icon: Icons.people,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatItem(
title: 'Actifs',
value: nombreMembresActifs.toString(),
color: AppTheme.successColor,
icon: Icons.check_circle,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatItem(
title: 'Inactifs',
value: nombreMembresInactifs.toString(),
color: AppTheme.warningColor,
icon: Icons.pause_circle,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatItem(
title: 'Suspendus',
value: nombreMembresSuspendus.toString(),
color: AppTheme.errorColor,
icon: Icons.block,
),
),
],
),
if (total > 0) ...[
const SizedBox(height: 24),
// Graphique en secteurs
SizedBox(
height: 200,
child: PieChart(
PieChartData(
sections: [
if (nombreMembresActifs > 0)
PieChartSectionData(
value: nombreMembresActifs.toDouble(),
title: '${(nombreMembresActifs / total * 100).round()}%',
color: AppTheme.successColor,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (nombreMembresInactifs > 0)
PieChartSectionData(
value: nombreMembresInactifs.toDouble(),
title: '${(nombreMembresInactifs / total * 100).round()}%',
color: AppTheme.warningColor,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (nombreMembresSuspendus > 0)
PieChartSectionData(
value: nombreMembresSuspendus.toDouble(),
title: '${(nombreMembresSuspendus / total * 100).round()}%',
color: AppTheme.errorColor,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
centerSpaceRadius: 40,
sectionsSpace: 2,
),
),
),
const SizedBox(height: 16),
// Légende
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (nombreMembresActifs > 0)
_buildLegendItem('Actifs', AppTheme.successColor),
if (nombreMembresInactifs > 0)
_buildLegendItem('Inactifs', AppTheme.warningColor),
if (nombreMembresSuspendus > 0)
_buildLegendItem('Suspendus', AppTheme.errorColor),
],
),
],
],
),
),
);
}
/// Widget pour une statistique individuelle
Widget _buildStatItem({
required String title,
required String value,
required Color color,
required IconData icon,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Column(
children: [
Icon(
icon,
color: color,
size: 24,
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: color,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
);
}
/// Widget pour un élément de légende
Widget _buildLegendItem(String label, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
}

View File

@@ -1,281 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget de statistiques pour la liste des membres
class MembresStatsOverview extends StatelessWidget {
final List<MembreModel> membres;
final String searchQuery;
const MembresStatsOverview({
super.key,
required this.membres,
this.searchQuery = '',
});
@override
Widget build(BuildContext context) {
final stats = _calculateStats();
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
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
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.analytics,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 12),
const Text(
'Vue d\'ensemble',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const Spacer(),
if (searchQuery.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Filtré',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 16),
// Statistiques principales
Row(
children: [
Expanded(
child: _buildStatCard(
'Total',
stats['total'].toString(),
Icons.people,
AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Actifs',
stats['actifs'].toString(),
Icons.check_circle,
AppTheme.successColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Âge moyen',
'${stats['ageMoyen']} ans',
Icons.cake,
AppTheme.warningColor,
),
),
],
),
if (stats['total'] > 0) ...[
const SizedBox(height: 16),
// Statistiques détaillées
Row(
children: [
Expanded(
child: _buildDetailedStat(
'Nouveaux (30j)',
stats['nouveaux'].toString(),
stats['nouveauxPourcentage'],
AppTheme.infoColor,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildDetailedStat(
'Anciens (>1an)',
stats['anciens'].toString(),
stats['anciensPourcentage'],
AppTheme.secondaryColor,
),
),
],
),
],
],
),
);
}
Map<String, dynamic> _calculateStats() {
if (membres.isEmpty) {
return {
'total': 0,
'actifs': 0,
'ageMoyen': 0,
'nouveaux': 0,
'nouveauxPourcentage': 0.0,
'anciens': 0,
'anciensPourcentage': 0.0,
};
}
final now = DateTime.now();
final total = membres.length;
final actifs = membres.where((m) => m.statut.toUpperCase() == 'ACTIF').length;
// Calcul de l'âge moyen
final ages = membres.map((m) => m.age).where((age) => age > 0).toList();
final ageMoyen = ages.isNotEmpty ? (ages.reduce((a, b) => a + b) / ages.length).round() : 0;
// Nouveaux membres (moins de 30 jours)
final nouveaux = membres.where((m) {
final daysDiff = now.difference(m.dateAdhesion).inDays;
return daysDiff <= 30;
}).length;
final nouveauxPourcentage = total > 0 ? (nouveaux / total * 100) : 0.0;
// Anciens membres (plus d'un an)
final anciens = membres.where((m) {
final daysDiff = now.difference(m.dateAdhesion).inDays;
return daysDiff > 365;
}).length;
final anciensPourcentage = total > 0 ? (anciens / total * 100) : 0.0;
return {
'total': total,
'actifs': actifs,
'ageMoyen': ageMoyen,
'nouveaux': nouveaux,
'nouveauxPourcentage': nouveauxPourcentage,
'anciens': anciens,
'anciensPourcentage': anciensPourcentage,
};
}
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildDetailedStat(String label, String value, double percentage, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 8),
Text(
'(${percentage.toStringAsFixed(1)}%)',
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
}

View File

@@ -1,179 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget de contrôles pour les modes d'affichage et le tri
class MembresViewControls extends StatelessWidget {
final String viewMode;
final String sortBy;
final bool sortAscending;
final int totalCount;
final Function(String) onViewModeChanged;
final Function(String) onSortChanged;
final VoidCallback onSortDirectionChanged;
const MembresViewControls({
super.key,
required this.viewMode,
required this.sortBy,
required this.sortAscending,
required this.totalCount,
required this.onViewModeChanged,
required this.onSortChanged,
required this.onSortDirectionChanged,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
// Compteur
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$totalCount membre${totalCount > 1 ? 's' : ''}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.primaryColor,
),
),
),
const Spacer(),
// Contrôles de tri
_buildSortControls(),
const SizedBox(width: 12),
// Modes d'affichage
_buildViewModeControls(),
],
),
);
}
Widget _buildSortControls() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
PopupMenuButton<String>(
initialValue: sortBy,
onSelected: onSortChanged,
icon: const Icon(
Icons.sort,
size: 20,
color: AppTheme.textSecondary,
),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'name',
child: Row(
children: [
Icon(Icons.sort_by_alpha, size: 16),
SizedBox(width: 8),
Text('Nom'),
],
),
),
const PopupMenuItem(
value: 'date',
child: Row(
children: [
Icon(Icons.date_range, size: 16),
SizedBox(width: 8),
Text('Date d\'adhésion'),
],
),
),
const PopupMenuItem(
value: 'age',
child: Row(
children: [
Icon(Icons.cake, size: 16),
SizedBox(width: 8),
Text('Âge'),
],
),
),
const PopupMenuItem(
value: 'status',
child: Row(
children: [
Icon(Icons.info, size: 16),
SizedBox(width: 8),
Text('Statut'),
],
),
),
],
),
// Direction du tri
GestureDetector(
onTap: onSortDirectionChanged,
child: Container(
padding: const EdgeInsets.all(4),
child: Icon(
sortAscending ? Icons.arrow_upward : Icons.arrow_downward,
size: 16,
color: AppTheme.primaryColor,
),
),
),
],
);
}
Widget _buildViewModeControls() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildViewModeButton('list', Icons.view_list, 'Liste'),
_buildViewModeButton('card', Icons.view_module, 'Cartes'),
_buildViewModeButton('grid', Icons.grid_view, 'Grille'),
],
),
);
}
Widget _buildViewModeButton(String mode, IconData icon, String tooltip) {
final isSelected = viewMode == mode;
return GestureDetector(
onTap: () => onViewModeChanged(mode),
child: Tooltip(
message: tooltip,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
icon,
size: 18,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
),
),
);
}
}

View File

@@ -1,340 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Floating Action Button moderne avec animations et design professionnel
class ModernFloatingActionButton extends StatefulWidget {
const ModernFloatingActionButton({
super.key,
required this.onPressed,
required this.icon,
this.label,
this.backgroundColor,
this.foregroundColor,
this.heroTag,
this.tooltip,
this.mini = false,
this.extended = false,
});
final VoidCallback? onPressed;
final IconData icon;
final String? label;
final Color? backgroundColor;
final Color? foregroundColor;
final Object? heroTag;
final String? tooltip;
final bool mini;
final bool extended;
@override
State<ModernFloatingActionButton> createState() => _ModernFloatingActionButtonState();
}
class _ModernFloatingActionButtonState extends State<ModernFloatingActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.1,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTapDown(TapDownDetails details) {
setState(() => _isPressed = true);
_animationController.forward();
}
void _handleTapUp(TapUpDetails details) {
setState(() => _isPressed = false);
_animationController.reverse();
}
void _handleTapCancel() {
setState(() => _isPressed = false);
_animationController.reverse();
}
@override
Widget build(BuildContext context) {
if (widget.extended && widget.label != null) {
return _buildExtendedFAB();
}
return _buildRegularFAB();
}
Widget _buildRegularFAB() {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTapCancel: _handleTapCancel,
onTap: widget.onPressed,
child: Container(
width: widget.mini ? 40 : 56,
height: widget.mini ? 40 : 56,
decoration: BoxDecoration(
gradient: DesignSystem.primaryGradient,
borderRadius: BorderRadius.circular(
widget.mini ? 20 : 28,
),
boxShadow: [
BoxShadow(
color: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 6),
),
...DesignSystem.shadowCard,
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(
widget.mini ? 20 : 28,
),
onTap: widget.onPressed,
child: Center(
child: Icon(
widget.icon,
color: widget.foregroundColor ?? Colors.white,
size: widget.mini ? 20 : 24,
),
),
),
),
),
),
),
);
},
);
}
Widget _buildExtendedFAB() {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTapCancel: _handleTapCancel,
onTap: widget.onPressed,
child: Container(
height: 48,
padding: EdgeInsets.symmetric(
horizontal: DesignSystem.spacingLg,
vertical: DesignSystem.spacingSm,
),
decoration: BoxDecoration(
gradient: DesignSystem.primaryGradient,
borderRadius: BorderRadius.circular(DesignSystem.radiusXl),
boxShadow: [
BoxShadow(
color: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 6),
),
...DesignSystem.shadowCard,
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(DesignSystem.radiusXl),
onTap: widget.onPressed,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.icon,
color: widget.foregroundColor ?? Colors.white,
size: 20,
),
SizedBox(width: DesignSystem.spacingSm),
Text(
widget.label!,
style: DesignSystem.labelLarge.copyWith(
color: widget.foregroundColor ?? Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
);
},
);
}
}
/// Widget de FAB avec menu contextuel
class ModernFABWithMenu extends StatefulWidget {
const ModernFABWithMenu({
super.key,
required this.mainAction,
required this.menuItems,
this.heroTag,
});
final ModernFABAction mainAction;
final List<ModernFABAction> menuItems;
final Object? heroTag;
@override
State<ModernFABWithMenu> createState() => _ModernFABWithMenuState();
}
class _ModernFABWithMenuState extends State<ModernFABWithMenu>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
bool _isOpen = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationMedium,
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.75,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleMenu() {
setState(() {
_isOpen = !_isOpen;
if (_isOpen) {
_animationController.forward();
} else {
_animationController.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomRight,
children: [
// Menu items
...widget.menuItems.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final offset = (index + 1) * 70.0 * _animationController.value;
return Transform.translate(
offset: Offset(0, -offset),
child: Opacity(
opacity: _animationController.value,
child: ModernFloatingActionButton(
onPressed: () {
_toggleMenu();
item.onPressed?.call();
},
icon: item.icon,
mini: true,
backgroundColor: item.backgroundColor,
foregroundColor: item.foregroundColor,
heroTag: '${widget.heroTag}_$index',
),
),
);
},
);
}).toList(),
// Main FAB
AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: ModernFloatingActionButton(
onPressed: _toggleMenu,
icon: _isOpen ? Icons.close : widget.mainAction.icon,
backgroundColor: widget.mainAction.backgroundColor,
foregroundColor: widget.mainAction.foregroundColor,
heroTag: widget.heroTag,
),
);
},
),
],
);
}
}
/// Modèle pour une action de FAB
class ModernFABAction {
const ModernFABAction({
required this.icon,
this.onPressed,
this.backgroundColor,
this.foregroundColor,
this.label,
});
final IconData icon;
final VoidCallback? onPressed;
final Color? backgroundColor;
final Color? foregroundColor;
final String? label;
}

View File

@@ -1,205 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// TabBar moderne avec animations et design professionnel
class ModernTabBar extends StatefulWidget implements PreferredSizeWidget {
const ModernTabBar({
super.key,
required this.controller,
required this.tabs,
this.onTap,
});
final TabController controller;
final List<ModernTab> tabs;
final ValueChanged<int>? onTap;
@override
State<ModernTabBar> createState() => _ModernTabBarState();
@override
Size get preferredSize => Size.fromHeight(DesignSystem.goldenWidth(60));
}
class _ModernTabBarState extends State<ModernTabBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
widget.controller.addListener(_onTabChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onTabChanged);
_animationController.dispose();
super.dispose();
}
void _onTabChanged() {
if (mounted) {
_animationController.forward().then((_) {
_animationController.reverse();
});
}
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(
horizontal: DesignSystem.spacingLg,
vertical: DesignSystem.spacingSm,
),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: DesignSystem.shadowCard,
border: Border.all(
color: AppTheme.borderColor.withOpacity(0.1),
width: 1,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
child: TabBar(
controller: widget.controller,
onTap: widget.onTap,
indicator: BoxDecoration(
gradient: DesignSystem.primaryGradient,
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: EdgeInsets.all(DesignSystem.spacingXs),
labelColor: Colors.white,
unselectedLabelColor: AppTheme.textSecondary,
labelStyle: DesignSystem.labelLarge.copyWith(
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: DesignSystem.labelLarge.copyWith(
fontWeight: FontWeight.w500,
),
dividerColor: Colors.transparent,
tabs: widget.tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isSelected = widget.controller.index == index;
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: isSelected ? _scaleAnimation.value : 1.0,
child: _buildTab(tab, isSelected),
);
},
);
}).toList(),
),
),
);
}
Widget _buildTab(ModernTab tab, bool isSelected) {
return Container(
height: DesignSystem.goldenWidth(50),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: DesignSystem.animationFast,
child: Icon(
tab.icon,
size: isSelected ? 20 : 18,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
),
if (tab.label != null) ...[
SizedBox(width: DesignSystem.spacingXs),
AnimatedDefaultTextStyle(
duration: DesignSystem.animationFast,
style: (isSelected ? DesignSystem.labelLarge : DesignSystem.labelMedium).copyWith(
color: isSelected ? Colors.white : AppTheme.textSecondary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
child: Text(tab.label!),
),
],
if (tab.badge != null) ...[
SizedBox(width: DesignSystem.spacingXs),
_buildBadge(tab.badge!, isSelected),
],
],
),
);
}
Widget _buildBadge(String badge, bool isSelected) {
return AnimatedContainer(
duration: DesignSystem.animationFast,
padding: EdgeInsets.symmetric(
horizontal: DesignSystem.spacingXs,
vertical: 2,
),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
),
child: Text(
badge,
style: DesignSystem.labelSmall.copyWith(
color: isSelected ? Colors.white : AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 10,
),
),
);
}
}
/// Modèle pour un onglet moderne
class ModernTab {
const ModernTab({
required this.icon,
this.label,
this.badge,
});
final IconData icon;
final String? label;
final String? badge;
}
/// Extension pour créer facilement des onglets modernes
extension ModernTabExtension on Tab {
static ModernTab modern({
required IconData icon,
String? label,
String? badge,
}) {
return ModernTab(
icon: icon,
label: label,
badge: badge,
);
}
}

View File

@@ -1,269 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Graphique en barres professionnel avec animations et interactions
class ProfessionalBarChart extends StatefulWidget {
const ProfessionalBarChart({
super.key,
required this.data,
required this.title,
this.subtitle,
this.showGrid = true,
this.showValues = true,
this.animationDuration = const Duration(milliseconds: 1500),
this.barColor,
this.gradientColors,
});
final List<BarDataPoint> data;
final String title;
final String? subtitle;
final bool showGrid;
final bool showValues;
final Duration animationDuration;
final Color? barColor;
final List<Color>? gradientColors;
@override
State<ProfessionalBarChart> createState() => _ProfessionalBarChartState();
}
class _ProfessionalBarChartState extends State<ProfessionalBarChart>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
int _touchedIndex = -1;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: DesignSystem.spacingLg),
Expanded(
child: _buildChart(),
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: DesignSystem.titleLarge.copyWith(
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
SizedBox(height: DesignSystem.spacingXs),
Text(
widget.subtitle!,
style: DesignSystem.bodyMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
],
);
}
Widget _buildChart() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _getMaxY() * 1.2,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: AppTheme.textPrimary.withOpacity(0.9),
tooltipRoundedRadius: DesignSystem.radiusSm,
tooltipPadding: EdgeInsets.all(DesignSystem.spacingSm),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${widget.data[groupIndex].label}\n${rod.toY.toInt()}',
DesignSystem.labelMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
);
},
),
touchCallback: (FlTouchEvent event, barTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
barTouchResponse == null ||
barTouchResponse.spot == null) {
_touchedIndex = -1;
return;
}
_touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
});
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _buildBottomTitles,
reservedSize: 42,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _buildLeftTitles,
reservedSize: 40,
),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(
show: widget.showGrid,
drawVerticalLine: false,
horizontalInterval: _getMaxY() / 5,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.borderColor.withOpacity(0.3),
strokeWidth: 1,
);
},
),
barGroups: _buildBarGroups(),
),
);
},
);
}
List<BarChartGroupData> _buildBarGroups() {
return widget.data.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final isTouched = index == _touchedIndex;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: data.value * _animation.value,
color: _getBarColor(index, isTouched),
width: isTouched ? 24 : 20,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(DesignSystem.radiusXs),
topRight: Radius.circular(DesignSystem.radiusXs),
),
gradient: widget.gradientColors != null ? LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: widget.gradientColors!,
) : null,
),
],
showingTooltipIndicators: isTouched ? [0] : [],
);
}).toList();
}
Color _getBarColor(int index, bool isTouched) {
if (widget.barColor != null) {
return isTouched
? widget.barColor!
: widget.barColor!.withOpacity(0.8);
}
final colors = DesignSystem.chartColors;
final color = colors[index % colors.length];
return isTouched ? color : color.withOpacity(0.8);
}
Widget _buildBottomTitles(double value, TitleMeta meta) {
if (value.toInt() >= widget.data.length) return const SizedBox.shrink();
final data = widget.data[value.toInt()];
return SideTitleWidget(
axisSide: meta.axisSide,
child: Padding(
padding: EdgeInsets.only(top: DesignSystem.spacingXs),
child: Text(
data.label,
style: DesignSystem.labelSmall.copyWith(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildLeftTitles(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
value.toInt().toString(),
style: DesignSystem.labelSmall.copyWith(
color: AppTheme.textSecondary,
),
),
);
}
double _getMaxY() {
if (widget.data.isEmpty) return 10;
return widget.data.map((e) => e.value).reduce((a, b) => a > b ? a : b);
}
}
/// Modèle de données pour le graphique en barres
class BarDataPoint {
const BarDataPoint({
required this.label,
required this.value,
this.color,
});
final String label;
final double value;
final Color? color;
}

View File

@@ -1,282 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Graphique linéaire professionnel avec animations et interactions
class ProfessionalLineChart extends StatefulWidget {
const ProfessionalLineChart({
super.key,
required this.data,
required this.title,
this.subtitle,
this.showGrid = true,
this.showDots = true,
this.showArea = false,
this.animationDuration = const Duration(milliseconds: 1500),
this.lineColor,
this.gradientColors,
});
final List<LineDataPoint> data;
final String title;
final String? subtitle;
final bool showGrid;
final bool showDots;
final bool showArea;
final Duration animationDuration;
final Color? lineColor;
final List<Color>? gradientColors;
@override
State<ProfessionalLineChart> createState() => _ProfessionalLineChartState();
}
class _ProfessionalLineChartState extends State<ProfessionalLineChart>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
List<int> _showingTooltipOnSpots = [];
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DesignSystem.spacingLg),
Expanded(
child: _buildChart(),
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: DesignSystem.titleLarge.copyWith(
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
Text(
widget.subtitle!,
style: DesignSystem.bodyMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
],
);
}
Widget _buildChart() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: AppTheme.textPrimary.withOpacity(0.9),
tooltipRoundedRadius: DesignSystem.radiusSm,
tooltipPadding: const EdgeInsets.all(DesignSystem.spacingSm),
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
return touchedBarSpots.map((barSpot) {
final data = widget.data[barSpot.x.toInt()];
return LineTooltipItem(
'${data.label}\n${barSpot.y.toInt()}',
DesignSystem.labelMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
);
}).toList();
},
),
handleBuiltInTouches: true,
getTouchedSpotIndicator: (LineChartBarData barData, List<int> spotIndexes) {
return spotIndexes.map((index) {
return TouchedSpotIndicatorData(
FlLine(
color: widget.lineColor ?? AppTheme.primaryColor,
strokeWidth: 2,
dashArray: [3, 3],
),
FlDotData(
getDotPainter: (spot, percent, barData, index) =>
FlDotCirclePainter(
radius: 6,
color: widget.lineColor ?? AppTheme.primaryColor,
strokeWidth: 2,
strokeColor: Colors.white,
),
),
);
}).toList();
},
),
gridData: FlGridData(
show: widget.showGrid,
drawVerticalLine: false,
horizontalInterval: _getMaxY() / 5,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.borderColor.withOpacity(0.3),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _buildBottomTitles,
reservedSize: 42,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _buildLeftTitles,
reservedSize: 40,
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: widget.data.length.toDouble() - 1,
minY: 0,
maxY: _getMaxY() * 1.2,
lineBarsData: [
_buildLineBarData(),
],
),
);
},
);
}
LineChartBarData _buildLineBarData() {
final spots = widget.data.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
return FlSpot(index.toDouble(), data.value * _animation.value);
}).toList();
return LineChartBarData(
spots: spots,
isCurved: true,
curveSmoothness: 0.3,
color: widget.lineColor ?? AppTheme.primaryColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: widget.showDots,
getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(
radius: 4,
color: widget.lineColor ?? AppTheme.primaryColor,
strokeWidth: 2,
strokeColor: Colors.white,
),
),
belowBarData: widget.showArea ? BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: widget.gradientColors ?? [
(widget.lineColor ?? AppTheme.primaryColor).withOpacity(0.3),
(widget.lineColor ?? AppTheme.primaryColor).withOpacity(0.05),
],
),
) : BarAreaData(show: false),
);
}
Widget _buildBottomTitles(double value, TitleMeta meta) {
if (value.toInt() >= widget.data.length) return const SizedBox.shrink();
final data = widget.data[value.toInt()];
return SideTitleWidget(
axisSide: meta.axisSide,
child: Padding(
padding: const EdgeInsets.only(top: DesignSystem.spacingXs),
child: Text(
data.label,
style: DesignSystem.labelSmall.copyWith(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildLeftTitles(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
value.toInt().toString(),
style: DesignSystem.labelSmall.copyWith(
color: AppTheme.textSecondary,
),
),
);
}
double _getMaxY() {
if (widget.data.isEmpty) return 10;
return widget.data.map((e) => e.value).reduce((a, b) => a > b ? a : b);
}
}
/// Modèle de données pour le graphique linéaire
class LineDataPoint {
const LineDataPoint({
required this.label,
required this.value,
});
final String label;
final double value;
}

View File

@@ -1,307 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Graphique en secteurs professionnel avec animations et légendes
class ProfessionalPieChart extends StatefulWidget {
const ProfessionalPieChart({
super.key,
required this.data,
required this.title,
this.subtitle,
this.centerText,
this.showLegend = true,
this.showPercentages = true,
this.animationDuration = const Duration(milliseconds: 1500),
});
final List<ChartDataPoint> data;
final String title;
final String? subtitle;
final String? centerText;
final bool showLegend;
final bool showPercentages;
final Duration animationDuration;
@override
State<ProfessionalPieChart> createState() => _ProfessionalPieChartState();
}
class _ProfessionalPieChartState extends State<ProfessionalPieChart>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
int _touchedIndex = -1;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DesignSystem.spacingLg),
Expanded(
child: Row(
children: [
Expanded(
flex: 3,
child: _buildChart(),
),
if (widget.showLegend) ...[
const SizedBox(width: DesignSystem.spacingLg),
Expanded(
flex: 2,
child: _buildLegend(),
),
],
],
),
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: DesignSystem.titleLarge.copyWith(
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
Text(
widget.subtitle!,
style: DesignSystem.bodyMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
],
);
}
Widget _buildChart() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
height: 140, // Hauteur encore plus réduite
padding: const EdgeInsets.all(4), // Padding minimal pour contenir le graphique
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
_touchedIndex = -1;
return;
}
_touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
});
},
),
borderData: FlBorderData(show: false),
sectionsSpace: 1, // Espace réduit entre sections
centerSpaceRadius: widget.centerText != null ? 45 : 30, // Rayon central réduit
sections: _buildSections(),
),
),
);
},
);
}
List<PieChartSectionData> _buildSections() {
final total = widget.data.fold<double>(0, (sum, item) => sum + item.value);
return widget.data.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final isTouched = index == _touchedIndex;
final percentage = (data.value / total * 100);
return PieChartSectionData(
color: data.color,
value: data.value * _animation.value,
title: widget.showPercentages ? '${percentage.toStringAsFixed(1)}%' : '',
radius: isTouched ? 70 : 60,
titleStyle: DesignSystem.labelMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.3),
offset: const Offset(1, 1),
blurRadius: 2,
),
],
),
titlePositionPercentageOffset: 0.6,
badgeWidget: isTouched ? _buildBadge(data) : null,
badgePositionPercentageOffset: 1.3,
);
}).toList();
}
Widget _buildBadge(ChartDataPoint data) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingSm,
vertical: DesignSystem.spacingXs,
),
decoration: BoxDecoration(
color: data.color,
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
boxShadow: DesignSystem.shadowCard,
),
child: Text(
data.value.toInt().toString(),
style: DesignSystem.labelMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildLegend() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.centerText != null) ...[
_buildCenterInfo(),
const SizedBox(height: DesignSystem.spacingLg),
],
...widget.data.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final isSelected = index == _touchedIndex;
return AnimatedContainer(
duration: DesignSystem.animationFast,
margin: const EdgeInsets.only(bottom: DesignSystem.spacingSm),
padding: const EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: isSelected ? data.color.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
border: isSelected ? Border.all(
color: data.color.withOpacity(0.3),
width: 1,
) : null,
),
child: Row(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: data.color,
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
),
),
const SizedBox(width: DesignSystem.spacingSm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.label,
style: DesignSystem.labelLarge.copyWith(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
Text(
data.value.toInt().toString(),
style: DesignSystem.labelMedium.copyWith(
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
);
}).toList(),
],
);
}
Widget _buildCenterInfo() {
return Container(
padding: const EdgeInsets.all(DesignSystem.spacingMd),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Text(
'Total',
style: DesignSystem.labelMedium.copyWith(
color: AppTheme.textSecondary,
),
),
const SizedBox(height: DesignSystem.spacingXs),
Text(
widget.centerText!,
style: DesignSystem.headlineMedium.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
/// Modèle de données pour le graphique en secteurs
class ChartDataPoint {
const ChartDataPoint({
required this.label,
required this.value,
required this.color,
});
final String label;
final double value;
final Color color;
}

View File

@@ -1,544 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/cards/sophisticated_card.dart';
import '../../../../shared/widgets/avatars/sophisticated_avatar.dart';
import '../../../../shared/widgets/badges/status_badge.dart';
import '../../../../shared/widgets/badges/count_badge.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
class SophisticatedMemberCard extends StatefulWidget {
final Map<String, dynamic> member;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onMessage;
final VoidCallback? onCall;
final bool showActions;
final bool compact;
const SophisticatedMemberCard({
super.key,
required this.member,
this.onTap,
this.onEdit,
this.onMessage,
this.onCall,
this.showActions = true,
this.compact = false,
});
@override
State<SophisticatedMemberCard> createState() => _SophisticatedMemberCardState();
}
class _SophisticatedMemberCardState extends State<SophisticatedMemberCard>
with TickerProviderStateMixin {
late AnimationController _expandController;
late AnimationController _actionController;
late Animation<double> _expandAnimation;
late Animation<double> _actionAnimation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_expandController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_actionController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_expandAnimation = CurvedAnimation(
parent: _expandController,
curve: Curves.easeInOut,
);
_actionAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _actionController,
curve: Curves.elasticOut,
));
_actionController.forward();
}
@override
void dispose() {
_expandController.dispose();
_actionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SophisticatedCard(
variant: CardVariant.elevated,
size: widget.compact ? CardSize.compact : CardSize.standard,
onTap: widget.onTap,
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
child: Column(
children: [
_buildMainContent(),
AnimatedBuilder(
animation: _expandAnimation,
builder: (context, child) {
return ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: _expandAnimation.value,
child: child,
),
);
},
child: _buildExpandedContent(),
),
],
),
);
}
Widget _buildMainContent() {
return Row(
children: [
_buildAvatar(),
const SizedBox(width: 16),
Expanded(child: _buildMemberInfo()),
_buildTrailingActions(),
],
);
}
Widget _buildAvatar() {
final roleColor = _getRoleColor();
final isOnline = widget.member['status'] == 'Actif';
return SophisticatedAvatar(
initials: _getInitials(),
size: widget.compact ? AvatarSize.medium : AvatarSize.large,
variant: AvatarVariant.gradient,
backgroundColor: roleColor,
showOnlineStatus: true,
isOnline: isOnline,
badge: _buildRoleBadge(),
onTap: () => _toggleExpanded(),
);
}
Widget _buildRoleBadge() {
final role = widget.member['role'] as String;
if (role == 'Président' || role == 'Secrétaire' || role == 'Trésorier') {
return CountBadge(
count: 1,
backgroundColor: AppTheme.warningColor,
size: 16,
suffix: '',
);
}
return const SizedBox.shrink();
}
Widget _buildMemberInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'${widget.member['firstName']} ${widget.member['lastName']}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildStatusBadge(),
],
),
const SizedBox(height: 4),
_buildRoleChip(),
if (!widget.compact) ...[
const SizedBox(height: 8),
_buildQuickInfo(),
],
],
);
}
Widget _buildStatusBadge() {
final status = widget.member['status'] as String;
final cotisationStatus = widget.member['cotisationStatus'] as String;
if (cotisationStatus == 'En retard') {
return StatusBadge(
text: 'Retard',
type: BadgeType.error,
size: BadgeSize.small,
variant: BadgeVariant.ghost,
icon: Icons.warning,
);
}
return StatusBadge(
text: status,
type: status == 'Actif' ? BadgeType.success : BadgeType.neutral,
size: BadgeSize.small,
variant: BadgeVariant.ghost,
);
}
Widget _buildRoleChip() {
final role = widget.member['role'] as String;
final roleColor = _getRoleColor();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: roleColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: roleColor.withOpacity(0.3),
width: 1,
),
),
child: Text(
role,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: roleColor,
),
),
);
}
Widget _buildQuickInfo() {
return Row(
children: [
Expanded(
child: _buildInfoItem(
Icons.email_outlined,
widget.member['email'],
AppTheme.infoColor,
),
),
const SizedBox(width: 16),
_buildInfoItem(
Icons.phone_outlined,
_formatPhone(widget.member['phone']),
AppTheme.successColor,
),
],
);
}
Widget _buildInfoItem(IconData icon, String text, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Flexible(
child: Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildTrailingActions() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: _actionAnimation,
builder: (context, child) {
return Transform.scale(
scale: _actionAnimation.value,
child: IconButton(
onPressed: _toggleExpanded,
icon: AnimatedRotation(
turns: _isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 300),
child: const Icon(Icons.expand_more),
),
iconSize: 20,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
style: IconButton.styleFrom(
backgroundColor: AppTheme.backgroundLight,
foregroundColor: AppTheme.textSecondary,
),
),
);
},
),
if (widget.compact) ...[
const SizedBox(height: 4),
_buildQuickActionButton(),
],
],
);
}
Widget _buildQuickActionButton() {
return QuickButtons.iconGhost(
icon: Icons.edit,
onPressed: widget.onEdit ?? _editMember,
size: 32,
color: _getRoleColor(),
tooltip: 'Modifier',
);
}
Widget _buildExpandedContent() {
return Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildDetailedInfo(),
if (widget.showActions) ...[
const SizedBox(height: 16),
_buildActionButtons(),
],
],
),
);
}
Widget _buildDetailedInfo() {
return Column(
children: [
_buildDetailRow(
'Adhésion',
_formatDate(widget.member['joinDate']),
Icons.calendar_today,
AppTheme.primaryColor,
),
const SizedBox(height: 12),
_buildDetailRow(
'Dernière activité',
_formatDate(widget.member['lastActivity']),
Icons.access_time,
AppTheme.infoColor,
),
const SizedBox(height: 12),
_buildDetailRow(
'Cotisation',
widget.member['cotisationStatus'],
Icons.payment,
_getCotisationColor(),
),
],
);
}
Widget _buildDetailRow(String label, String value, IconData icon, Color color) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, size: 16, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: label == 'Cotisation' ? color : AppTheme.textPrimary,
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: QuickButtons.outline(
text: 'Appeler',
icon: Icons.phone,
onPressed: widget.onCall ?? _callMember,
size: ButtonSize.small,
color: AppTheme.successColor,
),
),
const SizedBox(width: 8),
Expanded(
child: QuickButtons.outline(
text: 'Message',
icon: Icons.message,
onPressed: widget.onMessage ?? _messageMember,
size: ButtonSize.small,
color: AppTheme.infoColor,
),
),
const SizedBox(width: 8),
Expanded(
child: QuickButtons.outline(
text: 'Modifier',
icon: Icons.edit,
onPressed: widget.onEdit ?? _editMember,
size: ButtonSize.small,
color: AppTheme.warningColor,
),
),
],
);
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_expandController.forward();
} else {
_expandController.reverse();
}
});
HapticFeedback.selectionClick();
}
String _getInitials() {
final firstName = widget.member['firstName'] as String;
final lastName = widget.member['lastName'] as String;
return '${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}'.toUpperCase();
}
Color _getRoleColor() {
switch (widget.member['role']) {
case 'Président':
return AppTheme.primaryColor;
case 'Secrétaire':
return AppTheme.secondaryColor;
case 'Trésorier':
return AppTheme.accentColor;
case 'Responsable événements':
return AppTheme.warningColor;
default:
return AppTheme.infoColor;
}
}
Color _getCotisationColor() {
switch (widget.member['cotisationStatus']) {
case 'À jour':
return AppTheme.successColor;
case 'En retard':
return AppTheme.errorColor;
case 'Exempt':
return AppTheme.infoColor;
default:
return AppTheme.textSecondary;
}
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays < 1) {
return 'Aujourd\'hui';
} else if (difference.inDays < 7) {
return 'Il y a ${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
} else if (difference.inDays < 30) {
final weeks = (difference.inDays / 7).floor();
return 'Il y a $weeks semaine${weeks > 1 ? 's' : ''}';
} else {
final months = [
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
} catch (e) {
return dateString;
}
}
String _formatPhone(String phone) {
if (phone.length >= 10) {
return '${phone.substring(0, 3)} ${phone.substring(3, 5)} ${phone.substring(5, 7)} ${phone.substring(7, 9)} ${phone.substring(9)}';
}
return phone;
}
void _callMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Appel vers ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _messageMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Message vers ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _editMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Modification de ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.warningColor,
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -1,243 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Grille de statistiques compacte pour mobile
class StatsGridCard extends StatefulWidget {
const StatsGridCard({
super.key,
required this.stats,
this.crossAxisCount = 2,
this.childAspectRatio = 1.2,
});
final Map<String, dynamic> stats;
final int crossAxisCount;
final double childAspectRatio;
@override
State<StatsGridCard> createState() => _StatsGridCardState();
}
class _StatsGridCardState extends State<StatsGridCard>
with TickerProviderStateMixin {
late List<AnimationController> _animationControllers;
late List<Animation<double>> _scaleAnimations;
late List<Animation<Offset>> _slideAnimations;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
const itemCount = 4; // Nombre de statistiques
_animationControllers = List.generate(
itemCount,
(index) => AnimationController(
duration: Duration(
milliseconds: DesignSystem.animationMedium.inMilliseconds + (index * 100),
),
vsync: this,
),
);
_scaleAnimations = _animationControllers.map((controller) {
return Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: controller,
curve: DesignSystem.animationCurveEnter,
));
}).toList();
_slideAnimations = _animationControllers.map((controller) {
return Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: controller,
curve: DesignSystem.animationCurveEnter,
));
}).toList();
// Démarrer les animations en cascade
for (int i = 0; i < _animationControllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 100), () {
if (mounted) {
_animationControllers[i].forward();
}
});
}
}
@override
void dispose() {
for (final controller in _animationControllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final statsItems = [
_StatItem(
title: 'Total Membres',
value: widget.stats['totalMembres'].toString(),
icon: Icons.people,
color: AppTheme.primaryColor,
trend: '+${widget.stats['nouveauxCeMois']}',
trendPositive: true,
),
_StatItem(
title: 'Membres Actifs',
value: widget.stats['membresActifs'].toString(),
icon: Icons.person,
color: AppTheme.successColor,
trend: '${widget.stats['tauxActivite']}%',
trendPositive: widget.stats['tauxActivite'] >= 70,
),
_StatItem(
title: 'Nouveaux ce mois',
value: widget.stats['nouveauxCeMois'].toString(),
icon: Icons.person_add,
color: AppTheme.infoColor,
trend: 'Ce mois',
trendPositive: widget.stats['nouveauxCeMois'] > 0,
),
_StatItem(
title: 'Taux d\'activité',
value: '${widget.stats['tauxActivite']}%',
icon: Icons.trending_up,
color: AppTheme.warningColor,
trend: widget.stats['tauxActivite'] >= 70 ? 'Excellent' : 'Moyen',
trendPositive: widget.stats['tauxActivite'] >= 70,
),
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
childAspectRatio: widget.childAspectRatio,
crossAxisSpacing: DesignSystem.spacingMd,
mainAxisSpacing: DesignSystem.spacingMd,
),
itemCount: statsItems.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _animationControllers[index],
builder: (context, child) {
return SlideTransition(
position: _slideAnimations[index],
child: ScaleTransition(
scale: _scaleAnimations[index],
child: _buildStatCard(statsItems[index]),
),
);
},
);
},
);
}
Widget _buildStatCard(_StatItem item) {
return Container(
padding: const EdgeInsets.all(DesignSystem.spacingMd),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: DesignSystem.shadowCard,
border: Border.all(
color: item.color.withOpacity(0.1),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: item.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
child: Icon(
item.icon,
color: item.color,
size: 20,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingXs,
vertical: 2,
),
decoration: BoxDecoration(
color: item.trendPositive
? AppTheme.successColor.withOpacity(0.1)
: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
),
child: Text(
item.trend,
style: DesignSystem.labelSmall.copyWith(
color: item.trendPositive
? AppTheme.successColor
: AppTheme.errorColor,
fontWeight: FontWeight.w600,
fontSize: 10,
),
),
),
],
),
const SizedBox(height: DesignSystem.spacingSm),
Text(
item.value,
style: DesignSystem.headlineMedium.copyWith(
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: DesignSystem.spacingXs),
Text(
item.title,
style: DesignSystem.labelMedium.copyWith(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
/// Modèle pour un élément de statistique
class _StatItem {
const _StatItem({
required this.title,
required this.value,
required this.icon,
required this.color,
required this.trend,
required this.trendPositive,
});
final String title;
final String value;
final IconData icon;
final Color color;
final String trend;
final bool trendPositive;
}

View File

@@ -1,281 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Card de vue d'ensemble des statistiques avec design professionnel
class StatsOverviewCard extends StatefulWidget {
const StatsOverviewCard({
super.key,
required this.stats,
this.onTap,
});
final Map<String, dynamic> stats;
final VoidCallback? onTap;
@override
State<StatsOverviewCard> createState() => _StatsOverviewCardState();
}
class _StatsOverviewCardState extends State<StatsOverviewCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationMedium,
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurveEnter,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: _buildCard(),
),
);
},
);
}
Widget _buildCard() {
return Container(
padding: const EdgeInsets.all(DesignSystem.spacingLg),
decoration: BoxDecoration(
gradient: DesignSystem.primaryGradient,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: DesignSystem.shadowCard,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DesignSystem.spacingLg),
_buildMainStats(),
const SizedBox(height: DesignSystem.spacingLg),
_buildSecondaryStats(),
const SizedBox(height: DesignSystem.spacingMd),
_buildProgressIndicator(),
],
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vue d\'ensemble',
style: DesignSystem.titleLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: DesignSystem.spacingXs),
Text(
'Statistiques générales',
style: DesignSystem.bodyMedium.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
],
),
Container(
padding: const EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
child: const Icon(
Icons.analytics,
color: Colors.white,
size: 24,
),
),
],
);
}
Widget _buildMainStats() {
return Row(
children: [
Expanded(
child: _buildStatItem(
'Total Membres',
widget.stats['totalMembres'].toString(),
Icons.people,
Colors.white,
),
),
const SizedBox(width: DesignSystem.spacingLg),
Expanded(
child: _buildStatItem(
'Membres Actifs',
widget.stats['membresActifs'].toString(),
Icons.person,
Colors.white,
),
),
],
);
}
Widget _buildSecondaryStats() {
return Row(
children: [
Expanded(
child: _buildStatItem(
'Nouveaux ce mois',
widget.stats['nouveauxCeMois'].toString(),
Icons.person_add,
Colors.white.withOpacity(0.9),
isSecondary: true,
),
),
const SizedBox(width: DesignSystem.spacingLg),
Expanded(
child: _buildStatItem(
'Taux d\'activité',
'${widget.stats['tauxActivite']}%',
Icons.trending_up,
Colors.white.withOpacity(0.9),
isSecondary: true,
),
),
],
);
}
Widget _buildStatItem(
String label,
String value,
IconData icon,
Color color, {
bool isSecondary = false,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: color,
size: isSecondary ? 16 : 20,
),
const SizedBox(width: DesignSystem.spacingXs),
Text(
label,
style: (isSecondary ? DesignSystem.labelMedium : DesignSystem.labelLarge).copyWith(
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: DesignSystem.spacingXs),
Text(
value,
style: (isSecondary ? DesignSystem.headlineMedium : DesignSystem.displayMedium).copyWith(
color: color,
fontWeight: FontWeight.w800,
fontSize: isSecondary ? 20 : 32,
),
),
],
);
}
Widget _buildProgressIndicator() {
final tauxActivite = widget.stats['tauxActivite'] as int;
final progress = tauxActivite / 100.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Engagement communautaire',
style: DesignSystem.labelMedium.copyWith(
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
Text(
'$tauxActivite%',
style: DesignSystem.labelMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: DesignSystem.spacingXs),
Container(
height: 6,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
),
),
),
],
);
}
}