Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -0,0 +1,307 @@
/// Configuration globale du Dashboard UnionFlow
class DashboardConfig {
// Version du dashboard
static const String version = '1.0.0';
static const String buildNumber = '2024.10.06.001';
// Configuration des couleurs
static const bool useCustomTheme = true;
static const String primaryColorHex = '#4169E1'; // Bleu Roi
static const String secondaryColorHex = '#008B8B'; // Bleu Pétrole
// Configuration des données
static const bool useMockData = false;
static const String apiBaseUrl = 'http://localhost:8080';
static const Duration networkTimeout = Duration(seconds: 30);
// Configuration du rafraîchissement
static const Duration autoRefreshInterval = Duration(minutes: 5);
static const Duration cacheExpiration = Duration(minutes: 10);
static const bool enableAutoRefresh = true;
static const bool enablePullToRefresh = true;
// Configuration des animations
static const bool enableAnimations = true;
static const Duration animationDuration = Duration(milliseconds: 300);
static const Duration chartAnimationDuration = Duration(milliseconds: 1500);
static const Duration counterAnimationDuration = Duration(milliseconds: 2000);
// Configuration des widgets
static const int maxRecentActivities = 10;
static const int maxUpcomingEvents = 5;
static const int maxNotifications = 5;
static const int maxShortcuts = 6;
// Configuration des graphiques
static const bool enableCharts = true;
static const bool enableInteractiveCharts = true;
static const double chartHeight = 200.0;
static const double largeChartHeight = 300.0;
// Configuration des métriques temps réel
static const bool enableRealTimeMetrics = true;
static const Duration metricsUpdateInterval = Duration(seconds: 30);
static const bool enableMetricsAnimations = true;
// Configuration des notifications
static const bool enableNotifications = true;
static const bool enableUrgentNotifications = true;
static const int maxUrgentNotifications = 3;
// Configuration de la recherche
static const bool enableSearch = true;
static const int maxSearchSuggestions = 5;
static const Duration searchDebounceDelay = Duration(milliseconds: 300);
// Configuration des raccourcis
static const bool enableShortcuts = true;
static const bool enableShortcutBadges = true;
static const bool enableShortcutCustomization = true;
// Configuration du logging
static const bool enableLogging = true;
static const bool enableVerboseLogging = false;
static const bool enableErrorReporting = true;
// Configuration de la performance
static const bool enablePerformanceMonitoring = true;
static const Duration performanceCheckInterval = Duration(minutes: 1);
static const double memoryWarningThreshold = 500.0; // MB
static const double cpuWarningThreshold = 80.0; // %
// Configuration de l'accessibilité
static const bool enableAccessibility = true;
static const bool enableHighContrast = false;
static const bool enableLargeText = false;
// Configuration des fonctionnalités expérimentales
static const bool enableExperimentalFeatures = false;
static const bool enableBetaWidgets = false;
static const bool enableAdvancedAnalytics = false;
// Seuils d'alerte
static const Map<String, dynamic> alertThresholds = {
'memoryUsage': 400.0, // MB
'cpuUsage': 70.0, // %
'networkLatency': 1000, // ms
'frameRate': 30.0, // fps
'batteryLevel': 20.0, // %
'errorRate': 5.0, // %
'crashRate': 1.0, // %
};
// Configuration des endpoints API
static const Map<String, String> apiEndpoints = {
'dashboard': '/api/v1/dashboard/data',
'stats': '/api/v1/dashboard/stats',
'activities': '/api/v1/dashboard/activities',
'events': '/api/v1/dashboard/events/upcoming',
'refresh': '/api/v1/dashboard/refresh',
'health': '/api/v1/dashboard/health',
};
// Configuration des préférences utilisateur par défaut
static const Map<String, dynamic> defaultUserPreferences = {
'theme': 'royal_teal',
'language': 'fr',
'notifications': true,
'autoRefresh': true,
'refreshInterval': 300, // 5 minutes
'enableAnimations': true,
'enableCharts': true,
'enableRealTimeMetrics': true,
'maxRecentActivities': 10,
'maxUpcomingEvents': 5,
'enableShortcuts': true,
'shortcuts': [
'new_member',
'create_event',
'add_contribution',
'send_message',
'generate_report',
'settings',
],
};
// Configuration des widgets par défaut
static const Map<String, dynamic> defaultWidgetConfig = {
'statsCards': {
'enabled': true,
'columns': 2,
'aspectRatio': 1.2,
'showSubtitle': true,
'showIcon': true,
},
'charts': {
'enabled': true,
'showLegend': true,
'showGrid': true,
'enableInteraction': true,
'animationDuration': 1500,
},
'activities': {
'enabled': true,
'showAvatar': true,
'showTimeAgo': true,
'maxItems': 10,
'enableActions': true,
},
'events': {
'enabled': true,
'showProgress': true,
'showTags': true,
'maxItems': 5,
'enableNavigation': true,
},
'notifications': {
'enabled': true,
'showBadges': true,
'enableActions': true,
'maxItems': 5,
'autoHide': false,
},
'search': {
'enabled': true,
'showSuggestions': true,
'enableHistory': true,
'maxSuggestions': 5,
'debounceDelay': 300,
},
'shortcuts': {
'enabled': true,
'columns': 3,
'showBadges': true,
'enableCustomization': true,
'maxItems': 6,
},
'metrics': {
'enabled': true,
'enableAnimations': true,
'updateInterval': 30,
'showProgress': true,
'enableAlerts': true,
},
};
// Configuration des couleurs du thème
static const Map<String, String> themeColors = {
'royalBlue': '#4169E1',
'royalBlueLight': '#6A8EF7',
'royalBlueDark': '#2E4BC6',
'tealBlue': '#008B8B',
'tealBlueLight': '#20B2AA',
'tealBlueDark': '#006666',
'success': '#10B981',
'warning': '#F59E0B',
'error': '#EF4444',
'info': '#3B82F6',
'grey50': '#F9FAFB',
'grey100': '#F3F4F6',
'grey200': '#E5E7EB',
'grey300': '#D1D5DB',
'grey400': '#9CA3AF',
'grey500': '#6B7280',
'grey600': '#4B5563',
'grey700': '#374151',
'grey800': '#1F2937',
'grey900': '#111827',
'white': '#FFFFFF',
'black': '#000000',
};
// Configuration des espacements
static const Map<String, double> spacing = {
'spacing2': 2.0,
'spacing4': 4.0,
'spacing6': 6.0,
'spacing8': 8.0,
'spacing12': 12.0,
'spacing16': 16.0,
'spacing20': 20.0,
'spacing24': 24.0,
'spacing32': 32.0,
'spacing40': 40.0,
};
// Configuration des bordures
static const Map<String, double> borderRadius = {
'borderRadiusSmall': 4.0,
'borderRadius': 8.0,
'borderRadiusLarge': 16.0,
'borderRadiusXLarge': 24.0,
};
// Configuration des ombres
static const Map<String, Map<String, dynamic>> shadows = {
'subtleShadow': {
'color': '#00000010',
'blurRadius': 4.0,
'offset': {'dx': 0.0, 'dy': 2.0},
},
'elevatedShadow': {
'color': '#00000020',
'blurRadius': 8.0,
'offset': {'dx': 0.0, 'dy': 4.0},
},
};
// Configuration des polices
static const Map<String, Map<String, dynamic>> typography = {
'titleLarge': {
'fontSize': 24.0,
'fontWeight': 'bold',
'letterSpacing': 0.0,
},
'titleMedium': {
'fontSize': 20.0,
'fontWeight': 'w600',
'letterSpacing': 0.0,
},
'titleSmall': {
'fontSize': 16.0,
'fontWeight': 'w600',
'letterSpacing': 0.0,
},
'bodyLarge': {
'fontSize': 16.0,
'fontWeight': 'normal',
'letterSpacing': 0.0,
},
'bodyMedium': {
'fontSize': 14.0,
'fontWeight': 'normal',
'letterSpacing': 0.0,
},
'bodySmall': {
'fontSize': 12.0,
'fontWeight': 'normal',
'letterSpacing': 0.0,
},
};
// Méthodes utilitaires
static bool get isDevelopment => useMockData;
static bool get isProduction => !useMockData;
static String get fullVersion => '$version+$buildNumber';
static Duration get effectiveRefreshInterval =>
enableAutoRefresh ? autoRefreshInterval : Duration.zero;
static Map<String, dynamic> getUserPreference(String key) {
return defaultUserPreferences[key] ?? {};
}
static Map<String, dynamic> getWidgetConfig(String widget) {
return defaultWidgetConfig[widget] ?? {};
}
static String getApiEndpoint(String endpoint) {
final path = apiEndpoints[endpoint] ?? '';
return '$apiBaseUrl$path';
}
static double getAlertThreshold(String metric) {
return alertThresholds[metric]?.toDouble() ?? 0.0;
}
}

View File

@@ -0,0 +1,400 @@
import 'dart:convert';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/dashboard_stats_model.dart';
import '../../config/dashboard_config.dart';
/// Gestionnaire de cache avancé pour le Dashboard
class DashboardCacheManager {
static const String _keyPrefix = 'dashboard_cache_';
static const String _keyDashboardData = '${_keyPrefix}data';
static const String _keyDashboardStats = '${_keyPrefix}stats';
static const String _keyRecentActivities = '${_keyPrefix}activities';
static const String _keyUpcomingEvents = '${_keyPrefix}events';
static const String _keyLastUpdate = '${_keyPrefix}last_update';
static const String _keyUserPreferences = '${_keyPrefix}user_prefs';
SharedPreferences? _prefs;
final Map<String, dynamic> _memoryCache = {};
final Map<String, DateTime> _cacheTimestamps = {};
Timer? _cleanupTimer;
/// Initialise le gestionnaire de cache
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
_startCleanupTimer();
await _loadMemoryCache();
}
/// Démarre le timer de nettoyage automatique
void _startCleanupTimer() {
_cleanupTimer = Timer.periodic(
const Duration(minutes: 30),
(_) => _cleanupExpiredCache(),
);
}
/// Charge le cache en mémoire au démarrage
Future<void> _loadMemoryCache() async {
if (_prefs == null) return;
final keys = _prefs!.getKeys().where((key) => key.startsWith(_keyPrefix));
for (final key in keys) {
final value = _prefs!.getString(key);
if (value != null) {
try {
final data = jsonDecode(value);
_memoryCache[key] = data;
// Charger le timestamp si disponible
final timestampKey = '${key}_timestamp';
final timestamp = _prefs!.getInt(timestampKey);
if (timestamp != null) {
_cacheTimestamps[key] = DateTime.fromMillisecondsSinceEpoch(timestamp);
}
} catch (e) {
// Supprimer les données corrompues
await _prefs!.remove(key);
}
}
}
}
/// Sauvegarde les données complètes du dashboard
Future<void> cacheDashboardData(
DashboardDataModel data,
String organizationId,
String userId,
) async {
final key = '${_keyDashboardData}_${organizationId}_$userId';
await _cacheData(key, data.toJson());
}
/// Récupère les données complètes du dashboard
Future<DashboardDataModel?> getCachedDashboardData(
String organizationId,
String userId,
) async {
final key = '${_keyDashboardData}_${organizationId}_$userId';
final data = await _getCachedData(key);
if (data != null) {
try {
return DashboardDataModel.fromJson(data);
} catch (e) {
// Supprimer les données corrompues
await _removeCachedData(key);
return null;
}
}
return null;
}
/// Sauvegarde les statistiques du dashboard
Future<void> cacheDashboardStats(
DashboardStatsModel stats,
String organizationId,
String userId,
) async {
final key = '${_keyDashboardStats}_${organizationId}_$userId';
await _cacheData(key, stats.toJson());
}
/// Récupère les statistiques du dashboard
Future<DashboardStatsModel?> getCachedDashboardStats(
String organizationId,
String userId,
) async {
final key = '${_keyDashboardStats}_${organizationId}_$userId';
final data = await _getCachedData(key);
if (data != null) {
try {
return DashboardStatsModel.fromJson(data);
} catch (e) {
await _removeCachedData(key);
return null;
}
}
return null;
}
/// Sauvegarde les activités récentes
Future<void> cacheRecentActivities(
List<RecentActivityModel> activities,
String organizationId,
String userId,
) async {
final key = '${_keyRecentActivities}_${organizationId}_$userId';
final data = activities.map((activity) => activity.toJson()).toList();
await _cacheData(key, data);
}
/// Récupère les activités récentes
Future<List<RecentActivityModel>?> getCachedRecentActivities(
String organizationId,
String userId,
) async {
final key = '${_keyRecentActivities}_${organizationId}_$userId';
final data = await _getCachedData(key);
if (data != null && data is List) {
try {
return data
.map((item) => RecentActivityModel.fromJson(item))
.toList();
} catch (e) {
await _removeCachedData(key);
return null;
}
}
return null;
}
/// Sauvegarde les événements à venir
Future<void> cacheUpcomingEvents(
List<UpcomingEventModel> events,
String organizationId,
String userId,
) async {
final key = '${_keyUpcomingEvents}_${organizationId}_$userId';
final data = events.map((event) => event.toJson()).toList();
await _cacheData(key, data);
}
/// Récupère les événements à venir
Future<List<UpcomingEventModel>?> getCachedUpcomingEvents(
String organizationId,
String userId,
) async {
final key = '${_keyUpcomingEvents}_${organizationId}_$userId';
final data = await _getCachedData(key);
if (data != null && data is List) {
try {
return data
.map((item) => UpcomingEventModel.fromJson(item))
.toList();
} catch (e) {
await _removeCachedData(key);
return null;
}
}
return null;
}
/// Sauvegarde les préférences utilisateur
Future<void> cacheUserPreferences(
Map<String, dynamic> preferences,
String userId,
) async {
final key = '${_keyUserPreferences}_$userId';
await _cacheData(key, preferences);
}
/// Récupère les préférences utilisateur
Future<Map<String, dynamic>?> getCachedUserPreferences(String userId) async {
final key = '${_keyUserPreferences}_$userId';
final data = await _getCachedData(key);
if (data != null && data is Map<String, dynamic>) {
return data;
}
return null;
}
/// Méthode générique pour sauvegarder des données
Future<void> _cacheData(String key, dynamic data) async {
if (_prefs == null) return;
try {
final jsonString = jsonEncode(data);
await _prefs!.setString(key, jsonString);
// Sauvegarder le timestamp
final timestamp = DateTime.now().millisecondsSinceEpoch;
await _prefs!.setInt('${key}_timestamp', timestamp);
// Mettre à jour le cache mémoire
_memoryCache[key] = data;
_cacheTimestamps[key] = DateTime.now();
} catch (e) {
// Erreur de sérialisation, ignorer
}
}
/// Méthode générique pour récupérer des données
Future<dynamic> _getCachedData(String key) async {
// Vérifier d'abord le cache mémoire
if (_memoryCache.containsKey(key)) {
if (_isCacheValid(key)) {
return _memoryCache[key];
} else {
// Cache expiré, le supprimer
await _removeCachedData(key);
return null;
}
}
// Vérifier le cache persistant
if (_prefs == null) return null;
final jsonString = _prefs!.getString(key);
if (jsonString != null) {
try {
final data = jsonDecode(jsonString);
// Vérifier la validité du cache
if (_isCacheValid(key)) {
// Charger en mémoire pour les prochains accès
_memoryCache[key] = data;
return data;
} else {
// Cache expiré, le supprimer
await _removeCachedData(key);
return null;
}
} catch (e) {
// Données corrompues, les supprimer
await _removeCachedData(key);
return null;
}
}
return null;
}
/// Vérifie si le cache est encore valide
bool _isCacheValid(String key) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) {
// Essayer de récupérer le timestamp depuis SharedPreferences
final timestampMs = _prefs?.getInt('${key}_timestamp');
if (timestampMs != null) {
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
_cacheTimestamps[key] = cacheTime;
return DateTime.now().difference(cacheTime) < DashboardConfig.cacheExpiration;
}
return false;
}
return DateTime.now().difference(timestamp) < DashboardConfig.cacheExpiration;
}
/// Supprime des données du cache
Future<void> _removeCachedData(String key) async {
_memoryCache.remove(key);
_cacheTimestamps.remove(key);
if (_prefs != null) {
await _prefs!.remove(key);
await _prefs!.remove('${key}_timestamp');
}
}
/// Nettoie le cache expiré
Future<void> _cleanupExpiredCache() async {
final keysToRemove = <String>[];
for (final key in _cacheTimestamps.keys) {
if (!_isCacheValid(key)) {
keysToRemove.add(key);
}
}
for (final key in keysToRemove) {
await _removeCachedData(key);
}
}
/// Vide tout le cache
Future<void> clearCache() async {
_memoryCache.clear();
_cacheTimestamps.clear();
if (_prefs != null) {
final keys = _prefs!.getKeys().where((key) => key.startsWith(_keyPrefix));
for (final key in keys) {
await _prefs!.remove(key);
}
}
}
/// Vide le cache pour un utilisateur spécifique
Future<void> clearUserCache(String organizationId, String userId) async {
final userKeys = [
'${_keyDashboardData}_${organizationId}_$userId',
'${_keyDashboardStats}_${organizationId}_$userId',
'${_keyRecentActivities}_${organizationId}_$userId',
'${_keyUpcomingEvents}_${organizationId}_$userId',
'${_keyUserPreferences}_$userId',
];
for (final key in userKeys) {
await _removeCachedData(key);
}
}
/// Obtient les statistiques du cache
Map<String, dynamic> getCacheStats() {
final totalKeys = _memoryCache.length;
final validKeys = _cacheTimestamps.keys.where(_isCacheValid).length;
final expiredKeys = totalKeys - validKeys;
return {
'totalKeys': totalKeys,
'validKeys': validKeys,
'expiredKeys': expiredKeys,
'memoryUsage': _calculateMemoryUsage(),
'oldestEntry': _getOldestEntryAge(),
'newestEntry': _getNewestEntryAge(),
};
}
/// Calcule l'utilisation mémoire approximative
int _calculateMemoryUsage() {
int totalSize = 0;
for (final data in _memoryCache.values) {
try {
totalSize += jsonEncode(data).length;
} catch (e) {
// Ignorer les erreurs de sérialisation
}
}
return totalSize;
}
/// Obtient l'âge de l'entrée la plus ancienne
Duration? _getOldestEntryAge() {
if (_cacheTimestamps.isEmpty) return null;
final oldestTimestamp = _cacheTimestamps.values
.reduce((a, b) => a.isBefore(b) ? a : b);
return DateTime.now().difference(oldestTimestamp);
}
/// Obtient l'âge de l'entrée la plus récente
Duration? _getNewestEntryAge() {
if (_cacheTimestamps.isEmpty) return null;
final newestTimestamp = _cacheTimestamps.values
.reduce((a, b) => a.isAfter(b) ? a : b);
return DateTime.now().difference(newestTimestamp);
}
/// Libère les ressources
void dispose() {
_cleanupTimer?.cancel();
_memoryCache.clear();
_cacheTimestamps.clear();
}
}

