feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles

- Config Spec-Kit pour Spec-Driven Development
- CONSTITUTION.md + .specify/memory/constitution.md
- Commandes Cursor /speckit.*, règles projet
- Mission: associations + mutuelles d'épargne et de financement
- .gitignore: versionner config spec-kit unionflow

Made-with: Cursor
This commit is contained in:
dahoud
2026-02-27 14:41:07 +00:00
parent 144b68f8e7
commit b1957c1c81
631 changed files with 104070 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
import '../../../core/config/environment.dart';
/// 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 String get apiBaseUrl => AppConfig.apiBaseUrl;
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,392 @@
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';
import '../../../../core/config/environment.dart';
/// Service de notifications temps réel pour le Dashboard
class DashboardNotificationService {
static String get _wsEndpoint => AppConfig.wsDashboardUrl;
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,498 @@
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
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 Dio _dio;
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, this._dio);
/// 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 (rafraîchit le cache)
Future<void> _syncDashboardData(OfflineAction action) async {
final orgId = action.data['organizationId'] as String?;
final userId = action.data['userId'] as String?;
if (orgId == null || userId == null) return;
final response = await _dio.get('/api/dashboard/stats', queryParameters: {
'organisationId': orgId,
});
if (response.statusCode == 200 && response.data != null) {
await _cacheManager.cacheDashboardData(
DashboardDataModel.fromJson(response.data as Map<String, dynamic>),
orgId,
userId,
);
}
}
/// Synchronise les préférences utilisateur
Future<void> _syncUserPreferences(OfflineAction action) async {
final userId = action.data['userId'] as String?;
final preferences = action.data['preferences'] as Map<String, dynamic>?;
if (userId == null || preferences == null) return;
await _dio.put('/api/membres/$userId/preferences', data: preferences);
}
/// Synchronise le marquage d'activité comme lue
Future<void> _syncActivityRead(OfflineAction action) async {
final activityId = action.data['activityId'] as String?;
if (activityId == null) return;
await _dio.put('/api/notifications/$activityId/read');
}
/// Synchronise l'inscription à un événement
Future<void> _syncEventJoin(OfflineAction action) async {
final eventId = action.data['eventId'] as String?;
final membreId = action.data['membreId'] as String?;
if (eventId == null || membreId == null) return;
await _dio.post('/api/evenements/$eventId/inscription', data: {
'membreId': membreId,
});
}
/// Synchronise l'export de rapport
Future<void> _syncReportExport(OfflineAction action) async {
final reportType = action.data['reportType'] as String?;
final params = action.data['params'] as Map<String, dynamic>?;
if (reportType == null) return;
await _dio.post('/api/export/$reportType', data: params ?? {});
}
/// 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

@@ -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: () {
// Navigation vers paramètres non encore connectée
},
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: () {
// Génération de rapport non encore implémentée
},
icon: Icon(
Icons.download,
color: color,
),
),
],
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: () {
// Actions rapides non encore implémentées
},
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

