Refactoring - Version OK
This commit is contained in:
@@ -1,250 +0,0 @@
|
||||
# 🚀 Widgets Dashboard Améliorés - UnionFlow Mobile
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
Cette documentation présente les **3 widgets dashboard améliorés** avec des fonctionnalités avancées, des styles multiples et une architecture moderne.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Widgets Améliorés
|
||||
|
||||
### 1. **DashboardQuickActionButton** - Boutons d'Action Sophistiqués
|
||||
|
||||
#### ✨ Nouvelles Fonctionnalités :
|
||||
- **7 types d'actions** : `primary`, `secondary`, `success`, `warning`, `error`, `info`, `custom`
|
||||
- **6 styles** : `elevated`, `filled`, `outlined`, `text`, `gradient`, `minimal`
|
||||
- **4 tailles** : `small`, `medium`, `large`, `extraLarge`
|
||||
- **5 états** : `enabled`, `disabled`, `loading`, `success`, `error`
|
||||
- **Animations fluides** avec contrôle granulaire
|
||||
- **Feedback haptique** configurable
|
||||
- **Badges et indicateurs** visuels
|
||||
- **Icônes secondaires** pour plus de contexte
|
||||
- **Tooltips** avec descriptions détaillées
|
||||
- **Support long press** pour actions avancées
|
||||
|
||||
#### 🎨 Constructeurs Spécialisés :
|
||||
```dart
|
||||
// Action primaire
|
||||
DashboardQuickAction.primary(
|
||||
icon: Icons.person_add,
|
||||
title: 'Ajouter Membre',
|
||||
subtitle: 'Nouveau',
|
||||
badge: '+',
|
||||
onTap: () => handleAction(),
|
||||
)
|
||||
|
||||
// Action avec gradient
|
||||
DashboardQuickAction.gradient(
|
||||
icon: Icons.star,
|
||||
title: 'Premium',
|
||||
gradient: LinearGradient(...),
|
||||
onTap: () => handlePremium(),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **DashboardQuickActionsGrid** - Grilles Flexibles et Responsives
|
||||
|
||||
#### ✨ Nouvelles Fonctionnalités :
|
||||
- **7 layouts** : `grid2x2`, `grid3x2`, `grid4x2`, `horizontal`, `vertical`, `staggered`, `carousel`
|
||||
- **5 styles** : `standard`, `compact`, `expanded`, `minimal`, `card`
|
||||
- **Animations d'apparition** avec délais configurables
|
||||
- **Filtrage par permissions** utilisateur
|
||||
- **Limitation du nombre d'actions** affichées
|
||||
- **Support "Voir tout"** pour navigation
|
||||
- **Mode debug** pour développement
|
||||
- **Responsive design** adaptatif
|
||||
|
||||
#### 🎨 Constructeurs Spécialisés :
|
||||
```dart
|
||||
// Grille compacte
|
||||
DashboardQuickActionsGrid.compact(
|
||||
title: 'Actions Rapides',
|
||||
onActionTap: (type) => handleAction(type),
|
||||
)
|
||||
|
||||
// Carrousel horizontal
|
||||
DashboardQuickActionsGrid.carousel(
|
||||
title: 'Actions Populaires',
|
||||
animated: true,
|
||||
)
|
||||
|
||||
// Grille étendue avec "Voir tout"
|
||||
DashboardQuickActionsGrid.expanded(
|
||||
title: 'Toutes les Actions',
|
||||
subtitle: 'Accès complet',
|
||||
onSeeAll: () => navigateToAllActions(),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **DashboardStatsCard** - Cartes de Statistiques Avancées
|
||||
|
||||
#### ✨ Nouvelles Fonctionnalités :
|
||||
- **7 types de stats** : `count`, `percentage`, `currency`, `duration`, `rate`, `score`, `custom`
|
||||
- **7 styles** : `standard`, `minimal`, `elevated`, `outlined`, `gradient`, `compact`, `detailed`
|
||||
- **4 tailles** : `small`, `medium`, `large`, `extraLarge`
|
||||
- **Indicateurs de tendance** : `up`, `down`, `stable`, `unknown`
|
||||
- **Comparaisons temporelles** avec pourcentages de changement
|
||||
- **Graphiques miniatures** (sparklines)
|
||||
- **Badges et notifications** visuels
|
||||
- **Formatage automatique** des valeurs
|
||||
- **Animations d'apparition** sophistiquées
|
||||
|
||||
#### 🎨 Constructeurs Spécialisés :
|
||||
```dart
|
||||
// Statistique de comptage
|
||||
DashboardStat.count(
|
||||
icon: Icons.people,
|
||||
value: '1,247',
|
||||
title: 'Membres Actifs',
|
||||
changePercentage: 12.5,
|
||||
trend: StatTrend.up,
|
||||
period: 'ce mois',
|
||||
)
|
||||
|
||||
// Statistique avec devise
|
||||
DashboardStat.currency(
|
||||
icon: Icons.euro,
|
||||
value: '45,230',
|
||||
title: 'Revenus',
|
||||
sparklineData: [100, 120, 110, 140, 135, 160],
|
||||
style: StatCardStyle.detailed,
|
||||
)
|
||||
|
||||
// Statistique avec gradient
|
||||
DashboardStat.gradient(
|
||||
icon: Icons.star,
|
||||
value: '4.8',
|
||||
title: 'Satisfaction',
|
||||
gradient: LinearGradient(...),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Utilisation Pratique
|
||||
|
||||
### Import des Widgets :
|
||||
```dart
|
||||
import 'dashboard_quick_action_button.dart';
|
||||
import 'dashboard_quick_actions_grid.dart';
|
||||
import 'dashboard_stats_card.dart';
|
||||
```
|
||||
|
||||
### Exemple d'Intégration :
|
||||
```dart
|
||||
Column(
|
||||
children: [
|
||||
// Grille d'actions rapides
|
||||
DashboardQuickActionsGrid.expanded(
|
||||
title: 'Actions Principales',
|
||||
onActionTap: (type) => _handleQuickAction(type),
|
||||
userPermissions: currentUser.permissions,
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Statistiques en grille
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
children: [
|
||||
DashboardStatsCard(
|
||||
stat: DashboardStat.count(
|
||||
icon: Icons.people,
|
||||
value: '${memberCount}',
|
||||
title: 'Membres',
|
||||
changePercentage: memberGrowth,
|
||||
trend: memberTrend,
|
||||
),
|
||||
),
|
||||
// ... autres stats
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
### Couleurs Utilisées :
|
||||
- **Primary** : `#6C5CE7` (Violet principal)
|
||||
- **Success** : `#00B894` (Vert succès)
|
||||
- **Warning** : `#FDCB6E` (Orange alerte)
|
||||
- **Error** : `#E17055` (Rouge erreur)
|
||||
|
||||
### Espacements :
|
||||
- **Small** : `8px`
|
||||
- **Medium** : `16px`
|
||||
- **Large** : `24px`
|
||||
- **Extra Large** : `32px`
|
||||
|
||||
### Animations :
|
||||
- **Durée standard** : `200ms`
|
||||
- **Courbe** : `Curves.easeOutBack`
|
||||
- **Délai entre éléments** : `100ms`
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test et Démonstration
|
||||
|
||||
### Page de Test :
|
||||
```dart
|
||||
import 'test_improved_widgets.dart';
|
||||
|
||||
// Navigation vers la page de test
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TestImprovedWidgetsPage(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Fonctionnalités Testées :
|
||||
- ✅ Tous les styles et tailles
|
||||
- ✅ Animations et transitions
|
||||
- ✅ Feedback haptique
|
||||
- ✅ Gestion des états
|
||||
- ✅ Responsive design
|
||||
- ✅ Accessibilité
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métriques d'Amélioration
|
||||
|
||||
### Performance :
|
||||
- **Réduction du code** : -60% de duplication
|
||||
- **Temps de développement** : -75% pour nouveaux dashboards
|
||||
- **Maintenance** : +80% plus facile
|
||||
|
||||
### Fonctionnalités :
|
||||
- **Styles disponibles** : 6x plus qu'avant
|
||||
- **Layouts supportés** : 7 types différents
|
||||
- **États gérés** : 5 états interactifs
|
||||
- **Animations** : 100% fluides et configurables
|
||||
|
||||
### Dimensions Optimisées :
|
||||
- **Largeur des boutons** : Réduite de 50% (140px → 100px)
|
||||
- **Hauteur des boutons** : Optimisée (100px → 70px)
|
||||
- **Format rectangulaire** : Ratio d'aspect 1.6 au lieu de 2.2
|
||||
- **Bordures** : Moins arrondies (12px → 6px)
|
||||
- **Espacement** : Réduit pour plus de compacité
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Prochaines Étapes
|
||||
|
||||
1. **Tests unitaires** complets
|
||||
2. **Documentation API** détaillée
|
||||
3. **Exemples d'usage** avancés
|
||||
4. **Intégration** dans tous les dashboards
|
||||
5. **Optimisations** de performance
|
||||
|
||||
---
|
||||
|
||||
**Les widgets dashboard UnionFlow Mobile sont maintenant de niveau professionnel avec une architecture moderne et des fonctionnalités avancées !** 🎯✨
|
||||
@@ -0,0 +1,410 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de graphique pour le dashboard
|
||||
class DashboardChartWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final DashboardChartType chartType;
|
||||
final double height;
|
||||
|
||||
const DashboardChartWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.chartType,
|
||||
this.height = 200,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingChart();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildChart(data);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorChart();
|
||||
}
|
||||
return _buildEmptyChart();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
_getChartIcon(),
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChart(DashboardEntity data) {
|
||||
switch (chartType) {
|
||||
case DashboardChartType.memberActivity:
|
||||
return _buildMemberActivityChart(data.stats);
|
||||
case DashboardChartType.contributionTrend:
|
||||
return _buildContributionTrendChart(data.stats);
|
||||
case DashboardChartType.eventParticipation:
|
||||
return _buildEventParticipationChart(data.upcomingEvents);
|
||||
case DashboardChartType.monthlyGrowth:
|
||||
return _buildMonthlyGrowthChart(data.stats);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMemberActivityChart(DashboardStatsEntity stats) {
|
||||
return PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: DashboardTheme.success,
|
||||
value: stats.activeMembers.toDouble(),
|
||||
title: '${stats.activeMembers}',
|
||||
radius: 50,
|
||||
titleStyle: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: DashboardTheme.grey300,
|
||||
value: (stats.totalMembers - stats.activeMembers).toDouble(),
|
||||
title: '${stats.totalMembers - stats.activeMembers}',
|
||||
radius: 45,
|
||||
titleStyle: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContributionTrendChart(DashboardStatsEntity stats) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: stats.totalContributionAmount / 4,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return const FlLine(
|
||||
color: DashboardTheme.grey200,
|
||||
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', 'Jun'];
|
||||
if (value.toInt() >= 0 && value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: DashboardTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: stats.totalContributionAmount / 4,
|
||||
reservedSize: 60,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toStringAsFixed(0)}K',
|
||||
style: DashboardTheme.bodySmall,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: 5,
|
||||
minY: 0,
|
||||
maxY: stats.totalContributionAmount,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: _generateContributionSpots(stats),
|
||||
isCurved: true,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
DashboardTheme.tealBlue,
|
||||
DashboardTheme.royalBlue,
|
||||
],
|
||||
),
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
DashboardTheme.tealBlue.withOpacity(0.3),
|
||||
DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventParticipationChart(List<UpcomingEventEntity> events) {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyChart();
|
||||
}
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: events.map((e) => e.maxParticipants).reduce((a, b) => a > b ? a : b).toDouble(),
|
||||
barTouchData: BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
if (value.toInt() < events.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
events[value.toInt()].title.length > 8
|
||||
? '${events[value.toInt()].title.substring(0, 8)}...'
|
||||
: events[value.toInt()].title,
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
reservedSize: 40,
|
||||
),
|
||||
),
|
||||
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: events.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final event = entry.value;
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: event.currentParticipants.toDouble(),
|
||||
color: event.isFull
|
||||
? DashboardTheme.error
|
||||
: event.isAlmostFull
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.success,
|
||||
width: 16,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthlyGrowthChart(DashboardStatsEntity stats) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: 11,
|
||||
minY: -5,
|
||||
maxY: 20,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: _generateGrowthSpots(stats.monthlyGrowth),
|
||||
isCurved: true,
|
||||
color: stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: (stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error)
|
||||
.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<FlSpot> _generateContributionSpots(DashboardStatsEntity stats) {
|
||||
final baseAmount = stats.totalContributionAmount / 6;
|
||||
return [
|
||||
FlSpot(0, baseAmount * 0.8),
|
||||
FlSpot(1, baseAmount * 1.2),
|
||||
FlSpot(2, baseAmount * 0.9),
|
||||
FlSpot(3, baseAmount * 1.5),
|
||||
FlSpot(4, baseAmount * 1.1),
|
||||
FlSpot(5, baseAmount * 1.3),
|
||||
];
|
||||
}
|
||||
|
||||
List<FlSpot> _generateGrowthSpots(double currentGrowth) {
|
||||
final baseGrowth = currentGrowth;
|
||||
return List.generate(12, (index) {
|
||||
final variation = (index % 3 - 1) * 2.0;
|
||||
return FlSpot(index.toDouble(), baseGrowth + variation);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildLoadingChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: DashboardTheme.royalBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.bar_chart,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getChartIcon() {
|
||||
switch (chartType) {
|
||||
case DashboardChartType.memberActivity:
|
||||
return Icons.pie_chart;
|
||||
case DashboardChartType.contributionTrend:
|
||||
return Icons.trending_up;
|
||||
case DashboardChartType.eventParticipation:
|
||||
return Icons.bar_chart;
|
||||
case DashboardChartType.monthlyGrowth:
|
||||
return Icons.show_chart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DashboardChartType {
|
||||
memberActivity,
|
||||
contributionTrend,
|
||||
eventParticipation,
|
||||
monthlyGrowth,
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
|
||||
/// Widget réutilisable pour afficher un élément d'activité
|
||||
///
|
||||
///
|
||||
/// Composant standardisé pour les listes d'activités récentes,
|
||||
/// notifications, historiques, etc.
|
||||
///
|
||||
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
|
||||
class ActivityItem extends StatelessWidget {
|
||||
/// Titre principal de l'activité
|
||||
final String title;
|
||||
@@ -53,7 +56,7 @@ class ActivityItem extends StatelessWidget {
|
||||
required this.timestamp,
|
||||
this.onTap,
|
||||
}) : icon = Icons.settings,
|
||||
color = const Color(0xFF6C5CE7),
|
||||
color = ColorTokens.primary,
|
||||
type = ActivityType.system,
|
||||
style = ActivityItemStyle.normal,
|
||||
showStatusIndicator = true;
|
||||
@@ -66,7 +69,7 @@ class ActivityItem extends StatelessWidget {
|
||||
required this.timestamp,
|
||||
this.onTap,
|
||||
}) : icon = Icons.person,
|
||||
color = const Color(0xFF00B894),
|
||||
color = ColorTokens.success,
|
||||
type = ActivityType.user,
|
||||
style = ActivityItemStyle.normal,
|
||||
showStatusIndicator = true;
|
||||
@@ -79,7 +82,7 @@ class ActivityItem extends StatelessWidget {
|
||||
required this.timestamp,
|
||||
this.onTap,
|
||||
}) : icon = Icons.warning,
|
||||
color = Colors.orange,
|
||||
color = ColorTokens.warning,
|
||||
type = ActivityType.alert,
|
||||
style = ActivityItemStyle.alert,
|
||||
showStatusIndicator = true;
|
||||
@@ -339,24 +342,24 @@ class ActivityItem extends StatelessWidget {
|
||||
/// Couleur effective selon le type
|
||||
Color _getEffectiveColor() {
|
||||
if (color != null) return color!;
|
||||
|
||||
|
||||
switch (type) {
|
||||
case ActivityType.system:
|
||||
return const Color(0xFF6C5CE7);
|
||||
return ColorTokens.primary;
|
||||
case ActivityType.user:
|
||||
return const Color(0xFF00B894);
|
||||
return ColorTokens.success;
|
||||
case ActivityType.organization:
|
||||
return const Color(0xFF0984E3);
|
||||
return ColorTokens.info;
|
||||
case ActivityType.event:
|
||||
return const Color(0xFFE17055);
|
||||
return ColorTokens.secondary;
|
||||
case ActivityType.alert:
|
||||
return Colors.orange;
|
||||
return ColorTokens.warning;
|
||||
case ActivityType.error:
|
||||
return Colors.red;
|
||||
return ColorTokens.error;
|
||||
case ActivityType.success:
|
||||
return const Color(0xFF00B894);
|
||||
return ColorTokens.success;
|
||||
case null:
|
||||
return const Color(0xFF6C5CE7);
|
||||
return ColorTokens.primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
|
||||
/// Widget réutilisable pour les en-têtes de section
|
||||
///
|
||||
///
|
||||
/// Composant standardisé pour tous les titres de section dans les dashboards
|
||||
/// avec support pour actions, sous-titres et styles personnalisés.
|
||||
///
|
||||
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
|
||||
class SectionHeader extends StatelessWidget {
|
||||
/// Titre principal de la section
|
||||
final String title;
|
||||
@@ -48,7 +51,7 @@ class SectionHeader extends StatelessWidget {
|
||||
this.subtitle,
|
||||
this.action,
|
||||
this.icon,
|
||||
}) : color = const Color(0xFF6C5CE7),
|
||||
}) : color = ColorTokens.primary,
|
||||
fontSize = 20,
|
||||
style = SectionHeaderStyle.primary,
|
||||
bottomSpacing = 16;
|
||||
@@ -60,7 +63,7 @@ class SectionHeader extends StatelessWidget {
|
||||
this.subtitle,
|
||||
this.action,
|
||||
this.icon,
|
||||
}) : color = const Color(0xFF6C5CE7),
|
||||
}) : color = ColorTokens.primary,
|
||||
fontSize = 16,
|
||||
style = SectionHeaderStyle.normal,
|
||||
bottomSpacing = 12;
|
||||
@@ -100,25 +103,21 @@ class SectionHeader extends StatelessWidget {
|
||||
|
||||
/// En-tête principal avec fond coloré
|
||||
Widget _buildPrimaryHeader() {
|
||||
final effectiveColor = color ?? ColorTokens.primary;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
color ?? const Color(0xFF6C5CE7),
|
||||
(color ?? const Color(0xFF6C5CE7)).withOpacity(0.8),
|
||||
effectiveColor,
|
||||
effectiveColor.withOpacity(0.8),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (color ?? const Color(0xFF6C5CE7)).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: ShadowTokens.primary,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -175,10 +174,10 @@ class SectionHeader extends StatelessWidget {
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
color: color ?? const Color(0xFF6C5CE7),
|
||||
color: color ?? ColorTokens.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -189,7 +188,7 @@ class SectionHeader extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color ?? const Color(0xFF6C5CE7),
|
||||
color: color ?? ColorTokens.primary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
@@ -257,10 +256,10 @@ class SectionHeader extends StatelessWidget {
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
color: color ?? const Color(0xFF6C5CE7),
|
||||
color: color ?? ColorTokens.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -271,7 +270,7 @@ class SectionHeader extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color ?? const Color(0xFF6C5CE7),
|
||||
color: color ?? ColorTokens.primary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
|
||||
/// Carte de performance système réutilisable
|
||||
///
|
||||
///
|
||||
/// Widget spécialisé pour afficher les métriques de performance
|
||||
/// avec barres de progression et indicateurs colorés.
|
||||
///
|
||||
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
|
||||
class PerformanceCard extends StatelessWidget {
|
||||
/// Titre de la carte
|
||||
final String title;
|
||||
@@ -48,21 +51,21 @@ class PerformanceCard extends StatelessWidget {
|
||||
label: 'CPU',
|
||||
value: 67.3,
|
||||
unit: '%',
|
||||
color: Colors.orange,
|
||||
color: ColorTokens.warning,
|
||||
threshold: 80,
|
||||
),
|
||||
PerformanceMetric(
|
||||
label: 'RAM',
|
||||
value: 78.5,
|
||||
unit: '%',
|
||||
color: Colors.blue,
|
||||
color: ColorTokens.info,
|
||||
threshold: 85,
|
||||
),
|
||||
PerformanceMetric(
|
||||
label: 'Disque',
|
||||
value: 45.2,
|
||||
unit: '%',
|
||||
color: Colors.green,
|
||||
color: ColorTokens.success,
|
||||
threshold: 90,
|
||||
),
|
||||
],
|
||||
@@ -81,21 +84,21 @@ class PerformanceCard extends StatelessWidget {
|
||||
label: 'Latence',
|
||||
value: 12.0,
|
||||
unit: 'ms',
|
||||
color: Color(0xFF00B894),
|
||||
color: ColorTokens.success,
|
||||
threshold: 100.0,
|
||||
),
|
||||
PerformanceMetric(
|
||||
label: 'Débit',
|
||||
value: 85.0,
|
||||
unit: 'Mbps',
|
||||
color: Color(0xFF6C5CE7),
|
||||
color: ColorTokens.primary,
|
||||
threshold: 100.0,
|
||||
),
|
||||
PerformanceMetric(
|
||||
label: 'Paquets perdus',
|
||||
value: 0.2,
|
||||
unit: '%',
|
||||
color: Color(0xFFE17055),
|
||||
color: ColorTokens.secondary,
|
||||
threshold: 5.0,
|
||||
),
|
||||
],
|
||||
@@ -107,14 +110,13 @@ class PerformanceCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: _getDecoration(),
|
||||
child: UFCard(
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
_buildMetrics(),
|
||||
],
|
||||
),
|
||||
@@ -129,19 +131,17 @@ class PerformanceCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
style: TypographyTokens.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
color: ColorTokens.primary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -153,7 +153,7 @@ class PerformanceCard extends StatelessWidget {
|
||||
Widget _buildMetrics() {
|
||||
return Column(
|
||||
children: metrics.map((metric) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.only(bottom: SpacingTokens.md),
|
||||
child: _buildMetricRow(metric),
|
||||
)).toList(),
|
||||
);
|
||||
@@ -163,12 +163,12 @@ class PerformanceCard extends StatelessWidget {
|
||||
Widget _buildMetricRow(PerformanceMetric metric) {
|
||||
final isWarning = metric.value > metric.threshold * 0.8;
|
||||
final isCritical = metric.value > metric.threshold;
|
||||
|
||||
|
||||
Color effectiveColor = metric.color;
|
||||
if (isCritical) {
|
||||
effectiveColor = Colors.red;
|
||||
effectiveColor = ColorTokens.error;
|
||||
} else if (isWarning) {
|
||||
effectiveColor = Colors.orange;
|
||||
effectiveColor = ColorTokens.warning;
|
||||
}
|
||||
|
||||
return Column(
|
||||
@@ -183,28 +183,26 @@ class PerformanceCard extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Text(
|
||||
metric.label,
|
||||
style: const TextStyle(
|
||||
style: TypographyTokens.labelMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showValues)
|
||||
Text(
|
||||
'${metric.value.toStringAsFixed(1)}${metric.unit}',
|
||||
style: TextStyle(
|
||||
style: TypographyTokens.labelMedium.copyWith(
|
||||
color: effectiveColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showProgressBars) ...[
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
_buildProgressBar(metric, effectiveColor),
|
||||
],
|
||||
],
|
||||
@@ -214,12 +212,12 @@ class PerformanceCard extends StatelessWidget {
|
||||
/// Barre de progression
|
||||
Widget _buildProgressBar(PerformanceMetric metric, Color color) {
|
||||
final progress = (metric.value / metric.threshold).clamp(0.0, 1.0);
|
||||
|
||||
|
||||
return Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: ColorTokens.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -227,44 +225,14 @@ class PerformanceCard extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Décoration selon le style
|
||||
BoxDecoration _getDecoration() {
|
||||
switch (style) {
|
||||
case PerformanceCardStyle.elevated:
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
case PerformanceCardStyle.outlined:
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
);
|
||||
case PerformanceCardStyle.minimal:
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Modèle de données pour une métrique de performance
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget des activités récentes connecté au backend
|
||||
class ConnectedRecentActivities extends StatelessWidget {
|
||||
final int maxItems;
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
const ConnectedRecentActivities({
|
||||
super.key,
|
||||
this.maxItems = 5,
|
||||
this.onSeeAll,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingList();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildActivitiesList(data.recentActivities);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
return _buildEmptyState();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.tealBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.history,
|
||||
color: DashboardTheme.tealBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Activités récentes',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
child: Text(
|
||||
'Voir tout',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivitiesList(List<RecentActivityEntity> activities) {
|
||||
if (activities.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayActivities = activities.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayActivities.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final activity = entry.value;
|
||||
final isLast = index == displayActivities.length - 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildActivityItem(activity),
|
||||
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityItem(RecentActivityEntity activity) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar ou icône
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getActivityColor(activity.type).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: activity.userAvatar != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.network(
|
||||
activity.userAvatar!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Icon(
|
||||
_getActivityIcon(activity.type),
|
||||
color: _getActivityColor(activity.type),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_getActivityIcon(activity.type),
|
||||
color: _getActivityColor(activity.type),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
// Contenu
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
activity.description,
|
||||
style: DashboardTheme.bodySmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
activity.userName,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: DashboardTheme.royalBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' • ${activity.timeAgo}',
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Action button si disponible
|
||||
if (activity.hasAction)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// TODO: Naviguer vers l'action
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingList() {
|
||||
return Column(
|
||||
children: List.generate(3, (index) => Column(
|
||||
children: [
|
||||
_buildLoadingItem(),
|
||||
if (index < 2) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingItem() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
message,
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.history,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucune activité récente',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const Text(
|
||||
'Les activités apparaîtront ici',
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getActivityIcon(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'member':
|
||||
return Icons.person_add;
|
||||
case 'event':
|
||||
return Icons.event;
|
||||
case 'contribution':
|
||||
return Icons.payment;
|
||||
case 'organization':
|
||||
return Icons.business;
|
||||
case 'system':
|
||||
return Icons.settings;
|
||||
default:
|
||||
return Icons.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getActivityColor(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'member':
|
||||
return DashboardTheme.success;
|
||||
case 'event':
|
||||
return DashboardTheme.info;
|
||||
case 'contribution':
|
||||
return DashboardTheme.tealBlue;
|
||||
case 'organization':
|
||||
return DashboardTheme.royalBlue;
|
||||
case 'system':
|
||||
return DashboardTheme.warning;
|
||||
default:
|
||||
return DashboardTheme.grey500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de carte de statistiques connecté au backend
|
||||
class ConnectedStatsCard extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final String Function(DashboardStatsEntity) valueExtractor;
|
||||
final String? Function(DashboardStatsEntity)? subtitleExtractor;
|
||||
final Color? customColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ConnectedStatsCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.valueExtractor,
|
||||
this.subtitleExtractor,
|
||||
this.customColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingCard();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildDataCard(data.stats);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorCard(state.message);
|
||||
}
|
||||
return _buildLoadingCard();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataCard(DashboardStatsEntity stats) {
|
||||
final value = valueExtractor(stats);
|
||||
final subtitle = subtitleExtractor?.call(stats);
|
||||
final color = customColor ?? DashboardTheme.royalBlue;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Text(
|
||||
value,
|
||||
style: DashboardTheme.metricLarge.copyWith(color: color),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Container(
|
||||
height: 32,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorCard(String message) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Text(
|
||||
'--',
|
||||
style: DashboardTheme.metricLarge.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
message,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget des événements à venir connecté au backend
|
||||
class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
final int maxItems;
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
const ConnectedUpcomingEvents({
|
||||
super.key,
|
||||
this.maxItems = 3,
|
||||
this.onSeeAll,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingList();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildEventsList(data.upcomingEvents);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
return _buildEmptyState();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Événements à venir',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
child: Text(
|
||||
'Voir tout',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsList(List<UpcomingEventEntity> events) {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayEvents = events.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayEvents.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final event = entry.value;
|
||||
final isLast = index == displayEvents.length - 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildEventCard(event),
|
||||
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard(UpcomingEventEntity event) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.grey200,
|
||||
width: event.isToday || event.isTomorrow ? 2 : 1,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Image ou icône
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: event.imageUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
child: Image.network(
|
||||
event.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.location,
|
||||
style: DashboardTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Badge de temps
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success.withOpacity(0.1)
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning.withOpacity(0.1)
|
||||
: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
event.daysUntilEvent,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
// Barre de progression des participants
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Participants',
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'${event.currentParticipants}/${event.maxParticipants}',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
LinearProgressIndicator(
|
||||
value: event.fillPercentage,
|
||||
backgroundColor: DashboardTheme.grey200,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
event.isFull
|
||||
? DashboardTheme.error
|
||||
: event.isAlmostFull
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Tags
|
||||
if (event.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Wrap(
|
||||
spacing: DashboardTheme.spacing4,
|
||||
runSpacing: DashboardTheme.spacing4,
|
||||
children: event.tags.take(3).map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.tealBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.tealBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingList() {
|
||||
return Column(
|
||||
children: List.generate(2, (index) => Column(
|
||||
children: [
|
||||
_buildLoadingCard(),
|
||||
if (index < 1) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: DashboardTheme.grey200),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
Container(
|
||||
height: 4,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
message,
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event_busy,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucun événement à venir',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const Text(
|
||||
'Les événements apparaîtront ici',
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/// Widget de tuile d'activité individuelle
|
||||
/// Affiche une activité récente avec icône, titre et timestamp
|
||||
library dashboard_activity_tile;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
|
||||
/// Modèle de données pour une activité récente
|
||||
class DashboardActivity {
|
||||
/// Titre principal de l'activité
|
||||
final String title;
|
||||
|
||||
/// Description détaillée de l'activité
|
||||
final String subtitle;
|
||||
|
||||
/// Icône représentative de l'activité
|
||||
final IconData icon;
|
||||
|
||||
/// Couleur thématique de l'activité
|
||||
final Color color;
|
||||
|
||||
/// Timestamp de l'activité
|
||||
final String time;
|
||||
|
||||
/// Callback optionnel lors du tap sur l'activité
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Constructeur du modèle d'activité
|
||||
const DashboardActivity({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.time,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
|
||||
/// Widget de tuile d'activité
|
||||
///
|
||||
/// Affiche une activité récente avec :
|
||||
/// - Avatar coloré avec icône thématique
|
||||
/// - Titre et description de l'activité
|
||||
/// - Timestamp relatif
|
||||
/// - Design compact et lisible
|
||||
/// - Support du tap pour détails
|
||||
class DashboardActivityTile extends StatelessWidget {
|
||||
/// Données de l'activité à afficher
|
||||
final DashboardActivity activity;
|
||||
|
||||
/// Constructeur de la tuile d'activité
|
||||
const DashboardActivityTile({
|
||||
super.key,
|
||||
required this.activity,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: activity.onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.sm,
|
||||
vertical: SpacingTokens.xs,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: activity.color.withOpacity(0.1),
|
||||
child: Icon(
|
||||
activity.icon,
|
||||
color: activity.color,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
activity.title,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
activity.subtitle,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
activity.time,
|
||||
style: TypographyTokens.labelSmall.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
library dashboard_drawer;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/typography_tokens.dart';
|
||||
|
||||
/// Modèle de données pour un élément de menu
|
||||
class DrawerMenuItem {
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'common/section_header.dart';
|
||||
|
||||
/// Widget d'en-tête principal du dashboard
|
||||
///
|
||||
/// Composant réutilisable pour l'en-tête des dashboards avec
|
||||
/// informations système, statut et actions rapides.
|
||||
class DashboardHeader extends StatelessWidget {
|
||||
/// Titre principal du dashboard
|
||||
final String title;
|
||||
|
||||
/// Sous-titre ou description
|
||||
final String? subtitle;
|
||||
|
||||
/// Afficher les informations système
|
||||
final bool showSystemInfo;
|
||||
|
||||
/// Afficher les actions rapides
|
||||
final bool showQuickActions;
|
||||
|
||||
/// Callback pour les actions personnalisées
|
||||
final List<DashboardAction>? actions;
|
||||
|
||||
/// Métriques système à afficher
|
||||
final List<SystemMetric>? systemMetrics;
|
||||
|
||||
/// Style de l'en-tête
|
||||
final DashboardHeaderStyle style;
|
||||
|
||||
const DashboardHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.showSystemInfo = true,
|
||||
this.showQuickActions = true,
|
||||
this.actions,
|
||||
this.systemMetrics,
|
||||
this.style = DashboardHeaderStyle.gradient,
|
||||
});
|
||||
|
||||
/// Constructeur pour un en-tête Super Admin
|
||||
const DashboardHeader.superAdmin({
|
||||
super.key,
|
||||
this.actions,
|
||||
}) : title = 'Administration Système',
|
||||
subtitle = 'Surveillance et gestion globale',
|
||||
showSystemInfo = true,
|
||||
showQuickActions = true,
|
||||
systemMetrics = null,
|
||||
style = DashboardHeaderStyle.gradient;
|
||||
|
||||
/// Constructeur pour un en-tête Admin Organisation
|
||||
const DashboardHeader.orgAdmin({
|
||||
super.key,
|
||||
this.actions,
|
||||
}) : title = 'Administration Organisation',
|
||||
subtitle = 'Gestion de votre organisation',
|
||||
showSystemInfo = false,
|
||||
showQuickActions = true,
|
||||
systemMetrics = null,
|
||||
style = DashboardHeaderStyle.gradient;
|
||||
|
||||
/// Constructeur pour un en-tête Membre
|
||||
const DashboardHeader.member({
|
||||
super.key,
|
||||
this.actions,
|
||||
}) : title = 'Tableau de bord',
|
||||
subtitle = 'Bienvenue dans UnionFlow',
|
||||
showSystemInfo = false,
|
||||
showQuickActions = false,
|
||||
systemMetrics = null,
|
||||
style = DashboardHeaderStyle.simple;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (style) {
|
||||
case DashboardHeaderStyle.gradient:
|
||||
return _buildGradientHeader();
|
||||
case DashboardHeaderStyle.simple:
|
||||
return _buildSimpleHeader();
|
||||
case DashboardHeaderStyle.card:
|
||||
return _buildCardHeader();
|
||||
}
|
||||
}
|
||||
|
||||
/// En-tête avec gradient (style principal)
|
||||
Widget _buildGradientHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderContent(),
|
||||
if (showSystemInfo && systemMetrics != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSystemMetrics(),
|
||||
],
|
||||
if (showQuickActions && actions != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActions(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// En-tête simple sans fond
|
||||
Widget _buildSimpleHeader() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionHeader.primary(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
action: actions?.isNotEmpty == true ? _buildActionsRow() : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// En-tête avec fond de carte
|
||||
Widget _buildCardHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderContent(isWhiteBackground: true),
|
||||
if (showSystemInfo && systemMetrics != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSystemMetrics(isWhiteBackground: true),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contenu principal de l'en-tête
|
||||
Widget _buildHeaderContent({bool isWhiteBackground = false}) {
|
||||
final textColor = isWhiteBackground ? const Color(0xFF1F2937) : Colors.white;
|
||||
final subtitleColor = isWhiteBackground ? Colors.grey[600] : Colors.white.withOpacity(0.8);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: subtitleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (actions?.isNotEmpty == true) _buildActionsRow(isWhiteBackground: isWhiteBackground),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Métriques système
|
||||
Widget _buildSystemMetrics({bool isWhiteBackground = false}) {
|
||||
if (systemMetrics == null || systemMetrics!.isEmpty) {
|
||||
return _buildDefaultSystemMetrics(isWhiteBackground: isWhiteBackground);
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: systemMetrics!.map((metric) => _buildMetricChip(
|
||||
metric.label,
|
||||
metric.value,
|
||||
metric.icon,
|
||||
isWhiteBackground: isWhiteBackground,
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Métriques système par défaut
|
||||
Widget _buildDefaultSystemMetrics({bool isWhiteBackground = false}) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: _buildMetricChip('Uptime', '99.97%', Icons.trending_up, isWhiteBackground: isWhiteBackground)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildMetricChip('CPU', '23%', Icons.memory, isWhiteBackground: isWhiteBackground)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildMetricChip('Users', '1,247', Icons.people, isWhiteBackground: isWhiteBackground)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Chip de métrique
|
||||
Widget _buildMetricChip(String label, String value, IconData icon, {bool isWhiteBackground = false}) {
|
||||
final backgroundColor = isWhiteBackground
|
||||
? const Color(0xFF6C5CE7).withOpacity(0.1)
|
||||
: Colors.white.withOpacity(0.15);
|
||||
final textColor = isWhiteBackground ? const Color(0xFF6C5CE7) : Colors.white;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: textColor, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: textColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions rapides
|
||||
Widget _buildQuickActions({bool isWhiteBackground = false}) {
|
||||
if (actions == null || actions!.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: actions!.map((action) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: _buildActionButton(action, isWhiteBackground: isWhiteBackground),
|
||||
),
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Ligne d'actions
|
||||
Widget _buildActionsRow({bool isWhiteBackground = false}) {
|
||||
if (actions == null || actions!.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: actions!.map((action) => Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: _buildActionButton(action, isWhiteBackground: isWhiteBackground),
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Bouton d'action
|
||||
Widget _buildActionButton(DashboardAction action, {bool isWhiteBackground = false}) {
|
||||
final backgroundColor = isWhiteBackground
|
||||
? Colors.white
|
||||
: Colors.white.withOpacity(0.2);
|
||||
final iconColor = isWhiteBackground ? const Color(0xFF6C5CE7) : Colors.white;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: action.onPressed,
|
||||
icon: Icon(action.icon, color: iconColor),
|
||||
tooltip: action.tooltip,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Action du dashboard
|
||||
class DashboardAction {
|
||||
final IconData icon;
|
||||
final String tooltip;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const DashboardAction({
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
/// Métrique système
|
||||
class SystemMetric {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
|
||||
const SystemMetric({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
/// Styles d'en-tête de dashboard
|
||||
enum DashboardHeaderStyle {
|
||||
gradient,
|
||||
simple,
|
||||
card,
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/// Widget de section d'insights du dashboard
|
||||
/// Affiche les métriques de performance dans une carte
|
||||
library dashboard_insights_section;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import 'dashboard_metric_row.dart';
|
||||
|
||||
/// Widget de section d'insights
|
||||
///
|
||||
/// Affiche les métriques de performance :
|
||||
/// - Taux de cotisation
|
||||
/// - Participation aux événements
|
||||
/// - Demandes traitées
|
||||
///
|
||||
/// Chaque métrique peut être tapée pour plus de détails
|
||||
class DashboardInsightsSection extends StatelessWidget {
|
||||
/// Callback pour les actions sur les métriques
|
||||
final Function(String metricType)? onMetricTap;
|
||||
|
||||
/// Liste des métriques à afficher
|
||||
final List<DashboardMetric>? metrics;
|
||||
|
||||
/// Constructeur de la section d'insights
|
||||
const DashboardInsightsSection({
|
||||
super.key,
|
||||
this.onMetricTap,
|
||||
this.metrics,
|
||||
});
|
||||
|
||||
/// Génère la liste des métriques par défaut
|
||||
List<DashboardMetric> _getDefaultMetrics() {
|
||||
return [
|
||||
DashboardMetric(
|
||||
label: 'Taux de cotisation',
|
||||
value: '85%',
|
||||
progress: 0.85,
|
||||
color: ColorTokens.success,
|
||||
onTap: () => onMetricTap?.call('cotisation_rate'),
|
||||
),
|
||||
DashboardMetric(
|
||||
label: 'Participation événements',
|
||||
value: '72%',
|
||||
progress: 0.72,
|
||||
color: ColorTokens.primary,
|
||||
onTap: () => onMetricTap?.call('event_participation'),
|
||||
),
|
||||
DashboardMetric(
|
||||
label: 'Demandes traitées',
|
||||
value: '95%',
|
||||
progress: 0.95,
|
||||
color: ColorTokens.tertiary,
|
||||
onTap: () => onMetricTap?.call('requests_processed'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metricsToShow = metrics ?? _getDefaultMetrics();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Insights',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Performance ce mois-ci',
|
||||
style: TypographyTokens.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
...metricsToShow.map((metric) {
|
||||
final isLast = metric == metricsToShow.last;
|
||||
return Column(
|
||||
children: [
|
||||
DashboardMetricRow(metric: metric),
|
||||
if (!isLast) const SizedBox(height: SpacingTokens.sm),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/// Widget de ligne de métrique avec barre de progression
|
||||
/// Affiche une métrique avec label, valeur et indicateur visuel
|
||||
library dashboard_metric_row;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
|
||||
/// Modèle de données pour une métrique
|
||||
class DashboardMetric {
|
||||
/// Label descriptif de la métrique
|
||||
final String label;
|
||||
|
||||
/// Valeur formatée à afficher
|
||||
final String value;
|
||||
|
||||
/// Progression entre 0.0 et 1.0
|
||||
final double progress;
|
||||
|
||||
/// Couleur thématique de la métrique
|
||||
final Color color;
|
||||
|
||||
/// Callback optionnel lors du tap sur la métrique
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Constructeur du modèle de métrique
|
||||
const DashboardMetric({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.progress,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
|
||||
/// Widget de ligne de métrique
|
||||
///
|
||||
/// Affiche une métrique avec :
|
||||
/// - Label et valeur alignés horizontalement
|
||||
/// - Barre de progression colorée
|
||||
/// - Design compact et lisible
|
||||
/// - Support du tap pour détails
|
||||
class DashboardMetricRow extends StatelessWidget {
|
||||
/// Données de la métrique à afficher
|
||||
final DashboardMetric metric;
|
||||
|
||||
/// Constructeur de la ligne de métrique
|
||||
const DashboardMetricRow({
|
||||
super.key,
|
||||
required this.metric,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: metric.onTap,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xs),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
metric.label,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
metric.value,
|
||||
style: TypographyTokens.labelLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: metric.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
LinearProgressIndicator(
|
||||
value: metric.progress,
|
||||
backgroundColor: metric.color.withOpacity(0.1),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(metric.color),
|
||||
minHeight: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
/// Widget de bouton d'action rapide individuel - Version Améliorée
|
||||
/// Bouton stylisé sophistiqué pour les actions principales du dashboard
|
||||
/// avec support d'animations, badges, états et styles multiples
|
||||
library dashboard_quick_action_button;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
|
||||
/// Types d'actions rapides disponibles
|
||||
enum QuickActionType {
|
||||
primary,
|
||||
secondary,
|
||||
success,
|
||||
warning,
|
||||
error,
|
||||
info,
|
||||
custom,
|
||||
}
|
||||
|
||||
/// Styles de boutons d'action rapide
|
||||
enum QuickActionStyle {
|
||||
elevated,
|
||||
filled,
|
||||
outlined,
|
||||
text,
|
||||
gradient,
|
||||
minimal,
|
||||
}
|
||||
|
||||
/// Tailles de boutons d'action rapide
|
||||
enum QuickActionSize {
|
||||
small,
|
||||
medium,
|
||||
large,
|
||||
extraLarge,
|
||||
}
|
||||
|
||||
/// États du bouton d'action rapide
|
||||
enum QuickActionState {
|
||||
enabled,
|
||||
disabled,
|
||||
loading,
|
||||
success,
|
||||
error,
|
||||
}
|
||||
|
||||
/// Modèle de données avancé pour une action rapide
|
||||
class DashboardQuickAction {
|
||||
/// Icône représentative de l'action
|
||||
final IconData icon;
|
||||
|
||||
/// Titre de l'action
|
||||
final String title;
|
||||
|
||||
/// Sous-titre optionnel
|
||||
final String? subtitle;
|
||||
|
||||
/// Description détaillée (tooltip)
|
||||
final String? description;
|
||||
|
||||
/// Couleur thématique du bouton
|
||||
final Color color;
|
||||
|
||||
/// Type d'action (détermine le style par défaut)
|
||||
final QuickActionType type;
|
||||
|
||||
/// Style du bouton
|
||||
final QuickActionStyle style;
|
||||
|
||||
/// Taille du bouton
|
||||
final QuickActionSize size;
|
||||
|
||||
/// État actuel du bouton
|
||||
final QuickActionState state;
|
||||
|
||||
/// Callback lors du tap sur le bouton
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Callback lors du long press
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Badge à afficher (nombre ou texte)
|
||||
final String? badge;
|
||||
|
||||
/// Couleur du badge
|
||||
final Color? badgeColor;
|
||||
|
||||
/// Icône secondaire (affichée en bas à droite)
|
||||
final IconData? secondaryIcon;
|
||||
|
||||
/// Gradient personnalisé
|
||||
final Gradient? gradient;
|
||||
|
||||
/// Animation activée
|
||||
final bool animated;
|
||||
|
||||
/// Feedback haptique activé
|
||||
final bool hapticFeedback;
|
||||
|
||||
/// Constructeur du modèle d'action rapide amélioré
|
||||
const DashboardQuickAction({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
required this.color,
|
||||
this.type = QuickActionType.primary,
|
||||
this.style = QuickActionStyle.elevated,
|
||||
this.size = QuickActionSize.medium,
|
||||
this.state = QuickActionState.enabled,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.badge,
|
||||
this.badgeColor,
|
||||
this.secondaryIcon,
|
||||
this.gradient,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
});
|
||||
|
||||
/// Constructeur pour action primaire
|
||||
const DashboardQuickAction.primary({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.badge,
|
||||
this.size = QuickActionSize.medium,
|
||||
this.state = QuickActionState.enabled,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : color = ColorTokens.primary,
|
||||
type = QuickActionType.primary,
|
||||
style = QuickActionStyle.elevated,
|
||||
badgeColor = null,
|
||||
secondaryIcon = null,
|
||||
gradient = null;
|
||||
|
||||
/// Constructeur pour action de succès
|
||||
const DashboardQuickAction.success({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.badge,
|
||||
this.size = QuickActionSize.medium,
|
||||
this.state = QuickActionState.enabled,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : color = ColorTokens.success,
|
||||
type = QuickActionType.success,
|
||||
style = QuickActionStyle.filled,
|
||||
badgeColor = null,
|
||||
secondaryIcon = null,
|
||||
gradient = null;
|
||||
|
||||
/// Constructeur pour action d'alerte
|
||||
const DashboardQuickAction.warning({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.badge,
|
||||
this.size = QuickActionSize.medium,
|
||||
this.state = QuickActionState.enabled,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : color = ColorTokens.warning,
|
||||
type = QuickActionType.warning,
|
||||
style = QuickActionStyle.outlined,
|
||||
badgeColor = null,
|
||||
secondaryIcon = null,
|
||||
gradient = null;
|
||||
|
||||
/// Constructeur pour action avec gradient
|
||||
const DashboardQuickAction.gradient({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
required this.gradient,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.badge,
|
||||
this.size = QuickActionSize.medium,
|
||||
this.state = QuickActionState.enabled,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : color = ColorTokens.primary,
|
||||
type = QuickActionType.custom,
|
||||
style = QuickActionStyle.gradient,
|
||||
badgeColor = null,
|
||||
secondaryIcon = null;
|
||||
}
|
||||
|
||||
/// Widget de bouton d'action rapide amélioré
|
||||
///
|
||||
/// Affiche un bouton stylisé sophistiqué avec :
|
||||
/// - Icône thématique avec animations
|
||||
/// - Titre et sous-titre descriptifs
|
||||
/// - Badges et indicateurs visuels
|
||||
/// - Styles multiples (elevated, filled, outlined, gradient)
|
||||
/// - États interactifs (loading, success, error)
|
||||
/// - Feedback haptique et animations
|
||||
/// - Support tooltip et long press
|
||||
/// - Design Material 3 avec bordures arrondies
|
||||
class DashboardQuickActionButton extends StatefulWidget {
|
||||
/// Données de l'action à afficher
|
||||
final DashboardQuickAction action;
|
||||
|
||||
/// Constructeur du bouton d'action rapide amélioré
|
||||
const DashboardQuickActionButton({
|
||||
super.key,
|
||||
required this.action,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardQuickActionButton> createState() => _DashboardQuickActionButtonState();
|
||||
}
|
||||
|
||||
class _DashboardQuickActionButtonState extends State<DashboardQuickActionButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Obtient les dimensions selon la taille (format rectangulaire compact)
|
||||
EdgeInsets _getPadding() {
|
||||
switch (widget.action.size) {
|
||||
case QuickActionSize.small:
|
||||
return const EdgeInsets.symmetric(horizontal: SpacingTokens.xs, vertical: SpacingTokens.xs);
|
||||
case QuickActionSize.medium:
|
||||
return const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.sm);
|
||||
case QuickActionSize.large:
|
||||
return const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm);
|
||||
case QuickActionSize.extraLarge:
|
||||
return const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.md);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la taille de l'icône selon la taille du bouton (réduite pour format compact)
|
||||
double _getIconSize() {
|
||||
switch (widget.action.size) {
|
||||
case QuickActionSize.small:
|
||||
return 14.0;
|
||||
case QuickActionSize.medium:
|
||||
return 16.0;
|
||||
case QuickActionSize.large:
|
||||
return 18.0;
|
||||
case QuickActionSize.extraLarge:
|
||||
return 20.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le style de texte pour le titre
|
||||
TextStyle _getTitleStyle() {
|
||||
final baseSize = widget.action.size == QuickActionSize.small ? 11.0 :
|
||||
widget.action.size == QuickActionSize.medium ? 12.0 :
|
||||
widget.action.size == QuickActionSize.large ? 13.0 : 14.0;
|
||||
|
||||
return TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: baseSize,
|
||||
color: _getTextColor(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient le style de texte pour le sous-titre
|
||||
TextStyle _getSubtitleStyle() {
|
||||
final baseSize = widget.action.size == QuickActionSize.small ? 9.0 :
|
||||
widget.action.size == QuickActionSize.medium ? 10.0 :
|
||||
widget.action.size == QuickActionSize.large ? 11.0 : 12.0;
|
||||
|
||||
return TextStyle(
|
||||
fontSize: baseSize,
|
||||
color: _getTextColor().withOpacity(0.7),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur du texte selon le style
|
||||
Color _getTextColor() {
|
||||
switch (widget.action.style) {
|
||||
case QuickActionStyle.filled:
|
||||
case QuickActionStyle.gradient:
|
||||
return Colors.white;
|
||||
case QuickActionStyle.elevated:
|
||||
case QuickActionStyle.outlined:
|
||||
case QuickActionStyle.text:
|
||||
case QuickActionStyle.minimal:
|
||||
return widget.action.color;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère le tap avec feedback haptique
|
||||
void _handleTap() {
|
||||
if (widget.action.state != QuickActionState.enabled) return;
|
||||
|
||||
if (widget.action.hapticFeedback) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
|
||||
if (widget.action.animated) {
|
||||
_animationController.forward().then((_) {
|
||||
_animationController.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
widget.action.onTap?.call();
|
||||
}
|
||||
|
||||
/// Gère le long press
|
||||
void _handleLongPress() {
|
||||
if (widget.action.state != QuickActionState.enabled) return;
|
||||
|
||||
if (widget.action.hapticFeedback) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
|
||||
widget.action.onLongPress?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget button = _buildButton();
|
||||
|
||||
// Ajouter tooltip si description fournie
|
||||
if (widget.action.description != null) {
|
||||
button = Tooltip(
|
||||
message: widget.action.description!,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter animation si activée
|
||||
if (widget.action.animated) {
|
||||
button = AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotationAnimation.value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/// Construit le bouton selon le style défini
|
||||
Widget _buildButton() {
|
||||
switch (widget.action.style) {
|
||||
case QuickActionStyle.elevated:
|
||||
return _buildElevatedButton();
|
||||
case QuickActionStyle.filled:
|
||||
return _buildFilledButton();
|
||||
case QuickActionStyle.outlined:
|
||||
return _buildOutlinedButton();
|
||||
case QuickActionStyle.text:
|
||||
return _buildTextButton();
|
||||
case QuickActionStyle.gradient:
|
||||
return _buildGradientButton();
|
||||
case QuickActionStyle.minimal:
|
||||
return _buildMinimalButton();
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit un bouton élevé
|
||||
Widget _buildElevatedButton() {
|
||||
return ElevatedButton(
|
||||
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.action.color.withOpacity(0.1),
|
||||
foregroundColor: widget.action.color,
|
||||
elevation: widget.action.state == QuickActionState.enabled ? 2 : 0,
|
||||
padding: _getPadding(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton rempli
|
||||
Widget _buildFilledButton() {
|
||||
return ElevatedButton(
|
||||
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.action.color,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: _getPadding(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton avec contour
|
||||
Widget _buildOutlinedButton() {
|
||||
return OutlinedButton(
|
||||
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: widget.action.color,
|
||||
side: BorderSide(color: widget.action.color, width: 1.5),
|
||||
padding: _getPadding(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton texte
|
||||
Widget _buildTextButton() {
|
||||
return TextButton(
|
||||
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: widget.action.color,
|
||||
padding: _getPadding(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton avec gradient
|
||||
Widget _buildGradientButton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: widget.action.gradient ?? LinearGradient(
|
||||
colors: [widget.action.color, widget.action.color.withOpacity(0.8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: widget.action.color.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: _buildButtonContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton minimal
|
||||
Widget _buildMinimalButton() {
|
||||
return InkWell(
|
||||
onTap: widget.action.state == QuickActionState.enabled ? _handleTap : null,
|
||||
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
child: Container(
|
||||
padding: _getPadding(),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.action.color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
border: Border.all(
|
||||
color: widget.action.color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le contenu du bouton (icône, texte, badge)
|
||||
Widget _buildButtonContent() {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildIcon(),
|
||||
const SizedBox(height: 6),
|
||||
_buildTitle(),
|
||||
if (widget.action.subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
_buildSubtitle(),
|
||||
],
|
||||
],
|
||||
),
|
||||
// Badge en haut à droite
|
||||
if (widget.action.badge != null)
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: _buildBadge(),
|
||||
),
|
||||
// Icône secondaire en bas à droite
|
||||
if (widget.action.secondaryIcon != null)
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: _buildSecondaryIcon(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'icône principale avec état
|
||||
Widget _buildIcon() {
|
||||
IconData iconToShow = widget.action.icon;
|
||||
|
||||
// Changer l'icône selon l'état
|
||||
switch (widget.action.state) {
|
||||
case QuickActionState.loading:
|
||||
return SizedBox(
|
||||
width: _getIconSize(),
|
||||
height: _getIconSize(),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(_getTextColor()),
|
||||
),
|
||||
);
|
||||
case QuickActionState.success:
|
||||
iconToShow = Icons.check_circle;
|
||||
break;
|
||||
case QuickActionState.error:
|
||||
iconToShow = Icons.error;
|
||||
break;
|
||||
case QuickActionState.disabled:
|
||||
case QuickActionState.enabled:
|
||||
break;
|
||||
}
|
||||
|
||||
return Icon(
|
||||
iconToShow,
|
||||
size: _getIconSize(),
|
||||
color: _getTextColor().withOpacity(
|
||||
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le titre
|
||||
Widget _buildTitle() {
|
||||
return Text(
|
||||
widget.action.title,
|
||||
style: _getTitleStyle().copyWith(
|
||||
color: _getTitleStyle().color?.withOpacity(
|
||||
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le sous-titre
|
||||
Widget _buildSubtitle() {
|
||||
return Text(
|
||||
widget.action.subtitle!,
|
||||
style: _getSubtitleStyle().copyWith(
|
||||
color: _getSubtitleStyle().color?.withOpacity(
|
||||
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le badge
|
||||
Widget _buildBadge() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.action.badgeColor ?? ColorTokens.error,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
widget.action.badge!,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'icône secondaire
|
||||
Widget _buildSecondaryIcon() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.action.color,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
widget.action.secondaryIcon!,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
/// Widget de grille d'actions rapides du dashboard - Version Améliorée
|
||||
/// Affiche les actions principales dans une grille responsive et configurable
|
||||
/// avec support d'animations, layouts multiples et personnalisation avancée
|
||||
library dashboard_quick_actions_grid;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import 'dashboard_quick_action_button.dart';
|
||||
|
||||
/// Types de layout pour la grille d'actions
|
||||
enum QuickActionsLayout {
|
||||
grid2x2,
|
||||
grid3x2,
|
||||
grid4x2,
|
||||
horizontal,
|
||||
vertical,
|
||||
staggered,
|
||||
carousel,
|
||||
}
|
||||
|
||||
/// Styles de la grille d'actions
|
||||
enum QuickActionsGridStyle {
|
||||
standard,
|
||||
compact,
|
||||
expanded,
|
||||
minimal,
|
||||
card,
|
||||
}
|
||||
|
||||
/// Widget de grille d'actions rapides amélioré
|
||||
///
|
||||
/// Affiche les actions principales dans différents layouts :
|
||||
/// - Grille 2x2, 3x2, 4x2
|
||||
/// - Layout horizontal ou vertical
|
||||
/// - Grille décalée (staggered)
|
||||
/// - Carrousel horizontal
|
||||
///
|
||||
/// Fonctionnalités avancées :
|
||||
/// - Animations d'apparition
|
||||
/// - Personnalisation complète
|
||||
/// - Gestion des permissions
|
||||
/// - Analytics intégrés
|
||||
/// - Support responsive
|
||||
class DashboardQuickActionsGrid extends StatefulWidget {
|
||||
/// Callback pour les actions rapides
|
||||
final Function(String actionType)? onActionTap;
|
||||
|
||||
/// Liste des actions à afficher
|
||||
final List<DashboardQuickAction>? actions;
|
||||
|
||||
/// Layout de la grille
|
||||
final QuickActionsLayout layout;
|
||||
|
||||
/// Style de la grille
|
||||
final QuickActionsGridStyle style;
|
||||
|
||||
/// Titre de la section
|
||||
final String? title;
|
||||
|
||||
/// Sous-titre de la section
|
||||
final String? subtitle;
|
||||
|
||||
/// Afficher le titre
|
||||
final bool showTitle;
|
||||
|
||||
/// Afficher les animations
|
||||
final bool animated;
|
||||
|
||||
/// Délai entre les animations (en millisecondes)
|
||||
final int animationDelay;
|
||||
|
||||
/// Nombre maximum d'actions à afficher
|
||||
final int? maxActions;
|
||||
|
||||
/// Espacement entre les éléments
|
||||
final double? spacing;
|
||||
|
||||
/// Ratio d'aspect des boutons
|
||||
final double? aspectRatio;
|
||||
|
||||
/// Callback pour voir toutes les actions
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
/// Permissions utilisateur (pour filtrer les actions)
|
||||
final List<String>? userPermissions;
|
||||
|
||||
/// Mode de débogage (affiche des infos supplémentaires)
|
||||
final bool debugMode;
|
||||
|
||||
/// Constructeur de la grille d'actions rapides améliorée
|
||||
const DashboardQuickActionsGrid({
|
||||
super.key,
|
||||
this.onActionTap,
|
||||
this.actions,
|
||||
this.layout = QuickActionsLayout.grid2x2,
|
||||
this.style = QuickActionsGridStyle.standard,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.showTitle = true,
|
||||
this.animated = true,
|
||||
this.animationDelay = 100,
|
||||
this.maxActions,
|
||||
this.spacing,
|
||||
this.aspectRatio,
|
||||
this.onSeeAll,
|
||||
this.userPermissions,
|
||||
this.debugMode = false,
|
||||
});
|
||||
|
||||
/// Constructeur pour grille compacte avec format rectangulaire
|
||||
const DashboardQuickActionsGrid.compact({
|
||||
super.key,
|
||||
this.onActionTap,
|
||||
this.actions,
|
||||
this.title,
|
||||
this.userPermissions,
|
||||
}) : layout = QuickActionsLayout.grid2x2,
|
||||
style = QuickActionsGridStyle.compact,
|
||||
subtitle = null,
|
||||
showTitle = true,
|
||||
animated = false,
|
||||
animationDelay = 0,
|
||||
maxActions = 4,
|
||||
spacing = null,
|
||||
aspectRatio = 1.8, // Ratio rectangulaire compact
|
||||
onSeeAll = null,
|
||||
debugMode = false;
|
||||
|
||||
/// Constructeur pour carrousel horizontal avec format rectangulaire
|
||||
const DashboardQuickActionsGrid.carousel({
|
||||
super.key,
|
||||
this.onActionTap,
|
||||
this.actions,
|
||||
this.title,
|
||||
this.animated = true,
|
||||
this.userPermissions,
|
||||
}) : layout = QuickActionsLayout.carousel,
|
||||
style = QuickActionsGridStyle.standard,
|
||||
subtitle = null,
|
||||
showTitle = true,
|
||||
animationDelay = 150,
|
||||
maxActions = null,
|
||||
spacing = 8.0, // Espacement réduit
|
||||
aspectRatio = 1.0, // Ratio plus carré pour format rectangulaire
|
||||
onSeeAll = null,
|
||||
debugMode = false;
|
||||
|
||||
/// Constructeur pour layout étendu avec format rectangulaire
|
||||
const DashboardQuickActionsGrid.expanded({
|
||||
super.key,
|
||||
this.onActionTap,
|
||||
this.actions,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.onSeeAll,
|
||||
this.userPermissions,
|
||||
}) : layout = QuickActionsLayout.grid3x2,
|
||||
style = QuickActionsGridStyle.expanded,
|
||||
showTitle = true,
|
||||
animated = true,
|
||||
animationDelay = 80,
|
||||
maxActions = 6,
|
||||
spacing = null,
|
||||
aspectRatio = 1.5, // Ratio rectangulaire pour layout étendu
|
||||
debugMode = false;
|
||||
|
||||
@override
|
||||
State<DashboardQuickActionsGrid> createState() => _DashboardQuickActionsGridState();
|
||||
}
|
||||
|
||||
class _DashboardQuickActionsGridState extends State<DashboardQuickActionsGrid>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late List<Animation<double>> _itemAnimations;
|
||||
List<DashboardQuickAction> _filteredActions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_filterActions();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DashboardQuickActionsGrid oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.actions != widget.actions ||
|
||||
oldWidget.userPermissions != widget.userPermissions) {
|
||||
_filterActions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Configure les animations
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: Duration(milliseconds: widget.animationDelay * 6),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
if (widget.animated) {
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtre les actions selon les permissions
|
||||
void _filterActions() {
|
||||
final actions = widget.actions ?? _getDefaultActions();
|
||||
|
||||
_filteredActions = actions.where((action) {
|
||||
// Filtrer selon les permissions si définies
|
||||
if (widget.userPermissions != null) {
|
||||
// Logique de filtrage basée sur les permissions
|
||||
// À implémenter selon les besoins spécifiques
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
// Limiter le nombre d'actions si spécifié
|
||||
if (widget.maxActions != null && _filteredActions.length > widget.maxActions!) {
|
||||
_filteredActions = _filteredActions.take(widget.maxActions!).toList();
|
||||
}
|
||||
|
||||
// Recréer les animations pour le nouveau nombre d'éléments
|
||||
_itemAnimations = List.generate(
|
||||
_filteredActions.length,
|
||||
(index) => Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(
|
||||
index * 0.1,
|
||||
(index * 0.1) + 0.6,
|
||||
curve: Curves.easeOutBack,
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// Génère la liste des actions rapides par défaut
|
||||
List<DashboardQuickAction> _getDefaultActions() {
|
||||
return [
|
||||
DashboardQuickAction.primary(
|
||||
icon: Icons.person_add,
|
||||
title: 'Ajouter Membre',
|
||||
subtitle: 'Nouveau membre',
|
||||
description: 'Ajouter un nouveau membre à l\'organisation',
|
||||
onTap: () => widget.onActionTap?.call('add_member'),
|
||||
badge: '+',
|
||||
),
|
||||
DashboardQuickAction.success(
|
||||
icon: Icons.payment,
|
||||
title: 'Cotisation',
|
||||
subtitle: 'Enregistrer',
|
||||
description: 'Enregistrer une nouvelle cotisation',
|
||||
onTap: () => widget.onActionTap?.call('add_cotisation'),
|
||||
),
|
||||
DashboardQuickAction(
|
||||
icon: Icons.event_note,
|
||||
title: 'Événement',
|
||||
subtitle: 'Créer',
|
||||
description: 'Créer un nouvel événement',
|
||||
color: ColorTokens.tertiary,
|
||||
type: QuickActionType.info,
|
||||
style: QuickActionStyle.outlined,
|
||||
onTap: () => widget.onActionTap?.call('create_event'),
|
||||
),
|
||||
DashboardQuickAction(
|
||||
icon: Icons.volunteer_activism,
|
||||
title: 'Solidarité',
|
||||
subtitle: 'Demande',
|
||||
description: 'Créer une demande de solidarité',
|
||||
color: ColorTokens.warning,
|
||||
type: QuickActionType.warning,
|
||||
style: QuickActionStyle.outlined,
|
||||
onTap: () => widget.onActionTap?.call('solidarity_request'),
|
||||
secondaryIcon: Icons.favorite,
|
||||
),
|
||||
DashboardQuickAction(
|
||||
icon: Icons.analytics,
|
||||
title: 'Rapports',
|
||||
subtitle: 'Générer',
|
||||
description: 'Générer des rapports analytiques',
|
||||
color: ColorTokens.secondary,
|
||||
type: QuickActionType.secondary,
|
||||
style: QuickActionStyle.minimal,
|
||||
onTap: () => widget.onActionTap?.call('generate_reports'),
|
||||
),
|
||||
DashboardQuickAction.gradient(
|
||||
icon: Icons.settings,
|
||||
title: 'Paramètres',
|
||||
subtitle: 'Configurer',
|
||||
description: 'Accéder aux paramètres système',
|
||||
gradient: const LinearGradient(
|
||||
colors: [ColorTokens.primary, ColorTokens.secondary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
onTap: () => widget.onActionTap?.call('settings'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_filteredActions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showTitle) _buildHeader(),
|
||||
if (widget.showTitle) const SizedBox(height: SpacingTokens.md),
|
||||
_buildActionsLayout(),
|
||||
if (widget.debugMode) _buildDebugInfo(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'en-tête de la section
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title ?? 'Actions rapides',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (widget.subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.subtitle!,
|
||||
style: TypographyTokens.bodyMedium.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: widget.onSeeAll,
|
||||
child: const Text('Voir tout'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le layout des actions selon le type choisi
|
||||
Widget _buildActionsLayout() {
|
||||
switch (widget.layout) {
|
||||
case QuickActionsLayout.grid2x2:
|
||||
return _buildGridLayout(2);
|
||||
case QuickActionsLayout.grid3x2:
|
||||
return _buildGridLayout(3);
|
||||
case QuickActionsLayout.grid4x2:
|
||||
return _buildGridLayout(4);
|
||||
case QuickActionsLayout.horizontal:
|
||||
return _buildHorizontalLayout();
|
||||
case QuickActionsLayout.vertical:
|
||||
return _buildVerticalLayout();
|
||||
case QuickActionsLayout.staggered:
|
||||
return _buildStaggeredLayout();
|
||||
case QuickActionsLayout.carousel:
|
||||
return _buildCarouselLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit une grille standard avec format rectangulaire compact
|
||||
Widget _buildGridLayout(int crossAxisCount) {
|
||||
final spacing = widget.spacing ?? SpacingTokens.sm;
|
||||
// Ratio d'aspect plus rectangulaire (largeur réduite de moitié)
|
||||
final aspectRatio = widget.aspectRatio ??
|
||||
(widget.style == QuickActionsGridStyle.compact ? 1.8 : 1.6);
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: spacing,
|
||||
mainAxisSpacing: spacing,
|
||||
childAspectRatio: aspectRatio,
|
||||
),
|
||||
itemCount: _filteredActions.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildAnimatedActionButton(index);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un layout horizontal avec boutons rectangulaires compacts
|
||||
Widget _buildHorizontalLayout() {
|
||||
final spacing = widget.spacing ?? SpacingTokens.sm;
|
||||
|
||||
return SizedBox(
|
||||
height: 80, // Hauteur réduite pour format rectangulaire
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _filteredActions.length,
|
||||
separatorBuilder: (context, index) => SizedBox(width: spacing),
|
||||
itemBuilder: (context, index) {
|
||||
return SizedBox(
|
||||
width: 100, // Largeur réduite de moitié (140 -> 100)
|
||||
child: _buildAnimatedActionButton(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un layout vertical
|
||||
Widget _buildVerticalLayout() {
|
||||
final spacing = widget.spacing ?? SpacingTokens.sm;
|
||||
|
||||
return Column(
|
||||
children: _filteredActions.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: index < _filteredActions.length - 1 ? spacing : 0),
|
||||
child: _buildAnimatedActionButton(index),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un layout décalé (staggered) avec format rectangulaire
|
||||
Widget _buildStaggeredLayout() {
|
||||
// Implémentation simplifiée du staggered layout avec dimensions réduites
|
||||
return Wrap(
|
||||
spacing: widget.spacing ?? SpacingTokens.sm,
|
||||
runSpacing: widget.spacing ?? SpacingTokens.sm,
|
||||
children: _filteredActions.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return SizedBox(
|
||||
width: (MediaQuery.of(context).size.width - 48 - (widget.spacing ?? SpacingTokens.sm)) / 2,
|
||||
height: index.isEven ? 70 : 85, // Hauteurs alternées réduites
|
||||
child: _buildAnimatedActionButton(index),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un carrousel horizontal avec format rectangulaire compact
|
||||
Widget _buildCarouselLayout() {
|
||||
return SizedBox(
|
||||
height: 90, // Hauteur réduite pour format rectangulaire
|
||||
child: PageView.builder(
|
||||
controller: PageController(viewportFraction: 0.6), // Fraction réduite pour largeur plus petite
|
||||
itemCount: _filteredActions.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: widget.spacing ?? 6.0),
|
||||
child: _buildAnimatedActionButton(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton d'action avec animation
|
||||
Widget _buildAnimatedActionButton(int index) {
|
||||
if (!widget.animated || _itemAnimations.isEmpty || index >= _itemAnimations.length) {
|
||||
return DashboardQuickActionButton(action: _filteredActions[index]);
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _itemAnimations[index],
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _itemAnimations[index].value,
|
||||
child: Opacity(
|
||||
opacity: _itemAnimations[index].value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: DashboardQuickActionButton(action: _filteredActions[index]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit les informations de débogage
|
||||
Widget _buildDebugInfo() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: SpacingTokens.md),
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.warning.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorTokens.warning.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Debug Info:',
|
||||
style: TypographyTokens.labelSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorTokens.warning,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Layout: ${widget.layout.name}',
|
||||
style: TypographyTokens.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'Style: ${widget.style.name}',
|
||||
style: TypographyTokens.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'Actions: ${_filteredActions.length}',
|
||||
style: TypographyTokens.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'Animated: ${widget.animated}',
|
||||
style: TypographyTokens.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/// Widget de section d'activité récente du dashboard
|
||||
/// Affiche les dernières activités dans une liste compacte
|
||||
library dashboard_recent_activity_section;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import 'dashboard_activity_tile.dart';
|
||||
|
||||
/// Widget de section d'activité récente
|
||||
///
|
||||
/// Affiche les dernières activités de l'union :
|
||||
/// - Nouveaux membres
|
||||
/// - Cotisations reçues
|
||||
/// - Événements créés
|
||||
/// - Demandes de solidarité
|
||||
///
|
||||
/// Chaque activité peut être tapée pour plus de détails
|
||||
class DashboardRecentActivitySection extends StatelessWidget {
|
||||
/// Callback pour les actions sur les activités
|
||||
final Function(String activityId)? onActivityTap;
|
||||
|
||||
/// Liste des activités à afficher
|
||||
final List<DashboardActivity>? activities;
|
||||
|
||||
/// Constructeur de la section d'activité récente
|
||||
const DashboardRecentActivitySection({
|
||||
super.key,
|
||||
this.onActivityTap,
|
||||
this.activities,
|
||||
});
|
||||
|
||||
/// Génère la liste des activités récentes par défaut
|
||||
List<DashboardActivity> _getDefaultActivities() {
|
||||
return [
|
||||
DashboardActivity(
|
||||
title: 'Nouveau membre ajouté',
|
||||
subtitle: 'Marie Dupont a rejoint l\'union',
|
||||
icon: Icons.person_add,
|
||||
color: ColorTokens.primary,
|
||||
time: 'Il y a 2h',
|
||||
onTap: () => onActivityTap?.call('member_added_001'),
|
||||
),
|
||||
DashboardActivity(
|
||||
title: 'Cotisation reçue',
|
||||
subtitle: 'Paiement de 50€ de Jean Martin',
|
||||
icon: Icons.payment,
|
||||
color: ColorTokens.success,
|
||||
time: 'Il y a 4h',
|
||||
onTap: () => onActivityTap?.call('cotisation_002'),
|
||||
),
|
||||
DashboardActivity(
|
||||
title: 'Événement créé',
|
||||
subtitle: 'Assemblée générale programmée',
|
||||
icon: Icons.event,
|
||||
color: ColorTokens.tertiary,
|
||||
time: 'Hier',
|
||||
onTap: () => onActivityTap?.call('event_003'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activitiesToShow = activities ?? _getDefaultActivities();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Activité récente',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
|
||||
child: Column(
|
||||
children: activitiesToShow.map((activity) {
|
||||
final isLast = activity == activitiesToShow.last;
|
||||
return Column(
|
||||
children: [
|
||||
DashboardActivityTile(activity: activity),
|
||||
if (!isLast) const Divider(height: 1),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,946 +0,0 @@
|
||||
/// Widget de carte de statistique individuelle - Version Améliorée
|
||||
/// Affiche une métrique sophistiquée avec animations, tendances et comparaisons
|
||||
library dashboard_stats_card;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
|
||||
/// Types de statistiques disponibles
|
||||
enum StatType {
|
||||
count,
|
||||
percentage,
|
||||
currency,
|
||||
duration,
|
||||
rate,
|
||||
score,
|
||||
custom,
|
||||
}
|
||||
|
||||
/// Styles de cartes de statistiques
|
||||
enum StatCardStyle {
|
||||
standard,
|
||||
minimal,
|
||||
elevated,
|
||||
outlined,
|
||||
gradient,
|
||||
compact,
|
||||
detailed,
|
||||
}
|
||||
|
||||
/// Tailles de cartes de statistiques
|
||||
enum StatCardSize {
|
||||
small,
|
||||
medium,
|
||||
large,
|
||||
extraLarge,
|
||||
}
|
||||
|
||||
/// Tendances des statistiques
|
||||
enum StatTrend {
|
||||
up,
|
||||
down,
|
||||
stable,
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Modèle de données avancé pour une statistique
|
||||
class DashboardStat {
|
||||
/// Icône représentative de la statistique
|
||||
final IconData icon;
|
||||
|
||||
/// Valeur numérique à afficher
|
||||
final String value;
|
||||
|
||||
/// Titre descriptif de la statistique
|
||||
final String title;
|
||||
|
||||
/// Sous-titre ou description
|
||||
final String? subtitle;
|
||||
|
||||
/// Couleur thématique de la carte
|
||||
final Color color;
|
||||
|
||||
/// Type de statistique
|
||||
final StatType type;
|
||||
|
||||
/// Style de la carte
|
||||
final StatCardStyle style;
|
||||
|
||||
/// Taille de la carte
|
||||
final StatCardSize size;
|
||||
|
||||
/// Callback optionnel lors du tap sur la carte
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Callback optionnel lors du long press
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Valeur précédente pour comparaison
|
||||
final String? previousValue;
|
||||
|
||||
/// Pourcentage de changement
|
||||
final double? changePercentage;
|
||||
|
||||
/// Tendance de la statistique
|
||||
final StatTrend trend;
|
||||
|
||||
/// Période de comparaison
|
||||
final String? period;
|
||||
|
||||
/// Icône de tendance personnalisée
|
||||
final IconData? trendIcon;
|
||||
|
||||
/// Gradient personnalisé
|
||||
final Gradient? gradient;
|
||||
|
||||
/// Badge à afficher
|
||||
final String? badge;
|
||||
|
||||
/// Couleur du badge
|
||||
final Color? badgeColor;
|
||||
|
||||
/// Graphique miniature (sparkline)
|
||||
final List<double>? sparklineData;
|
||||
|
||||
/// Animation activée
|
||||
final bool animated;
|
||||
|
||||
/// Feedback haptique activé
|
||||
final bool hapticFeedback;
|
||||
|
||||
/// Formatage personnalisé de la valeur
|
||||
final String Function(String)? valueFormatter;
|
||||
|
||||
/// Constructeur du modèle de statistique amélioré
|
||||
const DashboardStat({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.color,
|
||||
this.type = StatType.count,
|
||||
this.style = StatCardStyle.standard,
|
||||
this.size = StatCardSize.medium,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.previousValue,
|
||||
this.changePercentage,
|
||||
this.trend = StatTrend.unknown,
|
||||
this.period,
|
||||
this.trendIcon,
|
||||
this.gradient,
|
||||
this.badge,
|
||||
this.badgeColor,
|
||||
this.sparklineData,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
this.valueFormatter,
|
||||
});
|
||||
|
||||
/// Constructeur pour statistique de comptage
|
||||
const DashboardStat.count({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.previousValue,
|
||||
this.changePercentage,
|
||||
this.trend = StatTrend.unknown,
|
||||
this.period,
|
||||
this.badge,
|
||||
this.size = StatCardSize.medium,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : type = StatType.count,
|
||||
style = StatCardStyle.standard,
|
||||
trendIcon = null,
|
||||
gradient = null,
|
||||
badgeColor = null,
|
||||
sparklineData = null,
|
||||
valueFormatter = null;
|
||||
|
||||
/// Constructeur pour pourcentage
|
||||
const DashboardStat.percentage({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.changePercentage,
|
||||
this.trend = StatTrend.unknown,
|
||||
this.period,
|
||||
this.size = StatCardSize.medium,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : type = StatType.percentage,
|
||||
style = StatCardStyle.elevated,
|
||||
previousValue = null,
|
||||
trendIcon = null,
|
||||
gradient = null,
|
||||
badge = null,
|
||||
badgeColor = null,
|
||||
sparklineData = null,
|
||||
valueFormatter = null;
|
||||
|
||||
/// Constructeur pour devise
|
||||
const DashboardStat.currency({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.previousValue,
|
||||
this.changePercentage,
|
||||
this.trend = StatTrend.unknown,
|
||||
this.period,
|
||||
this.sparklineData,
|
||||
this.size = StatCardSize.medium,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : type = StatType.currency,
|
||||
style = StatCardStyle.detailed,
|
||||
trendIcon = null,
|
||||
gradient = null,
|
||||
badge = null,
|
||||
badgeColor = null,
|
||||
valueFormatter = null;
|
||||
|
||||
/// Constructeur avec gradient
|
||||
const DashboardStat.gradient({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.gradient,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.changePercentage,
|
||||
this.trend = StatTrend.unknown,
|
||||
this.period,
|
||||
this.size = StatCardSize.medium,
|
||||
this.animated = true,
|
||||
this.hapticFeedback = true,
|
||||
}) : type = StatType.custom,
|
||||
style = StatCardStyle.gradient,
|
||||
color = ColorTokens.primary,
|
||||
previousValue = null,
|
||||
trendIcon = null,
|
||||
badge = null,
|
||||
badgeColor = null,
|
||||
sparklineData = null,
|
||||
valueFormatter = null;
|
||||
}
|
||||
|
||||
/// Widget de carte de statistique amélioré
|
||||
///
|
||||
/// Affiche une métrique sophistiquée avec :
|
||||
/// - Icône colorée thématique avec animations
|
||||
/// - Valeur numérique formatée et mise en évidence
|
||||
/// - Titre et sous-titre descriptifs
|
||||
/// - Indicateurs de tendance et comparaisons
|
||||
/// - Graphiques miniatures (sparklines)
|
||||
/// - Badges et notifications
|
||||
/// - Styles multiples (standard, gradient, minimal)
|
||||
/// - Design Material 3 avec élévation adaptative
|
||||
/// - Support du tap et long press avec feedback haptique
|
||||
class DashboardStatsCard extends StatefulWidget {
|
||||
/// Données de la statistique à afficher
|
||||
final DashboardStat stat;
|
||||
|
||||
/// Constructeur de la carte de statistique améliorée
|
||||
const DashboardStatsCard({
|
||||
super.key,
|
||||
required this.stat,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardStatsCard> createState() => _DashboardStatsCardState();
|
||||
}
|
||||
|
||||
class _DashboardStatsCardState extends State<DashboardStatsCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Configure les animations
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 0.8,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
_slideAnimation = Tween<double>(
|
||||
begin: 30.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.2, 0.8, curve: Curves.easeOutCubic),
|
||||
));
|
||||
|
||||
if (widget.stat.animated) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.value = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient les dimensions selon la taille
|
||||
EdgeInsets _getPadding() {
|
||||
switch (widget.stat.size) {
|
||||
case StatCardSize.small:
|
||||
return const EdgeInsets.all(SpacingTokens.sm);
|
||||
case StatCardSize.medium:
|
||||
return const EdgeInsets.all(SpacingTokens.md);
|
||||
case StatCardSize.large:
|
||||
return const EdgeInsets.all(SpacingTokens.lg);
|
||||
case StatCardSize.extraLarge:
|
||||
return const EdgeInsets.all(SpacingTokens.xl);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la taille de l'icône selon la taille de la carte
|
||||
double _getIconSize() {
|
||||
switch (widget.stat.size) {
|
||||
case StatCardSize.small:
|
||||
return 20.0;
|
||||
case StatCardSize.medium:
|
||||
return 28.0;
|
||||
case StatCardSize.large:
|
||||
return 36.0;
|
||||
case StatCardSize.extraLarge:
|
||||
return 44.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le style de texte pour la valeur
|
||||
TextStyle _getValueStyle() {
|
||||
final baseStyle = widget.stat.size == StatCardSize.small
|
||||
? TypographyTokens.headlineSmall
|
||||
: widget.stat.size == StatCardSize.medium
|
||||
? TypographyTokens.headlineMedium
|
||||
: widget.stat.size == StatCardSize.large
|
||||
? TypographyTokens.headlineLarge
|
||||
: TypographyTokens.displaySmall;
|
||||
|
||||
return baseStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _getTextColor(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient le style de texte pour le titre
|
||||
TextStyle _getTitleStyle() {
|
||||
final baseStyle = widget.stat.size == StatCardSize.small
|
||||
? TypographyTokens.bodySmall
|
||||
: widget.stat.size == StatCardSize.medium
|
||||
? TypographyTokens.bodyMedium
|
||||
: TypographyTokens.bodyLarge;
|
||||
|
||||
return baseStyle.copyWith(
|
||||
color: _getSecondaryTextColor(),
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur du texte selon le style
|
||||
Color _getTextColor() {
|
||||
switch (widget.stat.style) {
|
||||
case StatCardStyle.gradient:
|
||||
return Colors.white;
|
||||
case StatCardStyle.standard:
|
||||
case StatCardStyle.minimal:
|
||||
case StatCardStyle.elevated:
|
||||
case StatCardStyle.outlined:
|
||||
case StatCardStyle.compact:
|
||||
case StatCardStyle.detailed:
|
||||
return widget.stat.color;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la couleur du texte secondaire
|
||||
Color _getSecondaryTextColor() {
|
||||
switch (widget.stat.style) {
|
||||
case StatCardStyle.gradient:
|
||||
return Colors.white.withOpacity(0.9);
|
||||
case StatCardStyle.standard:
|
||||
case StatCardStyle.minimal:
|
||||
case StatCardStyle.elevated:
|
||||
case StatCardStyle.outlined:
|
||||
case StatCardStyle.compact:
|
||||
case StatCardStyle.detailed:
|
||||
return ColorTokens.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère le tap avec feedback haptique
|
||||
void _handleTap() {
|
||||
if (widget.stat.hapticFeedback) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
widget.stat.onTap?.call();
|
||||
}
|
||||
|
||||
/// Gère le long press
|
||||
void _handleLongPress() {
|
||||
if (widget.stat.hapticFeedback) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
widget.stat.onLongPress?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.stat.animated) {
|
||||
return _buildCard();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
child: Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _buildCard(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la carte selon le style défini
|
||||
Widget _buildCard() {
|
||||
switch (widget.stat.style) {
|
||||
case StatCardStyle.standard:
|
||||
return _buildStandardCard();
|
||||
case StatCardStyle.minimal:
|
||||
return _buildMinimalCard();
|
||||
case StatCardStyle.elevated:
|
||||
return _buildElevatedCard();
|
||||
case StatCardStyle.outlined:
|
||||
return _buildOutlinedCard();
|
||||
case StatCardStyle.gradient:
|
||||
return _buildGradientCard();
|
||||
case StatCardStyle.compact:
|
||||
return _buildCompactCard();
|
||||
case StatCardStyle.detailed:
|
||||
return _buildDetailedCard();
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit une carte standard
|
||||
Widget _buildStandardCard() {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: _buildCardContent(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte minimale
|
||||
Widget _buildMinimalCard() {
|
||||
return InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: _getPadding(),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.stat.color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: widget.stat.color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: _buildCardContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte élevée
|
||||
Widget _buildElevatedCard() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: widget.stat.color.withOpacity(0.3),
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: _buildCardContent(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte avec contour
|
||||
Widget _buildOutlinedCard() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: widget.stat.color,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: _buildCardContent(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte avec gradient
|
||||
Widget _buildGradientCard() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: widget.stat.gradient ?? LinearGradient(
|
||||
colors: [widget.stat.color, widget.stat.color.withOpacity(0.8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: widget.stat.color.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: _buildCardContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte compacte
|
||||
Widget _buildCompactCard() {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.stat.icon,
|
||||
size: 24,
|
||||
color: widget.stat.color,
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.stat.value,
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: widget.stat.color,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.stat.title,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.stat.trend != StatTrend.unknown)
|
||||
_buildTrendIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte détaillée
|
||||
Widget _buildDetailedCard() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: _getPadding(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
widget.stat.icon,
|
||||
size: _getIconSize(),
|
||||
color: widget.stat.color,
|
||||
),
|
||||
if (widget.stat.badge != null) _buildBadge(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Text(
|
||||
_formatValue(widget.stat.value),
|
||||
style: _getValueStyle(),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
widget.stat.title,
|
||||
style: _getTitleStyle(),
|
||||
),
|
||||
if (widget.stat.subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
widget.stat.subtitle!,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: _getSecondaryTextColor().withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.stat.changePercentage != null) ...[
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
if (widget.stat.sparklineData != null) ...[
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
_buildSparkline(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le contenu standard de la carte
|
||||
Widget _buildCardContent() {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
widget.stat.icon,
|
||||
size: _getIconSize(),
|
||||
color: _getTextColor(),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
_formatValue(widget.stat.value),
|
||||
style: _getValueStyle(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
widget.stat.title,
|
||||
style: _getTitleStyle(),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (widget.stat.subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
widget.stat.subtitle!,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: _getSecondaryTextColor().withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (widget.stat.changePercentage != null) ...[
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
_buildChangeIndicator(),
|
||||
],
|
||||
],
|
||||
),
|
||||
// Badge en haut à droite
|
||||
if (widget.stat.badge != null)
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: _buildBadge(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Formate la valeur selon le type
|
||||
String _formatValue(String value) {
|
||||
if (widget.stat.valueFormatter != null) {
|
||||
return widget.stat.valueFormatter!(value);
|
||||
}
|
||||
|
||||
switch (widget.stat.type) {
|
||||
case StatType.percentage:
|
||||
return '$value%';
|
||||
case StatType.currency:
|
||||
return '€$value';
|
||||
case StatType.duration:
|
||||
return '${value}h';
|
||||
case StatType.rate:
|
||||
return '$value/min';
|
||||
case StatType.count:
|
||||
case StatType.score:
|
||||
case StatType.custom:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit l'indicateur de changement
|
||||
Widget _buildChangeIndicator() {
|
||||
if (widget.stat.changePercentage == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isPositive = widget.stat.changePercentage! > 0;
|
||||
final color = isPositive ? ColorTokens.success : ColorTokens.error;
|
||||
final icon = isPositive ? Icons.trending_up : Icons.trending_down;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
widget.stat.trendIcon ?? icon,
|
||||
size: 14,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${isPositive ? '+' : ''}${widget.stat.changePercentage!.toStringAsFixed(1)}%',
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (widget.stat.period != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.stat.period!,
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: _getSecondaryTextColor().withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'indicateur de tendance
|
||||
Widget _buildTrendIndicator() {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
switch (widget.stat.trend) {
|
||||
case StatTrend.up:
|
||||
icon = Icons.trending_up;
|
||||
color = ColorTokens.success;
|
||||
break;
|
||||
case StatTrend.down:
|
||||
icon = Icons.trending_down;
|
||||
color = ColorTokens.error;
|
||||
break;
|
||||
case StatTrend.stable:
|
||||
icon = Icons.trending_flat;
|
||||
color = ColorTokens.warning;
|
||||
break;
|
||||
case StatTrend.unknown:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
widget.stat.trendIcon ?? icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le badge
|
||||
Widget _buildBadge() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.stat.badgeColor ?? ColorTokens.error,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
widget.stat.badge!,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un graphique miniature (sparkline)
|
||||
Widget _buildSparkline() {
|
||||
if (widget.stat.sparklineData == null || widget.stat.sparklineData!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: CustomPaint(
|
||||
painter: SparklinePainter(
|
||||
data: widget.stat.sparklineData!,
|
||||
color: widget.stat.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Painter pour dessiner un graphique miniature
|
||||
class SparklinePainter extends CustomPainter {
|
||||
final List<double> data;
|
||||
final Color color;
|
||||
|
||||
SparklinePainter({
|
||||
required this.data,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (data.length < 2) return;
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path();
|
||||
final maxValue = data.reduce((a, b) => a > b ? a : b);
|
||||
final minValue = data.reduce((a, b) => a < b ? a : b);
|
||||
final range = maxValue - minValue;
|
||||
|
||||
if (range == 0) return;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final x = (i / (data.length - 1)) * size.width;
|
||||
final y = size.height - ((data[i] - minValue) / range) * size.height;
|
||||
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Dessiner des points aux extrémités
|
||||
final pointPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(0, size.height - ((data.first - minValue) / range) * size.height),
|
||||
2,
|
||||
pointPaint,
|
||||
);
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(size.width, size.height - ((data.last - minValue) / range) * size.height),
|
||||
2,
|
||||
pointPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/// Widget de grille de statistiques du dashboard
|
||||
/// Affiche les métriques principales dans une grille responsive
|
||||
library dashboard_stats_grid;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import 'dashboard_stats_card.dart';
|
||||
|
||||
/// Widget de grille de statistiques
|
||||
///
|
||||
/// Affiche les statistiques principales dans une grille 2x2 :
|
||||
/// - Membres actifs
|
||||
/// - Cotisations du mois
|
||||
/// - Événements programmés
|
||||
/// - Demandes de solidarité
|
||||
///
|
||||
/// Chaque carte est interactive et peut déclencher une navigation
|
||||
class DashboardStatsGrid extends StatelessWidget {
|
||||
/// Callback pour les actions sur les statistiques
|
||||
final Function(String statType)? onStatTap;
|
||||
|
||||
/// Liste des statistiques à afficher
|
||||
final List<DashboardStat>? stats;
|
||||
|
||||
/// Constructeur de la grille de statistiques
|
||||
const DashboardStatsGrid({
|
||||
super.key,
|
||||
this.onStatTap,
|
||||
this.stats,
|
||||
});
|
||||
|
||||
/// Génère la liste des statistiques par défaut
|
||||
List<DashboardStat> _getDefaultStats() {
|
||||
return [
|
||||
DashboardStat(
|
||||
icon: Icons.people,
|
||||
value: '25',
|
||||
title: 'Membres',
|
||||
color: ColorTokens.primary,
|
||||
onTap: () => onStatTap?.call('members'),
|
||||
),
|
||||
DashboardStat(
|
||||
icon: Icons.account_balance_wallet,
|
||||
value: '15',
|
||||
title: 'Cotisations',
|
||||
color: ColorTokens.success,
|
||||
onTap: () => onStatTap?.call('cotisations'),
|
||||
),
|
||||
DashboardStat(
|
||||
icon: Icons.event,
|
||||
value: '8',
|
||||
title: 'Événements',
|
||||
color: ColorTokens.tertiary,
|
||||
onTap: () => onStatTap?.call('events'),
|
||||
),
|
||||
DashboardStat(
|
||||
icon: Icons.favorite,
|
||||
value: '3',
|
||||
title: 'Solidarité',
|
||||
color: ColorTokens.error,
|
||||
onTap: () => onStatTap?.call('solidarity'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statsToShow = stats ?? _getDefaultStats();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Statistiques',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: SpacingTokens.md,
|
||||
mainAxisSpacing: SpacingTokens.md,
|
||||
childAspectRatio: 1.4,
|
||||
),
|
||||
itemCount: statsToShow.length,
|
||||
itemBuilder: (context, index) {
|
||||
return DashboardStatsCard(stat: statsToShow[index]);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/// Widget de section de bienvenue du dashboard
|
||||
/// Affiche un message d'accueil avec gradient et design moderne
|
||||
library dashboard_welcome_section;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
|
||||
/// Widget de section de bienvenue
|
||||
///
|
||||
/// Affiche un message d'accueil personnalisé avec :
|
||||
/// - Gradient de fond élégant
|
||||
/// - Typographie hiérarchisée
|
||||
/// - Design responsive et moderne
|
||||
class DashboardWelcomeSection extends StatelessWidget {
|
||||
/// Titre principal de la section
|
||||
final String title;
|
||||
|
||||
/// Sous-titre descriptif
|
||||
final String subtitle;
|
||||
|
||||
/// Constructeur du widget de bienvenue
|
||||
const DashboardWelcomeSection({
|
||||
super.key,
|
||||
this.title = 'Bienvenue sur UnionFlow',
|
||||
this.subtitle = 'Votre plateforme de gestion d\'union familiale',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
ColorTokens.primary.withOpacity(0.1),
|
||||
ColorTokens.secondary.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(
|
||||
color: ColorTokens.outline.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorTokens.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TypographyTokens.bodyMedium.copyWith(
|
||||
color: ColorTokens.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,191 +1,12 @@
|
||||
library dashboard_widgets;
|
||||
|
||||
/// Exports pour tous les widgets du dashboard UnionFlow
|
||||
///
|
||||
/// Ce fichier centralise tous les imports des composants du dashboard
|
||||
/// pour faciliter leur utilisation dans les pages et autres widgets.
|
||||
|
||||
// Widgets communs réutilisables
|
||||
export 'common/stat_card.dart';
|
||||
export 'common/section_header.dart';
|
||||
export 'common/activity_item.dart';
|
||||
|
||||
// Sections principales du dashboard
|
||||
export 'dashboard_header.dart';
|
||||
export 'quick_stats_section.dart';
|
||||
export 'recent_activities_section.dart';
|
||||
export 'upcoming_events_section.dart';
|
||||
|
||||
// Composants spécialisés
|
||||
export 'components/cards/performance_card.dart';
|
||||
|
||||
// Widgets existants (legacy) - gardés pour compatibilité
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/tokens.dart';
|
||||
import '../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget pour afficher une grille d'actions rapides
|
||||
class DashboardQuickActionsGrid extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final int crossAxisCount;
|
||||
|
||||
const DashboardQuickActionsGrid({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.crossAxisCount = 2,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: SpacingTokens.md,
|
||||
mainAxisSpacing: SpacingTokens.md,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour une action rapide
|
||||
class DashboardQuickAction extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const DashboardQuickAction({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.color,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 32,
|
||||
color: color ?? ColorTokens.primary,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
title,
|
||||
style: TypographyTokens.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour afficher une section d'activité récente
|
||||
class DashboardRecentActivitySection extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final String title;
|
||||
|
||||
const DashboardRecentActivitySection({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.title = 'Activité Récente',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TypographyTokens.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour une activité
|
||||
class DashboardActivity extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
|
||||
const DashboardActivity({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: SpacingTokens.sm),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: color ?? ColorTokens.primary,
|
||||
child: Icon(icon, color: Colors.white),
|
||||
),
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour une section d'insights
|
||||
class DashboardInsightsSection extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const DashboardInsightsSection({
|
||||
super.key,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Insights',
|
||||
style: TypographyTokens.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour une statistique
|
||||
/// Widget de statistique simple pour les dashboards de rôle
|
||||
class DashboardStat extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const DashboardStat({
|
||||
super.key,
|
||||
@@ -193,59 +14,56 @@ class DashboardStat extends StatelessWidget {
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.color,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 32,
|
||||
color: color ?? ColorTokens.primary,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color ?? ColorTokens.primary,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
title,
|
||||
style: TypographyTokens.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour la grille de statistiques
|
||||
/// Widget de grille de statistiques
|
||||
class DashboardStatsGrid extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final int crossAxisCount;
|
||||
final List<DashboardStat> stats;
|
||||
final Function(String)? onStatTap;
|
||||
|
||||
const DashboardStatsGrid({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.crossAxisCount = 2,
|
||||
required this.stats,
|
||||
this.onStatTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -253,64 +71,182 @@ class DashboardStatsGrid extends StatelessWidget {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: SpacingTokens.md,
|
||||
mainAxisSpacing: SpacingTokens.md,
|
||||
children: stats,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget de grille d'actions rapides
|
||||
class DashboardQuickActionsGrid extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const DashboardQuickActionsGrid({
|
||||
super.key,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.5,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour le drawer du dashboard
|
||||
class DashboardDrawer extends StatelessWidget {
|
||||
const DashboardDrawer({super.key});
|
||||
/// Widget d'action rapide
|
||||
class DashboardQuickAction extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final Color? color;
|
||||
|
||||
const DashboardQuickAction({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.primary,
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.cardShadow,
|
||||
border: Border.all(
|
||||
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 32,
|
||||
),
|
||||
child: Text(
|
||||
'UnionFlow',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget de section d'activités récentes
|
||||
class DashboardRecentActivitySection extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const DashboardRecentActivitySection({
|
||||
super.key,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Activités récentes',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'activité
|
||||
class DashboardActivity extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String time;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
|
||||
const DashboardActivity({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.time,
|
||||
required this.icon,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: DashboardTheme.spacing12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dashboard),
|
||||
title: const Text('Dashboard'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people),
|
||||
title: const Text('Membres'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.event),
|
||||
title: const Text('Événements'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Paramètres'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
Text(
|
||||
time,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:async';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de métriques en temps réel avec animations
|
||||
class RealTimeMetricsWidget extends StatefulWidget {
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
final Duration refreshInterval;
|
||||
|
||||
const RealTimeMetricsWidget({
|
||||
super.key,
|
||||
required this.organizationId,
|
||||
required this.userId,
|
||||
this.refreshInterval = const Duration(minutes: 5),
|
||||
});
|
||||
|
||||
@override
|
||||
State<RealTimeMetricsWidget> createState() => _RealTimeMetricsWidgetState();
|
||||
}
|
||||
|
||||
class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
with TickerProviderStateMixin {
|
||||
Timer? _refreshTimer;
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _countController;
|
||||
late Animation<double> _pulseAnimation;
|
||||
late Animation<double> _countAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_countController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_countAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _countController,
|
||||
curve: Curves.easeOutCubic,
|
||||
));
|
||||
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer = Timer.periodic(widget.refreshInterval, (timer) {
|
||||
if (mounted) {
|
||||
context.read<DashboardBloc>().add(RefreshDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.gradientCardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
BlocConsumer<DashboardBloc, DashboardState>(
|
||||
listener: (context, state) {
|
||||
if (state is DashboardLoaded) {
|
||||
_countController.forward(from: 0);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingMetrics();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildMetrics(data);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorMetrics();
|
||||
}
|
||||
return _buildEmptyMetrics();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Métriques Temps Réel',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'Mise à jour automatique',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildRefreshIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRefreshIndicator() {
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardRefreshing) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DashboardBloc>().add(RefreshDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: DashboardTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetrics(DashboardEntity data) {
|
||||
return AnimatedBuilder(
|
||||
animation: _countAnimation,
|
||||
builder: (context, child) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Membres Actifs',
|
||||
(data.stats.activeMembers * _countAnimation.value).round(),
|
||||
data.stats.totalMembers,
|
||||
Icons.people,
|
||||
DashboardTheme.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Engagement',
|
||||
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
|
||||
100,
|
||||
Icons.favorite,
|
||||
DashboardTheme.warning,
|
||||
suffix: '%',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Événements',
|
||||
(data.stats.upcomingEvents * _countAnimation.value).round(),
|
||||
data.stats.totalEvents,
|
||||
Icons.event,
|
||||
DashboardTheme.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Croissance',
|
||||
(data.stats.monthlyGrowth * _countAnimation.value),
|
||||
null,
|
||||
Icons.trending_up,
|
||||
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
suffix: '%',
|
||||
isDecimal: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricItem(
|
||||
String label,
|
||||
dynamic value,
|
||||
int? maxValue,
|
||||
IconData icon,
|
||||
Color color, {
|
||||
String suffix = '',
|
||||
bool isDecimal = false,
|
||||
}) {
|
||||
String displayValue;
|
||||
if (isDecimal) {
|
||||
displayValue = value.toStringAsFixed(1) + suffix;
|
||||
} else {
|
||||
displayValue = value.toString() + suffix;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
displayValue,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
if (maxValue != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'sur $maxValue',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingMetrics() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingMetricItem() {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorMetrics() {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyMetrics() {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_pulseController.dispose();
|
||||
_countController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../data/services/dashboard_performance_monitor.dart';
|
||||
|
||||
/// Widget de monitoring des performances en temps réel
|
||||
class PerformanceMonitorWidget extends StatefulWidget {
|
||||
final bool showDetails;
|
||||
final Duration updateInterval;
|
||||
|
||||
const PerformanceMonitorWidget({
|
||||
super.key,
|
||||
this.showDetails = false,
|
||||
this.updateInterval = const Duration(seconds: 2),
|
||||
});
|
||||
|
||||
@override
|
||||
State<PerformanceMonitorWidget> createState() => _PerformanceMonitorWidgetState();
|
||||
}
|
||||
|
||||
class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
with TickerProviderStateMixin {
|
||||
final DashboardPerformanceMonitor _monitor = DashboardPerformanceMonitor();
|
||||
StreamSubscription<PerformanceMetrics>? _metricsSubscription;
|
||||
StreamSubscription<PerformanceAlert>? _alertSubscription;
|
||||
|
||||
PerformanceMetrics? _currentMetrics;
|
||||
final List<PerformanceAlert> _recentAlerts = [];
|
||||
|
||||
late AnimationController _pulseController;
|
||||
late Animation<double> _pulseAnimation;
|
||||
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_startMonitoring();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 0.8,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
Future<void> _startMonitoring() async {
|
||||
await _monitor.startMonitoring();
|
||||
|
||||
_metricsSubscription = _monitor.metricsStream.listen((metrics) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentMetrics = metrics;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_alertSubscription = _monitor.alertStream.listen((alert) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_recentAlerts.insert(0, alert);
|
||||
if (_recentAlerts.length > 5) {
|
||||
_recentAlerts.removeLast();
|
||||
}
|
||||
});
|
||||
|
||||
// Afficher une notification pour les alertes critiques
|
||||
if (alert.severity == AlertSeverity.error ||
|
||||
alert.severity == AlertSeverity.critical) {
|
||||
_showAlertSnackBar(alert);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showAlertSnackBar(PerformanceAlert alert) {
|
||||
final color = _getAlertColor(alert.severity);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getAlertIcon(alert.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alert.message,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: color,
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'Détails',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isExpanded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_currentMetrics == null) {
|
||||
return _buildLoadingWidget();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
if (_isExpanded || widget.showDetails) ...[
|
||||
const Divider(height: 1),
|
||||
_buildDetailedMetrics(),
|
||||
if (_recentAlerts.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
_buildAlertsSection(),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.royalBlue),
|
||||
),
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing12),
|
||||
Text(
|
||||
'Initialisation du monitoring...',
|
||||
style: DashboardTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: _getOverallHealthColor(),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _getOverallHealthColor().withOpacity(0.5),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Performances Système',
|
||||
style: DashboardTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
_buildQuickMetrics(),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Icon(
|
||||
_isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMetrics() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildQuickMetric(
|
||||
'MEM',
|
||||
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildQuickMetric(
|
||||
'CPU',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildQuickMetric(
|
||||
'NET',
|
||||
'${_currentMetrics!.networkLatency}ms',
|
||||
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMetric(String label, String value, Color color) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailedMetrics() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMetricRow(
|
||||
'Mémoire',
|
||||
'${_currentMetrics!.memoryUsage.toStringAsFixed(1)} MB',
|
||||
_currentMetrics!.memoryUsage / 1000, // Normaliser sur 1000MB
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
Icons.memory,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Processeur',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
|
||||
_currentMetrics!.cpuUsage / 100,
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
Icons.speed,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Réseau',
|
||||
'${_currentMetrics!.networkLatency} ms',
|
||||
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
|
||||
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
|
||||
Icons.wifi,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Images/sec',
|
||||
'${_currentMetrics!.frameRate.toStringAsFixed(1)} fps',
|
||||
_currentMetrics!.frameRate / 60,
|
||||
_getMetricColor(60 - _currentMetrics!.frameRate, 10, 30), // Inversé car plus c'est haut, mieux c'est
|
||||
Icons.videocam,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Batterie',
|
||||
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
|
||||
_currentMetrics!.batteryLevel / 100,
|
||||
_getBatteryColor(_currentMetrics!.batteryLevel),
|
||||
Icons.battery_std,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricRow(
|
||||
String label,
|
||||
String value,
|
||||
double progress,
|
||||
Color color,
|
||||
IconData icon,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
backgroundColor: DashboardTheme.grey200,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertsSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Alertes Récentes',
|
||||
style: DashboardTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
..._recentAlerts.take(3).map((alert) => _buildAlertItem(alert)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertItem(PerformanceAlert alert) {
|
||||
final color = _getAlertColor(alert.severity);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getAlertIcon(alert.type),
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alert.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: DashboardTheme.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatTime(alert.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getOverallHealthColor() {
|
||||
if (_currentMetrics == null) return DashboardTheme.grey400;
|
||||
|
||||
final metrics = _currentMetrics!;
|
||||
|
||||
// Calculer un score de santé global
|
||||
int issues = 0;
|
||||
if (metrics.memoryUsage > 500) issues++;
|
||||
if (metrics.cpuUsage > 70) issues++;
|
||||
if (metrics.networkLatency > 1000) issues++;
|
||||
if (metrics.frameRate < 30) issues++;
|
||||
|
||||
switch (issues) {
|
||||
case 0:
|
||||
return DashboardTheme.success;
|
||||
case 1:
|
||||
return DashboardTheme.warning;
|
||||
default:
|
||||
return DashboardTheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
|
||||
if (value >= errorThreshold) return DashboardTheme.error;
|
||||
if (value >= warningThreshold) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
}
|
||||
|
||||
Color _getBatteryColor(double batteryLevel) {
|
||||
if (batteryLevel <= 20) return DashboardTheme.error;
|
||||
if (batteryLevel <= 50) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
}
|
||||
|
||||
Color _getAlertColor(AlertSeverity severity) {
|
||||
switch (severity) {
|
||||
case AlertSeverity.info:
|
||||
return DashboardTheme.info;
|
||||
case AlertSeverity.warning:
|
||||
return DashboardTheme.warning;
|
||||
case AlertSeverity.error:
|
||||
return DashboardTheme.error;
|
||||
case AlertSeverity.critical:
|
||||
return DashboardTheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getAlertIcon(AlertType type) {
|
||||
switch (type) {
|
||||
case AlertType.memory:
|
||||
return Icons.memory;
|
||||
case AlertType.cpu:
|
||||
return Icons.speed;
|
||||
case AlertType.network:
|
||||
return Icons.wifi_off;
|
||||
case AlertType.performance:
|
||||
return Icons.slow_motion_video;
|
||||
case AlertType.battery:
|
||||
return Icons.battery_alert;
|
||||
case AlertType.disk:
|
||||
return Icons.storage;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(time);
|
||||
|
||||
if (diff.inMinutes < 1) return 'maintenant';
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes}min';
|
||||
if (diff.inHours < 24) return '${diff.inHours}h';
|
||||
return '${diff.inDays}j';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseController.dispose();
|
||||
_metricsSubscription?.cancel();
|
||||
_alertSubscription?.cancel();
|
||||
_monitor.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../pages/connected_dashboard_page.dart';
|
||||
import '../../pages/advanced_dashboard_page.dart';
|
||||
|
||||
/// Widget de navigation pour les différents types de dashboard
|
||||
class DashboardNavigation extends StatefulWidget {
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
|
||||
const DashboardNavigation({
|
||||
super.key,
|
||||
required this.organizationId,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardNavigation> createState() => _DashboardNavigationState();
|
||||
}
|
||||
|
||||
class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<DashboardTab> _tabs = [
|
||||
const DashboardTab(
|
||||
title: 'Accueil',
|
||||
icon: Icons.home,
|
||||
activeIcon: Icons.home,
|
||||
type: DashboardType.home,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Analytics',
|
||||
icon: Icons.analytics_outlined,
|
||||
activeIcon: Icons.analytics,
|
||||
type: DashboardType.analytics,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Rapports',
|
||||
icon: Icons.assessment_outlined,
|
||||
activeIcon: Icons.assessment,
|
||||
type: DashboardType.reports,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Paramètres',
|
||||
icon: Icons.settings_outlined,
|
||||
activeIcon: Icons.settings,
|
||||
type: DashboardType.settings,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _buildCurrentPage(),
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentPage() {
|
||||
switch (_tabs[_currentIndex].type) {
|
||||
case DashboardType.home:
|
||||
return ConnectedDashboardPage(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
);
|
||||
case DashboardType.analytics:
|
||||
return AdvancedDashboardPage(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
);
|
||||
case DashboardType.reports:
|
||||
return _buildReportsPage();
|
||||
case DashboardType.settings:
|
||||
return _buildSettingsPage();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: DashboardTheme.grey900.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BottomAppBar(
|
||||
shape: const CircularNotchedRectangle(),
|
||||
notchMargin: 8,
|
||||
color: DashboardTheme.white,
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DashboardTheme.spacing8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _tabs.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final tab = entry.value;
|
||||
final isActive = index == _currentIndex;
|
||||
|
||||
// Skip the middle item for FAB space
|
||||
if (index == 2) {
|
||||
return const SizedBox(width: 40);
|
||||
}
|
||||
|
||||
return _buildNavItem(tab, isActive, index);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(DashboardTab tab, bool isActive, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _currentIndex = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: DashboardTheme.spacing12,
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? tab.activeIcon : tab.icon,
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
tab.title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: DashboardTheme.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
onPressed: _showQuickActions,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: DashboardTheme.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Rapports'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.assessment,
|
||||
size: 64,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const Text(
|
||||
'Page Rapports',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Fonctionnalité en cours de développement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Paramètres'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
children: [
|
||||
_buildSettingsSection(
|
||||
'Apparence',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Thème',
|
||||
'Bleu Roi & Pétrole',
|
||||
Icons.palette,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Langue',
|
||||
'Français',
|
||||
Icons.language,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
_buildSettingsSection(
|
||||
'Notifications',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Notifications push',
|
||||
'Activées',
|
||||
Icons.notifications,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Emails',
|
||||
'Quotidien',
|
||||
Icons.email,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
_buildSettingsSection(
|
||||
'Données',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Synchronisation',
|
||||
'Automatique',
|
||||
Icons.sync,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Cache',
|
||||
'Vider le cache',
|
||||
Icons.storage,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection(String title, List<Widget> children) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
child: Column(children: children),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsTile(
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: DashboardTheme.royalBlue),
|
||||
title: Text(title, style: DashboardTheme.bodyMedium),
|
||||
subtitle: Text(subtitle, style: DashboardTheme.bodySmall),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
void _showQuickActions() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadiusLarge),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadiusLarge),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const Text(
|
||||
'Actions Rapides',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
GridView.count(
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: DashboardTheme.spacing16,
|
||||
mainAxisSpacing: DashboardTheme.spacing16,
|
||||
children: [
|
||||
_buildQuickActionItem('Nouveau\nMembre', Icons.person_add, DashboardTheme.success),
|
||||
_buildQuickActionItem('Créer\nÉvénement', Icons.event_available, DashboardTheme.royalBlue),
|
||||
_buildQuickActionItem('Ajouter\nContribution', Icons.payment, DashboardTheme.tealBlue),
|
||||
_buildQuickActionItem('Envoyer\nMessage', Icons.message, DashboardTheme.warning),
|
||||
_buildQuickActionItem('Générer\nRapport', Icons.assessment, DashboardTheme.info),
|
||||
_buildQuickActionItem('Paramètres', Icons.settings, DashboardTheme.grey600),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionItem(String title, IconData icon, Color color) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implémenter l'action
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardTab {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final DashboardType type;
|
||||
|
||||
const DashboardTab({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.activeIcon,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
|
||||
enum DashboardType {
|
||||
home,
|
||||
analytics,
|
||||
reports,
|
||||
settings,
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de notifications pour le dashboard
|
||||
class DashboardNotificationsWidget extends StatelessWidget {
|
||||
final int maxNotifications;
|
||||
|
||||
const DashboardNotificationsWidget({
|
||||
super.key,
|
||||
this.maxNotifications = 5,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingNotifications();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildNotifications(data);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorNotifications();
|
||||
}
|
||||
return _buildEmptyNotifications();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadius),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications,
|
||||
color: DashboardTheme.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Notifications',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
final urgentCount = _getUrgentNotificationsCount(data);
|
||||
|
||||
if (urgentCount > 0) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
urgentCount.toString(),
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotifications(DashboardEntity data) {
|
||||
final notifications = _generateNotifications(data);
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return _buildEmptyNotifications();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: notifications.take(maxNotifications).map((notification) {
|
||||
return _buildNotificationItem(notification);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotificationItem(DashboardNotification notification) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: notification.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
notification.icon,
|
||||
color: notification.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
notification.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (notification.isUrgent) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing6,
|
||||
vertical: DashboardTheme.spacing2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
'URGENT',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
notification.message,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
notification.timeAgo,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (notification.actionLabel != null) ...[
|
||||
GestureDetector(
|
||||
onTap: notification.onAction,
|
||||
child: Text(
|
||||
notification.actionLabel!,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingNotifications() {
|
||||
return Column(
|
||||
children: List.generate(3, (index) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorNotifications() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyNotifications() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.notifications_none,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucune notification',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'Vous êtes à jour !',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DashboardNotification> _generateNotifications(DashboardEntity data) {
|
||||
List<DashboardNotification> notifications = [];
|
||||
|
||||
// Notification pour les demandes en attente
|
||||
if (data.stats.pendingRequests > 0) {
|
||||
notifications.add(DashboardNotification(
|
||||
title: 'Demandes en attente',
|
||||
message: '${data.stats.pendingRequests} demandes nécessitent votre attention',
|
||||
icon: Icons.pending_actions,
|
||||
color: DashboardTheme.warning,
|
||||
timeAgo: '2h',
|
||||
isUrgent: data.stats.pendingRequests > 20,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
));
|
||||
}
|
||||
|
||||
// Notification pour les événements aujourd'hui
|
||||
if (data.todayEventsCount > 0) {
|
||||
notifications.add(DashboardNotification(
|
||||
title: 'Événements aujourd\'hui',
|
||||
message: '${data.todayEventsCount} événement(s) programmé(s) aujourd\'hui',
|
||||
icon: Icons.event_available,
|
||||
color: DashboardTheme.info,
|
||||
timeAgo: '30min',
|
||||
isUrgent: false,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
));
|
||||
}
|
||||
|
||||
// Notification pour la croissance
|
||||
if (data.stats.hasGrowth) {
|
||||
notifications.add(DashboardNotification(
|
||||
title: 'Croissance positive',
|
||||
message: 'Croissance de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
|
||||
icon: Icons.trending_up,
|
||||
color: DashboardTheme.success,
|
||||
timeAgo: '1j',
|
||||
isUrgent: false,
|
||||
actionLabel: null,
|
||||
onAction: null,
|
||||
));
|
||||
}
|
||||
|
||||
// Notification pour l'engagement faible
|
||||
if (!data.stats.isHighEngagement) {
|
||||
notifications.add(DashboardNotification(
|
||||
title: 'Engagement à améliorer',
|
||||
message: 'Taux d\'engagement: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
|
||||
icon: Icons.trending_down,
|
||||
color: DashboardTheme.error,
|
||||
timeAgo: '3h',
|
||||
isUrgent: data.stats.engagementRate < 0.5,
|
||||
actionLabel: 'Améliorer',
|
||||
onAction: () {},
|
||||
));
|
||||
}
|
||||
|
||||
// Notification pour les nouveaux membres
|
||||
if (data.recentActivitiesCount > 0) {
|
||||
notifications.add(DashboardNotification(
|
||||
title: 'Nouvelles activités',
|
||||
message: '${data.recentActivitiesCount} nouvelles activités aujourd\'hui',
|
||||
icon: Icons.fiber_new,
|
||||
color: DashboardTheme.tealBlue,
|
||||
timeAgo: '15min',
|
||||
isUrgent: false,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
));
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
int _getUrgentNotificationsCount(DashboardEntity data) {
|
||||
final notifications = _generateNotifications(data);
|
||||
return notifications.where((n) => n.isUrgent).length;
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardNotification {
|
||||
final String title;
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String timeAgo;
|
||||
final bool isUrgent;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const DashboardNotification({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.timeAgo,
|
||||
required this.isUrgent,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'common/section_header.dart';
|
||||
import 'common/stat_card.dart';
|
||||
|
||||
/// Section des statistiques rapides du dashboard
|
||||
///
|
||||
/// Widget réutilisable pour afficher les KPIs et métriques principales
|
||||
/// avec différents layouts et styles selon le contexte.
|
||||
class QuickStatsSection extends StatelessWidget {
|
||||
/// Titre de la section
|
||||
final String title;
|
||||
|
||||
/// Sous-titre optionnel
|
||||
final String? subtitle;
|
||||
|
||||
/// Liste des statistiques à afficher
|
||||
final List<QuickStat> stats;
|
||||
|
||||
/// Layout des cartes (grid, row, column)
|
||||
final StatsLayout layout;
|
||||
|
||||
/// Nombre de colonnes pour le layout grid
|
||||
final int gridColumns;
|
||||
|
||||
/// Style des cartes de statistiques
|
||||
final StatCardStyle cardStyle;
|
||||
|
||||
/// Taille des cartes
|
||||
final StatCardSize cardSize;
|
||||
|
||||
/// Callback lors du tap sur une statistique
|
||||
final Function(QuickStat)? onStatTap;
|
||||
|
||||
/// Afficher ou non l'en-tête de section
|
||||
final bool showHeader;
|
||||
|
||||
const QuickStatsSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.stats,
|
||||
this.layout = StatsLayout.grid,
|
||||
this.gridColumns = 2,
|
||||
this.cardStyle = StatCardStyle.elevated,
|
||||
this.cardSize = StatCardSize.compact,
|
||||
this.onStatTap,
|
||||
this.showHeader = true,
|
||||
});
|
||||
|
||||
/// Constructeur pour les KPIs système (Super Admin)
|
||||
const QuickStatsSection.systemKPIs({
|
||||
super.key,
|
||||
this.onStatTap,
|
||||
}) : title = 'Métriques Système',
|
||||
subtitle = null,
|
||||
stats = const [
|
||||
QuickStat(
|
||||
title: 'Organisations',
|
||||
value: '247',
|
||||
subtitle: '+12 ce mois',
|
||||
icon: Icons.business,
|
||||
color: Color(0xFF0984E3),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Utilisateurs',
|
||||
value: '15,847',
|
||||
subtitle: '+1,234 ce mois',
|
||||
icon: Icons.people,
|
||||
color: Color(0xFF00B894),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Uptime',
|
||||
value: '99.97%',
|
||||
subtitle: '30 derniers jours',
|
||||
icon: Icons.trending_up,
|
||||
color: Color(0xFF00CEC9),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Temps Réponse',
|
||||
value: '1.2s',
|
||||
subtitle: 'Moyenne 24h',
|
||||
icon: Icons.speed,
|
||||
color: Color(0xFFE17055),
|
||||
),
|
||||
],
|
||||
layout = StatsLayout.grid,
|
||||
gridColumns = 2,
|
||||
cardStyle = StatCardStyle.elevated,
|
||||
cardSize = StatCardSize.compact,
|
||||
showHeader = true;
|
||||
|
||||
/// Constructeur pour les statistiques d'organisation
|
||||
const QuickStatsSection.organizationStats({
|
||||
super.key,
|
||||
this.onStatTap,
|
||||
}) : title = 'Vue d\'ensemble',
|
||||
subtitle = null,
|
||||
stats = const [
|
||||
QuickStat(
|
||||
title: 'Membres',
|
||||
value: '156',
|
||||
subtitle: '+12 ce mois',
|
||||
icon: Icons.people,
|
||||
color: Color(0xFF00B894),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Événements',
|
||||
value: '23',
|
||||
subtitle: '8 à venir',
|
||||
icon: Icons.event,
|
||||
color: Color(0xFFE17055),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Projets',
|
||||
value: '8',
|
||||
subtitle: '3 actifs',
|
||||
icon: Icons.work,
|
||||
color: Color(0xFF0984E3),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Taux engagement',
|
||||
value: '78%',
|
||||
subtitle: '+5% ce mois',
|
||||
icon: Icons.trending_up,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
],
|
||||
layout = StatsLayout.grid,
|
||||
gridColumns = 2,
|
||||
cardStyle = StatCardStyle.elevated,
|
||||
cardSize = StatCardSize.compact,
|
||||
showHeader = true;
|
||||
|
||||
/// Constructeur pour les métriques de performance
|
||||
const QuickStatsSection.performanceMetrics({
|
||||
super.key,
|
||||
this.onStatTap,
|
||||
}) : title = 'Performance',
|
||||
subtitle = 'Métriques temps réel',
|
||||
stats = const [
|
||||
QuickStat(
|
||||
title: 'CPU',
|
||||
value: '23%',
|
||||
subtitle: 'Normal',
|
||||
icon: Icons.memory,
|
||||
color: Color(0xFF00B894),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'RAM',
|
||||
value: '67%',
|
||||
subtitle: 'Élevé',
|
||||
icon: Icons.storage,
|
||||
color: Color(0xFFE17055),
|
||||
),
|
||||
QuickStat(
|
||||
title: 'Réseau',
|
||||
value: '12 MB/s',
|
||||
subtitle: 'Stable',
|
||||
icon: Icons.network_check,
|
||||
color: Color(0xFF0984E3),
|
||||
),
|
||||
],
|
||||
layout = StatsLayout.row,
|
||||
gridColumns = 3,
|
||||
cardStyle = StatCardStyle.outlined,
|
||||
cardSize = StatCardSize.normal,
|
||||
showHeader = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showHeader) ...[
|
||||
SectionHeader.section(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
),
|
||||
],
|
||||
_buildStatsLayout(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construction du layout des statistiques
|
||||
Widget _buildStatsLayout() {
|
||||
switch (layout) {
|
||||
case StatsLayout.grid:
|
||||
return _buildGridLayout();
|
||||
case StatsLayout.row:
|
||||
return _buildRowLayout();
|
||||
case StatsLayout.column:
|
||||
return _buildColumnLayout();
|
||||
case StatsLayout.wrap:
|
||||
return _buildWrapLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout en grille
|
||||
Widget _buildGridLayout() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: gridColumns,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: _getChildAspectRatio(),
|
||||
),
|
||||
itemCount: stats.length,
|
||||
itemBuilder: (context, index) => _buildStatCard(stats[index]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Layout en ligne
|
||||
Widget _buildRowLayout() {
|
||||
return Row(
|
||||
children: stats.map((stat) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: _buildStatCard(stat),
|
||||
),
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Layout en colonne
|
||||
Widget _buildColumnLayout() {
|
||||
return Column(
|
||||
children: stats.map((stat) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildStatCard(stat),
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Layout wrap (adaptatif)
|
||||
Widget _buildWrapLayout() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: stats.map((stat) => SizedBox(
|
||||
width: (constraints.maxWidth - 8) / 2, // 2 colonnes avec espacement
|
||||
child: _buildStatCard(stat),
|
||||
)).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Construction d'une carte de statistique
|
||||
Widget _buildStatCard(QuickStat stat) {
|
||||
return StatCard(
|
||||
title: stat.title,
|
||||
value: stat.value,
|
||||
subtitle: stat.subtitle,
|
||||
icon: stat.icon,
|
||||
color: stat.color,
|
||||
size: cardSize,
|
||||
style: cardStyle,
|
||||
onTap: onStatTap != null ? () => onStatTap!(stat) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Ratio d'aspect selon la taille des cartes
|
||||
double _getChildAspectRatio() {
|
||||
switch (cardSize) {
|
||||
case StatCardSize.compact:
|
||||
return 1.4;
|
||||
case StatCardSize.normal:
|
||||
return 1.2;
|
||||
case StatCardSize.large:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour une statistique rapide
|
||||
class QuickStat {
|
||||
final String title;
|
||||
final String value;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const QuickStat({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
/// Constructeur pour une métrique système
|
||||
const QuickStat.system({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = const Color(0xFF6C5CE7),
|
||||
metadata = null;
|
||||
|
||||
/// Constructeur pour une métrique utilisateur
|
||||
const QuickStat.user({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = const Color(0xFF00B894),
|
||||
metadata = null;
|
||||
|
||||
/// Constructeur pour une métrique d'organisation
|
||||
const QuickStat.organization({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = const Color(0xFF0984E3),
|
||||
metadata = null;
|
||||
|
||||
/// Constructeur pour une métrique d'événement
|
||||
const QuickStat.event({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = const Color(0xFFE17055),
|
||||
metadata = null;
|
||||
|
||||
/// Constructeur pour une alerte
|
||||
const QuickStat.alert({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = Colors.orange,
|
||||
metadata = null;
|
||||
|
||||
/// Constructeur pour une erreur
|
||||
const QuickStat.error({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
}) : color = Colors.red,
|
||||
metadata = null;
|
||||
}
|
||||
|
||||
/// Types de layout pour les statistiques
|
||||
enum StatsLayout {
|
||||
grid,
|
||||
row,
|
||||
column,
|
||||
wrap,
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'common/activity_item.dart';
|
||||
|
||||
/// Section des activités récentes du dashboard
|
||||
///
|
||||
/// Widget réutilisable pour afficher les dernières activités,
|
||||
/// notifications, logs ou événements selon le contexte.
|
||||
class RecentActivitiesSection extends StatelessWidget {
|
||||
/// Titre de la section
|
||||
final String title;
|
||||
|
||||
/// Sous-titre optionnel
|
||||
final String? subtitle;
|
||||
|
||||
/// Liste des activités à afficher
|
||||
final List<RecentActivity> activities;
|
||||
|
||||
/// Nombre maximum d'activités à afficher
|
||||
final int maxItems;
|
||||
|
||||
/// Style des éléments d'activité
|
||||
final ActivityItemStyle itemStyle;
|
||||
|
||||
/// Callback lors du tap sur une activité
|
||||
final Function(RecentActivity)? onActivityTap;
|
||||
|
||||
/// Callback pour voir toutes les activités
|
||||
final VoidCallback? onViewAll;
|
||||
|
||||
/// Afficher ou non l'en-tête de section
|
||||
final bool showHeader;
|
||||
|
||||
/// Afficher ou non le bouton "Voir tout"
|
||||
final bool showViewAll;
|
||||
|
||||
/// Message à afficher si aucune activité
|
||||
final String? emptyMessage;
|
||||
|
||||
const RecentActivitiesSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.activities,
|
||||
this.maxItems = 5,
|
||||
this.itemStyle = ActivityItemStyle.normal,
|
||||
this.onActivityTap,
|
||||
this.onViewAll,
|
||||
this.showHeader = true,
|
||||
this.showViewAll = true,
|
||||
this.emptyMessage,
|
||||
});
|
||||
|
||||
/// Constructeur pour les activités système (Super Admin)
|
||||
const RecentActivitiesSection.system({
|
||||
super.key,
|
||||
this.onActivityTap,
|
||||
this.onViewAll,
|
||||
}) : title = 'Activité Système',
|
||||
subtitle = 'Événements récents',
|
||||
activities = const [
|
||||
RecentActivity(
|
||||
title: 'Sauvegarde automatique terminée',
|
||||
description: 'Sauvegarde complète réussie (2.3 GB)',
|
||||
timestamp: 'il y a 1h',
|
||||
type: ActivityType.system,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Nouvelle organisation créée',
|
||||
description: 'TechCorp a rejoint la plateforme',
|
||||
timestamp: 'il y a 2h',
|
||||
type: ActivityType.organization,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Mise à jour système',
|
||||
description: 'Version 2.1.0 déployée avec succès',
|
||||
timestamp: 'il y a 4h',
|
||||
type: ActivityType.system,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Alerte CPU résolue',
|
||||
description: 'Charge CPU revenue à la normale',
|
||||
timestamp: 'il y a 6h',
|
||||
type: ActivityType.success,
|
||||
),
|
||||
],
|
||||
maxItems = 4,
|
||||
itemStyle = ActivityItemStyle.normal,
|
||||
showHeader = true,
|
||||
showViewAll = true,
|
||||
emptyMessage = null;
|
||||
|
||||
/// Constructeur pour les activités d'organisation
|
||||
const RecentActivitiesSection.organization({
|
||||
super.key,
|
||||
this.onActivityTap,
|
||||
this.onViewAll,
|
||||
}) : title = 'Activité Récente',
|
||||
subtitle = null,
|
||||
activities = const [
|
||||
RecentActivity(
|
||||
title: 'Nouveau membre inscrit',
|
||||
description: 'Marie Dubois a rejoint l\'organisation',
|
||||
timestamp: 'il y a 30min',
|
||||
type: ActivityType.user,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Événement créé',
|
||||
description: 'Réunion mensuelle programmée',
|
||||
timestamp: 'il y a 2h',
|
||||
type: ActivityType.event,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Document partagé',
|
||||
description: 'Rapport Q4 2024 publié',
|
||||
timestamp: 'il y a 1j',
|
||||
type: ActivityType.organization,
|
||||
),
|
||||
],
|
||||
maxItems = 3,
|
||||
itemStyle = ActivityItemStyle.normal,
|
||||
showHeader = true,
|
||||
showViewAll = true,
|
||||
emptyMessage = null;
|
||||
|
||||
/// Constructeur pour les alertes système
|
||||
const RecentActivitiesSection.alerts({
|
||||
super.key,
|
||||
this.onActivityTap,
|
||||
this.onViewAll,
|
||||
}) : title = 'Alertes Récentes',
|
||||
subtitle = 'Notifications importantes',
|
||||
activities = const [
|
||||
RecentActivity(
|
||||
title: 'Charge CPU élevée',
|
||||
description: 'Serveur principal à 85%',
|
||||
timestamp: 'il y a 15min',
|
||||
type: ActivityType.alert,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Espace disque faible',
|
||||
description: 'Base de données à 90%',
|
||||
timestamp: 'il y a 1h',
|
||||
type: ActivityType.error,
|
||||
),
|
||||
RecentActivity(
|
||||
title: 'Connexions élevées',
|
||||
description: 'Load balancer surchargé',
|
||||
timestamp: 'il y a 2h',
|
||||
type: ActivityType.alert,
|
||||
),
|
||||
],
|
||||
maxItems = 3,
|
||||
itemStyle = ActivityItemStyle.alert,
|
||||
showHeader = true,
|
||||
showViewAll = true,
|
||||
emptyMessage = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showHeader) _buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitiesList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// En-tête de la section
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showViewAll && onViewAll != null)
|
||||
TextButton(
|
||||
onPressed: onViewAll,
|
||||
child: const Text(
|
||||
'Voir tout',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6C5CE7),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des activités
|
||||
Widget _buildActivitiesList() {
|
||||
if (activities.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayedActivities = activities.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayedActivities.map((activity) => ActivityItem(
|
||||
title: activity.title,
|
||||
description: activity.description,
|
||||
timestamp: activity.timestamp,
|
||||
icon: activity.icon,
|
||||
color: activity.color,
|
||||
type: activity.type,
|
||||
style: itemStyle,
|
||||
onTap: onActivityTap != null ? () => onActivityTap!(activity) : null,
|
||||
)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// État vide
|
||||
Widget _buildEmptyState() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inbox_outlined,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
emptyMessage ?? 'Aucune activité récente',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour une activité récente
|
||||
class RecentActivity {
|
||||
final String title;
|
||||
final String? description;
|
||||
final String timestamp;
|
||||
final IconData? icon;
|
||||
final Color? color;
|
||||
final ActivityType? type;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const RecentActivity({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.icon,
|
||||
this.color,
|
||||
this.type,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
/// Constructeur pour une activité système
|
||||
const RecentActivity.system({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.settings,
|
||||
color = const Color(0xFF6C5CE7),
|
||||
type = ActivityType.system;
|
||||
|
||||
/// Constructeur pour une activité utilisateur
|
||||
const RecentActivity.user({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.person,
|
||||
color = const Color(0xFF00B894),
|
||||
type = ActivityType.user;
|
||||
|
||||
/// Constructeur pour une activité d'organisation
|
||||
const RecentActivity.organization({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.business,
|
||||
color = const Color(0xFF0984E3),
|
||||
type = ActivityType.organization;
|
||||
|
||||
/// Constructeur pour une activité d'événement
|
||||
const RecentActivity.event({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.event,
|
||||
color = const Color(0xFFE17055),
|
||||
type = ActivityType.event;
|
||||
|
||||
/// Constructeur pour une alerte
|
||||
const RecentActivity.alert({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.warning,
|
||||
color = Colors.orange,
|
||||
type = ActivityType.alert;
|
||||
|
||||
/// Constructeur pour une erreur
|
||||
const RecentActivity.error({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.error,
|
||||
color = Colors.red,
|
||||
type = ActivityType.error;
|
||||
|
||||
/// Constructeur pour un succès
|
||||
const RecentActivity.success({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
}) : icon = Icons.check_circle,
|
||||
color = const Color(0xFF00B894),
|
||||
type = ActivityType.success;
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de recherche rapide pour le dashboard
|
||||
class DashboardSearchWidget extends StatefulWidget {
|
||||
final Function(String)? onSearch;
|
||||
final String? hintText;
|
||||
final List<SearchSuggestion>? suggestions;
|
||||
|
||||
const DashboardSearchWidget({
|
||||
super.key,
|
||||
this.onSearch,
|
||||
this.hintText,
|
||||
this.suggestions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardSearchWidget> createState() => _DashboardSearchWidgetState();
|
||||
}
|
||||
|
||||
class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
with TickerProviderStateMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isExpanded = false;
|
||||
List<SearchSuggestion> _filteredSuggestions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_setupListeners();
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.05,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
void _setupListeners() {
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
_isExpanded = _focusNode.hasFocus;
|
||||
});
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
});
|
||||
|
||||
_searchController.addListener(() {
|
||||
_filterSuggestions(_searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
void _filterSuggestions(String query) {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
|
||||
.where((suggestion) =>
|
||||
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_filteredSuggestions = filtered;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
_buildSuggestions(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _focusNode,
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
widget.onSearch?.call(value);
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'Rechercher...',
|
||||
hintStyle: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderSide: const BorderSide(
|
||||
color: DashboardTheme.royalBlue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
vertical: DashboardTheme.spacing12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: DashboardTheme.white,
|
||||
),
|
||||
style: DashboardTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestions() {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = _filteredSuggestions[index];
|
||||
return _buildSuggestionItem(suggestion, index == _filteredSuggestions.length - 1);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestionItem(SearchSuggestion suggestion, bool isLast) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_searchController.text = suggestion.title;
|
||||
widget.onSearch?.call(suggestion.title);
|
||||
_focusNode.unfocus();
|
||||
suggestion.onTap?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: suggestion.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
suggestion.icon,
|
||||
color: suggestion.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
suggestion.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (suggestion.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing2),
|
||||
Text(
|
||||
suggestion.subtitle,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchSuggestion> _getDefaultSuggestions() {
|
||||
return [
|
||||
SearchSuggestion(
|
||||
title: 'Membres',
|
||||
subtitle: 'Rechercher des membres',
|
||||
icon: Icons.people,
|
||||
color: DashboardTheme.royalBlue,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Événements',
|
||||
subtitle: 'Trouver des événements',
|
||||
icon: Icons.event,
|
||||
color: DashboardTheme.tealBlue,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Contributions',
|
||||
subtitle: 'Historique des paiements',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.success,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Rapports',
|
||||
subtitle: 'Consulter les rapports',
|
||||
icon: Icons.assessment,
|
||||
color: DashboardTheme.warning,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Paramètres',
|
||||
subtitle: 'Configuration système',
|
||||
icon: Icons.settings,
|
||||
color: DashboardTheme.grey600,
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_focusNode.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSuggestion {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SearchSuggestion({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de sélection de thème pour le Dashboard
|
||||
class ThemeSelectorWidget extends StatefulWidget {
|
||||
final Function(String)? onThemeChanged;
|
||||
|
||||
const ThemeSelectorWidget({
|
||||
super.key,
|
||||
this.onThemeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ThemeSelectorWidget> createState() => _ThemeSelectorWidgetState();
|
||||
}
|
||||
|
||||
class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
String _selectedTheme = 'royalTeal';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedTheme = DashboardThemeManager.currentTheme.name == 'Bleu Roi & Pétrole'
|
||||
? 'royalTeal' : 'royalTeal'; // Par défaut
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.palette,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Thème de l\'interface',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
|
||||
// Grille des thèmes
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemCount: DashboardThemeManager.availableThemes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final themeOption = DashboardThemeManager.availableThemes[index];
|
||||
final isSelected = _selectedTheme == themeOption.key;
|
||||
|
||||
return _buildThemeCard(themeOption, isSelected);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
|
||||
// Aperçu du thème sélectionné
|
||||
_buildThemePreview(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeCard(ThemeOption themeOption, bool isSelected) {
|
||||
return GestureDetector(
|
||||
onTap: () => _selectTheme(themeOption.key),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? themeOption.theme.primaryColor
|
||||
: DashboardTheme.grey300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: themeOption.theme.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Gradient de démonstration
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
themeOption.theme.primaryColor,
|
||||
themeOption.theme.secondaryColor,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
// Nom du thème
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: themeOption.theme.cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
bottomRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
themeOption.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: themeOption.theme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemePreview() {
|
||||
final currentTheme = DashboardThemeManager.availableThemes
|
||||
.firstWhere((theme) => theme.key == _selectedTheme);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: DashboardTheme.grey300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Aperçu: ${currentTheme.name}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
|
||||
// Exemple de carte avec le thème
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: currentTheme.theme.primaryColor.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: currentTheme.theme.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.dashboard,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dashboard UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Exemple avec ce thème',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: currentTheme.theme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.success.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
'Actif',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
|
||||
// Palette de couleurs
|
||||
Row(
|
||||
children: [
|
||||
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Succès', currentTheme.theme.success),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Attention', currentTheme.theme.warning),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorSwatch(String label, Color color) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectTheme(String themeKey) {
|
||||
setState(() {
|
||||
_selectedTheme = themeKey;
|
||||
});
|
||||
|
||||
// Appliquer le thème
|
||||
DashboardThemeManager.setTheme(themeKey);
|
||||
|
||||
// Notifier le changement
|
||||
widget.onThemeChanged?.call(themeKey);
|
||||
|
||||
// Afficher un message de confirmation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Thème "${DashboardThemeManager.availableThemes.firstWhere((t) => t.key == themeKey).name}" appliqué',
|
||||
),
|
||||
backgroundColor: DashboardThemeManager.currentTheme.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de raccourcis rapides pour le dashboard
|
||||
class DashboardShortcutsWidget extends StatelessWidget {
|
||||
final List<DashboardShortcut>? customShortcuts;
|
||||
final int maxShortcuts;
|
||||
|
||||
const DashboardShortcutsWidget({
|
||||
super.key,
|
||||
this.customShortcuts,
|
||||
this.maxShortcuts = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shortcuts = customShortcuts ?? _getDefaultShortcuts();
|
||||
final displayShortcuts = shortcuts.take(maxShortcuts).toList();
|
||||
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
_buildShortcutsGrid(displayShortcuts),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.tealBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.flash_on,
|
||||
color: DashboardTheme.tealBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Actions Rapides',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Personnaliser les raccourcis
|
||||
},
|
||||
child: Text(
|
||||
'Personnaliser',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.tealBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShortcutsGrid(List<DashboardShortcut> shortcuts) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
itemCount: shortcuts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildShortcutItem(shortcuts[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShortcutItem(DashboardShortcut shortcut) {
|
||||
return GestureDetector(
|
||||
onTap: shortcut.onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: shortcut.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: shortcut.color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: shortcut.color.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
),
|
||||
child: Icon(
|
||||
shortcut.icon,
|
||||
color: shortcut.color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
shortcut.title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: shortcut.color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (shortcut.badge != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing6,
|
||||
vertical: DashboardTheme.spacing2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: shortcut.badgeColor ?? DashboardTheme.error,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
shortcut.badge!,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DashboardShortcut> _getDefaultShortcuts() {
|
||||
return [
|
||||
DashboardShortcut(
|
||||
title: 'Nouveau\nMembre',
|
||||
icon: Icons.person_add,
|
||||
color: DashboardTheme.success,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers ajout membre
|
||||
},
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Créer\nÉvénement',
|
||||
icon: Icons.event_available,
|
||||
color: DashboardTheme.royalBlue,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers création événement
|
||||
},
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Ajouter\nContribution',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.tealBlue,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers ajout contribution
|
||||
},
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Envoyer\nMessage',
|
||||
icon: Icons.message,
|
||||
color: DashboardTheme.warning,
|
||||
badge: '3',
|
||||
badgeColor: DashboardTheme.error,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers messagerie
|
||||
},
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Générer\nRapport',
|
||||
icon: Icons.assessment,
|
||||
color: DashboardTheme.info,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers génération rapport
|
||||
},
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Paramètres',
|
||||
icon: Icons.settings,
|
||||
color: DashboardTheme.grey600,
|
||||
onTap: () {
|
||||
// TODO: Naviguer vers paramètres
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardShortcut {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
final String? badge;
|
||||
final Color? badgeColor;
|
||||
|
||||
const DashboardShortcut({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
this.badge,
|
||||
this.badgeColor,
|
||||
});
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
/// Test rapide pour vérifier les boutons rectangulaires compacts
|
||||
/// Démontre les nouvelles dimensions et le format rectangulaire
|
||||
library test_rectangular_buttons;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/design_system/tokens/typography_tokens.dart';
|
||||
import 'dashboard_quick_action_button.dart';
|
||||
import 'dashboard_quick_actions_grid.dart';
|
||||
|
||||
/// Page de test pour les boutons rectangulaires
|
||||
class TestRectangularButtonsPage extends StatelessWidget {
|
||||
const TestRectangularButtonsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Boutons Rectangulaires - Test'),
|
||||
backgroundColor: ColorTokens.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('🔲 Boutons Rectangulaires Compacts'),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildIndividualButtons(),
|
||||
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
_buildSectionTitle('📊 Grilles avec Format Rectangulaire'),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildGridLayouts(),
|
||||
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
_buildSectionTitle('📏 Comparaison des Dimensions'),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
_buildDimensionComparison(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un titre de section
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: TypographyTokens.headlineMedium.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorTokens.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Test des boutons individuels
|
||||
Widget _buildIndividualButtons() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Boutons Individuels - Largeur Réduite de Moitié',
|
||||
style: TypographyTokens.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Ligne de boutons rectangulaires
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100, // Largeur réduite
|
||||
height: 70, // Hauteur rectangulaire
|
||||
child: DashboardQuickActionButton(
|
||||
action: DashboardQuickAction.primary(
|
||||
icon: Icons.add,
|
||||
title: 'Ajouter',
|
||||
subtitle: 'Nouveau',
|
||||
onTap: () => _showMessage('Bouton Ajouter'),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 70,
|
||||
child: DashboardQuickActionButton(
|
||||
action: DashboardQuickAction.success(
|
||||
icon: Icons.check,
|
||||
title: 'Valider',
|
||||
subtitle: 'OK',
|
||||
onTap: () => _showMessage('Bouton Valider'),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 70,
|
||||
child: DashboardQuickActionButton(
|
||||
action: DashboardQuickAction.warning(
|
||||
icon: Icons.warning,
|
||||
title: 'Alerte',
|
||||
subtitle: 'Urgent',
|
||||
onTap: () => _showMessage('Bouton Alerte'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Test des grilles avec différents layouts
|
||||
Widget _buildGridLayouts() {
|
||||
return const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Grille compacte 2x2
|
||||
DashboardQuickActionsGrid.compact(
|
||||
title: 'Grille Compacte 2x2 - Format Rectangulaire',
|
||||
),
|
||||
|
||||
SizedBox(height: SpacingTokens.xl),
|
||||
|
||||
// Grille étendue 3x2
|
||||
DashboardQuickActionsGrid.expanded(
|
||||
title: 'Grille Étendue 3x2 - Boutons Plus Petits',
|
||||
subtitle: 'Ratio d\'aspect 1.5 au lieu de 2.0',
|
||||
),
|
||||
|
||||
SizedBox(height: SpacingTokens.xl),
|
||||
|
||||
// Carrousel horizontal
|
||||
DashboardQuickActionsGrid.carousel(
|
||||
title: 'Carrousel - Hauteur Réduite (90px)',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Comparaison visuelle des dimensions
|
||||
Widget _buildDimensionComparison() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Comparaison Avant/Après',
|
||||
style: TypographyTokens.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Simulation ancien format (plus large)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorTokens.error.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'❌ AVANT - Trop Large (140x100)',
|
||||
style: TypographyTokens.labelMedium.copyWith(
|
||||
color: ColorTokens.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Container(
|
||||
width: 140,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: ColorTokens.primary.withOpacity(0.3)),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('Ancien Format\n140x100'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Nouveau format (rectangulaire compact)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.success.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorTokens.success.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'✅ APRÈS - Rectangulaire Compact (100x70)',
|
||||
style: TypographyTokens.labelMedium.copyWith(
|
||||
color: ColorTokens.success,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 70,
|
||||
child: DashboardQuickActionButton(
|
||||
action: DashboardQuickAction.success(
|
||||
icon: Icons.thumb_up,
|
||||
title: 'Nouveau',
|
||||
subtitle: '100x70',
|
||||
onTap: () => _showMessage('Nouveau Format!'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Résumé des améliorations
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'📊 Améliorations Apportées',
|
||||
style: TypographyTokens.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorTokens.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
const Text('• Largeur réduite de 50% (140px → 100px)'),
|
||||
const Text('• Hauteur optimisée (100px → 70px)'),
|
||||
const Text('• Format rectangulaire plus compact'),
|
||||
const Text('• Bordures moins arrondies (12px → 6px)'),
|
||||
const Text('• Espacement réduit entre éléments'),
|
||||
const Text('• Ratio d\'aspect optimisé (2.2 → 1.6)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche un message de test
|
||||
void _showMessage(String message) {
|
||||
// Note: Cette méthode nécessiterait un BuildContext pour afficher un SnackBar
|
||||
// Dans un vrai contexte, on utiliserait ScaffoldMessenger
|
||||
debugPrint('Test: $message');
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Section des événements à venir du dashboard
|
||||
///
|
||||
/// Widget réutilisable pour afficher les prochains événements,
|
||||
/// réunions, échéances ou tâches selon le contexte.
|
||||
class UpcomingEventsSection extends StatelessWidget {
|
||||
/// Titre de la section
|
||||
final String title;
|
||||
|
||||
/// Sous-titre optionnel
|
||||
final String? subtitle;
|
||||
|
||||
/// Liste des événements à afficher
|
||||
final List<UpcomingEvent> events;
|
||||
|
||||
/// Nombre maximum d'événements à afficher
|
||||
final int maxItems;
|
||||
|
||||
/// Callback lors du tap sur un événement
|
||||
final Function(UpcomingEvent)? onEventTap;
|
||||
|
||||
/// Callback pour voir tous les événements
|
||||
final VoidCallback? onViewAll;
|
||||
|
||||
/// Afficher ou non l'en-tête de section
|
||||
final bool showHeader;
|
||||
|
||||
/// Afficher ou non le bouton "Voir tout"
|
||||
final bool showViewAll;
|
||||
|
||||
/// Message à afficher si aucun événement
|
||||
final String? emptyMessage;
|
||||
|
||||
/// Style de la section
|
||||
final EventsSectionStyle style;
|
||||
|
||||
const UpcomingEventsSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.events,
|
||||
this.maxItems = 3,
|
||||
this.onEventTap,
|
||||
this.onViewAll,
|
||||
this.showHeader = true,
|
||||
this.showViewAll = true,
|
||||
this.emptyMessage,
|
||||
this.style = EventsSectionStyle.card,
|
||||
});
|
||||
|
||||
/// Constructeur pour les événements d'organisation
|
||||
const UpcomingEventsSection.organization({
|
||||
super.key,
|
||||
this.onEventTap,
|
||||
this.onViewAll,
|
||||
}) : title = 'Événements à venir',
|
||||
subtitle = 'Prochaines échéances',
|
||||
events = const [
|
||||
UpcomingEvent(
|
||||
title: 'Réunion mensuelle',
|
||||
description: 'Point équipe et objectifs',
|
||||
date: '15 Jan 2025',
|
||||
time: '14:00',
|
||||
location: 'Salle de conférence',
|
||||
type: EventType.meeting,
|
||||
),
|
||||
UpcomingEvent(
|
||||
title: 'Formation sécurité',
|
||||
description: 'Session obligatoire',
|
||||
date: '18 Jan 2025',
|
||||
time: '09:00',
|
||||
location: 'En ligne',
|
||||
type: EventType.training,
|
||||
),
|
||||
UpcomingEvent(
|
||||
title: 'Assemblée générale',
|
||||
description: 'Vote budget 2025',
|
||||
date: '25 Jan 2025',
|
||||
time: '10:00',
|
||||
location: 'Auditorium',
|
||||
type: EventType.assembly,
|
||||
),
|
||||
],
|
||||
maxItems = 3,
|
||||
showHeader = true,
|
||||
showViewAll = true,
|
||||
emptyMessage = null,
|
||||
style = EventsSectionStyle.card;
|
||||
|
||||
/// Constructeur pour les tâches système
|
||||
const UpcomingEventsSection.systemTasks({
|
||||
super.key,
|
||||
this.onEventTap,
|
||||
this.onViewAll,
|
||||
}) : title = 'Tâches Programmées',
|
||||
subtitle = 'Maintenance et sauvegardes',
|
||||
events = const [
|
||||
UpcomingEvent(
|
||||
title: 'Sauvegarde hebdomadaire',
|
||||
description: 'Sauvegarde complète BDD',
|
||||
date: 'Aujourd\'hui',
|
||||
time: '02:00',
|
||||
location: 'Automatique',
|
||||
type: EventType.maintenance,
|
||||
),
|
||||
UpcomingEvent(
|
||||
title: 'Mise à jour sécurité',
|
||||
description: 'Patches système',
|
||||
date: 'Demain',
|
||||
time: '01:00',
|
||||
location: 'Serveurs',
|
||||
type: EventType.maintenance,
|
||||
),
|
||||
UpcomingEvent(
|
||||
title: 'Nettoyage logs',
|
||||
description: 'Archivage automatique',
|
||||
date: '20 Jan 2025',
|
||||
time: '03:00',
|
||||
location: 'Système',
|
||||
type: EventType.maintenance,
|
||||
),
|
||||
],
|
||||
maxItems = 3,
|
||||
showHeader = true,
|
||||
showViewAll = true,
|
||||
emptyMessage = null,
|
||||
style = EventsSectionStyle.minimal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (style) {
|
||||
case EventsSectionStyle.card:
|
||||
return _buildCardStyle();
|
||||
case EventsSectionStyle.minimal:
|
||||
return _buildMinimalStyle();
|
||||
case EventsSectionStyle.timeline:
|
||||
return _buildTimelineStyle();
|
||||
}
|
||||
}
|
||||
|
||||
/// Style carte avec fond
|
||||
Widget _buildCardStyle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showHeader) _buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildEventsList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Style minimal sans fond
|
||||
Widget _buildMinimalStyle() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showHeader) _buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildEventsList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Style timeline avec ligne temporelle
|
||||
Widget _buildTimelineStyle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showHeader) _buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildTimelineList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// En-tête de la section
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showViewAll && onViewAll != null)
|
||||
TextButton(
|
||||
onPressed: onViewAll,
|
||||
child: const Text(
|
||||
'Voir tout',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6C5CE7),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des événements
|
||||
Widget _buildEventsList() {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayedEvents = events.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayedEvents.map((event) => _buildEventItem(event)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste timeline
|
||||
Widget _buildTimelineList() {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayedEvents = events.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayedEvents.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final event = entry.value;
|
||||
final isLast = index == displayedEvents.length - 1;
|
||||
|
||||
return _buildTimelineItem(event, isLast);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Élément d'événement
|
||||
Widget _buildEventItem(UpcomingEvent event) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: event.type.color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: event.type.color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onEventTap != null ? () => onEventTap!(event) : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: event.type.color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
event.type.icon,
|
||||
color: event.type.color,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
if (event.description != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.access_time, size: 12, color: Colors.grey[500]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${event.date} à ${event.time}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (event.location != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.location_on, size: 12, color: Colors.grey[500]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
event.location!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Élément timeline
|
||||
Widget _buildTimelineItem(UpcomingEvent event, bool isLast) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: event.type.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 40,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: isLast ? 0 : 16),
|
||||
child: _buildEventItem(event),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// État vide
|
||||
Widget _buildEmptyState() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event_available,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
emptyMessage ?? 'Aucun événement à venir',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour un événement à venir
|
||||
class UpcomingEvent {
|
||||
final String title;
|
||||
final String? description;
|
||||
final String date;
|
||||
final String time;
|
||||
final String? location;
|
||||
final EventType type;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const UpcomingEvent({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.date,
|
||||
required this.time,
|
||||
this.location,
|
||||
required this.type,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/// Types d'événement
|
||||
enum EventType {
|
||||
meeting(Icons.meeting_room, Color(0xFF6C5CE7)),
|
||||
training(Icons.school, Color(0xFF00B894)),
|
||||
assembly(Icons.groups, Color(0xFF0984E3)),
|
||||
maintenance(Icons.build, Color(0xFFE17055)),
|
||||
deadline(Icons.schedule, Colors.orange),
|
||||
celebration(Icons.celebration, Color(0xFFE84393));
|
||||
|
||||
const EventType(this.icon, this.color);
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
/// Styles de section d'événements
|
||||
enum EventsSectionStyle {
|
||||
card,
|
||||
minimal,
|
||||
timeline,
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
/// Fichier d'index pour tous les widgets du dashboard
|
||||
/// Facilite les imports et maintient une API propre
|
||||
library dashboard_widgets;
|
||||
// Export des widgets dashboard connectés
|
||||
export 'connected/connected_stats_card.dart';
|
||||
export 'connected/connected_recent_activities.dart';
|
||||
export 'connected/connected_upcoming_events.dart';
|
||||
|
||||
// === WIDGETS DE SECTION ===
|
||||
export 'dashboard_welcome_section.dart';
|
||||
export 'dashboard_stats_grid.dart';
|
||||
export 'dashboard_quick_actions_grid.dart';
|
||||
export 'dashboard_recent_activity_section.dart';
|
||||
export 'dashboard_insights_section.dart';
|
||||
export 'dashboard_drawer.dart';
|
||||
// Export des widgets charts
|
||||
export 'charts/dashboard_chart_widget.dart';
|
||||
|
||||
// === WIDGETS ATOMIQUES ===
|
||||
export 'dashboard_stats_card.dart';
|
||||
export 'dashboard_quick_action_button.dart';
|
||||
export 'dashboard_activity_tile.dart';
|
||||
export 'dashboard_metric_row.dart';
|
||||
// Export des widgets metrics
|
||||
export 'metrics/real_time_metrics_widget.dart';
|
||||
|
||||
// Export des widgets monitoring
|
||||
export 'monitoring/performance_monitor_widget.dart';
|
||||
|
||||
// Export des widgets navigation
|
||||
export 'navigation/dashboard_navigation.dart';
|
||||
|
||||
// Export des widgets notifications
|
||||
export 'notifications/dashboard_notifications_widget.dart';
|
||||
|
||||
// Export des widgets search
|
||||
export 'search/dashboard_search_widget.dart';
|
||||
|
||||
// Export des widgets settings
|
||||
export 'settings/theme_selector_widget.dart';
|
||||
|
||||
// Export des widgets shortcuts
|
||||
export 'shortcuts/dashboard_shortcuts_widget.dart';
|
||||
|
||||
Reference in New Issue
Block a user