View File

@@ -0,0 +1,121 @@
import 'package:dio/dio.dart';
import '../models/dashboard_stats_model.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/error/exceptions.dart';
abstract class DashboardRemoteDataSource {
Future<DashboardDataModel> getDashboardData(String organizationId, String userId);
Future<DashboardStatsModel> getDashboardStats(String organizationId, String userId);
Future<List<RecentActivityModel>> getRecentActivities(String organizationId, String userId, {int limit = 10});
Future<List<UpcomingEventModel>> getUpcomingEvents(String organizationId, String userId, {int limit = 5});
}
class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
final DioClient dioClient;
DashboardRemoteDataSourceImpl({required this.dioClient});
@override
Future<DashboardDataModel> getDashboardData(String organizationId, String userId) async {
try {
final response = await dioClient.get(
'/api/v1/dashboard/data',
queryParameters: {
'organizationId': organizationId,
'userId': userId,
},
);
if (response.statusCode == 200) {
return DashboardDataModel.fromJson(response.data);
} else {
throw ServerException('Failed to load dashboard data: ${response.statusCode}');
}
} on DioException catch (e) {
throw ServerException('Network error: ${e.message}');
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
@override
Future<DashboardStatsModel> getDashboardStats(String organizationId, String userId) async {
try {
final response = await dioClient.get(
'/api/v1/dashboard/stats',
queryParameters: {
'organizationId': organizationId,
'userId': userId,
},
);
if (response.statusCode == 200) {
return DashboardStatsModel.fromJson(response.data);
} else {
throw ServerException('Failed to load dashboard stats: ${response.statusCode}');
}
} on DioException catch (e) {
throw ServerException('Network error: ${e.message}');
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
@override
Future<List<RecentActivityModel>> getRecentActivities(
String organizationId,
String userId, {
int limit = 10,
}) async {
try {
final response = await dioClient.get(
'/api/v1/dashboard/activities',
queryParameters: {
'organizationId': organizationId,
'userId': userId,
'limit': limit,
},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data['activities'] ?? [];
return data.map((json) => RecentActivityModel.fromJson(json)).toList();
} else {
throw ServerException('Failed to load recent activities: ${response.statusCode}');
}
} on DioException catch (e) {
throw ServerException('Network error: ${e.message}');
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
@override
Future<List<UpcomingEventModel>> getUpcomingEvents(
String organizationId,
String userId, {
int limit = 5,
}) async {
try {
final response = await dioClient.get(
'/api/v1/dashboard/events/upcoming',
queryParameters: {
'organizationId': organizationId,
'userId': userId,
'limit': limit,
},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data['events'] ?? [];
return data.map((json) => UpcomingEventModel.fromJson(json)).toList();
} else {
throw ServerException('Failed to load upcoming events: ${response.statusCode}');
}
} on DioException catch (e) {
throw ServerException('Network error: ${e.message}');
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
}

View File

@@ -0,0 +1,216 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'dashboard_stats_model.g.dart';
/// Modèle pour les statistiques du dashboard
@JsonSerializable()
class DashboardStatsModel extends Equatable {
final int totalMembers;
final int activeMembers;
final int totalEvents;
final int upcomingEvents;
final int totalContributions;
final double totalContributionAmount;
final int pendingRequests;
final int completedProjects;
final double monthlyGrowth;
final double engagementRate;
final DateTime lastUpdated;
const DashboardStatsModel({
required this.totalMembers,
required this.activeMembers,
required this.totalEvents,
required this.upcomingEvents,
required this.totalContributions,
required this.totalContributionAmount,
required this.pendingRequests,
required this.completedProjects,
required this.monthlyGrowth,
required this.engagementRate,
required this.lastUpdated,
});
factory DashboardStatsModel.fromJson(Map<String, dynamic> json) =>
_$DashboardStatsModelFromJson(json);
Map<String, dynamic> toJson() => _$DashboardStatsModelToJson(this);
// Getters calculés
String get formattedContributionAmount {
return '${totalContributionAmount.toStringAsFixed(2)}';
}
bool get hasGrowth => monthlyGrowth > 0;
bool get isHighEngagement => engagementRate > 0.7;
double get activeMemberPercentage {
return totalMembers > 0 ? (activeMembers / totalMembers) : 0.0;
}
@override
List<Object?> get props => [
totalMembers,
activeMembers,
totalEvents,
upcomingEvents,
totalContributions,
totalContributionAmount,
pendingRequests,
completedProjects,
monthlyGrowth,
engagementRate,
lastUpdated,
];
}
/// Modèle pour les activités récentes
@JsonSerializable()
class RecentActivityModel extends Equatable {
final String id;
final String type;
final String title;
final String description;
final String? userAvatar;
final String userName;
final DateTime timestamp;
final String? actionUrl;
final Map<String, dynamic>? metadata;
const RecentActivityModel({
required this.id,
required this.type,
required this.title,
required this.description,
this.userAvatar,
required this.userName,
required this.timestamp,
this.actionUrl,
this.metadata,
});
factory RecentActivityModel.fromJson(Map<String, dynamic> json) =>
_$RecentActivityModelFromJson(json);
Map<String, dynamic> toJson() => _$RecentActivityModelToJson(this);
// Getter calculé pour l'affichage du temps
String get timeAgo {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inDays > 0) {
return 'il y a ${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
} else if (difference.inHours > 0) {
return 'il y a ${difference.inHours} heure${difference.inHours > 1 ? 's' : ''}';
} else if (difference.inMinutes > 0) {
return 'il y a ${difference.inMinutes} minute${difference.inMinutes > 1 ? 's' : ''}';
} else {
return 'à l\'instant';
}
}
@override
List<Object?> get props => [
id,
type,
title,
description,
userAvatar,
userName,
timestamp,
actionUrl,
metadata,
];
}
/// Modèle pour les événements à venir
@JsonSerializable()
class UpcomingEventModel extends Equatable {
final String id;
final String title;
final String description;
final DateTime startDate;
final DateTime? endDate;
final String location;
final int maxParticipants;
final int currentParticipants;
final String status;
final String? imageUrl;
final List<String> tags;
const UpcomingEventModel({
required this.id,
required this.title,
required this.description,
required this.startDate,
this.endDate,
required this.location,
required this.maxParticipants,
required this.currentParticipants,
required this.status,
this.imageUrl,
required this.tags,
});
factory UpcomingEventModel.fromJson(Map<String, dynamic> json) =>
_$UpcomingEventModelFromJson(json);
Map<String, dynamic> toJson() => _$UpcomingEventModelToJson(this);
bool get isAlmostFull => currentParticipants >= (maxParticipants * 0.8);
bool get isFull => currentParticipants >= maxParticipants;
double get fillPercentage => maxParticipants > 0 ? currentParticipants / maxParticipants : 0.0;
@override
List<Object?> get props => [
id,
title,
description,
startDate,
endDate,
location,
maxParticipants,
currentParticipants,
status,
imageUrl,
tags,
];
}
/// Modèle pour les données du dashboard complet
@JsonSerializable()
class DashboardDataModel extends Equatable {
final DashboardStatsModel stats;
final List<RecentActivityModel> recentActivities;
final List<UpcomingEventModel> upcomingEvents;
final Map<String, dynamic> userPreferences;
final String organizationId;
final String userId;
const DashboardDataModel({
required this.stats,
required this.recentActivities,
required this.upcomingEvents,
required this.userPreferences,
required this.organizationId,
required this.userId,
});
factory DashboardDataModel.fromJson(Map<String, dynamic> json) =>
_$DashboardDataModelFromJson(json);
Map<String, dynamic> toJson() => _$DashboardDataModelToJson(this);
@override
List<Object?> get props => [
stats,
recentActivities,
upcomingEvents,
userPreferences,
organizationId,
userId,
];
}

View File

@@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dashboard_stats_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DashboardStatsModel _$DashboardStatsModelFromJson(Map<String, dynamic> json) =>
DashboardStatsModel(
totalMembers: (json['totalMembers'] as num).toInt(),
activeMembers: (json['activeMembers'] as num).toInt(),
totalEvents: (json['totalEvents'] as num).toInt(),
upcomingEvents: (json['upcomingEvents'] as num).toInt(),
totalContributions: (json['totalContributions'] as num).toInt(),
totalContributionAmount:
(json['totalContributionAmount'] as num).toDouble(),
pendingRequests: (json['pendingRequests'] as num).toInt(),
completedProjects: (json['completedProjects'] as num).toInt(),
monthlyGrowth: (json['monthlyGrowth'] as num).toDouble(),
engagementRate: (json['engagementRate'] as num).toDouble(),
lastUpdated: DateTime.parse(json['lastUpdated'] as String),
);
Map<String, dynamic> _$DashboardStatsModelToJson(
DashboardStatsModel instance) =>
<String, dynamic>{
'totalMembers': instance.totalMembers,
'activeMembers': instance.activeMembers,
'totalEvents': instance.totalEvents,
'upcomingEvents': instance.upcomingEvents,
'totalContributions': instance.totalContributions,
'totalContributionAmount': instance.totalContributionAmount,
'pendingRequests': instance.pendingRequests,
'completedProjects': instance.completedProjects,
'monthlyGrowth': instance.monthlyGrowth,
'engagementRate': instance.engagementRate,
'lastUpdated': instance.lastUpdated.toIso8601String(),
};
RecentActivityModel _$RecentActivityModelFromJson(Map<String, dynamic> json) =>
RecentActivityModel(
id: json['id'] as String,
type: json['type'] as String,
title: json['title'] as String,
description: json['description'] as String,
userAvatar: json['userAvatar'] as String?,
userName: json['userName'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
actionUrl: json['actionUrl'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
);
Map<String, dynamic> _$RecentActivityModelToJson(
RecentActivityModel instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'title': instance.title,
'description': instance.description,
'userAvatar': instance.userAvatar,
'userName': instance.userName,
'timestamp': instance.timestamp.toIso8601String(),
'actionUrl': instance.actionUrl,
'metadata': instance.metadata,
};
UpcomingEventModel _$UpcomingEventModelFromJson(Map<String, dynamic> json) =>
UpcomingEventModel(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
startDate: DateTime.parse(json['startDate'] as String),
endDate: json['endDate'] == null
? null
: DateTime.parse(json['endDate'] as String),
location: json['location'] as String,
maxParticipants: (json['maxParticipants'] as num).toInt(),
currentParticipants: (json['currentParticipants'] as num).toInt(),
status: json['status'] as String,
imageUrl: json['imageUrl'] as String?,
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> _$UpcomingEventModelToJson(UpcomingEventModel instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'description': instance.description,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate?.toIso8601String(),
'location': instance.location,
'maxParticipants': instance.maxParticipants,
'currentParticipants': instance.currentParticipants,
'status': instance.status,
'imageUrl': instance.imageUrl,
'tags': instance.tags,
};
DashboardDataModel _$DashboardDataModelFromJson(Map<String, dynamic> json) =>
DashboardDataModel(
stats:
DashboardStatsModel.fromJson(json['stats'] as Map<String, dynamic>),
recentActivities: (json['recentActivities'] as List<dynamic>)
.map((e) => RecentActivityModel.fromJson(e as Map<String, dynamic>))
.toList(),
upcomingEvents: (json['upcomingEvents'] as List<dynamic>)
.map((e) => UpcomingEventModel.fromJson(e as Map<String, dynamic>))
.toList(),
userPreferences: json['userPreferences'] as Map<String, dynamic>,
organizationId: json['organizationId'] as String,
userId: json['userId'] as String,
);
Map<String, dynamic> _$DashboardDataModelToJson(DashboardDataModel instance) =>
<String, dynamic>{
'stats': instance.stats,
'recentActivities': instance.recentActivities,
'upcomingEvents': instance.upcomingEvents,
'userPreferences': instance.userPreferences,
'organizationId': instance.organizationId,
'userId': instance.userId,
};

View File

@@ -0,0 +1,162 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/dashboard_entity.dart';
import '../../domain/repositories/dashboard_repository.dart';
import '../datasources/dashboard_remote_datasource.dart';
import '../models/dashboard_stats_model.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/network/network_info.dart';
class DashboardRepositoryImpl implements DashboardRepository {
final DashboardRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo;
DashboardRepositoryImpl({
required this.remoteDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, DashboardEntity>> getDashboardData(
String organizationId,
String userId,
) async {
if (await networkInfo.isConnected) {
try {
final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId);
return Right(_mapToEntity(dashboardData));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
} else {
return const Left(NetworkFailure('No internet connection'));
}
}
@override
Future<Either<Failure, DashboardStatsEntity>> getDashboardStats(
String organizationId,
String userId,
) async {
if (await networkInfo.isConnected) {
try {
final stats = await remoteDataSource.getDashboardStats(organizationId, userId);
return Right(_mapStatsToEntity(stats));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
} else {
return const Left(NetworkFailure('No internet connection'));
}
}
@override
Future<Either<Failure, List<RecentActivityEntity>>> getRecentActivities(
String organizationId,
String userId, {
int limit = 10,
}) async {
if (await networkInfo.isConnected) {
try {
final activities = await remoteDataSource.getRecentActivities(
organizationId,
userId,
limit: limit,
);
return Right(activities.map(_mapActivityToEntity).toList());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
} else {
return const Left(NetworkFailure('No internet connection'));
}
}
@override
Future<Either<Failure, List<UpcomingEventEntity>>> getUpcomingEvents(
String organizationId,
String userId, {
int limit = 5,
}) async {
if (await networkInfo.isConnected) {
try {
final events = await remoteDataSource.getUpcomingEvents(
organizationId,
userId,
limit: limit,
);
return Right(events.map(_mapEventToEntity).toList());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
} else {
return const Left(NetworkFailure('No internet connection'));
}
}
// Mappers
DashboardEntity _mapToEntity(DashboardDataModel model) {
return DashboardEntity(
stats: _mapStatsToEntity(model.stats),
recentActivities: model.recentActivities.map(_mapActivityToEntity).toList(),
upcomingEvents: model.upcomingEvents.map(_mapEventToEntity).toList(),
userPreferences: model.userPreferences,
organizationId: model.organizationId,
userId: model.userId,
);
}
DashboardStatsEntity _mapStatsToEntity(DashboardStatsModel model) {
return DashboardStatsEntity(
totalMembers: model.totalMembers,
activeMembers: model.activeMembers,
totalEvents: model.totalEvents,
upcomingEvents: model.upcomingEvents,
totalContributions: model.totalContributions,
totalContributionAmount: model.totalContributionAmount,
pendingRequests: model.pendingRequests,
completedProjects: model.completedProjects,
monthlyGrowth: model.monthlyGrowth,
engagementRate: model.engagementRate,
lastUpdated: model.lastUpdated,
);
}
RecentActivityEntity _mapActivityToEntity(RecentActivityModel model) {
return RecentActivityEntity(
id: model.id,
type: model.type,
title: model.title,
description: model.description,
userAvatar: model.userAvatar,
userName: model.userName,
timestamp: model.timestamp,
actionUrl: model.actionUrl,
metadata: model.metadata,
);
}
UpcomingEventEntity _mapEventToEntity(UpcomingEventModel model) {
return UpcomingEventEntity(
id: model.id,
title: model.title,
description: model.description,
startDate: model.startDate,
endDate: model.endDate,
location: model.location,
maxParticipants: model.maxParticipants,
currentParticipants: model.currentParticipants,
status: model.status,
imageUrl: model.imageUrl,
tags: model.tags,
);
}
}

View File

@@ -0,0 +1,507 @@
import 'dart:io';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:flutter/services.dart';
import '../models/dashboard_stats_model.dart';
/// Service d'export de rapports PDF pour le Dashboard
class DashboardExportService {
static const String _reportsFolder = 'UnionFlow_Reports';
/// Exporte un rapport complet du dashboard en PDF
Future<String> exportDashboardReport({
required DashboardDataModel dashboardData,
required String organizationName,
required String reportTitle,
bool includeCharts = true,
bool includeActivities = true,
bool includeEvents = true,
}) async {
final pdf = pw.Document();
// Charger les polices personnalisées si disponibles
final font = await _loadFont();
// Page 1: Couverture et statistiques principales
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: _createTheme(font),
build: (context) => [
_buildHeader(organizationName, reportTitle),
pw.SizedBox(height: 20),
_buildStatsSection(dashboardData.stats),
pw.SizedBox(height: 20),
_buildSummarySection(dashboardData),
],
),
);
// Page 2: Activités récentes (si incluses)
if (includeActivities && dashboardData.recentActivities.isNotEmpty) {
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: _createTheme(font),
build: (context) => [
_buildSectionTitle('Activités Récentes'),
pw.SizedBox(height: 10),
_buildActivitiesSection(dashboardData.recentActivities),
],
),
);
}
// Page 3: Événements à venir (si inclus)
if (includeEvents && dashboardData.upcomingEvents.isNotEmpty) {
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: _createTheme(font),
build: (context) => [
_buildSectionTitle('Événements à Venir'),
pw.SizedBox(height: 10),
_buildEventsSection(dashboardData.upcomingEvents),
],
),
);
}
// Page 4: Graphiques et analyses (si inclus)
if (includeCharts) {
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: _createTheme(font),
build: (context) => [
_buildSectionTitle('Analyses et Tendances'),
pw.SizedBox(height: 10),
_buildAnalyticsSection(dashboardData.stats),
],
),
);
}
// Sauvegarder le PDF
final fileName = _generateFileName(reportTitle);
final filePath = await _savePdf(pdf, fileName);
return filePath;
}
/// Exporte uniquement les statistiques en PDF
Future<String> exportStatsReport({
required DashboardStatsModel stats,
required String organizationName,
String? customTitle,
}) async {
final pdf = pw.Document();
final font = await _loadFont();
final title = customTitle ?? 'Rapport Statistiques - ${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year}';
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
theme: _createTheme(font),
build: (context) => [
_buildHeader(organizationName, title),
pw.SizedBox(height: 30),
_buildStatsSection(stats),
pw.SizedBox(height: 30),
_buildStatsAnalysis(stats),
],
),
);
final fileName = _generateFileName('Stats_${DateTime.now().millisecondsSinceEpoch}');
final filePath = await _savePdf(pdf, fileName);
return filePath;
}
/// Charge une police personnalisée
Future<pw.Font?> _loadFont() async {
try {
final fontData = await rootBundle.load('assets/fonts/Inter-Regular.ttf');
return pw.Font.ttf(fontData);
} catch (e) {
// Police par défaut si la police personnalisée n'est pas disponible
return null;
}
}
/// Crée le thème PDF
pw.ThemeData _createTheme(pw.Font? font) {
return pw.ThemeData.withFont(
base: font ?? pw.Font.helvetica(),
bold: font ?? pw.Font.helveticaBold(),
);
}
/// Construit l'en-tête du rapport
pw.Widget _buildHeader(String organizationName, String reportTitle) {
return pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(20),
decoration: pw.BoxDecoration(
gradient: pw.LinearGradient(
colors: [
PdfColor.fromHex('#4169E1'), // Bleu Roi
PdfColor.fromHex('#008B8B'), // Bleu Pétrole
],
),
borderRadius: pw.BorderRadius.circular(10),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
organizationName,
style: pw.TextStyle(
fontSize: 24,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white,
),
),
pw.SizedBox(height: 5),
pw.Text(
reportTitle,
style: const pw.TextStyle(
fontSize: 16,
color: PdfColors.white,
),
),
pw.SizedBox(height: 10),
pw.Text(
'Généré le ${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year} à ${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}',
style: const pw.TextStyle(
fontSize: 12,
color: PdfColors.white,
),
),
],
),
);
}
/// Construit la section des statistiques
pw.Widget _buildStatsSection(DashboardStatsModel stats) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionTitle('Statistiques Principales'),
pw.SizedBox(height: 15),
pw.Row(
children: [
pw.Expanded(
child: _buildStatCard('Membres Total', stats.totalMembers.toString(), PdfColor.fromHex('#4169E1')),
),
pw.SizedBox(width: 10),
pw.Expanded(
child: _buildStatCard('Membres Actifs', stats.activeMembers.toString(), PdfColor.fromHex('#10B981')),
),
],
),
pw.SizedBox(height: 10),
pw.Row(
children: [
pw.Expanded(
child: _buildStatCard('Événements', stats.totalEvents.toString(), PdfColor.fromHex('#008B8B')),
),
pw.SizedBox(width: 10),
pw.Expanded(
child: _buildStatCard('Contributions', stats.formattedContributionAmount, PdfColor.fromHex('#F59E0B')),
),
],
),
pw.SizedBox(height: 10),
pw.Row(
children: [
pw.Expanded(
child: _buildStatCard('Croissance', '${stats.monthlyGrowth.toStringAsFixed(1)}%',
stats.hasGrowth ? PdfColor.fromHex('#10B981') : PdfColor.fromHex('#EF4444')),
),
pw.SizedBox(width: 10),
pw.Expanded(
child: _buildStatCard('Engagement', '${(stats.engagementRate * 100).toStringAsFixed(1)}%',
stats.isHighEngagement ? PdfColor.fromHex('#10B981') : PdfColor.fromHex('#F59E0B')),
),
],
),
],
);
}
/// Construit une carte de statistique
pw.Widget _buildStatCard(String title, String value, PdfColor color) {
return pw.Container(
padding: const pw.EdgeInsets.all(15),
decoration: pw.BoxDecoration(
border: pw.Border.all(color: color, width: 2),
borderRadius: pw.BorderRadius.circular(8),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
title,
style: const pw.TextStyle(
fontSize: 12,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 5),
pw.Text(
value,
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
color: color,
),
),
],
),
);
}
/// Construit un titre de section
pw.Widget _buildSectionTitle(String title) {
return pw.Text(
title,
style: pw.TextStyle(
fontSize: 18,
fontWeight: pw.FontWeight.bold,
color: PdfColor.fromHex('#1F2937'),
),
);
}
/// Construit la section de résumé
pw.Widget _buildSummarySection(DashboardDataModel data) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionTitle('Résumé Exécutif'),
pw.SizedBox(height: 10),
pw.Text(
'Ce rapport présente un aperçu complet de l\'activité de l\'organisation. '
'Avec ${data.stats.totalMembers} membres dont ${data.stats.activeMembers} actifs '
'(${data.stats.activeMemberPercentage.toStringAsFixed(1)}%), l\'organisation maintient '
'un niveau d\'engagement de ${(data.stats.engagementRate * 100).toStringAsFixed(1)}%.',
style: const pw.TextStyle(fontSize: 12, lineSpacing: 1.5),
),
pw.SizedBox(height: 10),
pw.Text(
'La croissance mensuelle de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% '
'${data.stats.hasGrowth ? 'indique une tendance positive' : 'nécessite une attention particulière'}. '
'Les contributions totales s\'élèvent à ${data.stats.formattedContributionAmount} XOF.',
style: const pw.TextStyle(fontSize: 12, lineSpacing: 1.5),
),
],
);
}
/// Construit la section des activités
pw.Widget _buildActivitiesSection(List<RecentActivityModel> activities) {
return pw.Table(
border: pw.TableBorder.all(color: PdfColors.grey300),
children: [
// En-tête
pw.TableRow(
decoration: pw.BoxDecoration(color: PdfColor.fromHex('#F3F4F6')),
children: [
_buildTableHeader('Type'),
_buildTableHeader('Description'),
_buildTableHeader('Utilisateur'),
_buildTableHeader('Date'),
],
),
// Données
...activities.take(10).map((activity) => pw.TableRow(
children: [
_buildTableCell(activity.type),
_buildTableCell(activity.title),
_buildTableCell(activity.userName),
_buildTableCell(activity.timeAgo),
],
)),
],
);
}
/// Construit la section des événements
pw.Widget _buildEventsSection(List<UpcomingEventModel> events) {
return pw.Table(
border: pw.TableBorder.all(color: PdfColors.grey300),
children: [
// En-tête
pw.TableRow(
decoration: pw.BoxDecoration(color: PdfColor.fromHex('#F3F4F6')),
children: [
_buildTableHeader('Événement'),
_buildTableHeader('Date'),
_buildTableHeader('Lieu'),
_buildTableHeader('Participants'),
],
),
// Données
...events.take(10).map((event) => pw.TableRow(
children: [
_buildTableCell(event.title),
_buildTableCell('${event.startDate.day}/${event.startDate.month}'),
_buildTableCell(event.location),
_buildTableCell('${event.currentParticipants}/${event.maxParticipants}'),
],
)),
],
);
}
/// Construit l'en-tête de tableau
pw.Widget _buildTableHeader(String text) {
return pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(
text,
style: pw.TextStyle(
fontWeight: pw.FontWeight.bold,
fontSize: 10,
),
),
);
}
/// Construit une cellule de tableau
pw.Widget _buildTableCell(String text) {
return pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(
text,
style: const pw.TextStyle(fontSize: 9),
),
);
}
/// Construit la section d'analyse des statistiques
pw.Widget _buildStatsAnalysis(DashboardStatsModel stats) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionTitle('Analyse des Performances'),
pw.SizedBox(height: 10),
_buildAnalysisPoint('Taux d\'activité des membres',
'${stats.activeMemberPercentage.toStringAsFixed(1)}%',
stats.activeMemberPercentage > 70 ? 'Excellent' : 'À améliorer'),
_buildAnalysisPoint('Croissance mensuelle',
'${stats.monthlyGrowth.toStringAsFixed(1)}%',
stats.hasGrowth ? 'Positive' : 'Négative'),
_buildAnalysisPoint('Niveau d\'engagement',
'${(stats.engagementRate * 100).toStringAsFixed(1)}%',
stats.isHighEngagement ? 'Élevé' : 'Modéré'),
],
);
}
/// Construit un point d'analyse
pw.Widget _buildAnalysisPoint(String metric, String value, String assessment) {
return pw.Padding(
padding: const pw.EdgeInsets.symmetric(vertical: 5),
child: pw.Row(
children: [
pw.Expanded(flex: 2, child: pw.Text(metric, style: const pw.TextStyle(fontSize: 11))),
pw.Expanded(flex: 1, child: pw.Text(value, style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold))),
pw.Expanded(flex: 1, child: pw.Text(assessment, style: const pw.TextStyle(fontSize: 11))),
],
),
);
}
/// Construit la section d'analytics
pw.Widget _buildAnalyticsSection(DashboardStatsModel stats) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Tendances et Projections', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 15),
pw.Text('Basé sur les données actuelles, voici les principales tendances observées:', style: const pw.TextStyle(fontSize: 11)),
pw.SizedBox(height: 10),
pw.Bullet(text: 'Évolution du nombre de membres: ${stats.hasGrowth ? 'Croissance' : 'Déclin'} de ${stats.monthlyGrowth.abs().toStringAsFixed(1)}% ce mois'),
pw.Bullet(text: 'Participation aux événements: ${stats.upcomingEvents} événements programmés'),
pw.Bullet(text: 'Volume des contributions: ${stats.formattedContributionAmount} XOF collectés'),
pw.Bullet(text: 'Demandes en attente: ${stats.pendingRequests} nécessitent un traitement'),
],
);
}
/// Génère un nom de fichier unique
String _generateFileName(String baseName) {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final cleanName = baseName.replaceAll(RegExp(r'[^\w\s-]'), '').replaceAll(' ', '_');
return '${cleanName}_$timestamp.pdf';
}
/// Sauvegarde le PDF et retourne le chemin
Future<String> _savePdf(pw.Document pdf, String fileName) async {
final directory = await getApplicationDocumentsDirectory();
final reportsDir = Directory('${directory.path}/$_reportsFolder');
if (!await reportsDir.exists()) {
await reportsDir.create(recursive: true);
}
final file = File('${reportsDir.path}/$fileName');
await file.writeAsBytes(await pdf.save());
return file.path;
}
/// Partage un rapport PDF
Future<void> shareReport(String filePath, {String? subject}) async {
await Share.shareXFiles(
[XFile(filePath)],
subject: subject ?? 'Rapport Dashboard UnionFlow',
text: 'Rapport généré par l\'application UnionFlow',
);
}
/// Obtient la liste des rapports sauvegardés
Future<List<File>> getSavedReports() async {
final directory = await getApplicationDocumentsDirectory();
final reportsDir = Directory('${directory.path}/$_reportsFolder');
if (!await reportsDir.exists()) {
return [];
}
final files = await reportsDir.list().where((entity) =>
entity is File && entity.path.endsWith('.pdf')).cast<File>().toList();
// Trier par date de modification (plus récent en premier)
files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
return files;
}
/// Supprime un rapport
Future<void> deleteReport(String filePath) async {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
}
}
/// Supprime tous les rapports anciens (plus de 30 jours)
Future<void> cleanupOldReports() async {
final reports = await getSavedReports();
final cutoffDate = DateTime.now().subtract(const Duration(days: 30));
for (final report in reports) {
final lastModified = await report.lastModified();
if (lastModified.isBefore(cutoffDate)) {
await report.delete();
}
}
}
}