@@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
/// Dashboard simple pour Membre Actif
class ActiveMemberDashboard extends StatelessWidget {
const ActiveMemberDashboard({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête de bienvenue
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00CEC9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bonjour !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Bienvenue sur votre espace membre',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
const SizedBox(height: 24),
// Statistiques rapides
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildStatCard(
icon: Icons.event_available,
value: '12',
title: 'Événements',
color: const Color(0xFF00B894),
),
_buildStatCard(
icon: Icons.volunteer_activism,
value: '3',
title: 'Solidarité',
color: const Color(0xFF00CEC9),
),
_buildStatCard(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF0984E3),
),
_buildStatCard(
icon: Icons.star,
value: '4.8',
title: 'Engagement',
color: const Color(0xFFE17055),
),
],
),
const SizedBox(height: 24),
// Actions rapides
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildActionCard(
icon: Icons.event,
title: 'Créer Événement',
color: const Color(0xFF00B894),
onTap: () {},
),
_buildActionCard(
icon: Icons.volunteer_activism,
title: 'Demande Aide',
color: const Color(0xFF00CEC9),
onTap: () {},
),
_buildActionCard(
icon: Icons.account_circle,
title: 'Mon Profil',
color: const Color(0xFF0984E3),
onTap: () {},
),
_buildActionCard(
icon: Icons.message,
title: 'Contacter',
color: const Color(0xFFE17055),
onTap: () {},
),
],
),
const SizedBox(height: 24),
// Activités récentes
const Text(
'Activités Récentes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Card(
child: Column(
children: [
_buildActivityItem(
icon: Icons.check_circle,
title: 'Participation confirmée',
subtitle: 'Assemblée Générale - Il y a 2h',
color: const Color(0xFF00B894),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.payment,
title: 'Cotisation payée',
subtitle: 'Décembre 2024 - Il y a 1j',
color: const Color(0xFF0984E3),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.event,
title: 'Événement créé',
subtitle: 'Sortie ski de fond - Il y a 3j',
color: const Color(0xFF00CEC9),
),
],
),
),
],
),
);
}
Widget _buildStatCard({
required IconData icon,
required String value,
required String title,
required Color color,
}) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildActionCard({
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildActivityItem({
required IconData icon,
required String title,
required String subtitle,
required Color color,
}) {
return ListTile(
leading: CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Icon(icon, color: color, size: 20),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
);
}
}

View File

@@ -0,0 +1,767 @@
/// Dashboard Consultant - Interface Limitée
/// Interface spécialisée pour consultants externes
library consultant_dashboard;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../../help/presentation/pages/help_support_page.dart';
import '../../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
/// Dashboard pour Consultant Externe
class ConsultantDashboard extends StatefulWidget {
const ConsultantDashboard({super.key});
@override
State<ConsultantDashboard> createState() => _ConsultantDashboardState();
}
class _ConsultantDashboardState extends State<ConsultantDashboard> {
int _selectedIndex = 0;
final List<String> _consultantSections = [
'Mes Projets',
'Contacts',
'Profil',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'Consultant - ${_consultantSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Notifications consultant
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF6C5CE7)),
onPressed: () => _showConsultantNotifications(),
tooltip: 'Mes notifications',
),
// Menu consultant
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF6C5CE7)),
onSelected: (value) {
switch (value) {
case 'profile':
_editProfile();
break;
case 'contact':
_contactSupport();
break;
case 'help':
_showHelp();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'profile',
child: Row(
children: [
Icon(Icons.person, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Mon Profil'),
],
),
),
const PopupMenuItem(
value: 'contact',
child: Row(
children: [
Icon(Icons.support_agent, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Support'),
],
),
),
const PopupMenuItem(
value: 'help',
child: Row(
children: [
Icon(Icons.help, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Aide'),
],
),
),
],
),
],
),
drawer: _buildConsultantDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide consultant
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildConsultantQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation consultant
Widget _buildConsultantDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
Color(0xFF4834D4),
],
),
),
child: Column(
children: [
// Header consultant
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sophie Martin',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Consultant IT',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _consultantSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getConsultantSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_consultantSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section consultant
IconData _getConsultantSectionIcon(int index) {
switch (index) {
case 0: return Icons.work;
case 1: return Icons.contacts;
case 2: return Icons.person;
default: return Icons.work;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildProjectsContent();
case 1:
return _buildContactsContent();
case 2:
return _buildProfileContent();
default:
return _buildProjectsContent();
}
}
/// Mes Projets - Vue des projets assignés
Widget _buildProjectsContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header projets
_buildProjectsHeader(),
const SizedBox(height: 20),
// Projets actifs
_buildActiveProjects(),
const SizedBox(height: 20),
// Tâches en cours
_buildCurrentTasks(),
const SizedBox(height: 20),
// Statistiques consultant
_buildConsultantStats(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildContactsContent() {
return const Center(
child: Text(
'Contacts\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildProfileContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
const Text(
'Mon Profil',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _editProfile,
icon: const Icon(Icons.edit, size: 20),
label: const Text('Éditer mon profil'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
],
),
);
}
/// Header projets
Widget _buildProjectsHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Row(
children: [
Icon(Icons.work, color: Color(0xFF6C5CE7), size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Projets Assignés',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'3 projets actifs',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
),
],
),
);
}
/// Projets actifs
Widget _buildActiveProjects() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Projets Actifs',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildProjectCard(
'Refonte Site Web',
'Développement frontend',
'75%',
const Color(0xFF00B894),
),
const SizedBox(height: 8),
_buildProjectCard(
'App Mobile',
'Interface utilisateur',
'45%',
const Color(0xFF0984E3),
),
const SizedBox(height: 8),
_buildProjectCard(
'API Backend',
'Architecture serveur',
'90%',
const Color(0xFFE17055),
),
],
);
}
/// Widget pour une carte de projet
Widget _buildProjectCard(String title, String description, String progress, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.folder, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
Text(
description,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
Text(
progress,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: double.parse(progress.replaceAll('%', '')) / 100,
backgroundColor: color.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(color),
),
],
),
);
}
/// Tâches en cours
Widget _buildCurrentTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tâches du Jour',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildTaskItem('Révision code frontend', true),
const SizedBox(height: 8),
_buildTaskItem('Réunion client 15h', false),
const SizedBox(height: 8),
_buildTaskItem('Tests unitaires', false),
],
),
),
],
);
}
/// Widget pour un élément de tâche
Widget _buildTaskItem(String task, bool completed) {
return Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: completed ? const Color(0xFF6C5CE7) : Colors.transparent,
border: Border.all(color: const Color(0xFF6C5CE7), width: 2),
borderRadius: BorderRadius.circular(4),
),
child: completed
? const Icon(Icons.check, color: Colors.white, size: 14)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
task,
style: TextStyle(
fontSize: 14,
decoration: completed ? TextDecoration.lineThrough : null,
color: completed ? Colors.grey[600] : Colors.black,
),
),
),
],
);
}
/// Statistiques consultant
Widget _buildConsultantStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Projets', '3', Icons.work, const Color(0xFF6C5CE7)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Tâches', '12', Icons.task, const Color(0xFF00B894)),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Heures', '156h', Icons.schedule, const Color(0xFF0984E3)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Éval.', '4.8/5', Icons.star, const Color(0xFFFDAB00)),
),
],
),
],
);
}
/// Widget pour une carte de statistique
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
);
}
/// Navigation rapide consultant
Widget _buildConsultantQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(Icons.work, 'Projets', 0),
_buildNavItem(Icons.contacts, 'Contacts', 1),
_buildNavItem(Icons.person, 'Profil', 2),
],
),
);
}
/// Widget pour un élément de navigation
Widget _buildNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF6C5CE7).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showConsultantNotifications() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationsPageWrapper(),
),
);
}
void _editProfile() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProfilePageWrapper(),
),
);
}
void _contactSupport() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const HelpSupportPage(),
),
);
}
void _showHelp() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const HelpSupportPage(),
),
);
}
}

View File

