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:
@@ -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;
|
||||
}
|
||||
}
|
||||
400
unionflow/unionflow-mobile-apps/lib/features/dashboard/data/cache/dashboard_cache_manager.dart
vendored
Normal file
400
unionflow/unionflow-mobile-apps/lib/features/dashboard/data/cache/dashboard_cache_manager.dart
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de carte de statistiques connecté au backend
|
||||
class ConnectedStatsCard extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final String Function(DashboardStatsEntity) valueExtractor;
|
||||
final String? Function(DashboardStatsEntity)? subtitleExtractor;
|
||||
final Color? customColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ConnectedStatsCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.valueExtractor,
|
||||
this.subtitleExtractor,
|
||||
this.customColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingCard();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildDataCard(data.stats);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorCard(state.message);
|
||||
}
|
||||
return _buildLoadingCard();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataCard(DashboardStatsEntity stats) {
|
||||
final value = valueExtractor(stats);
|
||||
final subtitle = subtitleExtractor?.call(stats);
|
||||
final color = customColor ?? DashboardTheme.royalBlue;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Text(
|
||||
value,
|
||||
style: DashboardTheme.metricLarge.copyWith(color: color),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Container(
|
||||
height: 32,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorCard(String message) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Text(
|
||||
'--',
|
||||
style: DashboardTheme.metricLarge.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
message,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget des événements à venir connecté au backend
|
||||
class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
final int maxItems;
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
const ConnectedUpcomingEvents({
|
||||
super.key,
|
||||
this.maxItems = 3,
|
||||
this.onSeeAll,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingList();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildEventsList(data.upcomingEvents);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
return _buildEmptyState();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Événements à venir',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
child: Text(
|
||||
'Voir tout',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsList(List<UpcomingEventEntity> events) {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final displayEvents = events.take(maxItems).toList();
|
||||
|
||||
return Column(
|
||||
children: displayEvents.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final event = entry.value;
|
||||
final isLast = index == displayEvents.length - 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildEventCard(event),
|
||||
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard(UpcomingEventEntity event) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.grey200,
|
||||
width: event.isToday || event.isTomorrow ? 2 : 1,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Image ou icône
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: event.imageUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
child: Image.network(
|
||||
event.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.location,
|
||||
style: DashboardTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Badge de temps
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success.withOpacity(0.1)
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning.withOpacity(0.1)
|
||||
: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
event.daysUntilEvent,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
// Barre de progression des participants
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Participants',
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'${event.currentParticipants}/${event.maxParticipants}',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
LinearProgressIndicator(
|
||||
value: event.fillPercentage,
|
||||
backgroundColor: DashboardTheme.grey200,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
event.isFull
|
||||
? DashboardTheme.error
|
||||
: event.isAlmostFull
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Tags
|
||||
if (event.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Wrap(
|
||||
spacing: DashboardTheme.spacing4,
|
||||
runSpacing: DashboardTheme.spacing4,
|
||||
children: event.tags.take(3).map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.tealBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.tealBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingList() {
|
||||
return Column(
|
||||
children: List.generate(2, (index) => Column(
|
||||
children: [
|
||||
_buildLoadingCard(),
|
||||
if (index < 1) const SizedBox(height: DashboardTheme.spacing12),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: DashboardTheme.grey200),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
Container(
|
||||
height: 4,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
message,
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event_busy,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucun événement à venir',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const Text(
|
||||
'Les événements apparaîtront ici',
|
||||
style: DashboardTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:async';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de métriques en temps réel avec animations
|
||||
class RealTimeMetricsWidget extends StatefulWidget {
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
final Duration refreshInterval;
|
||||
|
||||
const RealTimeMetricsWidget({
|
||||
super.key,
|
||||
required this.organizationId,
|
||||
required this.userId,
|
||||
this.refreshInterval = const Duration(minutes: 5),
|
||||
});
|
||||
|
||||
@override
|
||||
State<RealTimeMetricsWidget> createState() => _RealTimeMetricsWidgetState();
|
||||
}
|
||||
|
||||
class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
with TickerProviderStateMixin {
|
||||
Timer? _refreshTimer;
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _countController;
|
||||
late Animation<double> _pulseAnimation;
|
||||
late Animation<double> _countAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_countController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_countAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _countController,
|
||||
curve: Curves.easeOutCubic,
|
||||
));
|
||||
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer = Timer.periodic(widget.refreshInterval, (timer) {
|
||||
if (mounted) {
|
||||
context.read<DashboardBloc>().add(RefreshDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.gradientCardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
BlocConsumer<DashboardBloc, DashboardState>(
|
||||
listener: (context, state) {
|
||||
if (state is DashboardLoaded) {
|
||||
_countController.forward(from: 0);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return _buildLoadingMetrics();
|
||||
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildMetrics(data);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorMetrics();
|
||||
}
|
||||
return _buildEmptyMetrics();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Métriques Temps Réel',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'Mise à jour automatique',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildRefreshIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRefreshIndicator() {
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardRefreshing) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DashboardBloc>().add(RefreshDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: DashboardTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetrics(DashboardEntity data) {
|
||||
return AnimatedBuilder(
|
||||
animation: _countAnimation,
|
||||
builder: (context, child) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Membres Actifs',
|
||||
(data.stats.activeMembers * _countAnimation.value).round(),
|
||||
data.stats.totalMembers,
|
||||
Icons.people,
|
||||
DashboardTheme.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Engagement',
|
||||
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
|
||||
100,
|
||||
Icons.favorite,
|
||||
DashboardTheme.warning,
|
||||
suffix: '%',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Événements',
|
||||
(data.stats.upcomingEvents * _countAnimation.value).round(),
|
||||
data.stats.totalEvents,
|
||||
Icons.event,
|
||||
DashboardTheme.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Croissance',
|
||||
(data.stats.monthlyGrowth * _countAnimation.value),
|
||||
null,
|
||||
Icons.trending_up,
|
||||
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
suffix: '%',
|
||||
isDecimal: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricItem(
|
||||
String label,
|
||||
dynamic value,
|
||||
int? maxValue,
|
||||
IconData icon,
|
||||
Color color, {
|
||||
String suffix = '',
|
||||
bool isDecimal = false,
|
||||
}) {
|
||||
String displayValue;
|
||||
if (isDecimal) {
|
||||
displayValue = value.toStringAsFixed(1) + suffix;
|
||||
} else {
|
||||
displayValue = value.toString() + suffix;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
displayValue,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
if (maxValue != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'sur $maxValue',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingMetrics() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingMetricItem() {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorMetrics() {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyMetrics() {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_pulseController.dispose();
|
||||
_countController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../data/services/dashboard_performance_monitor.dart';
|
||||
|
||||
/// Widget de monitoring des performances en temps réel
|
||||
class PerformanceMonitorWidget extends StatefulWidget {
|
||||
final bool showDetails;
|
||||
final Duration updateInterval;
|
||||
|
||||
const PerformanceMonitorWidget({
|
||||
super.key,
|
||||
this.showDetails = false,
|
||||
this.updateInterval = const Duration(seconds: 2),
|
||||
});
|
||||
|
||||
@override
|
||||
State<PerformanceMonitorWidget> createState() => _PerformanceMonitorWidgetState();
|
||||
}
|
||||
|
||||
class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
with TickerProviderStateMixin {
|
||||
final DashboardPerformanceMonitor _monitor = DashboardPerformanceMonitor();
|
||||
StreamSubscription<PerformanceMetrics>? _metricsSubscription;
|
||||
StreamSubscription<PerformanceAlert>? _alertSubscription;
|
||||
|
||||
PerformanceMetrics? _currentMetrics;
|
||||
final List<PerformanceAlert> _recentAlerts = [];
|
||||
|
||||
late AnimationController _pulseController;
|
||||
late Animation<double> _pulseAnimation;
|
||||
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_startMonitoring();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 0.8,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
Future<void> _startMonitoring() async {
|
||||
await _monitor.startMonitoring();
|
||||
|
||||
_metricsSubscription = _monitor.metricsStream.listen((metrics) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentMetrics = metrics;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_alertSubscription = _monitor.alertStream.listen((alert) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_recentAlerts.insert(0, alert);
|
||||
if (_recentAlerts.length > 5) {
|
||||
_recentAlerts.removeLast();
|
||||
}
|
||||
});
|
||||
|
||||
// Afficher une notification pour les alertes critiques
|
||||
if (alert.severity == AlertSeverity.error ||
|
||||
alert.severity == AlertSeverity.critical) {
|
||||
_showAlertSnackBar(alert);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showAlertSnackBar(PerformanceAlert alert) {
|
||||
final color = _getAlertColor(alert.severity);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getAlertIcon(alert.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alert.message,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: color,
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'Détails',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isExpanded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_currentMetrics == null) {
|
||||
return _buildLoadingWidget();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
if (_isExpanded || widget.showDetails) ...[
|
||||
const Divider(height: 1),
|
||||
_buildDetailedMetrics(),
|
||||
if (_recentAlerts.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
_buildAlertsSection(),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.royalBlue),
|
||||
),
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing12),
|
||||
Text(
|
||||
'Initialisation du monitoring...',
|
||||
style: DashboardTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: _getOverallHealthColor(),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _getOverallHealthColor().withOpacity(0.5),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Performances Système',
|
||||
style: DashboardTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
_buildQuickMetrics(),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Icon(
|
||||
_isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMetrics() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildQuickMetric(
|
||||
'MEM',
|
||||
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildQuickMetric(
|
||||
'CPU',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildQuickMetric(
|
||||
'NET',
|
||||
'${_currentMetrics!.networkLatency}ms',
|
||||
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMetric(String label, String value, Color color) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailedMetrics() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMetricRow(
|
||||
'Mémoire',
|
||||
'${_currentMetrics!.memoryUsage.toStringAsFixed(1)} MB',
|
||||
_currentMetrics!.memoryUsage / 1000, // Normaliser sur 1000MB
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
Icons.memory,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Processeur',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
|
||||
_currentMetrics!.cpuUsage / 100,
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
Icons.speed,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Réseau',
|
||||
'${_currentMetrics!.networkLatency} ms',
|
||||
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
|
||||
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
|
||||
Icons.wifi,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Images/sec',
|
||||
'${_currentMetrics!.frameRate.toStringAsFixed(1)} fps',
|
||||
_currentMetrics!.frameRate / 60,
|
||||
_getMetricColor(60 - _currentMetrics!.frameRate, 10, 30), // Inversé car plus c'est haut, mieux c'est
|
||||
Icons.videocam,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildMetricRow(
|
||||
'Batterie',
|
||||
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
|
||||
_currentMetrics!.batteryLevel / 100,
|
||||
_getBatteryColor(_currentMetrics!.batteryLevel),
|
||||
Icons.battery_std,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricRow(
|
||||
String label,
|
||||
String value,
|
||||
double progress,
|
||||
Color color,
|
||||
IconData icon,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
backgroundColor: DashboardTheme.grey200,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertsSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Alertes Récentes',
|
||||
style: DashboardTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
..._recentAlerts.take(3).map((alert) => _buildAlertItem(alert)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertItem(PerformanceAlert alert) {
|
||||
final color = _getAlertColor(alert.severity);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getAlertIcon(alert.type),
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alert.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: DashboardTheme.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatTime(alert.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getOverallHealthColor() {
|
||||
if (_currentMetrics == null) return DashboardTheme.grey400;
|
||||
|
||||
final metrics = _currentMetrics!;
|
||||
|
||||
// Calculer un score de santé global
|
||||
int issues = 0;
|
||||
if (metrics.memoryUsage > 500) issues++;
|
||||
if (metrics.cpuUsage > 70) issues++;
|
||||
if (metrics.networkLatency > 1000) issues++;
|
||||
if (metrics.frameRate < 30) issues++;
|
||||
|
||||
switch (issues) {
|
||||
case 0:
|
||||
return DashboardTheme.success;
|
||||
case 1:
|
||||
return DashboardTheme.warning;
|
||||
default:
|
||||
return DashboardTheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
|
||||
if (value >= errorThreshold) return DashboardTheme.error;
|
||||
if (value >= warningThreshold) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
}
|
||||
|
||||
Color _getBatteryColor(double batteryLevel) {
|
||||
if (batteryLevel <= 20) return DashboardTheme.error;
|
||||
if (batteryLevel <= 50) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
}
|
||||
|
||||
Color _getAlertColor(AlertSeverity severity) {
|
||||
switch (severity) {
|
||||
case AlertSeverity.info:
|
||||
return DashboardTheme.info;
|
||||
case AlertSeverity.warning:
|
||||
return DashboardTheme.warning;
|
||||
case AlertSeverity.error:
|
||||
return DashboardTheme.error;
|
||||
case AlertSeverity.critical:
|
||||
return DashboardTheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getAlertIcon(AlertType type) {
|
||||
switch (type) {
|
||||
case AlertType.memory:
|
||||
return Icons.memory;
|
||||
case AlertType.cpu:
|
||||
return Icons.speed;
|
||||
case AlertType.network:
|
||||
return Icons.wifi_off;
|
||||
case AlertType.performance:
|
||||
return Icons.slow_motion_video;
|
||||
case AlertType.battery:
|
||||
return Icons.battery_alert;
|
||||
case AlertType.disk:
|
||||
return Icons.storage;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(time);
|
||||
|
||||
if (diff.inMinutes < 1) return 'maintenant';
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes}min';
|
||||
if (diff.inHours < 24) return '${diff.inHours}h';
|
||||
return '${diff.inDays}j';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseController.dispose();
|
||||
_metricsSubscription?.cancel();
|
||||
_alertSubscription?.cancel();
|
||||
_monitor.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../pages/connected_dashboard_page.dart';
|
||||
import '../../pages/advanced_dashboard_page.dart';
|
||||
|
||||
/// Widget de navigation pour les différents types de dashboard
|
||||
class DashboardNavigation extends StatefulWidget {
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
|
||||
const DashboardNavigation({
|
||||
super.key,
|
||||
required this.organizationId,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardNavigation> createState() => _DashboardNavigationState();
|
||||
}
|
||||
|
||||
class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<DashboardTab> _tabs = [
|
||||
const DashboardTab(
|
||||
title: 'Accueil',
|
||||
icon: Icons.home,
|
||||
activeIcon: Icons.home,
|
||||
type: DashboardType.home,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Analytics',
|
||||
icon: Icons.analytics_outlined,
|
||||
activeIcon: Icons.analytics,
|
||||
type: DashboardType.analytics,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Rapports',
|
||||
icon: Icons.assessment_outlined,
|
||||
activeIcon: Icons.assessment,
|
||||
type: DashboardType.reports,
|
||||
),
|
||||
const DashboardTab(
|
||||
title: 'Paramètres',
|
||||
icon: Icons.settings_outlined,
|
||||
activeIcon: Icons.settings,
|
||||
type: DashboardType.settings,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _buildCurrentPage(),
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentPage() {
|
||||
switch (_tabs[_currentIndex].type) {
|
||||
case DashboardType.home:
|
||||
return ConnectedDashboardPage(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
);
|
||||
case DashboardType.analytics:
|
||||
return AdvancedDashboardPage(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
);
|
||||
case DashboardType.reports:
|
||||
return _buildReportsPage();
|
||||
case DashboardType.settings:
|
||||
return _buildSettingsPage();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: DashboardTheme.grey900.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BottomAppBar(
|
||||
shape: const CircularNotchedRectangle(),
|
||||
notchMargin: 8,
|
||||
color: DashboardTheme.white,
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DashboardTheme.spacing8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _tabs.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final tab = entry.value;
|
||||
final isActive = index == _currentIndex;
|
||||
|
||||
// Skip the middle item for FAB space
|
||||
if (index == 2) {
|
||||
return const SizedBox(width: 40);
|
||||
}
|
||||
|
||||
return _buildNavItem(tab, isActive, index);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(DashboardTab tab, bool isActive, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _currentIndex = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: DashboardTheme.spacing12,
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? tab.activeIcon : tab.icon,
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
tab.title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: DashboardTheme.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
onPressed: _showQuickActions,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: DashboardTheme.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Rapports'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.assessment,
|
||||
size: 64,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const Text(
|
||||
'Page Rapports',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Fonctionnalité en cours de développement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Paramètres'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
children: [
|
||||
_buildSettingsSection(
|
||||
'Apparence',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Thème',
|
||||
'Bleu Roi & Pétrole',
|
||||
Icons.palette,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Langue',
|
||||
'Français',
|
||||
Icons.language,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
_buildSettingsSection(
|
||||
'Notifications',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Notifications push',
|
||||
'Activées',
|
||||
Icons.notifications,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Emails',
|
||||
'Quotidien',
|
||||
Icons.email,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
_buildSettingsSection(
|
||||
'Données',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Synchronisation',
|
||||
'Automatique',
|
||||
Icons.sync,
|
||||
() {},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Cache',
|
||||
'Vider le cache',
|
||||
Icons.storage,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection(String title, List<Widget> children) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
child: Column(children: children),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsTile(
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: DashboardTheme.royalBlue),
|
||||
title: Text(title, style: DashboardTheme.bodyMedium),
|
||||
subtitle: Text(subtitle, style: DashboardTheme.bodySmall),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
void _showQuickActions() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadiusLarge),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadiusLarge),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const Text(
|
||||
'Actions Rapides',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
GridView.count(
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: DashboardTheme.spacing16,
|
||||
mainAxisSpacing: DashboardTheme.spacing16,
|
||||
children: [
|
||||
_buildQuickActionItem('Nouveau\nMembre', Icons.person_add, DashboardTheme.success),
|
||||
_buildQuickActionItem('Créer\nÉvénement', Icons.event_available, DashboardTheme.royalBlue),
|
||||
_buildQuickActionItem('Ajouter\nContribution', Icons.payment, DashboardTheme.tealBlue),
|
||||
_buildQuickActionItem('Envoyer\nMessage', Icons.message, DashboardTheme.warning),
|
||||
_buildQuickActionItem('Générer\nRapport', Icons.assessment, DashboardTheme.info),
|
||||
_buildQuickActionItem('Paramètres', Icons.settings, DashboardTheme.grey600),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionItem(String title, IconData icon, Color color) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 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,
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de recherche rapide pour le dashboard
|
||||
class DashboardSearchWidget extends StatefulWidget {
|
||||
final Function(String)? onSearch;
|
||||
final String? hintText;
|
||||
final List<SearchSuggestion>? suggestions;
|
||||
|
||||
const DashboardSearchWidget({
|
||||
super.key,
|
||||
this.onSearch,
|
||||
this.hintText,
|
||||
this.suggestions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardSearchWidget> createState() => _DashboardSearchWidgetState();
|
||||
}
|
||||
|
||||
class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
with TickerProviderStateMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isExpanded = false;
|
||||
List<SearchSuggestion> _filteredSuggestions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_setupListeners();
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.05,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
void _setupListeners() {
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
_isExpanded = _focusNode.hasFocus;
|
||||
});
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
});
|
||||
|
||||
_searchController.addListener(() {
|
||||
_filterSuggestions(_searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
void _filterSuggestions(String query) {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
|
||||
.where((suggestion) =>
|
||||
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_filteredSuggestions = filtered;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
_buildSuggestions(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _focusNode,
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
widget.onSearch?.call(value);
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'Rechercher...',
|
||||
hintStyle: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderSide: const BorderSide(
|
||||
color: DashboardTheme.royalBlue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
vertical: DashboardTheme.spacing12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: DashboardTheme.white,
|
||||
),
|
||||
style: DashboardTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestions() {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = _filteredSuggestions[index];
|
||||
return _buildSuggestionItem(suggestion, index == _filteredSuggestions.length - 1);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestionItem(SearchSuggestion suggestion, bool isLast) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_searchController.text = suggestion.title;
|
||||
widget.onSearch?.call(suggestion.title);
|
||||
_focusNode.unfocus();
|
||||
suggestion.onTap?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: suggestion.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
suggestion.icon,
|
||||
color: suggestion.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
suggestion.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (suggestion.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing2),
|
||||
Text(
|
||||
suggestion.subtitle,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchSuggestion> _getDefaultSuggestions() {
|
||||
return [
|
||||
SearchSuggestion(
|
||||
title: 'Membres',
|
||||
subtitle: 'Rechercher des membres',
|
||||
icon: Icons.people,
|
||||
color: DashboardTheme.royalBlue,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Événements',
|
||||
subtitle: 'Trouver des événements',
|
||||
icon: Icons.event,
|
||||
color: DashboardTheme.tealBlue,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Contributions',
|
||||
subtitle: 'Historique des paiements',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.success,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Rapports',
|
||||
subtitle: 'Consulter les rapports',
|
||||
icon: Icons.assessment,
|
||||
color: DashboardTheme.warning,
|
||||
onTap: () {},
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Paramètres',
|
||||
subtitle: 'Configuration système',
|
||||
icon: Icons.settings,
|
||||
color: DashboardTheme.grey600,
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_focusNode.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSuggestion {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SearchSuggestion({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
|
||||
/// Widget de sélection de thème pour le Dashboard
|
||||
class ThemeSelectorWidget extends StatefulWidget {
|
||||
final Function(String)? onThemeChanged;
|
||||
|
||||
const ThemeSelectorWidget({
|
||||
super.key,
|
||||
this.onThemeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ThemeSelectorWidget> createState() => _ThemeSelectorWidgetState();
|
||||
}
|
||||
|
||||
class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
String _selectedTheme = 'royalTeal';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedTheme = DashboardThemeManager.currentTheme.name == 'Bleu Roi & Pétrole'
|
||||
? 'royalTeal' : 'royalTeal'; // Par défaut
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.palette,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing8),
|
||||
Text(
|
||||
'Thème de l\'interface',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
|
||||
// Grille des thèmes
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemCount: DashboardThemeManager.availableThemes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final themeOption = DashboardThemeManager.availableThemes[index];
|
||||
final isSelected = _selectedTheme == themeOption.key;
|
||||
|
||||
return _buildThemeCard(themeOption, isSelected);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
|
||||
// Aperçu du thème sélectionné
|
||||
_buildThemePreview(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeCard(ThemeOption themeOption, bool isSelected) {
|
||||
return GestureDetector(
|
||||
onTap: () => _selectTheme(themeOption.key),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? themeOption.theme.primaryColor
|
||||
: DashboardTheme.grey300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: themeOption.theme.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: DashboardTheme.subtleShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Gradient de démonstration
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
themeOption.theme.primaryColor,
|
||||
themeOption.theme.secondaryColor,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
// Nom du thème
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: themeOption.theme.cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
bottomRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
themeOption.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: themeOption.theme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemePreview() {
|
||||
final currentTheme = DashboardThemeManager.availableThemes
|
||||
.firstWhere((theme) => theme.key == _selectedTheme);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: DashboardTheme.grey300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Aperçu: ${currentTheme.name}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
|
||||
// Exemple de carte avec le thème
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: currentTheme.theme.primaryColor.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: currentTheme.theme.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.dashboard,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dashboard UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Exemple avec ce thème',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: currentTheme.theme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.success.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
'Actif',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.theme.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
|
||||
// Palette de couleurs
|
||||
Row(
|
||||
children: [
|
||||
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Succès', currentTheme.theme.success),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
_buildColorSwatch('Attention', currentTheme.theme.warning),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorSwatch(String label, Color color) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectTheme(String themeKey) {
|
||||
setState(() {
|
||||
_selectedTheme = themeKey;
|
||||
});
|
||||
|
||||
// Appliquer le thème
|
||||
DashboardThemeManager.setTheme(themeKey);
|
||||
|
||||
// Notifier le changement
|
||||
widget.onThemeChanged?.call(themeKey);
|
||||
|
||||
// Afficher un message de confirmation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Thème "${DashboardThemeManager.availableThemes.firstWhere((t) => t.key == themeKey).name}" appliqué',
|
||||
),
|
||||
backgroundColor: DashboardThemeManager.currentTheme.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user