View File

@@ -0,0 +1,391 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../models/dashboard_stats_model.dart';
import '../../config/dashboard_config.dart';
/// Service de notifications temps réel pour le Dashboard
class DashboardNotificationService {
static const String _wsEndpoint = 'ws://localhost:8080/ws/dashboard';
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _reconnectTimer;
Timer? _heartbeatTimer;
bool _isConnected = false;
bool _shouldReconnect = true;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 5);
static const Duration _heartbeatInterval = Duration(seconds: 30);
// Streams pour les différents types de notifications
final StreamController<DashboardStatsModel> _statsController =
StreamController<DashboardStatsModel>.broadcast();
final StreamController<RecentActivityModel> _activityController =
StreamController<RecentActivityModel>.broadcast();
final StreamController<UpcomingEventModel> _eventController =
StreamController<UpcomingEventModel>.broadcast();
final StreamController<DashboardNotification> _notificationController =
StreamController<DashboardNotification>.broadcast();
final StreamController<ConnectionStatus> _connectionController =
StreamController<ConnectionStatus>.broadcast();
// Getters pour les streams
Stream<DashboardStatsModel> get statsStream => _statsController.stream;
Stream<RecentActivityModel> get activityStream => _activityController.stream;
Stream<UpcomingEventModel> get eventStream => _eventController.stream;
Stream<DashboardNotification> get notificationStream => _notificationController.stream;
Stream<ConnectionStatus> get connectionStream => _connectionController.stream;
/// Initialise le service de notifications
Future<void> initialize(String organizationId, String userId) async {
if (!DashboardConfig.enableNotifications) {
debugPrint('📱 Notifications désactivées dans la configuration');
return;
}
debugPrint('📱 Initialisation du service de notifications...');
await _connect(organizationId, userId);
}
/// Établit la connexion WebSocket
Future<void> _connect(String organizationId, String userId) async {
if (_isConnected) return;
try {
final uri = Uri.parse('$_wsEndpoint?orgId=$organizationId&userId=$userId');
_channel = WebSocketChannel.connect(uri);
debugPrint('📱 Connexion WebSocket en cours...');
_connectionController.add(ConnectionStatus.connecting);
// Écouter les messages
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
);
_isConnected = true;
_reconnectAttempts = 0;
_connectionController.add(ConnectionStatus.connected);
// Démarrer le heartbeat
_startHeartbeat();
debugPrint('✅ Connexion WebSocket établie');
} catch (e) {
debugPrint('❌ Erreur de connexion WebSocket: $e');
_connectionController.add(ConnectionStatus.error);
_scheduleReconnect(organizationId, userId);
}
}
/// Gère les messages reçus
void _handleMessage(dynamic message) {
try {
final data = jsonDecode(message as String);
final type = data['type'] as String?;
final payload = data['payload'];
debugPrint('📨 Message reçu: $type');
switch (type) {
case 'stats_update':
final stats = DashboardStatsModel.fromJson(payload);
_statsController.add(stats);
break;
case 'new_activity':
final activity = RecentActivityModel.fromJson(payload);
_activityController.add(activity);
break;
case 'event_update':
final event = UpcomingEventModel.fromJson(payload);
_eventController.add(event);
break;
case 'notification':
final notification = DashboardNotification.fromJson(payload);
_notificationController.add(notification);
break;
case 'pong':
// Réponse au heartbeat
debugPrint('💓 Heartbeat reçu');
break;
default:
debugPrint('⚠️ Type de message inconnu: $type');
}
} catch (e) {
debugPrint('❌ Erreur de parsing du message: $e');
}
}
/// Gère les erreurs de connexion
void _handleError(error) {
debugPrint('❌ Erreur WebSocket: $error');
_isConnected = false;
_connectionController.add(ConnectionStatus.error);
}
/// Gère la déconnexion
void _handleDisconnection() {
debugPrint('🔌 Connexion WebSocket fermée');
_isConnected = false;
_connectionController.add(ConnectionStatus.disconnected);
if (_shouldReconnect) {
// Programmer une reconnexion
_scheduleReconnect('', ''); // Les IDs seront récupérés du contexte
}
}
/// Programme une tentative de reconnexion
void _scheduleReconnect(String organizationId, String userId) {
if (_reconnectAttempts >= _maxReconnectAttempts) {
debugPrint('❌ Nombre maximum de tentatives de reconnexion atteint');
_connectionController.add(ConnectionStatus.failed);
return;
}
_reconnectAttempts++;
final delay = _reconnectDelay * _reconnectAttempts;
debugPrint('🔄 Reconnexion programmée dans ${delay.inSeconds}s (tentative $_reconnectAttempts)');
_reconnectTimer = Timer(delay, () {
if (_shouldReconnect) {
_connect(organizationId, userId);
}
});
}
/// Démarre le heartbeat
void _startHeartbeat() {
_heartbeatTimer = Timer.periodic(_heartbeatInterval, (timer) {
if (_isConnected && _channel != null) {
try {
_channel!.sink.add(jsonEncode({
'type': 'ping',
'timestamp': DateTime.now().toIso8601String(),
}));
} catch (e) {
debugPrint('❌ Erreur lors de l\'envoi du heartbeat: $e');
}
}
});
}
/// Envoie une demande de rafraîchissement
void requestRefresh(String organizationId, String userId) {
if (_isConnected && _channel != null) {
try {
_channel!.sink.add(jsonEncode({
'type': 'refresh_request',
'payload': {
'organizationId': organizationId,
'userId': userId,
'timestamp': DateTime.now().toIso8601String(),
},
}));
debugPrint('📤 Demande de rafraîchissement envoyée');
} catch (e) {
debugPrint('❌ Erreur lors de l\'envoi de la demande: $e');
}
}
}
/// S'abonne aux notifications pour un type spécifique
void subscribeToNotifications(List<String> notificationTypes) {
if (_isConnected && _channel != null) {
try {
_channel!.sink.add(jsonEncode({
'type': 'subscribe',
'payload': {
'notificationTypes': notificationTypes,
'timestamp': DateTime.now().toIso8601String(),
},
}));
debugPrint('📋 Abonnement aux notifications: $notificationTypes');
} catch (e) {
debugPrint('❌ Erreur lors de l\'abonnement: $e');
}
}
}
/// Se désabonne des notifications
void unsubscribeFromNotifications(List<String> notificationTypes) {
if (_isConnected && _channel != null) {
try {
_channel!.sink.add(jsonEncode({
'type': 'unsubscribe',
'payload': {
'notificationTypes': notificationTypes,
'timestamp': DateTime.now().toIso8601String(),
},
}));
debugPrint('📋 Désabonnement des notifications: $notificationTypes');
} catch (e) {
debugPrint('❌ Erreur lors du désabonnement: $e');
}
}
}
/// Obtient le statut de la connexion
bool get isConnected => _isConnected;
/// Obtient le nombre de tentatives de reconnexion
int get reconnectAttempts => _reconnectAttempts;
/// Force une reconnexion
Future<void> reconnect(String organizationId, String userId) async {
await disconnect();
_reconnectAttempts = 0;
await _connect(organizationId, userId);
}
/// Déconnecte le service
Future<void> disconnect() async {
_shouldReconnect = false;
_reconnectTimer?.cancel();
_heartbeatTimer?.cancel();
if (_channel != null) {
await _channel!.sink.close();
_channel = null;
}
await _subscription?.cancel();
_subscription = null;
_isConnected = false;
_connectionController.add(ConnectionStatus.disconnected);
debugPrint('🔌 Service de notifications déconnecté');
}
/// Libère les ressources
void dispose() {
disconnect();
_statsController.close();
_activityController.close();
_eventController.close();
_notificationController.close();
_connectionController.close();
}
}
/// Statut de la connexion
enum ConnectionStatus {
disconnected,
connecting,
connected,
error,
failed,
}
/// Notification du dashboard
class DashboardNotification {
final String id;
final String type;
final String title;
final String message;
final NotificationPriority priority;
final DateTime timestamp;
final Map<String, dynamic>? data;
final String? actionUrl;
final bool isRead;
const DashboardNotification({
required this.id,
required this.type,
required this.title,
required this.message,
required this.priority,
required this.timestamp,
this.data,
this.actionUrl,
this.isRead = false,
});
factory DashboardNotification.fromJson(Map<String, dynamic> json) {
return DashboardNotification(
id: json['id'] as String,
type: json['type'] as String,
title: json['title'] as String,
message: json['message'] as String,
priority: NotificationPriority.values.firstWhere(
(p) => p.name == json['priority'],
orElse: () => NotificationPriority.normal,
),
timestamp: DateTime.parse(json['timestamp'] as String),
data: json['data'] as Map<String, dynamic>?,
actionUrl: json['actionUrl'] as String?,
isRead: json['isRead'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'title': title,
'message': message,
'priority': priority.name,
'timestamp': timestamp.toIso8601String(),
'data': data,
'actionUrl': actionUrl,
'isRead': isRead,
};
}
/// Obtient l'icône pour le type de notification
String get icon {
switch (type) {
case 'new_member':
return '👤';
case 'new_event':
return '📅';
case 'contribution':
return '💰';
case 'urgent':
return '🚨';
case 'system':
return '⚙️';
default:
return '📢';
}
}
/// Obtient la couleur pour la priorité
String get priorityColor {
switch (priority) {
case NotificationPriority.low:
return '#6B7280';
case NotificationPriority.normal:
return '#3B82F6';
case NotificationPriority.high:
return '#F59E0B';
case NotificationPriority.urgent:
return '#EF4444';
}
}
}
/// Priorité des notifications
enum NotificationPriority {
low,
normal,
high,
urgent,
}

View File

@@ -0,0 +1,471 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../models/dashboard_stats_model.dart';
import '../cache/dashboard_cache_manager.dart';
/// Service de mode hors ligne avec synchronisation pour le Dashboard
class DashboardOfflineService {
static const String _offlineQueueKey = 'dashboard_offline_queue';
static const String _lastSyncKey = 'dashboard_last_sync';
static const String _offlineModeKey = 'dashboard_offline_mode';
final DashboardCacheManager _cacheManager;
final Connectivity _connectivity = Connectivity();
SharedPreferences? _prefs;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
Timer? _syncTimer;
final StreamController<OfflineStatus> _statusController =
StreamController<OfflineStatus>.broadcast();
final StreamController<SyncProgress> _syncController =
StreamController<SyncProgress>.broadcast();
final List<OfflineAction> _pendingActions = [];
bool _isOnline = true;
bool _isSyncing = false;
DateTime? _lastSyncTime;
// Streams publics
Stream<OfflineStatus> get statusStream => _statusController.stream;
Stream<SyncProgress> get syncStream => _syncController.stream;
DashboardOfflineService(this._cacheManager);
/// Initialise le service hors ligne
Future<void> initialize() async {
debugPrint('📱 Initialisation du service hors ligne...');
_prefs = await SharedPreferences.getInstance();
// Charger les actions en attente
await _loadPendingActions();
// Charger la dernière synchronisation
_loadLastSyncTime();
// Vérifier la connectivité initiale
final connectivityResult = await _connectivity.checkConnectivity();
_updateConnectivityStatus(connectivityResult);
// Écouter les changements de connectivité
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) => _updateConnectivityStatus(results),
);
// Démarrer la synchronisation automatique
_startAutoSync();
debugPrint('✅ Service hors ligne initialisé');
}
/// Met à jour le statut de connectivité
void _updateConnectivityStatus(dynamic result) {
final wasOnline = _isOnline;
if (result is List<ConnectivityResult>) {
_isOnline = result.any((r) => r != ConnectivityResult.none);
} else if (result is ConnectivityResult) {
_isOnline = result != ConnectivityResult.none;
} else {
_isOnline = false;
}
debugPrint('🌐 Connectivité: ${_isOnline ? 'En ligne' : 'Hors ligne'}');
_statusController.add(OfflineStatus(
isOnline: _isOnline,
pendingActionsCount: _pendingActions.length,
lastSyncTime: _lastSyncTime,
));
// Si on revient en ligne, synchroniser
if (!wasOnline && _isOnline && _pendingActions.isNotEmpty) {
_syncPendingActions();
}
}
/// Démarre la synchronisation automatique
void _startAutoSync() {
_syncTimer = Timer.periodic(
const Duration(minutes: 5),
(_) {
if (_isOnline && _pendingActions.isNotEmpty) {
_syncPendingActions();
}
},
);
}
/// Ajoute une action à la queue hors ligne
Future<void> queueAction(OfflineAction action) async {
_pendingActions.add(action);
await _savePendingActions();
debugPrint('📝 Action mise en queue: ${action.type} (${_pendingActions.length} en attente)');
_statusController.add(OfflineStatus(
isOnline: _isOnline,
pendingActionsCount: _pendingActions.length,
lastSyncTime: _lastSyncTime,
));
// Si en ligne, essayer de synchroniser immédiatement
if (_isOnline) {
_syncPendingActions();
}
}
/// Synchronise les actions en attente
Future<void> _syncPendingActions() async {
if (_isSyncing || _pendingActions.isEmpty || !_isOnline) {
return;
}
_isSyncing = true;
debugPrint('🔄 Début de la synchronisation (${_pendingActions.length} actions)');
_syncController.add(SyncProgress(
isActive: true,
totalActions: _pendingActions.length,
completedActions: 0,
currentAction: _pendingActions.first.type.toString(),
));
final actionsToSync = List<OfflineAction>.from(_pendingActions);
int completedCount = 0;
for (final action in actionsToSync) {
try {
await _executeAction(action);
_pendingActions.remove(action);
completedCount++;
_syncController.add(SyncProgress(
isActive: true,
totalActions: actionsToSync.length,
completedActions: completedCount,
currentAction: completedCount < actionsToSync.length
? actionsToSync[completedCount].type.toString()
: null,
));
debugPrint('✅ Action synchronisée: ${action.type}');
} catch (e) {
debugPrint('❌ Erreur lors de la synchronisation de ${action.type}: $e');
// Marquer l'action comme échouée si trop de tentatives
action.retryCount++;
if (action.retryCount >= 3) {
_pendingActions.remove(action);
debugPrint('🗑️ Action abandonnée après 3 tentatives: ${action.type}');
}
}
}
await _savePendingActions();
_lastSyncTime = DateTime.now();
await _saveLastSyncTime();
_syncController.add(SyncProgress(
isActive: false,
totalActions: actionsToSync.length,
completedActions: completedCount,
currentAction: null,
));
_statusController.add(OfflineStatus(
isOnline: _isOnline,
pendingActionsCount: _pendingActions.length,
lastSyncTime: _lastSyncTime,
));
_isSyncing = false;
debugPrint('✅ Synchronisation terminée ($completedCount/${actionsToSync.length} réussies)');
}
/// Exécute une action spécifique
Future<void> _executeAction(OfflineAction action) async {
switch (action.type) {
case OfflineActionType.refreshDashboard:
await _syncDashboardData(action);
break;
case OfflineActionType.updatePreferences:
await _syncUserPreferences(action);
break;
case OfflineActionType.markActivityRead:
await _syncActivityRead(action);
break;
case OfflineActionType.joinEvent:
await _syncEventJoin(action);
break;
case OfflineActionType.exportReport:
await _syncReportExport(action);
break;
}
}
/// Synchronise les données du dashboard
Future<void> _syncDashboardData(OfflineAction action) async {
// TODO: Implémenter la synchronisation des données
await Future.delayed(const Duration(milliseconds: 500)); // Simulation
}
/// Synchronise les préférences utilisateur
Future<void> _syncUserPreferences(OfflineAction action) async {
// TODO: Implémenter la synchronisation des préférences
await Future.delayed(const Duration(milliseconds: 300)); // Simulation
}
/// Synchronise le marquage d'activité comme lue
Future<void> _syncActivityRead(OfflineAction action) async {
// TODO: Implémenter la synchronisation du marquage
await Future.delayed(const Duration(milliseconds: 200)); // Simulation
}
/// Synchronise l'inscription à un événement
Future<void> _syncEventJoin(OfflineAction action) async {
// TODO: Implémenter la synchronisation d'inscription
await Future.delayed(const Duration(milliseconds: 400)); // Simulation
}
/// Synchronise l'export de rapport
Future<void> _syncReportExport(OfflineAction action) async {
// TODO: Implémenter la synchronisation d'export
await Future.delayed(const Duration(milliseconds: 800)); // Simulation
}
/// Sauvegarde les actions en attente
Future<void> _savePendingActions() async {
if (_prefs == null) return;
final actionsJson = _pendingActions
.map((action) => action.toJson())
.toList();
await _prefs!.setString(_offlineQueueKey, jsonEncode(actionsJson));
}
/// Charge les actions en attente
Future<void> _loadPendingActions() async {
if (_prefs == null) return;
final actionsJsonString = _prefs!.getString(_offlineQueueKey);
if (actionsJsonString != null) {
try {
final actionsJson = jsonDecode(actionsJsonString) as List;
_pendingActions.clear();
_pendingActions.addAll(
actionsJson.map((json) => OfflineAction.fromJson(json)),
);
debugPrint('📋 ${_pendingActions.length} actions chargées depuis le cache');
} catch (e) {
debugPrint('❌ Erreur lors du chargement des actions: $e');
await _prefs!.remove(_offlineQueueKey);
}
}
}
/// Sauvegarde l'heure de dernière synchronisation
Future<void> _saveLastSyncTime() async {
if (_prefs == null || _lastSyncTime == null) return;
await _prefs!.setInt(_lastSyncKey, _lastSyncTime!.millisecondsSinceEpoch);
}
/// Charge l'heure de dernière synchronisation
void _loadLastSyncTime() {
if (_prefs == null) return;
final lastSyncMs = _prefs!.getInt(_lastSyncKey);
if (lastSyncMs != null) {
_lastSyncTime = DateTime.fromMillisecondsSinceEpoch(lastSyncMs);
}
}
/// Force une synchronisation manuelle
Future<void> forcSync() async {
if (!_isOnline) {
throw Exception('Impossible de synchroniser hors ligne');
}
await _syncPendingActions();
}
/// Obtient les données en mode hors ligne
Future<DashboardDataModel?> getOfflineData(
String organizationId,
String userId,
) async {
return await _cacheManager.getCachedDashboardData(organizationId, userId);
}
/// Vérifie si des données sont disponibles hors ligne
Future<bool> hasOfflineData(String organizationId, String userId) async {
final data = await getOfflineData(organizationId, userId);
return data != null;
}
/// Obtient les statistiques du mode hors ligne
OfflineStats getStats() {
return OfflineStats(
isOnline: _isOnline,
pendingActionsCount: _pendingActions.length,
lastSyncTime: _lastSyncTime,
isSyncing: _isSyncing,
cacheStats: _cacheManager.getCacheStats(),
);
}
/// Nettoie les anciennes actions
Future<void> cleanupOldActions() async {
final cutoffTime = DateTime.now().subtract(const Duration(days: 7));
_pendingActions.removeWhere((action) =>
action.timestamp.isBefore(cutoffTime));
await _savePendingActions();
}
/// Libère les ressources
void dispose() {
_connectivitySubscription?.cancel();
_syncTimer?.cancel();
_statusController.close();
_syncController.close();
}
}
/// Action hors ligne
class OfflineAction {
final String id;
final OfflineActionType type;
final Map<String, dynamic> data;
final DateTime timestamp;
int retryCount;
OfflineAction({
required this.id,
required this.type,
required this.data,
required this.timestamp,
this.retryCount = 0,
});
factory OfflineAction.fromJson(Map<String, dynamic> json) {
return OfflineAction(
id: json['id'] as String,
type: OfflineActionType.values.firstWhere(
(t) => t.name == json['type'],
),
data: json['data'] as Map<String, dynamic>,
timestamp: DateTime.parse(json['timestamp'] as String),
retryCount: json['retryCount'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type.name,
'data': data,
'timestamp': timestamp.toIso8601String(),
'retryCount': retryCount,
};
}
}
/// Types d'actions hors ligne
enum OfflineActionType {
refreshDashboard,
updatePreferences,
markActivityRead,
joinEvent,
exportReport,
}
/// Statut hors ligne
class OfflineStatus {
final bool isOnline;
final int pendingActionsCount;
final DateTime? lastSyncTime;
const OfflineStatus({
required this.isOnline,
required this.pendingActionsCount,
this.lastSyncTime,
});
String get statusText {
if (isOnline) {
if (pendingActionsCount > 0) {
return 'En ligne - $pendingActionsCount actions en attente';
} else {
return 'En ligne - Synchronisé';
}
} else {
return 'Hors ligne - Mode cache activé';
}
}
}
/// Progression de synchronisation
class SyncProgress {
final bool isActive;
final int totalActions;
final int completedActions;
final String? currentAction;
const SyncProgress({
required this.isActive,
required this.totalActions,
required this.completedActions,
this.currentAction,
});
double get progress {
if (totalActions == 0) return 1.0;
return completedActions / totalActions;
}
String get progressText {
if (!isActive) return 'Synchronisation terminée';
if (currentAction != null) {
return 'Synchronisation: $currentAction ($completedActions/$totalActions)';
}
return 'Synchronisation en cours... ($completedActions/$totalActions)';
}
}
/// Statistiques du mode hors ligne
class OfflineStats {
final bool isOnline;
final int pendingActionsCount;
final DateTime? lastSyncTime;
final bool isSyncing;
final Map<String, dynamic> cacheStats;
const OfflineStats({
required this.isOnline,
required this.pendingActionsCount,
this.lastSyncTime,
required this.isSyncing,
required this.cacheStats,
});
String get lastSyncText {
if (lastSyncTime == null) return 'Jamais synchronisé';
final now = DateTime.now();
final diff = now.difference(lastSyncTime!);
if (diff.inMinutes < 1) return 'Synchronisé à l\'instant';
if (diff.inMinutes < 60) return 'Synchronisé il y a ${diff.inMinutes}min';
if (diff.inHours < 24) return 'Synchronisé il y a ${diff.inHours}h';
return 'Synchronisé il y a ${diff.inDays}j';
}
}

View File

@@ -0,0 +1,526 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../../config/dashboard_config.dart';
/// Moniteur de performances avancé pour le Dashboard
class DashboardPerformanceMonitor {
static const String _channelName = 'dashboard_performance';
static const MethodChannel _channel = MethodChannel(_channelName);
Timer? _monitoringTimer;
Timer? _reportTimer;
final List<PerformanceSnapshot> _snapshots = [];
final StreamController<PerformanceMetrics> _metricsController =
StreamController<PerformanceMetrics>.broadcast();
final StreamController<PerformanceAlert> _alertController =
StreamController<PerformanceAlert>.broadcast();
bool _isMonitoring = false;
DateTime _startTime = DateTime.now();
// Seuils d'alerte configurables
final double _memoryThreshold = DashboardConfig.getAlertThreshold('memoryUsage');
final double _cpuThreshold = DashboardConfig.getAlertThreshold('cpuUsage');
final int _networkLatencyThreshold = DashboardConfig.getAlertThreshold('networkLatency').toInt();
final double _frameRateThreshold = DashboardConfig.getAlertThreshold('frameRate');
// Streams publics
Stream<PerformanceMetrics> get metricsStream => _metricsController.stream;
Stream<PerformanceAlert> get alertStream => _alertController.stream;
/// Démarre le monitoring des performances
Future<void> startMonitoring() async {
if (_isMonitoring) return;
debugPrint('🔍 Démarrage du monitoring des performances...');
_isMonitoring = true;
_startTime = DateTime.now();
// Timer pour collecter les métriques
_monitoringTimer = Timer.periodic(
DashboardConfig.performanceCheckInterval,
(_) => _collectMetrics(),
);
// Timer pour générer les rapports
_reportTimer = Timer.periodic(
const Duration(minutes: 5),
(_) => _generateReport(),
);
// Collecte initiale
await _collectMetrics();
debugPrint('✅ Monitoring des performances démarré');
}
/// Arrête le monitoring
void stopMonitoring() {
if (!_isMonitoring) return;
_isMonitoring = false;
_monitoringTimer?.cancel();
_reportTimer?.cancel();
debugPrint('🛑 Monitoring des performances arrêté');
}
/// Collecte les métriques de performance
Future<void> _collectMetrics() async {
try {
final metrics = await _gatherMetrics();
final snapshot = PerformanceSnapshot(
timestamp: DateTime.now(),
metrics: metrics,
);
_snapshots.add(snapshot);
// Garder seulement les 1000 derniers snapshots
if (_snapshots.length > 1000) {
_snapshots.removeAt(0);
}
// Émettre les métriques
_metricsController.add(metrics);
// Vérifier les seuils d'alerte
_checkAlerts(metrics);
} catch (e) {
debugPrint('❌ Erreur lors de la collecte des métriques: $e');
}
}
/// Rassemble toutes les métriques
Future<PerformanceMetrics> _gatherMetrics() async {
final memoryUsage = await _getMemoryUsage();
final cpuUsage = await _getCpuUsage();
final networkLatency = await _getNetworkLatency();
final frameRate = await _getFrameRate();
final batteryLevel = await _getBatteryLevel();
final diskUsage = await _getDiskUsage();
final networkUsage = await _getNetworkUsage();
return PerformanceMetrics(
timestamp: DateTime.now(),
memoryUsage: memoryUsage,
cpuUsage: cpuUsage,
networkLatency: networkLatency,
frameRate: frameRate,
batteryLevel: batteryLevel,
diskUsage: diskUsage,
networkUsage: networkUsage,
uptime: DateTime.now().difference(_startTime),
);
}
/// Obtient l'utilisation mémoire
Future<double> _getMemoryUsage() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getMemoryUsage');
return (result as num).toDouble();
} else {
// Simulation pour les autres plateformes
return _simulateMemoryUsage();
}
} catch (e) {
return _simulateMemoryUsage();
}
}
/// Obtient l'utilisation CPU
Future<double> _getCpuUsage() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getCpuUsage');
return (result as num).toDouble();
} else {
return _simulateCpuUsage();
}
} catch (e) {
return _simulateCpuUsage();
}
}
/// Obtient la latence réseau
Future<int> _getNetworkLatency() async {
try {
final stopwatch = Stopwatch()..start();
// Ping vers le serveur de l'API
final socket = await Socket.connect('localhost', 8080)
.timeout(const Duration(seconds: 5));
stopwatch.stop();
await socket.close();
return stopwatch.elapsedMilliseconds;
} catch (e) {
return _simulateNetworkLatency();
}
}
/// Obtient le frame rate
Future<double> _getFrameRate() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getFrameRate');
return (result as num).toDouble();
} else {
return _simulateFrameRate();
}
} catch (e) {
return _simulateFrameRate();
}
}
/// Obtient le niveau de batterie
Future<double> _getBatteryLevel() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getBatteryLevel');
return (result as num).toDouble();
} else {
return _simulateBatteryLevel();
}
} catch (e) {
return _simulateBatteryLevel();
}
}
/// Obtient l'utilisation disque
Future<double> _getDiskUsage() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getDiskUsage');
return (result as num).toDouble();
} else {
return _simulateDiskUsage();
}
} catch (e) {
return _simulateDiskUsage();
}
}
/// Obtient l'utilisation réseau
Future<NetworkUsage> _getNetworkUsage() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final result = await _channel.invokeMethod('getNetworkUsage');
return NetworkUsage(
bytesReceived: (result['bytesReceived'] as num).toDouble(),
bytesSent: (result['bytesSent'] as num).toDouble(),
);
} else {
return _simulateNetworkUsage();
}
} catch (e) {
return _simulateNetworkUsage();
}
}
/// Vérifie les seuils d'alerte
void _checkAlerts(PerformanceMetrics metrics) {
// Alerte mémoire
if (metrics.memoryUsage > _memoryThreshold) {
_alertController.add(PerformanceAlert(
type: AlertType.memory,
severity: AlertSeverity.warning,
message: 'Utilisation mémoire élevée: ${metrics.memoryUsage.toStringAsFixed(1)}MB',
value: metrics.memoryUsage,
threshold: _memoryThreshold,
timestamp: DateTime.now(),
));
}
// Alerte CPU
if (metrics.cpuUsage > _cpuThreshold) {
_alertController.add(PerformanceAlert(
type: AlertType.cpu,
severity: AlertSeverity.warning,
message: 'Utilisation CPU élevée: ${metrics.cpuUsage.toStringAsFixed(1)}%',
value: metrics.cpuUsage,
threshold: _cpuThreshold,
timestamp: DateTime.now(),
));
}
// Alerte latence réseau
if (metrics.networkLatency > _networkLatencyThreshold) {
_alertController.add(PerformanceAlert(
type: AlertType.network,
severity: AlertSeverity.error,
message: 'Latence réseau élevée: ${metrics.networkLatency}ms',
value: metrics.networkLatency.toDouble(),
threshold: _networkLatencyThreshold.toDouble(),
timestamp: DateTime.now(),
));
}
// Alerte frame rate
if (metrics.frameRate < _frameRateThreshold) {
_alertController.add(PerformanceAlert(
type: AlertType.performance,
severity: AlertSeverity.warning,
message: 'Frame rate faible: ${metrics.frameRate.toStringAsFixed(1)}fps',
value: metrics.frameRate,
threshold: _frameRateThreshold,
timestamp: DateTime.now(),
));
}
}
/// Génère un rapport de performance
void _generateReport() {
if (_snapshots.isEmpty) return;
final recentSnapshots = _snapshots.where((snapshot) =>
DateTime.now().difference(snapshot.timestamp).inMinutes <= 5).toList();
if (recentSnapshots.isEmpty) return;
final report = PerformanceReport.fromSnapshots(recentSnapshots);
debugPrint('📊 RAPPORT DE PERFORMANCE (5 min)');
debugPrint('Mémoire: ${report.averageMemoryUsage.toStringAsFixed(1)}MB (max: ${report.maxMemoryUsage.toStringAsFixed(1)}MB)');
debugPrint('CPU: ${report.averageCpuUsage.toStringAsFixed(1)}% (max: ${report.maxCpuUsage.toStringAsFixed(1)}%)');
debugPrint('Latence: ${report.averageNetworkLatency.toStringAsFixed(0)}ms (max: ${report.maxNetworkLatency.toStringAsFixed(0)}ms)');
debugPrint('FPS: ${report.averageFrameRate.toStringAsFixed(1)}fps (min: ${report.minFrameRate.toStringAsFixed(1)}fps)');
}
/// Obtient les statistiques de performance
PerformanceStats getStats() {
if (_snapshots.isEmpty) {
return PerformanceStats.empty();
}
return PerformanceStats.fromSnapshots(_snapshots);
}
/// Méthodes de simulation pour le développement
double _simulateMemoryUsage() {
const base = 200.0;
final variation = 100.0 * (DateTime.now().millisecond / 1000.0);
return base + variation;
}
double _simulateCpuUsage() {
const base = 30.0;
final variation = 40.0 * (DateTime.now().second / 60.0);
return (base + variation).clamp(0.0, 100.0);
}
int _simulateNetworkLatency() {
const base = 150;
final variation = (200 * (DateTime.now().millisecond / 1000.0)).round();
return base + variation;
}
double _simulateFrameRate() {
const base = 58.0;
final variation = 5.0 * (DateTime.now().millisecond / 1000.0);
return (base + variation).clamp(30.0, 60.0);
}
double _simulateBatteryLevel() {
final elapsed = DateTime.now().difference(_startTime).inMinutes;
return (100.0 - elapsed * 0.1).clamp(0.0, 100.0);
}
double _simulateDiskUsage() {
return 45.0 + (10.0 * (DateTime.now().millisecond / 1000.0));
}
NetworkUsage _simulateNetworkUsage() {
const base = 1024.0;
final variation = 512.0 * (DateTime.now().millisecond / 1000.0);
return NetworkUsage(
bytesReceived: base + variation,
bytesSent: (base + variation) * 0.3,
);
}
/// Libère les ressources
void dispose() {
stopMonitoring();
_metricsController.close();
_alertController.close();
_snapshots.clear();
}
}
/// Métriques de performance
class PerformanceMetrics {
final DateTime timestamp;
final double memoryUsage; // MB
final double cpuUsage; // %
final int networkLatency; // ms
final double frameRate; // fps
final double batteryLevel; // %
final double diskUsage; // %
final NetworkUsage networkUsage;
final Duration uptime;
const PerformanceMetrics({
required this.timestamp,
required this.memoryUsage,
required this.cpuUsage,
required this.networkLatency,
required this.frameRate,
required this.batteryLevel,
required this.diskUsage,
required this.networkUsage,
required this.uptime,
});
}
/// Utilisation réseau
class NetworkUsage {
final double bytesReceived;
final double bytesSent;
const NetworkUsage({
required this.bytesReceived,
required this.bytesSent,
});
double get totalBytes => bytesReceived + bytesSent;
}
/// Snapshot de performance
class PerformanceSnapshot {
final DateTime timestamp;
final PerformanceMetrics metrics;
const PerformanceSnapshot({
required this.timestamp,
required this.metrics,
});
}
/// Alerte de performance
class PerformanceAlert {
final AlertType type;
final AlertSeverity severity;
final String message;
final double value;
final double threshold;
final DateTime timestamp;
const PerformanceAlert({
required this.type,
required this.severity,
required this.message,
required this.value,
required this.threshold,
required this.timestamp,
});
}
/// Type d'alerte
enum AlertType { memory, cpu, network, performance, battery, disk }
/// Sévérité d'alerte
enum AlertSeverity { info, warning, error, critical }
/// Rapport de performance
class PerformanceReport {
final DateTime startTime;
final DateTime endTime;
final double averageMemoryUsage;
final double maxMemoryUsage;
final double averageCpuUsage;
final double maxCpuUsage;
final double averageNetworkLatency;
final double maxNetworkLatency;
final double averageFrameRate;
final double minFrameRate;
const PerformanceReport({
required this.startTime,
required this.endTime,
required this.averageMemoryUsage,
required this.maxMemoryUsage,
required this.averageCpuUsage,
required this.maxCpuUsage,
required this.averageNetworkLatency,
required this.maxNetworkLatency,
required this.averageFrameRate,
required this.minFrameRate,
});
factory PerformanceReport.fromSnapshots(List<PerformanceSnapshot> snapshots) {
if (snapshots.isEmpty) {
throw ArgumentError('Cannot create report from empty snapshots');
}
final metrics = snapshots.map((s) => s.metrics).toList();
return PerformanceReport(
startTime: snapshots.first.timestamp,
endTime: snapshots.last.timestamp,
averageMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a + b) / metrics.length,
maxMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a > b ? a : b),
averageCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a + b) / metrics.length,
maxCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a > b ? a : b),
averageNetworkLatency: metrics.map((m) => m.networkLatency.toDouble()).reduce((a, b) => a + b) / metrics.length,
maxNetworkLatency: metrics.map((m) => m.networkLatency.toDouble()).reduce((a, b) => a > b ? a : b),
averageFrameRate: metrics.map((m) => m.frameRate).reduce((a, b) => a + b) / metrics.length,
minFrameRate: metrics.map((m) => m.frameRate).reduce((a, b) => a < b ? a : b),
);
}
}
/// Statistiques de performance
class PerformanceStats {
final int totalSnapshots;
final Duration totalUptime;
final double averageMemoryUsage;
final double peakMemoryUsage;
final double averageCpuUsage;
final double peakCpuUsage;
final int alertsGenerated;
const PerformanceStats({
required this.totalSnapshots,
required this.totalUptime,
required this.averageMemoryUsage,
required this.peakMemoryUsage,
required this.averageCpuUsage,
required this.peakCpuUsage,
required this.alertsGenerated,
});
factory PerformanceStats.empty() {
return const PerformanceStats(
totalSnapshots: 0,
totalUptime: Duration.zero,
averageMemoryUsage: 0.0,
peakMemoryUsage: 0.0,
averageCpuUsage: 0.0,
peakCpuUsage: 0.0,
alertsGenerated: 0,
);
}
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots) {
if (snapshots.isEmpty) return PerformanceStats.empty();
final metrics = snapshots.map((s) => s.metrics).toList();
return PerformanceStats(
totalSnapshots: snapshots.length,
totalUptime: snapshots.last.timestamp.difference(snapshots.first.timestamp),
averageMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a + b) / metrics.length,
peakMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a > b ? a : b),
averageCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a + b) / metrics.length,
peakCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a > b ? a : b),
alertsGenerated: 0, // À implémenter si nécessaire
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:get_it/get_it.dart';
import '../data/datasources/dashboard_remote_datasource.dart';
import '../data/repositories/dashboard_repository_impl.dart';
import '../domain/repositories/dashboard_repository.dart';
import '../domain/usecases/get_dashboard_data.dart';
import '../presentation/bloc/dashboard_bloc.dart';
import '../../../core/network/dio_client.dart';
import '../../../core/network/network_info.dart';
/// Configuration de l'injection de dépendances pour le module Dashboard
class DashboardDI {
static final GetIt _getIt = GetIt.instance;
/// Enregistre toutes les dépendances du module Dashboard
static void registerDependencies() {
// Data Sources
_getIt.registerLazySingleton<DashboardRemoteDataSource>(
() => DashboardRemoteDataSourceImpl(
dioClient: _getIt<DioClient>(),
),
);
// Repositories
_getIt.registerLazySingleton<DashboardRepository>(
() => DashboardRepositoryImpl(
remoteDataSource: _getIt<DashboardRemoteDataSource>(),
networkInfo: _getIt<NetworkInfo>(),
),
);
// Use Cases
_getIt.registerLazySingleton(() => GetDashboardData(_getIt<DashboardRepository>()));
_getIt.registerLazySingleton(() => GetDashboardStats(_getIt<DashboardRepository>()));
_getIt.registerLazySingleton(() => GetRecentActivities(_getIt<DashboardRepository>()));
_getIt.registerLazySingleton(() => GetUpcomingEvents(_getIt<DashboardRepository>()));
// BLoC
_getIt.registerFactory(
() => DashboardBloc(
getDashboardData: _getIt<GetDashboardData>(),
getDashboardStats: _getIt<GetDashboardStats>(),
getRecentActivities: _getIt<GetRecentActivities>(),
getUpcomingEvents: _getIt<GetUpcomingEvents>(),
),
);
}
/// Nettoie les dépendances du module Dashboard
static void unregisterDependencies() {
_getIt.unregister<DashboardBloc>();
_getIt.unregister<GetUpcomingEvents>();
_getIt.unregister<GetRecentActivities>();
_getIt.unregister<GetDashboardStats>();
_getIt.unregister<GetDashboardData>();
_getIt.unregister<DashboardRepository>();
_getIt.unregister<DashboardRemoteDataSource>();
}
}