@@ -0,0 +1,915 @@
/// Dashboard Gestionnaire RH - Interface Ressources Humaines
/// Outils spécialisés pour la gestion des employés et RH
library hr_manager_dashboard;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
/// Dashboard spécialisé pour Gestionnaire RH
///
/// Fonctionnalités RH :
/// - Gestion des employés
/// - Recrutement et onboarding
/// - Évaluations de performance
/// - Congés et absences
/// - Reporting RH
/// - Formation et développement
class HRManagerDashboard extends StatefulWidget {
const HRManagerDashboard({super.key});
@override
State<HRManagerDashboard> createState() => _HRManagerDashboardState();
}
class _HRManagerDashboardState extends State<HRManagerDashboard>
with TickerProviderStateMixin {
late TabController _tabController;
int _selectedIndex = 0;
final List<String> _hrSections = [
'Vue d\'ensemble',
'Employés',
'Recrutement',
'Évaluations',
'Congés',
'Formation',
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _hrSections.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'RH Manager - ${_hrSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF00B894),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Recherche employés
IconButton(
icon: const Icon(Icons.search, color: Color(0xFF00B894)),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembersPageWrapper(),
),
);
},
tooltip: 'Rechercher employés',
),
// Notifications RH
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF00B894)),
onPressed: () => _showHRNotifications(),
tooltip: 'Notifications RH',
),
// Menu RH
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF00B894)),
onSelected: (value) {
switch (value) {
case 'reports':
_generateHRReports();
break;
case 'settings':
_openHRSettings();
break;
case 'export':
_exportHRData();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'reports',
child: Row(
children: [
Icon(Icons.assessment, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Rapports RH'),
],
),
),
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Paramètres RH'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Exporter données'),
],
),
),
],
),
],
),
drawer: _buildHRDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide RH
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildHRQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation RH
Widget _buildHRDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF00B894),
Color(0xFF00A085),
Color(0xFF008B75),
],
),
),
child: Column(
children: [
// Header RH
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestionnaire RH',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Ressources Humaines',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _hrSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getHRSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_hrSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section RH
IconData _getHRSectionIcon(int index) {
switch (index) {
case 0: return Icons.dashboard;
case 1: return Icons.people;
case 2: return Icons.person_add;
case 3: return Icons.star_rate;
case 4: return Icons.event_busy;
case 5: return Icons.school;
default: return Icons.dashboard;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildOverviewContent();
case 1:
return _buildEmployeesContent();
case 2:
return _buildRecruitmentContent();
case 3:
return _buildEvaluationsContent();
case 4:
return _buildLeavesContent();
case 5:
return _buildTrainingContent();
default:
return _buildOverviewContent();
}
}
/// Vue d'ensemble RH
Widget _buildOverviewContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec statut RH
_buildHRStatusHeader(),
const SizedBox(height: 20),
// KPIs RH
_buildHRKPIsSection(),
const SizedBox(height: 20),
// Actions rapides RH
_buildHRQuickActions(),
const SizedBox(height: 20),
// Alertes RH importantes
_buildHRAlerts(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildEmployeesContent() {
return const Center(
child: Text(
'Gestion des Employés\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildRecruitmentContent() {
return const Center(
child: Text(
'Recrutement\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildEvaluationsContent() {
return const Center(
child: Text(
'Évaluations\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildLeavesContent() {
return const Center(
child: Text(
'Congés et Absences\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildTrainingContent() {
return const Center(
child: Text(
'Formation\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
/// Header avec statut RH
Widget _buildHRStatusHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00A085)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF00B894).withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Département RH Actif',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Dernière sync: ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 28,
),
),
],
),
);
}
/// Section KPIs RH
Widget _buildHRKPIsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Indicateurs RH',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'Employés Actifs',
'247',
'+12 ce mois',
Icons.people,
const Color(0xFF00B894),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Candidatures',
'34',
'+8 cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'En Congé',
'18',
'7.3% de l\'effectif',
Icons.event_busy,
const Color(0xFFFDAB00),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Évaluations',
'156',
'89% complétées',
Icons.star_rate,
const Color(0xFFE17055),
),
),
],
),
],
);
}
/// Widget pour une carte KPI RH
Widget _buildHRKPICard(
String title,
String value,
String subtitle,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const Spacer(),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
);
}
/// Actions rapides RH
Widget _buildHRQuickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.5,
children: [
_buildHRActionCard(
'Nouveau Employé',
Icons.person_add,
const Color(0xFF00B894),
() => _addNewEmployee(),
),
_buildHRActionCard(
'Demandes Congés',
Icons.event_busy,
const Color(0xFFFDAB00),
() => _viewLeaveRequests(),
),
_buildHRActionCard(
'Évaluations',
Icons.star_rate,
const Color(0xFFE17055),
() => _viewEvaluations(),
),
_buildHRActionCard(
'Recrutement',
Icons.work,
const Color(0xFF0984E3),
() => _viewRecruitment(),
),
],
),
],
);
}
/// Widget pour une action RH
Widget _buildHRActionCard(
String title,
IconData icon,
Color color,
VoidCallback onTap,
) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Alertes RH importantes
Widget _buildHRAlerts() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alertes Importantes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildHRAlertItem(
'Évaluations en retard',
'12 évaluations annuelles à finaliser',
Icons.warning,
const Color(0xFFE17055),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Congés à approuver',
'5 demandes de congé en attente',
Icons.pending_actions,
const Color(0xFFFDAB00),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Nouveaux candidats',
'8 candidatures reçues cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
],
);
}
/// Widget pour un élément d'alerte RH
Widget _buildHRAlertItem(
String title,
String message,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: color,
),
),
Text(
message,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
],
),
),
],
),
);
}
/// Navigation rapide RH
Widget _buildHRQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildHRNavItem(Icons.dashboard, 'Vue', 0),
_buildHRNavItem(Icons.people, 'Employés', 1),
_buildHRNavItem(Icons.person_add, 'Recrutement', 2),
_buildHRNavItem(Icons.star_rate, 'Évaluations', 3),
_buildHRNavItem(Icons.event_busy, 'Congés', 4),
],
),
);
}
/// Widget pour un élément de navigation RH
Widget _buildHRNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF00B894).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showHRNotifications() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationsPageWrapper(),
),
);
}
void _generateHRReports() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
}
void _openHRSettings() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
}
void _exportHRData() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
}
void _addNewEmployee() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ajouter employé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _viewLeaveRequests() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demandes de congé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFFDAB00),
),
);
}
void _viewEvaluations() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Évaluations - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFE17055),
),
);
}
void _viewRecruitment() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recrutement - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF0984E3),
),
);
}
}

View File