View File

@@ -0,0 +1,230 @@
import 'package:equatable/equatable.dart';
/// Entité pour les statistiques du dashboard
class DashboardStatsEntity extends Equatable {
final int totalMembers;
final int activeMembers;
final int totalEvents;
final int upcomingEvents;
final int totalContributions;
final double totalContributionAmount;
final int pendingRequests;
final int completedProjects;
final double monthlyGrowth;
final double engagementRate;
final DateTime lastUpdated;
const DashboardStatsEntity({
required this.totalMembers,
required this.activeMembers,
required this.totalEvents,
required this.upcomingEvents,
required this.totalContributions,
required this.totalContributionAmount,
required this.pendingRequests,
required this.completedProjects,
required this.monthlyGrowth,
required this.engagementRate,
required this.lastUpdated,
});
// Méthodes utilitaires
double get memberActivityRate => totalMembers > 0 ? activeMembers / totalMembers : 0.0;
bool get hasGrowth => monthlyGrowth > 0;
bool get isHighEngagement => engagementRate > 0.7;
String get formattedContributionAmount {
if (totalContributionAmount >= 1000000) {
return '${(totalContributionAmount / 1000000).toStringAsFixed(1)}M';
} else if (totalContributionAmount >= 1000) {
return '${(totalContributionAmount / 1000).toStringAsFixed(1)}K';
}
return totalContributionAmount.toStringAsFixed(0);
}
@override
List<Object?> get props => [
totalMembers,
activeMembers,
totalEvents,
upcomingEvents,
totalContributions,
totalContributionAmount,
pendingRequests,
completedProjects,
monthlyGrowth,
engagementRate,
lastUpdated,
];
}
/// Entité pour les activités récentes
class RecentActivityEntity extends Equatable {
final String id;
final String type;
final String title;
final String description;
final String? userAvatar;
final String userName;
final DateTime timestamp;
final String? actionUrl;
final Map<String, dynamic>? metadata;
const RecentActivityEntity({
required this.id,
required this.type,
required this.title,
required this.description,
this.userAvatar,
required this.userName,
required this.timestamp,
this.actionUrl,
this.metadata,
});
// Méthodes utilitaires
String get timeAgo {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inDays > 0) {
return '${difference.inDays}j';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}min';
} else {
return 'maintenant';
}
}
bool get isRecent => DateTime.now().difference(timestamp).inHours < 24;
bool get hasAction => actionUrl != null && actionUrl!.isNotEmpty;
@override
List<Object?> get props => [
id,
type,
title,
description,
userAvatar,
userName,
timestamp,
actionUrl,
metadata,
];
}
/// Entité pour les événements à venir
class UpcomingEventEntity extends Equatable {
final String id;
final String title;
final String description;
final DateTime startDate;
final DateTime? endDate;
final String location;
final int maxParticipants;
final int currentParticipants;
final String status;
final String? imageUrl;
final List<String> tags;
const UpcomingEventEntity({
required this.id,
required this.title,
required this.description,
required this.startDate,
this.endDate,
required this.location,
required this.maxParticipants,
required this.currentParticipants,
required this.status,
this.imageUrl,
required this.tags,
});
// Méthodes utilitaires
bool get isAlmostFull => currentParticipants >= (maxParticipants * 0.8);
bool get isFull => currentParticipants >= maxParticipants;
double get fillPercentage => maxParticipants > 0 ? currentParticipants / maxParticipants : 0.0;
String get daysUntilEvent {
final now = DateTime.now();
final difference = startDate.difference(now);
if (difference.inDays > 0) {
return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}min';
} else {
return 'En cours';
}
}
bool get isToday {
final now = DateTime.now();
return startDate.year == now.year &&
startDate.month == now.month &&
startDate.day == now.day;
}
bool get isTomorrow {
final tomorrow = DateTime.now().add(const Duration(days: 1));
return startDate.year == tomorrow.year &&
startDate.month == tomorrow.month &&
startDate.day == tomorrow.day;
}
@override
List<Object?> get props => [
id,
title,
description,
startDate,
endDate,
location,
maxParticipants,
currentParticipants,
status,
imageUrl,
tags,
];
}
/// Entité principale du dashboard
class DashboardEntity extends Equatable {
final DashboardStatsEntity stats;
final List<RecentActivityEntity> recentActivities;
final List<UpcomingEventEntity> upcomingEvents;
final Map<String, dynamic> userPreferences;
final String organizationId;
final String userId;
const DashboardEntity({
required this.stats,
required this.recentActivities,
required this.upcomingEvents,
required this.userPreferences,
required this.organizationId,
required this.userId,
});
// Méthodes utilitaires
bool get hasRecentActivity => recentActivities.isNotEmpty;
bool get hasUpcomingEvents => upcomingEvents.isNotEmpty;
int get todayEventsCount => upcomingEvents.where((e) => e.isToday).length;
int get tomorrowEventsCount => upcomingEvents.where((e) => e.isTomorrow).length;
int get recentActivitiesCount => recentActivities.length;
@override
List<Object?> get props => [
stats,
recentActivities,
upcomingEvents,
userPreferences,
organizationId,
userId,
];
}