@@ -0,0 +1,230 @@
/// Dashboard Modérateur - Management Hub Focalisé
/// Outils de modération et gestion partielle
library moderator_dashboard;
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Management Hub pour Modérateur
class ModeratorDashboard extends StatelessWidget {
const ModeratorDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Modérateur
SliverAppBar(
expandedHeight: 160,
floating: false,
pinned: true,
backgroundColor: const Color(0xFFE17055), // Orange focus
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Management Hub',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFE17055), Color(0xFFD63031)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Icon(Icons.manage_accounts, color: Colors.white, size: 60),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Métriques modération
_buildModerationMetrics(),
const SizedBox(height: SpacingTokens.xl),
// Actions modération
_buildModerationActions(),
const SizedBox(height: SpacingTokens.xl),
// Tâches en attente
_buildPendingTasks(),
const SizedBox(height: SpacingTokens.xl),
// Activité récente
_buildRecentActivity(),
],
),
),
),
],
),
);
}
Widget _buildModerationMetrics() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques de Modération',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
stats: const [
DashboardStat(
icon: Icons.flag,
value: '12',
title: 'Signalements',
color: Color(0xFFE17055),
),
DashboardStat(
icon: Icons.pending_actions,
value: '8',
title: 'En Attente',
color: Color(0xFFD63031),
),
DashboardStat(
icon: Icons.check_circle,
value: '45',
title: 'Résolus',
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.people,
value: '156',
title: 'Membres',
color: Color(0xFF0984E3),
),
],
onStatTap: (type) {},
),
],
);
}
Widget _buildModerationActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions de Modération',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
children: [
DashboardQuickAction(
icon: Icons.gavel,
title: 'Modérer',
color: const Color(0xFFE17055),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.person_remove,
title: 'Suspendre',
color: const Color(0xFFD63031),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.message,
title: 'Communiquer',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.report,
title: 'Rapport',
color: const Color(0xFF6C5CE7),
onTap: () {},
),
],
),
],
);
}
Widget _buildPendingTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tâches en Attente',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Column(
children: [
ListTile(
leading: CircleAvatar(
backgroundColor: Color(0xFFFFE0E0),
child: Icon(Icons.flag, color: Color(0xFFD63031)),
),
title: Text('Contenu inapproprié signalé'),
subtitle: Text('Commentaire sur événement'),
trailing: Text('Urgent'),
),
Divider(height: 1),
ListTile(
leading: CircleAvatar(
backgroundColor: Color(0xFFFFF3E0),
child: Icon(Icons.person_add, color: Color(0xFFE17055)),
),
title: Text('Demande d\'adhésion'),
subtitle: Text('Marie Dubois'),
trailing: Text('2j'),
),
],
),
),
],
);
}
Widget _buildRecentActivity() {
return const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Signalement traité',
subtitle: 'Contenu supprimé',
icon: Icons.check_circle,
color: Color(0xFF00B894),
time: 'Il y a 1h',
),
DashboardActivity(
title: 'Membre suspendu',
subtitle: 'Violation des règles',
icon: Icons.person_remove,
color: Color(0xFFD63031),
time: 'Il y a 3h',
),
],
);
}
}

View File

@@ -0,0 +1,11 @@
/// Export de tous les dashboards spécifiques par rôle
/// Facilite l'importation des dashboards dans l'application
library role_dashboards;
// Dashboards spécifiques par rôle
export 'super_admin_dashboard.dart';
export 'org_admin_dashboard.dart';
export 'moderator_dashboard.dart';
export 'active_member_dashboard.dart';
export 'simple_member_dashboard.dart';
export 'visitor_dashboard.dart';

View File

@@ -0,0 +1,360 @@
/// Dashboard Membre Simple - Personal Space Minimaliste
/// Interface simplifiée pour accès basique
library simple_member_dashboard;
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Personal Space pour Membre Simple
class SimpleMemberDashboard extends StatelessWidget {
const SimpleMemberDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Membre Simple
SliverAppBar(
expandedHeight: 140,
floating: false,
pinned: true,
backgroundColor: const Color(0xFF00CEC9), // Teal simple
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Mon Espace',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF00CEC9), Color(0xFF00B3B3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Icon(Icons.person, color: Colors.white, size: 50),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profil personnel
_buildPersonalProfile(),
const SizedBox(height: SpacingTokens.xl),
// Mes informations
_buildMyInfo(),
const SizedBox(height: SpacingTokens.xl),
// Actions simples
_buildSimpleActions(),
const SizedBox(height: SpacingTokens.xl),
// Événements publics
_buildPublicEvents(),
const SizedBox(height: SpacingTokens.xl),
// Mon historique
_buildMyHistory(),
],
),
),
),
],
),
);
}
Widget _buildPersonalProfile() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
const CircleAvatar(
radius: 35,
backgroundColor: Color(0xFF00CEC9),
child: Icon(Icons.person, color: Colors.white, size: 35),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pierre Dupont',
style: TypographyTokens.headlineMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Membre depuis 6 mois',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.textSecondary,
),
),
const SizedBox(height: SpacingTokens.sm),
Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: Text(
'Membre Simple',
style: TypographyTokens.bodySmall.copyWith(
color: const Color(0xFF00CEC9),
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
);
}
Widget _buildMyInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Informations',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
const DashboardStatsGrid(
stats: [
DashboardStat(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.event,
value: '2',
title: 'Événements',
color: Color(0xFF00CEC9),
),
DashboardStat(
icon: Icons.account_circle,
value: '100%',
title: 'Profil',
color: Color(0xFF0984E3),
),
DashboardStat(
icon: Icons.notifications,
value: '3',
title: 'Notifications',
color: Color(0xFFE17055),
),
],
),
],
);
}
Widget _buildSimpleActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions Disponibles',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
children: [
DashboardQuickAction(
icon: Icons.edit,
title: 'Modifier Profil',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.payment,
title: 'Mes Cotisations',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.event,
title: 'Événements',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.help,
title: 'Aide',
color: const Color(0xFFE17055),
onTap: () {},
),
],
),
],
);
}
Widget _buildPublicEvents() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Événements Disponibles',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Icon(
Icons.event,
color: Color(0xFF00B894),
),
),
title: const Text('Assemblée Générale'),
subtitle: const Text('15 décembre • 19h00'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'Public',
style: TextStyle(
color: Color(0xFF00B894),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
const Divider(height: 1),
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Icon(
Icons.celebration,
color: Color(0xFF00CEC9),
),
),
title: const Text('Soirée de Noël'),
subtitle: const Text('22 décembre • 20h00'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'Public',
style: TextStyle(
color: Color(0xFF00CEC9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
],
);
}
Widget _buildMyHistory() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mon Historique',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Cotisation payée',
subtitle: 'Décembre 2024',
icon: Icons.payment,
color: Color(0xFF00B894),
time: 'Il y a 1j',
),
DashboardActivity(
title: 'Profil mis à jour',
subtitle: 'Informations personnelles',
icon: Icons.edit,
color: Color(0xFF00CEC9),
time: 'Il y a 1 sem',
),
DashboardActivity(
title: 'Inscription événement',
subtitle: 'Assemblée Générale',
icon: Icons.event,
color: Color(0xFF0984E3),
time: 'Il y a 2 sem',
),
],
),
],
);
}
}

View File

@@ -0,0 +1,554 @@
/// Dashboard Visiteur - Landing Experience Accueillante
/// Interface publique pour découvrir l'organisation
library visitor_dashboard;
import 'package:flutter/material.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 {
const VisitorDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Visiteur
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
backgroundColor: const Color(0xFF6C5CE7), // Indigo accueillant
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Découvrir UnionFlow',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
children: [
// Motif d'accueil
Positioned.fill(
child: CustomPaint(
painter: _WelcomePatternPainter(),
),
),
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.waving_hand, color: Colors.white, size: 60),
SizedBox(height: 8),
Text(
'Bienvenue !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message d'accueil
_buildWelcomeMessage(),
const SizedBox(height: SpacingTokens.xl),
// À propos de l'organisation
_buildAboutOrganization(),
const SizedBox(height: SpacingTokens.xl),
// Événements publics
_buildPublicEvents(),
const SizedBox(height: SpacingTokens.xl),
// Comment rejoindre
_buildHowToJoin(),
const SizedBox(height: SpacingTokens.xl),
// Contact
_buildContactInfo(),
],
),
),
),
],
),
);
}
Widget _buildWelcomeMessage() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.info_outline, color: Colors.white, size: 30),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: Text(
'Découvrez notre communauté',
style: TypographyTokens.headlineMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: SpacingTokens.md),
Text(
'Bienvenue sur UnionFlow ! Explorez notre organisation, découvrez nos événements publics et apprenez comment nous rejoindre.',
style: TypographyTokens.bodyLarge.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: SpacingTokens.md),
ElevatedButton(
onPressed: () => _onJoinNow(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF6C5CE7),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
),
child: const Text(
'Nous Rejoindre',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
Widget _buildAboutOrganization() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'À Propos de Nous',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.business,
color: Color(0xFF6C5CE7),
size: 30,
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Association des Développeurs',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Communauté tech passionnée',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: SpacingTokens.md),
const Text(
'Nous sommes une association dynamique qui rassemble les passionnés de technologie. Notre mission est de favoriser l\'apprentissage, le partage de connaissances et l\'entraide dans le domaine du développement.',
style: TypographyTokens.bodyMedium,
),
const SizedBox(height: SpacingTokens.md),
// Statistiques publiques
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPublicStat('156', 'Membres'),
_buildPublicStat('24', 'Événements/an'),
_buildPublicStat('5', 'Ans d\'existence'),
],
),
],
),
),
],
);
}
Widget _buildPublicStat(String value, String label) {
return Column(
children: [
Text(
value,
style: TypographyTokens.headlineMedium.copyWith(
color: const Color(0xFF6C5CE7),
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.textSecondary,
),
),
],
);
}
Widget _buildPublicEvents() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Événements Publics',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('15', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
),
),
title: const Text('Assemblée Générale Publique'),
subtitle: const Text('Salle communale • 19h00 • Gratuit'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'OUVERT',
style: TextStyle(
color: Color(0xFF00B894),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
const Divider(height: 1),
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('20', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
),
),
title: const Text('Conférence Tech Trends 2025'),
subtitle: const Text('Amphithéâtre Université • 14h00 • Gratuit'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'OUVERT',
style: TextStyle(
color: Color(0xFF6C5CE7),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
);
}
Widget _buildHowToJoin() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comment Nous Rejoindre',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildJoinStep('1', 'Créer un compte', 'Inscription gratuite en 2 minutes'),
const SizedBox(height: SpacingTokens.md),
_buildJoinStep('2', 'Compléter le profil', 'Partagez vos centres d\'intérêt'),
const SizedBox(height: SpacingTokens.md),
_buildJoinStep('3', 'Validation', 'Approbation par nos modérateurs'),
const SizedBox(height: SpacingTokens.lg),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _onStartRegistration(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.md),
),
child: const Text(
'Commencer l\'inscription',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
);
}
Widget _buildJoinStep(String number, String title, String description) {
return Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7),
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
number,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
description,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.textSecondary,
),
),
],
),
),
],
);
}
Widget _buildContactInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nous Contacter',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Column(
children: [
ListTile(
leading: Icon(Icons.email, color: Color(0xFF6C5CE7)),
title: Text('Email'),
subtitle: Text('contact@association-dev.fr'),
contentPadding: EdgeInsets.zero,
),
ListTile(
leading: Icon(Icons.phone, color: Color(0xFF6C5CE7)),
title: Text('Téléphone'),
subtitle: Text('+33 1 23 45 67 89'),
contentPadding: EdgeInsets.zero,
),
ListTile(
leading: Icon(Icons.location_on, color: Color(0xFF6C5CE7)),
title: Text('Adresse'),
subtitle: Text('123 Rue de la Tech, 75001 Paris'),
contentPadding: EdgeInsets.zero,
),
],
),
),
],
);
}
// === CALLBACKS ===
void _onJoinNow() {
// Navigation vers l'inscription
}
void _onStartRegistration() {
// Démarrer le processus d'inscription
}
}
/// Painter pour le motif d'accueil
class _WelcomePatternPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.1)
..strokeWidth = 1
..style = PaintingStyle.stroke;
// Dessiner des cercles concentriques
for (int i = 1; i <= 5; i++) {
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
i * size.width / 10,
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

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

@@ -0,0 +1,463 @@
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;
/// Description ou détails de l'activité
final String? description;
/// Horodatage de l'activité
final String timestamp;
/// Icône représentative de l'activité
final IconData? icon;
/// Couleur thématique de l'activité
final Color? color;
/// Type d'activité pour le style automatique
final ActivityType? type;
/// Callback lors du tap sur l'élément
final VoidCallback? onTap;
/// Style de l'élément d'activité
final ActivityItemStyle style;
/// Afficher ou non l'indicateur de statut
final bool showStatusIndicator;
const ActivityItem({
super.key,
required this.title,
this.description,
required this.timestamp,
this.icon,
this.color,
this.type,
this.onTap,
this.style = ActivityItemStyle.normal,
this.showStatusIndicator = true,
});
/// Constructeur pour une activité système
const ActivityItem.system({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.settings,
color = ColorTokens.primary,
type = ActivityType.system,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
/// Constructeur pour une activité utilisateur
const ActivityItem.user({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.person,
color = ColorTokens.success,
type = ActivityType.user,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
/// Constructeur pour une alerte
const ActivityItem.alert({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.warning,
color = ColorTokens.warning,
type = ActivityType.alert,
style = ActivityItemStyle.alert,
showStatusIndicator = true;
/// Constructeur pour une erreur
const ActivityItem.error({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.error,
color = Colors.red,
type = ActivityType.error,
style = ActivityItemStyle.alert,
showStatusIndicator = true;
/// Constructeur pour une activité de succès
const ActivityItem.success({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.check_circle,
color = const Color(0xFF00B894),
type = ActivityType.success,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
@override
Widget build(BuildContext context) {
final effectiveColor = _getEffectiveColor();
final effectiveIcon = _getEffectiveIcon();
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: _getPadding(),
decoration: _getDecoration(effectiveColor),
child: _buildContent(effectiveColor, effectiveIcon),
),
);
}
/// Contenu principal de l'élément
Widget _buildContent(Color effectiveColor, IconData effectiveIcon) {
switch (style) {
case ActivityItemStyle.minimal:
return _buildMinimalContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.normal:
return _buildNormalContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.detailed:
return _buildDetailedContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.alert:
return _buildAlertContent(effectiveColor, effectiveIcon);
}
}
/// Contenu minimal (ligne simple)
Widget _buildMinimalContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
if (showStatusIndicator)
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
if (showStatusIndicator) const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Text(
timestamp,
style: const TextStyle(
color: Colors.grey,
fontSize: 10,
),
),
],
);
}
/// Contenu normal avec icône
Widget _buildNormalContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
if (showStatusIndicator) ...[
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
effectiveIcon,
color: effectiveColor,
size: 16,
),
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
if (description != null) ...[
const SizedBox(height: 2),
Text(
description!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
const SizedBox(width: 8),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
);
}
/// Contenu détaillé avec plus d'informations
Widget _buildDetailedContent(Color effectiveColor, IconData effectiveIcon) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
effectiveIcon,
color: effectiveColor,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
if (description != null) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 42),
child: Text(
description!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.4,
),
),
),
],
],
);
}
/// Contenu pour les alertes avec style spécial
Widget _buildAlertContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
Icon(
effectiveIcon,
color: effectiveColor,
size: 18,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: effectiveColor,
),
),
if (description != null) ...[
const SizedBox(height: 2),
Text(
description!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
const SizedBox(width: 8),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
],
);
}
/// Couleur effective selon le type
Color _getEffectiveColor() {
if (color != null) return color!;
switch (type) {
case ActivityType.system:
return ColorTokens.primary;
case ActivityType.user:
return ColorTokens.success;
case ActivityType.organization:
return ColorTokens.info;
case ActivityType.event:
return ColorTokens.secondary;
case ActivityType.alert:
return ColorTokens.warning;
case ActivityType.error:
return ColorTokens.error;
case ActivityType.success:
return ColorTokens.success;
case null:
return ColorTokens.primary;
}
}
/// Icône effective selon le type
IconData _getEffectiveIcon() {
if (icon != null) return icon!;
switch (type) {
case ActivityType.system:
return Icons.settings;
case ActivityType.user:
return Icons.person;
case ActivityType.organization:
return Icons.business;
case ActivityType.event:
return Icons.event;
case ActivityType.alert:
return Icons.warning;
case ActivityType.error:
return Icons.error;
case ActivityType.success:
return Icons.check_circle;
case null:
return Icons.circle;
}
}
/// Padding selon le style
EdgeInsets _getPadding() {
switch (style) {
case ActivityItemStyle.minimal:
return const EdgeInsets.symmetric(vertical: 4, horizontal: 8);
case ActivityItemStyle.normal:
return const EdgeInsets.all(8);
case ActivityItemStyle.detailed:
return const EdgeInsets.all(12);
case ActivityItemStyle.alert:
return const EdgeInsets.all(10);
}
}
/// Décoration selon le style
BoxDecoration _getDecoration(Color effectiveColor) {
switch (style) {
case ActivityItemStyle.minimal:
return const BoxDecoration();
case ActivityItemStyle.normal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
);
case ActivityItemStyle.detailed:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
case ActivityItemStyle.alert:
return BoxDecoration(
color: effectiveColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: effectiveColor.withOpacity(0.2),
width: 1,
),
);
}
}
}
/// Types d'activité
enum ActivityType {
system,
user,
organization,
event,
alert,
error,
success,
}
/// Styles d'élément d'activité
enum ActivityItemStyle {
minimal,
normal,
detailed,
alert,
}