View File

@@ -0,0 +1,27 @@
import 'package:dartz/dartz.dart';
import '../entities/dashboard_entity.dart';
import '../../../../core/error/failures.dart';
abstract class DashboardRepository {
Future<Either<Failure, DashboardEntity>> getDashboardData(
String organizationId,
String userId,
);
Future<Either<Failure, DashboardStatsEntity>> getDashboardStats(
String organizationId,
String userId,
);
Future<Either<Failure, List<RecentActivityEntity>>> getRecentActivities(
String organizationId,
String userId, {
int limit = 10,
});
Future<Either<Failure, List<UpcomingEventEntity>>> getUpcomingEvents(
String organizationId,
String userId, {
int limit = 5,
});
}

View File

@@ -0,0 +1,120 @@
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../entities/dashboard_entity.dart';
import '../repositories/dashboard_repository.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
class GetDashboardData implements UseCase<DashboardEntity, GetDashboardDataParams> {
final DashboardRepository repository;
GetDashboardData(this.repository);
@override
Future<Either<Failure, DashboardEntity>> call(GetDashboardDataParams params) async {
return await repository.getDashboardData(
params.organizationId,
params.userId,
);
}
}
class GetDashboardDataParams extends Equatable {
final String organizationId;
final String userId;
const GetDashboardDataParams({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class GetDashboardStats implements UseCase<DashboardStatsEntity, GetDashboardStatsParams> {
final DashboardRepository repository;
GetDashboardStats(this.repository);
@override
Future<Either<Failure, DashboardStatsEntity>> call(GetDashboardStatsParams params) async {
return await repository.getDashboardStats(
params.organizationId,
params.userId,
);
}
}
class GetDashboardStatsParams extends Equatable {
final String organizationId;
final String userId;
const GetDashboardStatsParams({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class GetRecentActivities implements UseCase<List<RecentActivityEntity>, GetRecentActivitiesParams> {
final DashboardRepository repository;
GetRecentActivities(this.repository);
@override
Future<Either<Failure, List<RecentActivityEntity>>> call(GetRecentActivitiesParams params) async {
return await repository.getRecentActivities(
params.organizationId,
params.userId,
limit: params.limit,
);
}
}
class GetRecentActivitiesParams extends Equatable {
final String organizationId;
final String userId;
final int limit;
const GetRecentActivitiesParams({
required this.organizationId,
required this.userId,
this.limit = 10,
});
@override
List<Object> get props => [organizationId, userId, limit];
}
class GetUpcomingEvents implements UseCase<List<UpcomingEventEntity>, GetUpcomingEventsParams> {
final DashboardRepository repository;
GetUpcomingEvents(this.repository);
@override
Future<Either<Failure, List<UpcomingEventEntity>>> call(GetUpcomingEventsParams params) async {
return await repository.getUpcomingEvents(
params.organizationId,
params.userId,
limit: params.limit,
);
}
}
class GetUpcomingEventsParams extends Equatable {
final String organizationId;
final String userId;
final int limit;
const GetUpcomingEventsParams({
required this.organizationId,
required this.userId,
this.limit = 5,
});
@override
List<Object> get props => [organizationId, userId, limit];
}

View File

@@ -0,0 +1,174 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/dashboard_entity.dart';
import '../../domain/usecases/get_dashboard_data.dart';
import '../../../../core/error/failures.dart';
part 'dashboard_event.dart';
part 'dashboard_state.dart';
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
final GetDashboardData getDashboardData;
final GetDashboardStats getDashboardStats;
final GetRecentActivities getRecentActivities;
final GetUpcomingEvents getUpcomingEvents;
DashboardBloc({
required this.getDashboardData,
required this.getDashboardStats,
required this.getRecentActivities,
required this.getUpcomingEvents,
}) : super(DashboardInitial()) {
on<LoadDashboardData>(_onLoadDashboardData);
on<RefreshDashboardData>(_onRefreshDashboardData);
on<LoadDashboardStats>(_onLoadDashboardStats);
on<LoadRecentActivities>(_onLoadRecentActivities);
on<LoadUpcomingEvents>(_onLoadUpcomingEvents);
}
Future<void> _onLoadDashboardData(
LoadDashboardData event,
Emitter<DashboardState> emit,
) async {
emit(DashboardLoading());
final result = await getDashboardData(
GetDashboardDataParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(dashboardData) => emit(DashboardLoaded(dashboardData)),
);
}
Future<void> _onRefreshDashboardData(
RefreshDashboardData event,
Emitter<DashboardState> emit,
) async {
// Garde l'état actuel pendant le refresh
if (state is DashboardLoaded) {
emit(DashboardRefreshing((state as DashboardLoaded).dashboardData));
} else {
emit(DashboardLoading());
}
final result = await getDashboardData(
GetDashboardDataParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(dashboardData) => emit(DashboardLoaded(dashboardData)),
);
}
Future<void> _onLoadDashboardStats(
LoadDashboardStats event,
Emitter<DashboardState> emit,
) async {
final result = await getDashboardStats(
GetDashboardStatsParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(stats) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: stats,
recentActivities: currentData.recentActivities,
upcomingEvents: currentData.upcomingEvents,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
Future<void> _onLoadRecentActivities(
LoadRecentActivities event,
Emitter<DashboardState> emit,
) async {
final result = await getRecentActivities(
GetRecentActivitiesParams(
organizationId: event.organizationId,
userId: event.userId,
limit: event.limit,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(activities) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: currentData.stats,
recentActivities: activities,
upcomingEvents: currentData.upcomingEvents,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
Future<void> _onLoadUpcomingEvents(
LoadUpcomingEvents event,
Emitter<DashboardState> emit,
) async {
final result = await getUpcomingEvents(
GetUpcomingEventsParams(
organizationId: event.organizationId,
userId: event.userId,
limit: event.limit,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(events) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: currentData.stats,
recentActivities: currentData.recentActivities,
upcomingEvents: events,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return 'Erreur serveur. Veuillez réessayer.';
case NetworkFailure:
return 'Pas de connexion internet. Vérifiez votre connexion.';
default:
return 'Une erreur inattendue s\'est produite.';
}
}
}

View File

@@ -0,0 +1,77 @@
part of 'dashboard_bloc.dart';
abstract class DashboardEvent extends Equatable {
const DashboardEvent();
@override
List<Object> get props => [];
}
class LoadDashboardData extends DashboardEvent {
final String organizationId;
final String userId;
const LoadDashboardData({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class RefreshDashboardData extends DashboardEvent {
final String organizationId;
final String userId;
const RefreshDashboardData({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class LoadDashboardStats extends DashboardEvent {
final String organizationId;
final String userId;
const LoadDashboardStats({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class LoadRecentActivities extends DashboardEvent {
final String organizationId;
final String userId;
final int limit;
const LoadRecentActivities({
required this.organizationId,
required this.userId,
this.limit = 10,
});
@override
List<Object> get props => [organizationId, userId, limit];
}
class LoadUpcomingEvents extends DashboardEvent {
final String organizationId;
final String userId;
final int limit;
const LoadUpcomingEvents({
required this.organizationId,
required this.userId,
this.limit = 5,
});
@override
List<Object> get props => [organizationId, userId, limit];
}

View File

@@ -0,0 +1,39 @@
part of 'dashboard_bloc.dart';
abstract class DashboardState extends Equatable {
const DashboardState();
@override
List<Object> get props => [];
}
class DashboardInitial extends DashboardState {}
class DashboardLoading extends DashboardState {}
class DashboardLoaded extends DashboardState {
final DashboardEntity dashboardData;
const DashboardLoaded(this.dashboardData);
@override
List<Object> get props => [dashboardData];
}
class DashboardRefreshing extends DashboardState {
final DashboardEntity dashboardData;
const DashboardRefreshing(this.dashboardData);
@override
List<Object> get props => [dashboardData];
}
class DashboardError extends DashboardState {
final String message;
const DashboardError(this.message);
@override
List<Object> get props => [message];
}

View File

@@ -1,360 +0,0 @@
import 'package:flutter/material.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.
class PerformanceCard extends StatelessWidget {
/// Titre de la carte
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Liste des métriques de performance
final List<PerformanceMetric> metrics;
/// Style de la carte
final PerformanceCardStyle style;
/// Callback lors du tap sur la carte
final VoidCallback? onTap;
/// Afficher ou non les valeurs numériques
final bool showValues;
/// Afficher ou non les barres de progression
final bool showProgressBars;
const PerformanceCard({
super.key,
required this.title,
this.subtitle,
required this.metrics,
this.style = PerformanceCardStyle.elevated,
this.onTap,
this.showValues = true,
this.showProgressBars = true,
});
/// Constructeur pour les métriques serveur
const PerformanceCard.server({
super.key,
this.onTap,
}) : title = 'Performance Serveur',
subtitle = 'Métriques temps réel',
metrics = const [
PerformanceMetric(
label: 'CPU',
value: 67.3,
unit: '%',
color: Colors.orange,
threshold: 80,
),
PerformanceMetric(
label: 'RAM',
value: 78.5,
unit: '%',
color: Colors.blue,
threshold: 85,
),
PerformanceMetric(
label: 'Disque',
value: 45.2,
unit: '%',
color: Colors.green,
threshold: 90,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
/// Constructeur pour les métriques réseau
const PerformanceCard.network({
super.key,
this.onTap,
}) : title = 'Réseau',
subtitle = 'Trafic et latence',
metrics = const [
PerformanceMetric(
label: 'Bande passante',
value: 23.4,
unit: 'MB/s',
color: Color(0xFF6C5CE7),
threshold: 100,
),
PerformanceMetric(
label: 'Latence',
value: 12.7,
unit: 'ms',
color: Color(0xFF00B894),
threshold: 50,
),
PerformanceMetric(
label: 'Paquets perdus',
value: 0.02,
unit: '%',
color: Colors.red,
threshold: 1,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: _getDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 12),
_buildMetrics(),
],
),
),
);
}
/// En-tête de la carte
Widget _buildHeader() {
return 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],
),
),
],
],
);
}
/// Construction des métriques
Widget _buildMetrics() {
return Column(
children: metrics.map((metric) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildMetricRow(metric),
)).toList(),
);
}
/// Ligne de métrique
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;
} else if (isWarning) {
effectiveColor = Colors.orange;
}
return Column(
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
metric.label,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const Spacer(),
if (showValues)
Text(
'${metric.value.toStringAsFixed(1)}${metric.unit}',
style: TextStyle(
color: effectiveColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
],
),
if (showProgressBars) ...[
const SizedBox(height: 4),
_buildProgressBar(metric, effectiveColor),
],
],
);
}
/// 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),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}
/// 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
class PerformanceMetric {
final String label;
final double value;
final String unit;
final Color color;
final double threshold;
final Map<String, dynamic>? metadata;
const PerformanceMetric({
required this.label,
required this.value,
required this.unit,
required this.color,
required this.threshold,
this.metadata,
});
/// Constructeur pour une métrique CPU
const PerformanceMetric.cpu(double value)
: label = 'CPU',
value = value,
unit = '%',
color = Colors.orange,
threshold = 80,
metadata = null;
/// Constructeur pour une métrique RAM
const PerformanceMetric.memory(double value)
: label = 'Mémoire',
value = value,
unit = '%',
color = Colors.blue,
threshold = 85,
metadata = null;
/// Constructeur pour une métrique disque
const PerformanceMetric.disk(double value)
: label = 'Disque',
value = value,
unit = '%',
color = Colors.green,
threshold = 90,
metadata = null;
/// Constructeur pour une métrique réseau
PerformanceMetric.network(double value, String unit)
: label = 'Réseau',
value = value,
unit = unit,
color = const Color(0xFF6C5CE7),
threshold = 100,
metadata = null;
/// Niveau de criticité de la métrique
MetricLevel get level {
if (value > threshold) return MetricLevel.critical;
if (value > threshold * 0.8) return MetricLevel.warning;
if (value > threshold * 0.6) return MetricLevel.normal;
return MetricLevel.good;
}
/// Couleur selon le niveau
Color get levelColor {
switch (level) {
case MetricLevel.good:
return Colors.green;
case MetricLevel.normal:
return color;
case MetricLevel.warning:
return Colors.orange;
case MetricLevel.critical:
return Colors.red;
}
}
}
/// Niveaux de métrique
enum MetricLevel {
good,
normal,
warning,
critical,
}
/// Styles de carte de performance
enum PerformanceCardStyle {
elevated,
outlined,
minimal,
}

View File

@@ -1,418 +0,0 @@
/// Dashboard Adaptatif Principal - Orchestrateur Intelligent
/// Sélectionne et affiche le dashboard approprié selon le rôle utilisateur
library adaptive_dashboard_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/models/user_role.dart';
import '../../../../core/widgets/adaptive_widget.dart';
import 'role_dashboards/super_admin_dashboard.dart';
import 'role_dashboards/org_admin_dashboard.dart';
import 'role_dashboards/moderator_dashboard.dart';
import 'role_dashboards/active_member_dashboard.dart';
import 'role_dashboards/simple_member_dashboard.dart';
import 'role_dashboards/visitor_dashboard.dart';
/// Page Dashboard Adaptatif - Le cœur du système morphique
///
/// Cette page utilise l'AdaptiveWidget pour afficher automatiquement
/// le dashboard approprié selon le rôle de l'utilisateur connecté.
///
/// Fonctionnalités :
/// - Morphing automatique entre les dashboards
/// - Animations fluides lors des changements de rôle
/// - Gestion des états de chargement et d'erreur
/// - Fallback gracieux pour les rôles non supportés
class AdaptiveDashboardPage extends StatefulWidget {
const AdaptiveDashboardPage({super.key});
@override
State<AdaptiveDashboardPage> createState() => _AdaptiveDashboardPageState();
}
class _AdaptiveDashboardPageState extends State<AdaptiveDashboardPage>
with TickerProviderStateMixin {
/// Contrôleur d'animation pour les transitions
late AnimationController _transitionController;
/// Animation de fade pour les transitions
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
}
@override
void dispose() {
_transitionController.dispose();
super.dispose();
}
/// Initialise les animations de transition
void _initializeAnimations() {
_transitionController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _transitionController,
curve: Curves.easeInOutCubic,
));
// Démarrer l'animation initiale
_transitionController.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
// Déclencher l'animation lors des changements d'état
if (state is AuthAuthenticated) {
_transitionController.reset();
_transitionController.forward();
}
},
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: _buildAdaptiveDashboard(),
);
},
),
),
);
}
/// Construit le dashboard adaptatif selon le rôle
Widget _buildAdaptiveDashboard() {
return AdaptiveWidget(
// Mapping des rôles vers leurs dashboards spécifiques
roleWidgets: {
UserRole.superAdmin: () => const SuperAdminDashboard(),
UserRole.orgAdmin: () => const OrgAdminDashboard(),
UserRole.moderator: () => const ModeratorDashboard(),
UserRole.activeMember: () => const ActiveMemberDashboard(),
UserRole.simpleMember: () => const SimpleMemberDashboard(),
UserRole.visitor: () => const VisitorDashboard(),
},
// Permissions requises pour accéder au dashboard
requiredPermissions: const [
'dashboard.view.own',
],
// Widget affiché si les permissions sont insuffisantes
fallbackWidget: _buildUnauthorizedDashboard(),
// Widget affiché pendant le chargement
loadingWidget: _buildLoadingDashboard(),
// Configuration des animations
enableMorphing: true,
morphingDuration: const Duration(milliseconds: 800),
animationCurve: Curves.easeInOutCubic,
// Audit trail activé
auditLog: true,
);
}
/// Dashboard affiché en cas d'accès non autorisé
Widget _buildUnauthorizedDashboard() {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFF8F9FA),
Color(0xFFE9ECEF),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône d'accès refusé
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
),
child: const Icon(
Icons.lock_outline,
size: 60,
color: Colors.red,
),
),
const SizedBox(height: 32),
// Titre
Text(
'Accès Non Autorisé',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 16),
// Description
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Vous n\'avez pas les permissions nécessaires pour accéder au dashboard. Veuillez contacter un administrateur.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
),
),
const SizedBox(height: 32),
// Bouton de contact
ElevatedButton.icon(
onPressed: () => _onContactSupport(),
icon: const Icon(Icons.support_agent),
label: const Text('Contacter le Support'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
),
);
}
/// Dashboard affiché pendant le chargement
Widget _buildLoadingDashboard() {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo animé
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(seconds: 2),
builder: (context, value, child) {
return Transform.rotate(
angle: value * 2 * 3.14159,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.dashboard,
color: Colors.white,
size: 40,
),
),
);
},
),
const SizedBox(height: 32),
// Titre
Text(
'UnionFlow',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Indicateur de chargement
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
const SizedBox(height: 16),
// Message de chargement
Text(
'Préparation de votre dashboard...',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
),
);
}
/// Gère le contact avec le support
void _onContactSupport() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Contacter le Support'),
content: const Text(
'Pour obtenir de l\'aide, veuillez envoyer un email à :\n\nsupport@unionflow.com\n\nOu appelez le :\n+33 1 23 45 67 89',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Ici, on pourrait ouvrir l'app email ou téléphone
},
child: const Text('Envoyer Email'),
),
],
),
);
}
}
/// Extension pour faciliter la navigation vers le dashboard adaptatif
extension AdaptiveDashboardNavigation on BuildContext {
/// Navigue vers le dashboard adaptatif
void navigateToAdaptiveDashboard() {
Navigator.of(this).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const AdaptiveDashboardPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 600),
),
);
}
}
/// Mixin pour les dashboards qui ont besoin de fonctionnalités communes
mixin DashboardMixin<T extends StatefulWidget> on State<T> {
/// Affiche une notification de succès
void showSuccessNotification(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
/// Affiche une notification d'erreur
void showErrorNotification(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
/// Affiche une boîte de dialogue de confirmation
Future<bool> showConfirmationDialog(String title, String message) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirmer'),
),
],
),
);
return result ?? false;
}
}