View File

@@ -0,0 +1,302 @@
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;
/// Sous-titre optionnel
final String? subtitle;
/// Widget d'action à droite (bouton, icône, etc.)
final Widget? action;
/// Icône optionnelle à gauche du titre
final IconData? icon;
/// Couleur du titre et de l'icône
final Color? color;
/// Taille du titre
final double? fontSize;
/// Style de l'en-tête
final SectionHeaderStyle style;
/// Espacement en bas de l'en-tête
final double bottomSpacing;
const SectionHeader({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
this.color,
this.fontSize,
this.style = SectionHeaderStyle.normal,
this.bottomSpacing = 12,
});
/// Constructeur pour un en-tête principal
const SectionHeader.primary({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = ColorTokens.primary,
fontSize = 20,
style = SectionHeaderStyle.primary,
bottomSpacing = 16;
/// Constructeur pour un en-tête de section
const SectionHeader.section({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = ColorTokens.primary,
fontSize = 16,
style = SectionHeaderStyle.normal,
bottomSpacing = 12;
/// Constructeur pour un en-tête de sous-section
const SectionHeader.subsection({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = const Color(0xFF374151),
fontSize = 14,
style = SectionHeaderStyle.minimal,
bottomSpacing = 8;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: bottomSpacing),
child: _buildContent(),
);
}
Widget _buildContent() {
switch (style) {
case SectionHeaderStyle.primary:
return _buildPrimaryHeader();
case SectionHeaderStyle.normal:
return _buildNormalHeader();
case SectionHeaderStyle.minimal:
return _buildMinimalHeader();
case SectionHeaderStyle.card:
return _buildCardHeader();
}
}
/// En-tête principal avec fond coloré
Widget _buildPrimaryHeader() {
final effectiveColor = color ?? ColorTokens.primary;
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
effectiveColor,
effectiveColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
),
child: Row(
children: [
if (icon != null) ...[
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
],
),
),
if (action != null) action!,
],
),
);
}
/// En-tête normal avec icône et action
Widget _buildNormalHeader() {
return Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (action != null) action!,
],
);
}
/// En-tête minimal simple
Widget _buildMinimalHeader() {
return Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? const Color(0xFF374151),
size: 16,
),
const SizedBox(width: 6),
],
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: fontSize ?? 14,
fontWeight: FontWeight.w600,
color: color ?? const Color(0xFF374151),
),
),
),
if (action != null) action!,
],
);
}
/// En-tête avec fond de carte
Widget _buildCardHeader() {
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: Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (action != null) action!,
],
),
);
}
}
/// Énumération des styles d'en-tête
enum SectionHeaderStyle {
primary,
normal,
minimal,
card,
}