View File

@@ -0,0 +1,483 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/dashboard_bloc.dart';
import '../widgets/connected/connected_stats_card.dart';
import '../widgets/connected/connected_recent_activities.dart';
import '../widgets/connected/connected_upcoming_events.dart';
import '../widgets/charts/dashboard_chart_widget.dart';
import '../widgets/metrics/real_time_metrics_widget.dart';
import '../widgets/notifications/dashboard_notifications_widget.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
import '../../../../core/di/injection_container.dart';
/// Page dashboard avancée avec graphiques et analytics
class AdvancedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const AdvancedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<AdvancedDashboardPage> createState() => _AdvancedDashboardPageState();
}
class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
with TickerProviderStateMixin {
late DashboardBloc _dashboardBloc;
late TabController _tabController;
@override
void initState() {
super.initState();
_dashboardBloc = sl<DashboardBloc>();
_tabController = TabController(length: 3, vsync: this);
_loadDashboardData();
}
void _loadDashboardData() {
_dashboardBloc.add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
void _refreshDashboardData() {
_dashboardBloc.add(RefreshDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _dashboardBloc,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
_buildSliverAppBar(),
],
body: Column(
children: [
_buildTabBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(),
_buildAnalyticsTab(),
_buildReportsTab(),
],
),
),
],
),
),
floatingActionButton: _buildFloatingActionButton(),
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: DashboardTheme.headerDecoration,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: const Icon(
Icons.dashboard,
color: DashboardTheme.white,
size: 32,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard Avancé',
style: DashboardTheme.titleLarge.copyWith(
color: DashboardTheme.white,
fontSize: 28,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Analytics & Insights',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white.withOpacity(0.9),
),
),
],
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return Row(
children: [
_buildQuickStat(
'Membres',
'${data.stats.activeMembers}/${data.stats.totalMembers}',
Icons.people,
),
const SizedBox(width: DashboardTheme.spacing16),
_buildQuickStat(
'Événements',
'${data.stats.upcomingEvents}',
Icons.event,
),
const SizedBox(width: DashboardTheme.spacing16),
_buildQuickStat(
'Croissance',
'${data.stats.monthlyGrowth.toStringAsFixed(1)}%',
Icons.trending_up,
),
],
);
}
return const SizedBox.shrink();
},
),
],
),
),
),
),
),
actions: [
IconButton(
onPressed: _refreshDashboardData,
icon: const Icon(
Icons.refresh,
color: DashboardTheme.white,
),
),
IconButton(
onPressed: () {
// TODO: Ouvrir les paramètres
},
icon: const Icon(
Icons.settings,
color: DashboardTheme.white,
),
),
],
);
}
Widget _buildQuickStat(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing12,
vertical: DashboardTheme.spacing8,
),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: DashboardTheme.white,
size: 16,
),
const SizedBox(width: DashboardTheme.spacing8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
),
),
],
),
],
),
);
}
Widget _buildTabBar() {
return Container(
color: DashboardTheme.white,
child: TabBar(
controller: _tabController,
labelColor: DashboardTheme.royalBlue,
unselectedLabelColor: DashboardTheme.grey500,
indicatorColor: DashboardTheme.royalBlue,
tabs: const [
Tab(text: 'Vue d\'ensemble', icon: Icon(Icons.dashboard)),
Tab(text: 'Analytics', icon: Icon(Icons.analytics)),
Tab(text: 'Rapports', icon: Icon(Icons.assessment)),
],
),
);
}
Widget _buildOverviewTab() {
return RefreshIndicator(
onRefresh: () async => _refreshDashboardData(),
color: DashboardTheme.royalBlue,
child: SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
// Métriques temps réel
RealTimeMetricsWidget(
organizationId: widget.organizationId,
userId: widget.userId,
),
const SizedBox(height: DashboardTheme.spacing24),
// Grille de statistiques
_buildStatsGrid(),
const SizedBox(height: DashboardTheme.spacing24),
// Notifications
const DashboardNotificationsWidget(maxNotifications: 3),
const SizedBox(height: DashboardTheme.spacing24),
// Activités et événements
const Row(
children: [
Expanded(
child: ConnectedRecentActivities(maxItems: 3),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedUpcomingEvents(maxItems: 2),
),
],
),
],
),
),
);
}
Widget _buildAnalyticsTab() {
return const SingleChildScrollView(
padding: EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
Row(
children: [
Expanded(
child: DashboardChartWidget(
title: 'Activité des Membres',
chartType: DashboardChartType.memberActivity,
height: 250,
),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: DashboardChartWidget(
title: 'Croissance Mensuelle',
chartType: DashboardChartType.monthlyGrowth,
height: 250,
),
),
],
),
SizedBox(height: DashboardTheme.spacing24),
DashboardChartWidget(
title: 'Tendance des Contributions',
chartType: DashboardChartType.contributionTrend,
height: 300,
),
SizedBox(height: DashboardTheme.spacing24),
DashboardChartWidget(
title: 'Participation aux Événements',
chartType: DashboardChartType.eventParticipation,
height: 250,
),
],
),
);
}
Widget _buildReportsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
_buildReportCard(
'Rapport Mensuel',
'Synthèse complète des activités du mois',
Icons.calendar_month,
DashboardTheme.royalBlue,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport Financier',
'État des contributions et finances',
Icons.account_balance,
DashboardTheme.tealBlue,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport d\'Activité',
'Analyse de l\'engagement des membres',
Icons.trending_up,
DashboardTheme.success,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport Événements',
'Statistiques des événements organisés',
Icons.event_note,
DashboardTheme.warning,
),
],
),
);
}
Widget _buildStatsGrid() {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: DashboardTheme.spacing16,
mainAxisSpacing: DashboardTheme.spacing16,
childAspectRatio: 1.2,
children: [
ConnectedStatsCard(
title: 'Membres totaux',
icon: Icons.people,
valueExtractor: (stats) => stats.totalMembers.toString(),
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
customColor: DashboardTheme.royalBlue,
),
ConnectedStatsCard(
title: 'Contributions',
icon: Icons.payment,
valueExtractor: (stats) => stats.formattedContributionAmount,
subtitleExtractor: (stats) => '${stats.totalContributions} versements',
customColor: DashboardTheme.tealBlue,
),
ConnectedStatsCard(
title: 'Événements',
icon: Icons.event,
valueExtractor: (stats) => stats.totalEvents.toString(),
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
customColor: DashboardTheme.success,
),
ConnectedStatsCard(
title: 'Engagement',
icon: Icons.favorite,
valueExtractor: (stats) => '${(stats.engagementRate * 100).toStringAsFixed(0)}%',
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Moyen',
customColor: DashboardTheme.warning,
),
],
);
}
Widget _buildReportCard(String title, String description, IconData icon, Color color) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.titleSmall,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
description,
style: DashboardTheme.bodySmall,
),
],
),
),
IconButton(
onPressed: () {
// TODO: Générer le rapport
},
icon: Icon(
Icons.download,
color: color,
),
),
],
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: () {
// TODO: Actions rapides
},
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
icon: const Icon(Icons.add),
label: const Text('Action'),
);
}
@override
void dispose() {
_tabController.dispose();
_dashboardBloc.close();
super.dispose();
}
}

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/dashboard_bloc.dart';
import '../widgets/connected/connected_stats_card.dart';
import '../widgets/connected/connected_recent_activities.dart';
import '../widgets/connected/connected_upcoming_events.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
/// Page dashboard connectée au backend
class ConnectedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const ConnectedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
}
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> {
@override
void initState() {
super.initState();
// Charger les données du dashboard
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: DashboardTheme.grey50,
appBar: AppBar(
title: const Text('Dashboard'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
elevation: 0,
),
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(
color: DashboardTheme.royalBlue,
),
);
}
if (state is DashboardError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: DashboardTheme.error,
),
const SizedBox(height: DashboardTheme.spacing16),
const Text(
'Erreur de chargement',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
state.message,
style: DashboardTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: DashboardTheme.spacing24),
ElevatedButton(
onPressed: () {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
style: ElevatedButton.styleFrom(
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
),
child: const Text('Réessayer'),
),
],
),
);
}
if (state is DashboardLoaded) {
return RefreshIndicator(
onRefresh: () async {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
color: DashboardTheme.royalBlue,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Statistiques
Row(
children: [
Expanded(
child: ConnectedStatsCard(
title: 'Membres',
icon: Icons.people,
valueExtractor: (stats) => stats.totalMembers.toString(),
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedStatsCard(
title: 'Événements',
icon: Icons.event,
valueExtractor: (stats) => stats.totalEvents.toString(),
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
),
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
// Activités récentes et événements à venir
const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ConnectedRecentActivities(),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedUpcomingEvents(),
),
],
),
],
),
),
);
}
return const SizedBox.shrink();
},
),
);
}
}

View File

@@ -1,270 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Page principale du tableau de bord - Version simple
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('UnionFlow - Tableau de bord'),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifications - Fonctionnalité à venir'),
duration: Duration(seconds: 2),
),
);
},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paramètres - Fonctionnalité à venir'),
duration: Duration(seconds: 2),
),
);
},
),
],
),
body: RefreshIndicator(
onRefresh: _refreshDashboard,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bienvenue sur UnionFlow',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
'Votre plateforme de gestion d\'union familiale',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
),
const SizedBox(height: 24),
// Statistiques rapides
Row(
children: [
Expanded(
child: _buildStatCard(
'Membres',
'25',
Icons.people,
Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Cotisations',
'15',
Icons.payment,
Colors.green,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Événements',
'8',
Icons.event,
Colors.orange,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Solidarité',
'3',
Icons.favorite,
Colors.red,
),
),
],
),
const SizedBox(height: 24),
// Actions rapides
Text(
'Actions rapides',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
children: [
_buildActionCard(
'Nouveau membre',
Icons.person_add,
Colors.blue,
() => _showComingSoon('Nouveau membre'),
),
_buildActionCard(
'Nouvelle cotisation',
Icons.add_card,
Colors.green,
() => _showComingSoon('Nouvelle cotisation'),
),
_buildActionCard(
'Nouvel événement',
Icons.event_available,
Colors.orange,
() => _showComingSoon('Nouvel événement'),
),
_buildActionCard(
'Demande d\'aide',
Icons.help_outline,
Colors.red,
() => _showComingSoon('Demande d\'aide'),
),
],
),
const SizedBox(height: 24),
],
),
),
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: color, size: 24),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildActionCard(String title, IconData icon, Color color, VoidCallback onTap) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
void _showComingSoon(String feature) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - Fonctionnalité à venir'),
duration: const Duration(seconds: 2),
),
);
}
Future<void> _refreshDashboard() async {
// Simuler un délai de rafraîchissement
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tableau de bord actualisé'),
duration: Duration(seconds: 2),
backgroundColor: Colors.green,
),
);
}
}
}