View File

@@ -0,0 +1,292 @@
import 'package:flutter/material.dart';
/// Widget réutilisable pour afficher une carte de statistique
///
/// Composant générique utilisé dans tous les dashboards pour afficher
/// des métriques avec icône, valeur, titre et sous-titre.
class StatCard extends StatelessWidget {
/// Titre principal de la statistique
final String title;
/// Valeur numérique ou textuelle à afficher
final String value;
/// Sous-titre ou description complémentaire
final String subtitle;
/// Icône représentative de la métrique
final IconData icon;
/// Couleur thématique de la carte
final Color color;
/// Callback optionnel lors du tap sur la carte
final VoidCallback? onTap;
/// Taille de la carte (compact, normal, large)
final StatCardSize size;
/// Style de la carte (minimal, elevated, outlined)
final StatCardStyle style;
const StatCard({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
this.size = StatCardSize.normal,
this.style = StatCardStyle.elevated,
});
/// Constructeur pour une carte KPI simplifiée
const StatCard.kpi({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
}) : size = StatCardSize.compact,
style = StatCardStyle.elevated;
/// Constructeur pour une carte de métrique système
const StatCard.metric({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
}) : size = StatCardSize.normal,
style = StatCardStyle.minimal;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: _getPadding(),
decoration: _getDecoration(),
child: _buildContent(),
),
);
}
/// Contenu principal de la carte
Widget _buildContent() {
switch (size) {
case StatCardSize.compact:
return _buildCompactContent();
case StatCardSize.normal:
return _buildNormalContent();
case StatCardSize.large:
return _buildLargeContent();
}
}
/// Contenu compact pour les KPIs
Widget _buildCompactContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
const Spacer(),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 18,
),
),
],
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
fontSize: 12,
),
),
Text(
subtitle,
style: const TextStyle(
color: Colors.grey,
fontSize: 10,
),
),
],
);
}
/// Contenu normal pour les métriques
Widget _buildNormalContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 20,
),
),
if (subtitle.isNotEmpty)
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
],
),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
fontSize: 14,
),
),
],
);
}
/// Contenu large pour les dashboards principaux
Widget _buildLargeContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 24,
),
),
if (subtitle.isNotEmpty)
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
],
),
const SizedBox(height: 16),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
fontSize: 16,
),
),
],
);
}
/// Padding selon la taille
EdgeInsets _getPadding() {
switch (size) {
case StatCardSize.compact:
return const EdgeInsets.all(8);
case StatCardSize.normal:
return const EdgeInsets.all(12);
case StatCardSize.large:
return const EdgeInsets.all(16);
}
}
/// Décoration selon le style
BoxDecoration _getDecoration() {
switch (style) {
case StatCardStyle.minimal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
);
case StatCardStyle.elevated:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
case StatCardStyle.outlined:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
);
}
}
}
/// Énumération des tailles de carte
enum StatCardSize {
compact,
normal,
large,
}
/// Énumération des styles de carte
enum StatCardStyle {
minimal,
elevated,
outlined,
}

View File

@@ -0,0 +1,260 @@
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;
/// 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: ColorTokens.warning,
threshold: 80,
),
PerformanceMetric(
label: 'RAM',
value: 78.5,
unit: '%',
color: ColorTokens.info,
threshold: 85,
),
PerformanceMetric(
label: 'Disque',
value: 45.2,
unit: '%',
color: ColorTokens.success,
threshold: 90,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
/// Constructeur pour les métriques réseau
const PerformanceCard.network({
super.key,
this.onTap,
}) : title = 'Performance Réseau',
subtitle = 'Métriques temps réel',
metrics = const [
PerformanceMetric(
label: 'Latence',
value: 12.0,
unit: 'ms',
color: ColorTokens.success,
threshold: 100.0,
),
PerformanceMetric(
label: 'Débit',
value: 85.0,
unit: 'Mbps',
color: ColorTokens.primary,
threshold: 100.0,
),
PerformanceMetric(
label: 'Paquets perdus',
value: 0.2,
unit: '%',
color: ColorTokens.secondary,
threshold: 5.0,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: UFCard(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: SpacingTokens.lg),
_buildMetrics(),
],
),
),
);
}
/// En-tête de la carte
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.titleMedium.copyWith(
fontWeight: FontWeight.bold,
color: ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
);
}
/// Construction des métriques
Widget _buildMetrics() {
return Column(
children: metrics.map((metric) => Padding(
padding: const EdgeInsets.only(bottom: SpacingTokens.md),
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 = ColorTokens.error;
} else if (isWarning) {
effectiveColor = ColorTokens.warning;
}
return Column(
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: SpacingTokens.md),
Text(
metric.label,
style: TypographyTokens.labelMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (showValues)
Text(
'${metric.value.toStringAsFixed(1)}${metric.unit}',
style: TypographyTokens.labelMedium.copyWith(
color: effectiveColor,
fontWeight: FontWeight.w600,
),
),
],
),
if (showProgressBars) ...[
const SizedBox(height: SpacingTokens.xs),
_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: ColorTokens.surfaceVariant,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
),
),
);
}
}
/// 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;
const PerformanceMetric({
required this.label,
required this.value,
required this.unit,
required this.color,
required this.threshold,
});
}
/// Styles de carte de performance
enum PerformanceCardStyle {
elevated,
outlined,
minimal,
}

View File