View File

@@ -1,121 +0,0 @@
/// Dashboard Page Stable - Redirecteur vers Dashboard Adaptatif
/// Redirige automatiquement vers le nouveau système de dashboard adaptatif
library dashboard_page_stable;
import 'package:flutter/material.dart';
import 'adaptive_dashboard_page.dart';
/// Page Dashboard Stable - Maintenant un redirecteur
///
/// Cette page redirige automatiquement vers le nouveau système
/// de dashboard adaptatif basé sur les rôles utilisateurs.
class DashboardPageStable extends StatefulWidget {
const DashboardPageStable({super.key});
@override
State<DashboardPageStable> createState() => _DashboardPageStableState();
}
class _DashboardPageStableState extends State<DashboardPageStable> {
@override
void initState() {
super.initState();
// Rediriger automatiquement vers le dashboard adaptatif
WidgetsBinding.instance.addPostFrameCallback((_) {
_redirectToAdaptiveDashboard();
});
}
/// Redirige vers le dashboard adaptatif
void _redirectToAdaptiveDashboard() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const AdaptiveDashboardPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 600),
),
);
}
@override
Widget build(BuildContext context) {
// Afficher un écran de chargement pendant la redirection
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
],
),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Icon(
Icons.dashboard,
color: Colors.white,
size: 80,
),
SizedBox(height: 24),
// Titre
Text(
'UnionFlow',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
// Indicateur de chargement
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
SizedBox(height: 16),
// Message
Text(
'Chargement de votre dashboard...',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
),
);
}
}

View File

@@ -1,305 +0,0 @@
import 'package:flutter/material.dart';
import '../widgets/dashboard_widgets.dart';
/// Exemple de dashboard refactorisé utilisant les nouveaux composants
///
/// Ce fichier démontre comment créer un dashboard sophistiqué
/// en utilisant les composants modulaires créés lors de la refactorisation.
class ExampleRefactoredDashboard extends StatelessWidget {
const ExampleRefactoredDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec informations système et actions
DashboardHeader.superAdmin(
actions: [
DashboardAction(
icon: Icons.refresh,
tooltip: 'Actualiser',
onPressed: () => _handleRefresh(context),
),
DashboardAction(
icon: Icons.settings,
tooltip: 'Paramètres',
onPressed: () => _handleSettings(context),
),
],
),
const SizedBox(height: 16),
// Section des KPIs système
QuickStatsSection.systemKPIs(
onStatTap: (stat) => _handleStatTap(context, stat),
),
const SizedBox(height: 16),
// Carte de performance serveur
PerformanceCard.server(
onTap: () => _handlePerformanceTap(context),
),
const SizedBox(height: 16),
// Section des alertes récentes
RecentActivitiesSection.alerts(
onActivityTap: (activity) => _handleActivityTap(context, activity),
onViewAll: () => _handleViewAllAlerts(context),
),
const SizedBox(height: 16),
// Section des activités système
RecentActivitiesSection.system(
onActivityTap: (activity) => _handleActivityTap(context, activity),
onViewAll: () => _handleViewAllActivities(context),
),
const SizedBox(height: 16),
// Section des événements à venir
UpcomingEventsSection.systemTasks(
onEventTap: (event) => _handleEventTap(context, event),
onViewAll: () => _handleViewAllEvents(context),
),
const SizedBox(height: 16),
// Exemple de section personnalisée avec composants individuels
_buildCustomSection(context),
const SizedBox(height: 16),
// Exemple de métriques de performance réseau
PerformanceCard.network(
onTap: () => _handleNetworkTap(context),
),
],
),
),
);
}
/// Section personnalisée utilisant les composants de base
Widget _buildCustomSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader.section(
title: 'Section Personnalisée',
subtitle: 'Exemple d\'utilisation des composants de base',
icon: Icons.extension,
),
// Grille de statistiques personnalisées
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.4,
children: [
StatCard(
title: 'Connexions',
value: '1,247',
subtitle: 'Actives maintenant',
icon: Icons.wifi,
color: const Color(0xFF6C5CE7),
onTap: () => _showSnackBar(context, 'Connexions tappées'),
),
StatCard(
title: 'Erreurs',
value: '3',
subtitle: 'Dernière heure',
icon: Icons.error_outline,
color: Colors.red,
onTap: () => _showSnackBar(context, 'Erreurs tappées'),
),
StatCard(
title: 'Succès',
value: '98.7%',
subtitle: 'Taux de réussite',
icon: Icons.check_circle_outline,
color: const Color(0xFF00B894),
onTap: () => _showSnackBar(context, 'Succès tappés'),
),
StatCard(
title: 'Latence',
value: '12ms',
subtitle: 'Moyenne',
icon: Icons.speed,
color: Colors.orange,
onTap: () => _showSnackBar(context, 'Latence tappée'),
),
],
),
const SizedBox(height: 16),
// Liste d'activités personnalisées
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: [
const SectionHeader.subsection(
title: 'Activités Personnalisées',
),
ActivityItem.system(
title: 'Configuration mise à jour',
description: 'Paramètres de sécurité modifiés',
timestamp: 'il y a 10min',
onTap: () => _showSnackBar(context, 'Configuration tappée'),
),
ActivityItem.user(
title: 'Nouvel administrateur',
description: 'Jean Dupont ajouté comme admin',
timestamp: 'il y a 1h',
onTap: () => _showSnackBar(context, 'Administrateur tappé'),
),
ActivityItem.success(
title: 'Sauvegarde terminée',
description: 'Sauvegarde automatique réussie',
timestamp: 'il y a 2h',
onTap: () => _showSnackBar(context, 'Sauvegarde tappée'),
),
],
),
),
],
);
}
// Gestionnaires d'événements
void _handleRefresh(BuildContext context) {
_showSnackBar(context, 'Actualisation en cours...');
}
void _handleSettings(BuildContext context) {
_showSnackBar(context, 'Ouverture des paramètres...');
}
void _handleStatTap(BuildContext context, QuickStat stat) {
_showSnackBar(context, 'Statistique tappée: ${stat.title}');
}
void _handlePerformanceTap(BuildContext context) {
_showSnackBar(context, 'Ouverture des détails de performance...');
}
void _handleActivityTap(BuildContext context, RecentActivity activity) {
_showSnackBar(context, 'Activité tappée: ${activity.title}');
}
void _handleEventTap(BuildContext context, UpcomingEvent event) {
_showSnackBar(context, 'Événement tappé: ${event.title}');
}
void _handleViewAllAlerts(BuildContext context) {
_showSnackBar(context, 'Affichage de toutes les alertes...');
}
void _handleViewAllActivities(BuildContext context) {
_showSnackBar(context, 'Affichage de toutes les activités...');
}
void _handleViewAllEvents(BuildContext context) {
_showSnackBar(context, 'Affichage de tous les événements...');
}
void _handleNetworkTap(BuildContext context) {
_showSnackBar(context, 'Ouverture des métriques réseau...');
}
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: const Color(0xFF6C5CE7),
duration: const Duration(seconds: 2),
),
);
}
}
/// Widget de démonstration pour tester les composants
class DashboardComponentsDemo extends StatelessWidget {
const DashboardComponentsDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Démo Composants Dashboard'),
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader.primary(
title: 'Démonstration des Composants',
subtitle: 'Tous les widgets refactorisés',
icon: Icons.widgets,
),
SectionHeader.section(
title: 'En-têtes de Dashboard',
),
DashboardHeader.superAdmin(),
SizedBox(height: 16),
DashboardHeader.orgAdmin(),
SizedBox(height: 16),
DashboardHeader.member(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Sections de Statistiques',
),
QuickStatsSection.systemKPIs(),
SizedBox(height: 16),
QuickStatsSection.organizationStats(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Cartes de Performance',
),
PerformanceCard.server(),
SizedBox(height: 16),
PerformanceCard.network(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Sections d\'Activités',
),
RecentActivitiesSection.system(),
SizedBox(height: 16),
RecentActivitiesSection.alerts(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Événements à Venir',
),
UpcomingEventsSection.organization(),
SizedBox(height: 16),
UpcomingEventsSection.systemTasks(),
],
),
),
);
}
}

View File

@@ -3,8 +3,8 @@
library moderator_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Management Hub pour Modérateur
class ModeratorDashboard extends StatelessWidget {
@@ -81,34 +81,30 @@ class ModeratorDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
stats: [
stats: const [
DashboardStat(
icon: Icons.flag,
value: '12',
title: 'Signalements',
color: const Color(0xFFE17055),
onTap: () {},
color: Color(0xFFE17055),
),
DashboardStat(
icon: Icons.pending_actions,
value: '8',
title: 'En Attente',
color: const Color(0xFFD63031),
onTap: () {},
color: Color(0xFFD63031),
),
DashboardStat(
icon: Icons.check_circle,
value: '45',
title: 'Résolus',
color: const Color(0xFF00B894),
onTap: () {},
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.people,
value: '156',
title: 'Membres',
color: const Color(0xFF0984E3),
onTap: () {},
color: Color(0xFF0984E3),
),
],
onStatTap: (type) {},
@@ -127,37 +123,36 @@ class ModeratorDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
actions: [
children: [
DashboardQuickAction(
icon: Icons.gavel,
title: 'Modérer',
subtitle: 'Contenu signalé',
color: const Color(0xFFE17055),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.person_remove,
title: 'Suspendre',
subtitle: 'Membre problématique',
color: const Color(0xFFD63031),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.message,
title: 'Communiquer',
subtitle: 'Envoyer message',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.report,
title: 'Rapport',
subtitle: 'Activité modération',
color: const Color(0xFF6C5CE7),
onTap: () {},
),
],
onActionTap: (type) {},
),
],
);
@@ -213,8 +208,8 @@ class ModeratorDashboard extends StatelessWidget {
}
Widget _buildRecentActivity() {
return DashboardRecentActivitySection(
activities: const [
return const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Signalement traité',
subtitle: 'Contenu supprimé',
@@ -230,7 +225,6 @@ class ModeratorDashboard extends StatelessWidget {
time: 'Il y a 3h',
),
],
onActivityTap: (id) {},
);
}
}

View File

@@ -3,8 +3,7 @@
library org_admin_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/dashboard_widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
/// Dashboard Control Panel pour Administrateur d'Organisation
@@ -236,7 +235,31 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
/// Section métriques organisation
Widget _buildOrganizationMetricsSection() {
return const QuickStatsSection.organizationStats();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques Organisation',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Text('Statistiques de l\'organisation à implémenter'),
],
),
);
}
/// Section actions rapides admin
@@ -482,8 +505,32 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
const SizedBox(height: SpacingTokens.md),
// Remplacé par PerformanceCard pour les métriques
const PerformanceCard.server(),
// Métriques serveur
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance Serveur',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Métriques serveur à implémenter'),
],
),
),
],
);
}
@@ -501,8 +548,32 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
const SizedBox(height: SpacingTokens.md),
// Remplacé par RecentActivitiesSection
const RecentActivitiesSection.organization(),
// Activités récentes de l'organisation
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activités Récentes',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Activités de l\'organisation à implémenter'),
],
),
),
],
);
}

View File

@@ -3,8 +3,8 @@
library simple_member_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Personal Space pour Membre Simple
class SimpleMemberDashboard extends StatelessWidget {
@@ -148,38 +148,33 @@ class SimpleMemberDashboard extends StatelessWidget {
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
const DashboardStatsGrid(
stats: [
DashboardStat(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF00B894),
onTap: () {},
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.event,
value: '2',
title: 'Événements',
color: const Color(0xFF00CEC9),
onTap: () {},
color: Color(0xFF00CEC9),
),
DashboardStat(
icon: Icons.account_circle,
value: '100%',
title: 'Profil',
color: const Color(0xFF0984E3),
onTap: () {},
color: Color(0xFF0984E3),
),
DashboardStat(
icon: Icons.notifications,
value: '3',
title: 'Notifications',
color: const Color(0xFFE17055),
onTap: () {},
color: Color(0xFFE17055),
),
],
onStatTap: (type) {},
),
],
);
@@ -195,37 +190,32 @@ class SimpleMemberDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
actions: [
children: [
DashboardQuickAction(
icon: Icons.edit,
title: 'Modifier Profil',
subtitle: 'Mes informations',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.payment,
title: 'Mes Cotisations',
subtitle: 'Historique paiements',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.event,
title: 'Événements',
subtitle: 'Voir les événements',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.help,
title: 'Aide',
subtitle: 'Support & FAQ',
color: const Color(0xFFE17055),
onTap: () {},
),
],
onActionTap: (type) {},
),
],
);
@@ -339,8 +329,8 @@ class SimpleMemberDashboard extends StatelessWidget {
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardRecentActivitySection(
activities: const [
const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Cotisation payée',
subtitle: 'Décembre 2024',
@@ -363,7 +353,6 @@ class SimpleMemberDashboard extends StatelessWidget {
time: 'Il y a 2 sem',
),
],
onActivityTap: (id) {},
),
],
);

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import '../../widgets/dashboard_widgets.dart';
@@ -39,23 +38,131 @@ class _SuperAdminDashboardState extends State<SuperAdminDashboard> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec informations système
const DashboardHeader.superAdmin(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Super Admin Dashboard',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.red),
),
SizedBox(height: 8),
Text('Accès complet au système'),
],
),
),
const SizedBox(height: 16),
// KPIs système en temps réel
const QuickStatsSection.systemKPIs(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'KPIs Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Indicateurs système à implémenter'),
],
),
),
const SizedBox(height: 16),
// Performance serveur
const PerformanceCard.server(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance Serveur',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Métriques serveur à implémenter'),
],
),
),
const SizedBox(height: 16),
// Alertes importantes
const RecentActivitiesSection.alerts(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alertes Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange),
),
SizedBox(height: 8),
Text('Alertes importantes à implémenter'),
],
),
),
const SizedBox(height: 16),
// Activité récente
const RecentActivitiesSection.system(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activité Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Activités système à implémenter'),
],
),
),
const SizedBox(height: 16),
// Actions rapides système

View File

@@ -3,7 +3,10 @@
library visitor_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../../../../shared/design_system/tokens/color_tokens.dart';
import '../../../../../shared/design_system/tokens/radius_tokens.dart';
import '../../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../../shared/design_system/tokens/typography_tokens.dart';
/// Dashboard Landing Experience pour Visiteur
class VisitorDashboard extends StatelessWidget {

View File

@@ -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 !** 🎯✨

View File

@@ -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,
}

View File

@@ -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;
}
}

View File

@@ -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) ...[

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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 {

View File

@@ -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,
}

View File

@@ -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),
],
);
}),
],
),
),
),
],
);
}
}

View File

@@ -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,
),
],
),
),
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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(),
),
),
),
],
);
}
}

View File

@@ -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;
}

View File

@@ -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]);
},
),
],
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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,
),
),
],
),

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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,
}

View File

@@ -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,
});
}

View File

@@ -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,
}

View File

@@ -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;
}

View File

@@ -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,
});
}

View File

@@ -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),
),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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');
}
}

View File

@@ -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,
}

View File

@@ -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';