@@ -0,0 +1,365 @@
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';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.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: () => _navigateForActivity(context, activity),
icon: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: DashboardTheme.grey400,
),
),
],
);
}
void _navigateForActivity(BuildContext context, RecentActivityEntity activity) {
final type = activity.type.toLowerCase();
Widget? page;
if (type.contains('event') || type.contains('evenement')) {
page = const EventsPageWrapper();
} else if (type.contains('member') || type.contains('membre')) {
page = const MembersPageWrapper();
} else if (type.contains('adhesion') || type.contains('adhésion')) {
page = const AdhesionsPageWrapper();
} else if (type.contains('demande') || type.contains('solidarite') || type.contains('aide')) {
page = const DemandesAidePageWrapper();
}
if (page != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => page!));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(activity.title)),
);
}
}
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

@@ -0,0 +1,191 @@
/// Widget de menu latéral (drawer) du dashboard
/// Navigation principale de l'application
library dashboard_drawer;
import 'package:flutter/material.dart';
import '../../../../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 {
/// Icône de l'élément de menu
final IconData icon;
/// Titre de l'élément de menu
final String title;
/// Callback lors du tap sur l'élément
final VoidCallback? onTap;
/// Constructeur du modèle d'élément de menu
const DrawerMenuItem({
required this.icon,
required this.title,
this.onTap,
});
}
/// Widget de menu latéral
///
/// Affiche la navigation principale avec :
/// - Header avec profil utilisateur
/// - Menu de navigation structuré
/// - Actions secondaires
/// - Design Material avec gradient
class DashboardDrawer extends StatelessWidget {
/// Callback pour les actions de navigation
final Function(String route)? onNavigate;
/// Callback pour la déconnexion
final VoidCallback? onLogout;
/// Constructeur du menu latéral
const DashboardDrawer({
super.key,
this.onNavigate,
this.onLogout,
});
/// Génère la liste des éléments de menu principaux
List<DrawerMenuItem> _getMainMenuItems() {
return [
DrawerMenuItem(
icon: Icons.dashboard,
title: 'Dashboard',
onTap: () => onNavigate?.call('/dashboard'),
),
DrawerMenuItem(
icon: Icons.people,
title: 'Membres',
onTap: () => onNavigate?.call('/members'),
),
DrawerMenuItem(
icon: Icons.account_balance_wallet,
title: 'Cotisations',
onTap: () => onNavigate?.call('/cotisations'),
),
DrawerMenuItem(
icon: Icons.event,
title: 'Événements',
onTap: () => onNavigate?.call('/events'),
),
DrawerMenuItem(
icon: Icons.favorite,
title: 'Solidarité',
onTap: () => onNavigate?.call('/solidarity'),
),
];
}
/// Génère la liste des éléments de menu secondaires
List<DrawerMenuItem> _getSecondaryMenuItems() {
return [
DrawerMenuItem(
icon: Icons.analytics,
title: 'Rapports',
onTap: () => onNavigate?.call('/reports'),
),
DrawerMenuItem(
icon: Icons.settings,
title: 'Paramètres',
onTap: () => onNavigate?.call('/settings'),
),
DrawerMenuItem(
icon: Icons.help,
title: 'Aide',
onTap: () => onNavigate?.call('/help'),
),
];
}
@override
Widget build(BuildContext context) {
final mainItems = _getMainMenuItems();
final secondaryItems = _getSecondaryMenuItems();
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildDrawerHeader(),
...mainItems.map((item) => _buildMenuItem(item)),
const Divider(),
...secondaryItems.map((item) => _buildMenuItem(item)),
const Divider(),
_buildLogoutItem(),
],
),
);
}
/// Construit l'en-tête du drawer avec profil utilisateur
Widget _buildDrawerHeader() {
return DrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [ColorTokens.primary, ColorTokens.secondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
size: 35,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.md),
Text(
'Utilisateur UnionFlow',
style: TypographyTokens.titleMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
Text(
'admin@unionflow.dev',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
),
);
}
/// Construit un élément de menu
Widget _buildMenuItem(DrawerMenuItem item) {
return ListTile(
leading: Icon(item.icon),
title: Text(
item.title,
style: TypographyTokens.bodyMedium,
),
onTap: item.onTap,
);
}
/// Construit l'élément de déconnexion
Widget _buildLogoutItem() {
return ListTile(
leading: const Icon(
Icons.logout,
color: ColorTokens.error,
),
title: Text(
'Déconnexion',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.error,
),
),
onTap: onLogout,
);
}
}

View File

@@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
/// 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;
const DashboardStat({
super.key,
required this.title,
required this.value,
required this.icon,
this.color,
});
@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: [
Row(
children: [
Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 24,
),
const Spacer(),
Text(
value,
style: DashboardTheme.titleLarge.copyWith(
color: color ?? DashboardTheme.royalBlue,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodyMedium,
),
],
),
);
}
}
/// Widget de grille de statistiques
class DashboardStatsGrid extends StatelessWidget {
final List<DashboardStat> stats;
final Function(String)? onStatTap;
const DashboardStatsGrid({
super.key,
required this.stats,
this.onStatTap,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.2,
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 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 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,
),
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,
),
),
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,
),
],
),
),
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);
// Action rapide non encore connectée
},
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

@@ -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,255 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.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(context);
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: () {
// Personnalisation des raccourcis non encore implémentée
},
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(BuildContext context) {
return [
DashboardShortcut(
title: 'Nouveau\nMembre',
icon: Icons.person_add,
color: DashboardTheme.success,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembersPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Créer\nÉvénement',
icon: Icons.event_available,
color: DashboardTheme.royalBlue,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EventsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Ajouter\nContribution',
icon: Icons.payment,
color: DashboardTheme.tealBlue,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ContributionsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Envoyer\nMessage',
icon: Icons.message,
color: DashboardTheme.warning,
badge: '3',
badgeColor: DashboardTheme.error,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Messagerie à venir')),
);
},
),
DashboardShortcut(
title: 'Générer\nRapport',
icon: Icons.assessment,
color: DashboardTheme.info,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Paramètres',
icon: Icons.settings,
color: DashboardTheme.grey600,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
},
),
];
}
}
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

@@ -0,0 +1,28 @@
// Export des widgets dashboard connectés
export 'connected/connected_stats_card.dart';
export 'connected/connected_recent_activities.dart';
export 'connected/connected_upcoming_events.dart';
// Export des widgets charts
export 'charts/dashboard_chart_widget.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';