feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -11,8 +11,7 @@ class DashboardConfig {
|
||||
static const String primaryColorHex = '#4169E1'; // Bleu Roi
|
||||
static const String secondaryColorHex = '#008B8B'; // Bleu Pétrole
|
||||
|
||||
// Configuration des données
|
||||
static const bool useMockData = false;
|
||||
// Configuration des données (toujours API réelle, pas de données fictives)
|
||||
static String get apiBaseUrl => AppConfig.apiBaseUrl;
|
||||
static const Duration networkTimeout = Duration(seconds: 30);
|
||||
|
||||
@@ -282,9 +281,6 @@ class DashboardConfig {
|
||||
};
|
||||
|
||||
// Méthodes utilitaires
|
||||
static bool get isDevelopment => useMockData;
|
||||
static bool get isProduction => !useMockData;
|
||||
|
||||
static String get fullVersion => '$version+$buildNumber';
|
||||
|
||||
static Duration get effectiveRefreshInterval =>
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,36 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../models/membre_dashboard_synthese_model.dart';
|
||||
import '../models/compte_adherent_model.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
|
||||
abstract class DashboardRemoteDataSource {
|
||||
Future<DashboardDataModel> getDashboardData(String organizationId, String userId);
|
||||
/// Dashboard personnel du membre connecté (sans organisationId). GET /api/dashboard/membre/me
|
||||
Future<MembreDashboardSyntheseModel> getMemberDashboardData();
|
||||
/// Synthèse des cotisations du membre connecté. GET /api/cotisations/mes-cotisations/synthese
|
||||
/// Utilisé en fallback quand les montants de getMemberDashboardData() sont à 0.
|
||||
Future<Map<String, dynamic>?> getMesCotisationsSynthese();
|
||||
/// Compte adhérent unifié (soldes, crédits, capacité d'emprunt). GET /api/membres/mon-compte
|
||||
Future<CompteAdherentModel> getCompteAdherent();
|
||||
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});
|
||||
}
|
||||
|
||||
@Injectable(as: DashboardRemoteDataSource)
|
||||
class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
final DioClient dioClient;
|
||||
final ApiClient apiClient;
|
||||
|
||||
DashboardRemoteDataSourceImpl({required this.dioClient});
|
||||
DashboardRemoteDataSourceImpl(this.apiClient);
|
||||
|
||||
@override
|
||||
Future<DashboardDataModel> getDashboardData(String organizationId, String userId) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/data',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -32,16 +44,77 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load dashboard data: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardData', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardData', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreDashboardSyntheseModel> getMemberDashboardData() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/dashboard/membre/me');
|
||||
if (response.statusCode == 200) {
|
||||
return MembreDashboardSyntheseModel.fromJson(
|
||||
response.data is Map<String, dynamic> ? response.data as Map<String, dynamic> : Map<String, dynamic>.from(response.data as Map),
|
||||
);
|
||||
} else {
|
||||
throw ServerException('Failed to load member dashboard: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMemberDashboardData', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMemberDashboardData', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getMesCotisationsSynthese() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/cotisations/mes-cotisations/synthese');
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
}
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMesCotisationsSynthese échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
|
||||
Future<CompteAdherentModel> getCompteAdherent() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/membres/mon-compte');
|
||||
if (response.statusCode == 200) {
|
||||
return CompteAdherentModel.fromJson(
|
||||
response.data is Map<String, dynamic> ? response.data as Map<String, dynamic> : Map<String, dynamic>.from(response.data as Map),
|
||||
);
|
||||
} else {
|
||||
throw ServerException('Failed to load adherent account: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getCompteAdherent', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getCompteAdherent', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DashboardStatsModel> getDashboardStats(String organizationId, String userId) async {
|
||||
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/stats',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -55,9 +128,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load dashboard stats: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardStats', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardStats', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +143,7 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/activities',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -84,9 +159,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load recent activities: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getRecentActivities', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getRecentActivities', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +174,7 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
int limit = 5,
|
||||
}) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/events/upcoming',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -113,9 +190,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load upcoming events: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getUpcomingEvents', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getUpcomingEvents', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/// Modèle pour le "compte adhérent" unifié (GET /api/membres/mon-compte).
|
||||
class CompteAdherentModel {
|
||||
final String numeroMembre;
|
||||
final String nomComplet;
|
||||
final String? organisationNom;
|
||||
final String? dateAdhesion;
|
||||
final String statutCompte;
|
||||
|
||||
final double soldeCotisations;
|
||||
final double soldeEpargne;
|
||||
final double soldeBloque;
|
||||
final double soldeTotalDisponible;
|
||||
final double encoursCreditTotal;
|
||||
final double capaciteEmprunt;
|
||||
|
||||
final int nombreCotisationsPayees;
|
||||
final int nombreCotisationsTotal;
|
||||
final int nombreCotisationsEnRetard;
|
||||
final int? tauxEngagement;
|
||||
|
||||
final int nombreComptesEpargne;
|
||||
final String dateCalcul;
|
||||
|
||||
const CompteAdherentModel({
|
||||
required this.numeroMembre,
|
||||
required this.nomComplet,
|
||||
this.organisationNom,
|
||||
this.dateAdhesion,
|
||||
this.statutCompte = 'ACTIF',
|
||||
this.soldeCotisations = 0,
|
||||
this.soldeEpargne = 0,
|
||||
this.soldeBloque = 0,
|
||||
this.soldeTotalDisponible = 0,
|
||||
this.encoursCreditTotal = 0,
|
||||
this.capaciteEmprunt = 0,
|
||||
this.nombreCotisationsPayees = 0,
|
||||
this.nombreCotisationsTotal = 0,
|
||||
this.nombreCotisationsEnRetard = 0,
|
||||
this.tauxEngagement,
|
||||
this.nombreComptesEpargne = 0,
|
||||
required this.dateCalcul,
|
||||
});
|
||||
|
||||
factory CompteAdherentModel.fromJson(Map<String, dynamic> json) {
|
||||
return CompteAdherentModel(
|
||||
numeroMembre: json['numeroMembre'] as String? ?? 'N/A',
|
||||
nomComplet: json['nomComplet'] as String? ?? '',
|
||||
organisationNom: json['organisationNom'] as String?,
|
||||
dateAdhesion: json['dateAdhesion'] as String?,
|
||||
statutCompte: json['statutCompte'] as String? ?? 'ACTIF',
|
||||
soldeCotisations: _toDouble(json['soldeCotisations']),
|
||||
soldeEpargne: _toDouble(json['soldeEpargne']),
|
||||
soldeBloque: _toDouble(json['soldeBloque']),
|
||||
soldeTotalDisponible: _toDouble(json['soldeTotalDisponible']),
|
||||
encoursCreditTotal: _toDouble(json['encoursCreditTotal']),
|
||||
capaciteEmprunt: _toDouble(json['capaciteEmprunt']),
|
||||
nombreCotisationsPayees: (json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsTotal: (json['nombreCotisationsTotal'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsEnRetard: (json['nombreCotisationsEnRetard'] as num?)?.toInt() ?? 0,
|
||||
tauxEngagement: (json['tauxEngagement'] as num?)?.toInt(),
|
||||
nombreComptesEpargne: (json['nombreComptesEpargne'] as num?)?.toInt() ?? 0,
|
||||
dateCalcul: json['dateCalcul'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ class DashboardStatsModel extends Equatable {
|
||||
final double monthlyGrowth;
|
||||
final double engagementRate;
|
||||
final DateTime lastUpdated;
|
||||
final int? totalOrganizations;
|
||||
final Map<String, int>? organizationTypeDistribution;
|
||||
|
||||
const DashboardStatsModel({
|
||||
required this.totalMembers,
|
||||
@@ -30,6 +32,8 @@ class DashboardStatsModel extends Equatable {
|
||||
required this.monthlyGrowth,
|
||||
required this.engagementRate,
|
||||
required this.lastUpdated,
|
||||
this.totalOrganizations,
|
||||
this.organizationTypeDistribution,
|
||||
});
|
||||
|
||||
factory DashboardStatsModel.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -63,6 +67,8 @@ class DashboardStatsModel extends Equatable {
|
||||
monthlyGrowth,
|
||||
engagementRate,
|
||||
lastUpdated,
|
||||
totalOrganizations,
|
||||
organizationTypeDistribution,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ DashboardStatsModel _$DashboardStatsModelFromJson(Map<String, dynamic> json) =>
|
||||
monthlyGrowth: (json['monthlyGrowth'] as num).toDouble(),
|
||||
engagementRate: (json['engagementRate'] as num).toDouble(),
|
||||
lastUpdated: DateTime.parse(json['lastUpdated'] as String),
|
||||
totalOrganizations: (json['totalOrganizations'] as num?)?.toInt(),
|
||||
organizationTypeDistribution:
|
||||
(json['organizationTypeDistribution'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DashboardStatsModelToJson(
|
||||
@@ -36,6 +41,8 @@ Map<String, dynamic> _$DashboardStatsModelToJson(
|
||||
'monthlyGrowth': instance.monthlyGrowth,
|
||||
'engagementRate': instance.engagementRate,
|
||||
'lastUpdated': instance.lastUpdated.toIso8601String(),
|
||||
'totalOrganizations': instance.totalOrganizations,
|
||||
'organizationTypeDistribution': instance.organizationTypeDistribution,
|
||||
};
|
||||
|
||||
RecentActivityModel _$RecentActivityModelFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -11,6 +11,8 @@ class MembreDashboardSyntheseModel {
|
||||
final double totalCotisationsPayeesToutTemps;
|
||||
/// Nombre de cotisations payées (pour carte « Cotisations »).
|
||||
final int nombreCotisationsPayees;
|
||||
/// Nombre total de cotisations (toutes années, tous statuts).
|
||||
final int nombreCotisationsTotal;
|
||||
final String statutCotisations;
|
||||
final int? tauxCotisationsPerso;
|
||||
final double monSoldeEpargne;
|
||||
@@ -32,6 +34,7 @@ class MembreDashboardSyntheseModel {
|
||||
this.totalCotisationsPayeesAnnee = 0,
|
||||
this.totalCotisationsPayeesToutTemps = 0,
|
||||
this.nombreCotisationsPayees = 0,
|
||||
this.nombreCotisationsTotal = 0,
|
||||
this.statutCotisations = 'À jour',
|
||||
this.tauxCotisationsPerso,
|
||||
this.monSoldeEpargne = 0,
|
||||
@@ -55,6 +58,8 @@ class MembreDashboardSyntheseModel {
|
||||
totalCotisationsPayeesAnnee: _toDouble(json['totalCotisationsPayeesAnnee']),
|
||||
totalCotisationsPayeesToutTemps: _toDouble(json['totalCotisationsPayeesToutTemps']),
|
||||
nombreCotisationsPayees: (json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsTotal: (json['nombreCotisationsTotal'] as num?)?.toInt() ??
|
||||
(json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
statutCotisations: json['statutCotisations'] as String? ?? 'À jour',
|
||||
tauxCotisationsPerso: (json['tauxCotisationsPerso'] as num?)?.toInt(),
|
||||
monSoldeEpargne: _toDouble(json['monSoldeEpargne']),
|
||||
@@ -70,6 +75,7 @@ class MembreDashboardSyntheseModel {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/dashboard_entity.dart';
|
||||
import '../../domain/entities/compte_adherent_entity.dart';
|
||||
import '../../domain/repositories/dashboard_repository.dart';
|
||||
import '../datasources/dashboard_remote_datasource.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../models/membre_dashboard_synthese_model.dart';
|
||||
import '../models/compte_adherent_model.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/network/network_info.dart';
|
||||
@@ -19,6 +21,21 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
required this.networkInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, CompteAdherentEntity>> getCompteAdherent() async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return const Left(NetworkFailure('No internet connection'));
|
||||
}
|
||||
try {
|
||||
final model = await remoteDataSource.getCompteAdherent();
|
||||
return Right(_mapCompteToEntity(model));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('Unexpected error: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, DashboardEntity>> getDashboardData(
|
||||
String organizationId,
|
||||
@@ -31,9 +48,32 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
// Membre sans contexte org : utiliser l'API dashboard membre (GET /api/dashboard/membre/me)
|
||||
final useMemberDashboard = organizationId.trim().isEmpty;
|
||||
if (useMemberDashboard) {
|
||||
final synthese = await remoteDataSource.getMemberDashboardData();
|
||||
return Right(_mapMemberSyntheseToEntity(synthese, userId));
|
||||
// Chargement parallèle de la synthèse et du compte adhérent unifié
|
||||
final results = await Future.wait([
|
||||
remoteDataSource.getMemberDashboardData(),
|
||||
remoteDataSource.getCompteAdherent(),
|
||||
]);
|
||||
|
||||
final synthese = results[0] as MembreDashboardSyntheseModel;
|
||||
final compteModel = results[1] as CompteAdherentModel;
|
||||
|
||||
// Fallback : si les montants sont à zéro mais qu'il y a des cotisations,
|
||||
// on complète avec /api/cotisations/mes-cotisations/synthese
|
||||
Map<String, dynamic>? cotSynthese;
|
||||
if (synthese.totalCotisationsPayeesToutTemps == 0 ||
|
||||
synthese.tauxCotisationsPerso == null ||
|
||||
(synthese.tauxCotisationsPerso ?? 0) == 0) {
|
||||
cotSynthese = await remoteDataSource.getMesCotisationsSynthese();
|
||||
}
|
||||
|
||||
return Right(_mapMemberSyntheseToEntity(
|
||||
synthese,
|
||||
userId,
|
||||
cotSynthese: cotSynthese,
|
||||
compteModel: compteModel,
|
||||
));
|
||||
}
|
||||
|
||||
final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId);
|
||||
return Right(_mapToEntity(dashboardData));
|
||||
} on ServerException catch (e) {
|
||||
@@ -43,24 +83,65 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit une DashboardEntity à partir de la synthèse membre (même structure pour réutiliser l'UI).
|
||||
DashboardEntity _mapMemberSyntheseToEntity(MembreDashboardSyntheseModel s, String userId) {
|
||||
/// Construit une DashboardEntity à partir de la synthèse membre.
|
||||
/// [cotSynthese] est optionnel : utilisé en fallback quand les montants du dashboard
|
||||
/// membre sont à zéro (incohérence backend entre /api/dashboard/membre/me
|
||||
/// et /api/cotisations/mes-cotisations/synthese).
|
||||
DashboardEntity _mapMemberSyntheseToEntity(
|
||||
MembreDashboardSyntheseModel s,
|
||||
String userId, {
|
||||
Map<String, dynamic>? cotSynthese,
|
||||
CompteAdherentModel? compteModel,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
// Contribution Totale = cotisations payées tout temps ; MON SOLDE TOTAL = cotisations tout temps + épargne
|
||||
final totalCotisationsToutTemps = s.totalCotisationsPayeesToutTemps;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Montant des cotisations payées tout temps
|
||||
// ------------------------------------------------------------------
|
||||
double totalCotisationsToutTemps = s.totalCotisationsPayeesToutTemps;
|
||||
if (totalCotisationsToutTemps == 0 && cotSynthese != null) {
|
||||
// totalPayeAnnee = montant payé sur l'année en cours (meilleure approximation disponible)
|
||||
final totalPayeAnnee = _toDouble(cotSynthese['totalPayeAnnee']);
|
||||
if (totalPayeAnnee > 0) totalCotisationsToutTemps = totalPayeAnnee;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// MON SOLDE TOTAL = cotisations payées + épargne
|
||||
// ------------------------------------------------------------------
|
||||
final monSoldeTotal = totalCotisationsToutTemps + s.monSoldeEpargne;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Taux d'engagement (en %)
|
||||
// Priorité : tauxParticipationPerso > tauxCotisationsPerso > calculé depuis cotSynthese
|
||||
// ------------------------------------------------------------------
|
||||
int? tauxBrut = s.tauxParticipationPerso ?? s.tauxCotisationsPerso;
|
||||
double engagementRate = (tauxBrut ?? 0) / 100.0;
|
||||
if (engagementRate == 0 && cotSynthese != null) {
|
||||
final montantDu = _toDouble(cotSynthese['montantDu']);
|
||||
final totalPayeAnnee = _toDouble(cotSynthese['totalPayeAnnee']);
|
||||
final total = montantDu + totalPayeAnnee;
|
||||
if (total > 0) engagementRate = totalPayeAnnee / total;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Nombre de cotisations — utilize NEW nombreCotisationsTotal if available
|
||||
// ------------------------------------------------------------------
|
||||
final int nombreCotisations = s.nombreCotisationsTotal > 0
|
||||
? s.nombreCotisationsTotal
|
||||
: s.nombreCotisationsPayees;
|
||||
|
||||
final stats = DashboardStatsEntity(
|
||||
totalMembers: 0,
|
||||
activeMembers: 0,
|
||||
totalEvents: 0,
|
||||
upcomingEvents: s.evenementsAVenir,
|
||||
totalContributions: s.nombreCotisationsPayees,
|
||||
totalContributions: nombreCotisations,
|
||||
totalContributionAmount: monSoldeTotal,
|
||||
contributionsAmountOnly: totalCotisationsToutTemps,
|
||||
pendingRequests: 0,
|
||||
completedProjects: 0,
|
||||
monthlyGrowth: s.evolutionEpargneNombre,
|
||||
engagementRate: ((s.tauxParticipationPerso ?? s.tauxCotisationsPerso) ?? 0) / 100.0,
|
||||
engagementRate: engagementRate,
|
||||
lastUpdated: now,
|
||||
totalOrganizations: null,
|
||||
organizationTypeDistribution: null,
|
||||
@@ -69,10 +150,20 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
stats: stats,
|
||||
recentActivities: const [],
|
||||
upcomingEvents: const [],
|
||||
userPreferences: <String, dynamic>{},
|
||||
userPreferences: const <String, dynamic>{},
|
||||
organizationId: '',
|
||||
userId: userId,
|
||||
monCompte: compteModel != null ? _mapCompteToEntity(compteModel) : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -142,6 +233,28 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
}
|
||||
}
|
||||
|
||||
CompteAdherentEntity _mapCompteToEntity(CompteAdherentModel model) {
|
||||
return CompteAdherentEntity(
|
||||
numeroMembre: model.numeroMembre,
|
||||
nomComplet: model.nomComplet,
|
||||
organisationNom: model.organisationNom,
|
||||
dateAdhesion: model.dateAdhesion != null ? DateTime.tryParse(model.dateAdhesion!) : null,
|
||||
statutCompte: model.statutCompte,
|
||||
soldeCotisations: model.soldeCotisations,
|
||||
soldeEpargne: model.soldeEpargne,
|
||||
soldeBloque: model.soldeBloque,
|
||||
soldeTotalDisponible: model.soldeTotalDisponible,
|
||||
encoursCreditTotal: model.encoursCreditTotal,
|
||||
capaciteEmprunt: model.capaciteEmprunt,
|
||||
nombreCotisationsPayees: model.nombreCotisationsPayees,
|
||||
nombreCotisationsTotal: model.nombreCotisationsTotal,
|
||||
nombreCotisationsEnRetard: model.nombreCotisationsEnRetard,
|
||||
engagementRate: (model.tauxEngagement ?? 0) / 100.0,
|
||||
nombreComptesEpargne: model.nombreComptesEpargne,
|
||||
dateCalcul: DateTime.tryParse(model.dateCalcul) ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// Mappers
|
||||
DashboardEntity _mapToEntity(DashboardDataModel model) {
|
||||
return DashboardEntity(
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import 'package:unionflow_mobile_apps/core/utils/logger.dart';
|
||||
import '../../presentation/bloc/finance_state.dart';
|
||||
|
||||
/// Repository pour les données financières (cotisations, synthèse).
|
||||
/// Appelle les endpoints /api/cotisations/mes-cotisations/*.
|
||||
@lazySingleton
|
||||
class FinanceRepository {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
FinanceRepository(this._apiClient);
|
||||
|
||||
/// Synthèse des cotisations du membre connecté (GET /api/cotisations/mes-cotisations/synthese).
|
||||
Future<FinanceSummary> getFinancialSummary() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/api/cotisations/mes-cotisations/synthese');
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final totalPayeAnnee = (data['totalPayeAnnee'] is num)
|
||||
? (data['totalPayeAnnee'] as num).toDouble()
|
||||
: 0.0;
|
||||
final montantDu = (data['montantDu'] is num)
|
||||
? (data['montantDu'] as num).toDouble()
|
||||
: 0.0;
|
||||
final epargneBalance = (data['epargneBalance'] is num)
|
||||
? (data['epargneBalance'] as num).toDouble()
|
||||
: 0.0;
|
||||
return FinanceSummary(
|
||||
totalContributionsPaid: totalPayeAnnee,
|
||||
totalContributionsPending: montantDu,
|
||||
epargneBalance: epargneBalance,
|
||||
);
|
||||
} on DioException catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getFinancialSummary échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getFinancialSummary erreur inattendue', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cotisations en attente du membre connecté (GET /api/cotisations/mes-cotisations/en-attente).
|
||||
Future<List<FinanceTransaction>> getTransactions() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/api/cotisations/mes-cotisations/en-attente');
|
||||
final List<dynamic> data = response.data is List ? response.data as List : [];
|
||||
return data
|
||||
.map((json) => _transactionFromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on DioException catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getTransactions échoué', error: e, stackTrace: st);
|
||||
if (e.response?.statusCode == 404) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static FinanceTransaction _transactionFromJson(Map<String, dynamic> json) {
|
||||
final id = json['id']?.toString() ?? '';
|
||||
final ref = json['numeroReference']?.toString() ?? '';
|
||||
final nomMembre = json['nomMembre']?.toString() ?? 'Cotisation';
|
||||
final montantDu = (json['montantDu'] is num)
|
||||
? (json['montantDu'] as num).toDouble()
|
||||
: 0.0;
|
||||
final statutLibelle = json['statutLibelle']?.toString() ?? 'En attente';
|
||||
final dateEcheance = json['dateEcheance']?.toString();
|
||||
final dateStr = dateEcheance != null
|
||||
? _parseDateToDisplay(dateEcheance)
|
||||
: '';
|
||||
return FinanceTransaction(
|
||||
id: id,
|
||||
title: nomMembre.isNotEmpty ? nomMembre : 'Cotisation $ref',
|
||||
date: dateStr,
|
||||
amount: montantDu,
|
||||
status: statutLibelle,
|
||||
);
|
||||
}
|
||||
|
||||
static String _parseDateToDisplay(String isoDate) {
|
||||
try {
|
||||
final d = DateTime.parse(isoDate);
|
||||
return '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}';
|
||||
} catch (e) {
|
||||
AppLogger.warning('FinanceRepository: _parseDateToDisplay date invalide', tag: isoDate);
|
||||
return isoDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.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';
|
||||
import '../../../../core/storage/dashboard_cache_manager.dart';
|
||||
|
||||
/// Service de mode hors ligne avec synchronisation pour le Dashboard
|
||||
class DashboardOfflineService {
|
||||
@@ -14,7 +15,7 @@ class DashboardOfflineService {
|
||||
static const String _offlineModeKey = 'dashboard_offline_mode';
|
||||
|
||||
final DashboardCacheManager _cacheManager;
|
||||
final Dio _dio;
|
||||
final ApiClient _apiClient;
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
@@ -35,7 +36,7 @@ class DashboardOfflineService {
|
||||
Stream<OfflineStatus> get statusStream => _statusController.stream;
|
||||
Stream<SyncProgress> get syncStream => _syncController.stream;
|
||||
|
||||
DashboardOfflineService(this._cacheManager, this._dio);
|
||||
DashboardOfflineService(this._cacheManager, this._apiClient);
|
||||
|
||||
/// Initialise le service hors ligne
|
||||
Future<void> initialize() async {
|
||||
@@ -216,14 +217,13 @@ class DashboardOfflineService {
|
||||
final userId = action.data['userId'] as String?;
|
||||
if (orgId == null || userId == null) return;
|
||||
|
||||
final response = await _dio.get('/api/dashboard/stats', queryParameters: {
|
||||
final response = await _apiClient.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,
|
||||
await _cacheManager.setKey(
|
||||
'dashboard_${orgId}_$userId',
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,7 @@ class DashboardOfflineService {
|
||||
final preferences = action.data['preferences'] as Map<String, dynamic>?;
|
||||
if (userId == null || preferences == null) return;
|
||||
|
||||
await _dio.put('/api/membres/$userId/preferences', data: preferences);
|
||||
await _apiClient.put('/api/membres/$userId/preferences', data: preferences);
|
||||
}
|
||||
|
||||
/// Synchronise le marquage d'activité comme lue
|
||||
@@ -242,7 +242,7 @@ class DashboardOfflineService {
|
||||
final activityId = action.data['activityId'] as String?;
|
||||
if (activityId == null) return;
|
||||
|
||||
await _dio.put('/api/notifications/$activityId/read');
|
||||
await _apiClient.put('/api/notifications/$activityId/read');
|
||||
}
|
||||
|
||||
/// Synchronise l'inscription à un événement
|
||||
@@ -251,7 +251,7 @@ class DashboardOfflineService {
|
||||
final membreId = action.data['membreId'] as String?;
|
||||
if (eventId == null || membreId == null) return;
|
||||
|
||||
await _dio.post('/api/evenements/$eventId/inscription', data: {
|
||||
await _apiClient.post('/api/evenements/$eventId/inscription', data: {
|
||||
'membreId': membreId,
|
||||
});
|
||||
}
|
||||
@@ -262,7 +262,7 @@ class DashboardOfflineService {
|
||||
final params = action.data['params'] as Map<String, dynamic>?;
|
||||
if (reportType == null) return;
|
||||
|
||||
await _dio.post('/api/export/$reportType', data: params ?? {});
|
||||
await _apiClient.post('/api/export/$reportType', data: params ?? {});
|
||||
}
|
||||
|
||||
/// Sauvegarde les actions en attente
|
||||
@@ -315,7 +315,7 @@ class DashboardOfflineService {
|
||||
}
|
||||
|
||||
/// Force une synchronisation manuelle
|
||||
Future<void> forcSync() async {
|
||||
Future<void> forceSync() async {
|
||||
if (!_isOnline) {
|
||||
throw Exception('Impossible de synchroniser hors ligne');
|
||||
}
|
||||
@@ -328,7 +328,8 @@ class DashboardOfflineService {
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
return await _cacheManager.getCachedDashboardData(organizationId, userId);
|
||||
final m = _cacheManager.getKey<Map<String, dynamic>>('dashboard_${organizationId}_$userId');
|
||||
return m != null ? DashboardDataModel.fromJson(m) : null;
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles hors ligne
|
||||
|
||||
@@ -19,7 +19,8 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
bool _isMonitoring = false;
|
||||
DateTime _startTime = DateTime.now();
|
||||
|
||||
int _alertsGeneratedCount = 0;
|
||||
|
||||
// Seuils d'alerte configurables
|
||||
final double _memoryThreshold = DashboardConfig.getAlertThreshold('memoryUsage');
|
||||
final double _cpuThreshold = DashboardConfig.getAlertThreshold('cpuUsage');
|
||||
@@ -147,18 +148,16 @@ class DashboardPerformanceMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la latence réseau
|
||||
/// Obtient la latence réseau (hôte/port depuis DashboardConfig.apiBaseUrl).
|
||||
Future<int> _getNetworkLatency() async {
|
||||
try {
|
||||
final uri = Uri.parse(DashboardConfig.apiBaseUrl);
|
||||
final host = uri.host.isNotEmpty ? uri.host : 'localhost';
|
||||
final port = uri.hasPort ? uri.port : 8085;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// Ping vers le serveur de l'API
|
||||
final socket = await Socket.connect('localhost', 8080)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
|
||||
final socket = await Socket.connect(host, port).timeout(const Duration(seconds: 5));
|
||||
stopwatch.stop();
|
||||
await socket.close();
|
||||
|
||||
return stopwatch.elapsedMilliseconds;
|
||||
} catch (e) {
|
||||
return _simulateNetworkLatency();
|
||||
@@ -228,6 +227,7 @@ class DashboardPerformanceMonitor {
|
||||
void _checkAlerts(PerformanceMetrics metrics) {
|
||||
// Alerte mémoire
|
||||
if (metrics.memoryUsage > _memoryThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.memory,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -240,6 +240,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte CPU
|
||||
if (metrics.cpuUsage > _cpuThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.cpu,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -252,6 +253,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte latence réseau
|
||||
if (metrics.networkLatency > _networkLatencyThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.network,
|
||||
severity: AlertSeverity.error,
|
||||
@@ -264,6 +266,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte frame rate
|
||||
if (metrics.frameRate < _frameRateThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.performance,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -298,8 +301,7 @@ class DashboardPerformanceMonitor {
|
||||
if (_snapshots.isEmpty) {
|
||||
return PerformanceStats.empty();
|
||||
}
|
||||
|
||||
return PerformanceStats.fromSnapshots(_snapshots);
|
||||
return PerformanceStats.fromSnapshots(_snapshots, alertsGenerated: _alertsGeneratedCount);
|
||||
}
|
||||
|
||||
/// Méthodes de simulation pour le développement
|
||||
@@ -508,7 +510,7 @@ class PerformanceStats {
|
||||
);
|
||||
}
|
||||
|
||||
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots) {
|
||||
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots, {int alertsGenerated = 0}) {
|
||||
if (snapshots.isEmpty) return PerformanceStats.empty();
|
||||
|
||||
final metrics = snapshots.map((s) => s.metrics).toList();
|
||||
@@ -520,7 +522,7 @@ class PerformanceStats {
|
||||
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
|
||||
alertsGenerated: alertsGenerated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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,65 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CompteAdherentEntity extends Equatable {
|
||||
final String numeroMembre;
|
||||
final String nomComplet;
|
||||
final String? organisationNom;
|
||||
final DateTime? dateAdhesion;
|
||||
final String statutCompte;
|
||||
|
||||
final double soldeCotisations;
|
||||
final double soldeEpargne;
|
||||
final double soldeBloque;
|
||||
final double soldeTotalDisponible;
|
||||
final double encoursCreditTotal;
|
||||
final double capaciteEmprunt;
|
||||
|
||||
final int nombreCotisationsPayees;
|
||||
final int nombreCotisationsTotal;
|
||||
final int nombreCotisationsEnRetard;
|
||||
final double engagementRate;
|
||||
|
||||
final int nombreComptesEpargne;
|
||||
final DateTime dateCalcul;
|
||||
|
||||
const CompteAdherentEntity({
|
||||
required this.numeroMembre,
|
||||
required this.nomComplet,
|
||||
this.organisationNom,
|
||||
this.dateAdhesion,
|
||||
required this.statutCompte,
|
||||
required this.soldeCotisations,
|
||||
required this.soldeEpargne,
|
||||
required this.soldeBloque,
|
||||
required this.soldeTotalDisponible,
|
||||
required this.encoursCreditTotal,
|
||||
required this.capaciteEmprunt,
|
||||
required this.nombreCotisationsPayees,
|
||||
required this.nombreCotisationsTotal,
|
||||
required this.nombreCotisationsEnRetard,
|
||||
required this.engagementRate,
|
||||
required this.nombreComptesEpargne,
|
||||
required this.dateCalcul,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
numeroMembre,
|
||||
nomComplet,
|
||||
organisationNom,
|
||||
dateAdhesion,
|
||||
statutCompte,
|
||||
soldeCotisations,
|
||||
soldeEpargne,
|
||||
soldeBloque,
|
||||
soldeTotalDisponible,
|
||||
encoursCreditTotal,
|
||||
capaciteEmprunt,
|
||||
nombreCotisationsPayees,
|
||||
nombreCotisationsTotal,
|
||||
nombreCotisationsEnRetard,
|
||||
engagementRate,
|
||||
nombreComptesEpargne,
|
||||
dateCalcul,
|
||||
];
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'compte_adherent_entity.dart';
|
||||
|
||||
/// Entité pour les statistiques du dashboard
|
||||
|
||||
class DashboardStatsEntity extends Equatable {
|
||||
final int totalMembers;
|
||||
final int activeMembers;
|
||||
@@ -225,6 +227,8 @@ class DashboardEntity extends Equatable {
|
||||
final Map<String, dynamic> userPreferences;
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
/// Compte adhérent unifié (si disponible)
|
||||
final CompteAdherentEntity? monCompte;
|
||||
|
||||
const DashboardEntity({
|
||||
required this.stats,
|
||||
@@ -233,6 +237,7 @@ class DashboardEntity extends Equatable {
|
||||
required this.userPreferences,
|
||||
required this.organizationId,
|
||||
required this.userId,
|
||||
this.monCompte,
|
||||
});
|
||||
|
||||
// Méthodes utilitaires
|
||||
@@ -250,5 +255,7 @@ class DashboardEntity extends Equatable {
|
||||
userPreferences,
|
||||
organizationId,
|
||||
userId,
|
||||
monCompte,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../entities/dashboard_entity.dart';
|
||||
import '../entities/compte_adherent_entity.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
|
||||
abstract class DashboardRepository {
|
||||
/// Récupère le compte adhérent unifié (soldes, crédits, capacité d'emprunt).
|
||||
Future<Either<Failure, CompteAdherentEntity>> getCompteAdherent();
|
||||
|
||||
Future<Either<Failure, DashboardEntity>> getDashboardData(
|
||||
String organizationId,
|
||||
String userId,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/compte_adherent_entity.dart';
|
||||
import '../repositories/dashboard_repository.dart';
|
||||
|
||||
@injectable
|
||||
class GetCompteAdherent implements UseCase<CompteAdherentEntity, NoParams> {
|
||||
final DashboardRepository repository;
|
||||
|
||||
GetCompteAdherent(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, CompteAdherentEntity>> call(NoParams params) async {
|
||||
return await repository.getCompteAdherent();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../entities/dashboard_entity.dart';
|
||||
@@ -5,6 +6,7 @@ import '../repositories/dashboard_repository.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
|
||||
@injectable
|
||||
class GetDashboardData implements UseCase<DashboardEntity, GetDashboardDataParams> {
|
||||
final DashboardRepository repository;
|
||||
|
||||
@@ -32,6 +34,7 @@ class GetDashboardDataParams extends Equatable {
|
||||
List<Object> get props => [organizationId, userId];
|
||||
}
|
||||
|
||||
@injectable
|
||||
class GetDashboardStats implements UseCase<DashboardStatsEntity, GetDashboardStatsParams> {
|
||||
final DashboardRepository repository;
|
||||
|
||||
@@ -59,6 +62,7 @@ class GetDashboardStatsParams extends Equatable {
|
||||
List<Object> get props => [organizationId, userId];
|
||||
}
|
||||
|
||||
@injectable
|
||||
class GetRecentActivities implements UseCase<List<RecentActivityEntity>, GetRecentActivitiesParams> {
|
||||
final DashboardRepository repository;
|
||||
|
||||
@@ -89,6 +93,7 @@ class GetRecentActivitiesParams extends Equatable {
|
||||
List<Object> get props => [organizationId, userId, limit];
|
||||
}
|
||||
|
||||
@injectable
|
||||
class GetUpcomingEvents implements UseCase<List<UpcomingEventEntity>, GetUpcomingEventsParams> {
|
||||
final DashboardRepository repository;
|
||||
|
||||
|
||||
@@ -1,29 +1,83 @@
|
||||
import 'dart:async';
|
||||
import 'package:injectable/injectable.dart';
|
||||
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';
|
||||
import '../../../../core/websocket/websocket_service.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
|
||||
part 'dashboard_event.dart';
|
||||
part 'dashboard_state.dart';
|
||||
|
||||
@injectable
|
||||
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
|
||||
final GetDashboardData getDashboardData;
|
||||
final GetDashboardStats getDashboardStats;
|
||||
final GetRecentActivities getRecentActivities;
|
||||
final GetUpcomingEvents getUpcomingEvents;
|
||||
final WebSocketService webSocketService;
|
||||
|
||||
StreamSubscription<WebSocketEvent>? _webSocketEventSubscription;
|
||||
StreamSubscription<bool>? _webSocketConnectionSubscription;
|
||||
|
||||
DashboardBloc({
|
||||
required this.getDashboardData,
|
||||
required this.getDashboardStats,
|
||||
required this.getRecentActivities,
|
||||
required this.getUpcomingEvents,
|
||||
required this.webSocketService,
|
||||
}) : super(DashboardInitial()) {
|
||||
on<LoadDashboardData>(_onLoadDashboardData);
|
||||
on<RefreshDashboardData>(_onRefreshDashboardData);
|
||||
on<LoadDashboardStats>(_onLoadDashboardStats);
|
||||
on<LoadRecentActivities>(_onLoadRecentActivities);
|
||||
on<LoadUpcomingEvents>(_onLoadUpcomingEvents);
|
||||
on<RefreshDashboardFromWebSocket>(_onRefreshDashboardFromWebSocket);
|
||||
on<WebSocketConnectionChanged>(_onWebSocketConnectionChanged);
|
||||
|
||||
// Initialiser WebSocket et écouter les events
|
||||
_initializeWebSocket();
|
||||
}
|
||||
|
||||
/// Initialise la connexion WebSocket et écoute les events
|
||||
void _initializeWebSocket() {
|
||||
// Connexion au WebSocket
|
||||
webSocketService.connect();
|
||||
AppLogger.info('DashboardBloc: WebSocket initialisé');
|
||||
|
||||
// Écouter les events WebSocket
|
||||
_webSocketEventSubscription = webSocketService.eventStream.listen(
|
||||
(event) {
|
||||
AppLogger.info('DashboardBloc: Event WebSocket reçu - ${event.eventType}');
|
||||
|
||||
// Dispatcher uniquement les events pertinents au dashboard
|
||||
if (event is DashboardStatsEvent) {
|
||||
add(RefreshDashboardFromWebSocket(event.data));
|
||||
} else if (event is FinanceApprovalEvent) {
|
||||
// Les approbations affectent les stats, rafraîchir
|
||||
add(RefreshDashboardFromWebSocket(event.data));
|
||||
} else if (event is MemberEvent) {
|
||||
// Les changements de membres affectent les stats
|
||||
add(RefreshDashboardFromWebSocket(event.data));
|
||||
} else if (event is ContributionEvent) {
|
||||
// Les cotisations affectent les stats financières
|
||||
add(RefreshDashboardFromWebSocket(event.data));
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
AppLogger.error('DashboardBloc: Erreur WebSocket', error: error);
|
||||
},
|
||||
);
|
||||
|
||||
// Écouter le statut de connexion WebSocket
|
||||
_webSocketConnectionSubscription = webSocketService.connectionStatusStream.listen(
|
||||
(isConnected) {
|
||||
AppLogger.info('DashboardBloc: WebSocket ${isConnected ? "connecté" : "déconnecté"}');
|
||||
add(WebSocketConnectionChanged(isConnected));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLoadDashboardData(
|
||||
@@ -161,6 +215,61 @@ class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Rafraîchit le dashboard suite à un event WebSocket
|
||||
Future<void> _onRefreshDashboardFromWebSocket(
|
||||
RefreshDashboardFromWebSocket event,
|
||||
Emitter<DashboardState> emit,
|
||||
) async {
|
||||
AppLogger.info('DashboardBloc: Rafraîchissement depuis WebSocket');
|
||||
|
||||
// Si le dashboard est chargé, on rafraîchit uniquement les stats
|
||||
// pour éviter de recharger toutes les données
|
||||
if (state is DashboardLoaded) {
|
||||
final currentData = (state as DashboardLoaded).dashboardData;
|
||||
|
||||
// Rafraîchir les stats depuis le backend
|
||||
final result = await getDashboardStats(
|
||||
GetDashboardStatsParams(
|
||||
organizationId: currentData.organizationId,
|
||||
userId: currentData.userId,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
AppLogger.error('Erreur rafraîchissement stats WebSocket', error: failure);
|
||||
// Ne pas émettre d'erreur, garder les données actuelles
|
||||
},
|
||||
(stats) {
|
||||
final updatedData = DashboardEntity(
|
||||
stats: stats,
|
||||
recentActivities: currentData.recentActivities,
|
||||
upcomingEvents: currentData.upcomingEvents,
|
||||
userPreferences: currentData.userPreferences,
|
||||
organizationId: currentData.organizationId,
|
||||
userId: currentData.userId,
|
||||
);
|
||||
emit(DashboardLoaded(updatedData));
|
||||
AppLogger.info('DashboardBloc: Stats rafraîchies depuis WebSocket');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les changements de statut de connexion WebSocket
|
||||
void _onWebSocketConnectionChanged(
|
||||
WebSocketConnectionChanged event,
|
||||
Emitter<DashboardState> emit,
|
||||
) {
|
||||
// Pour l'instant, on log juste le statut
|
||||
// On pourrait ajouter un indicateur visuel dans l'UI plus tard
|
||||
if (event.isConnected) {
|
||||
AppLogger.info('DashboardBloc: WebSocket connecté - Temps réel actif');
|
||||
} else {
|
||||
AppLogger.warning('DashboardBloc: WebSocket déconnecté - Reconnexion en cours...');
|
||||
}
|
||||
}
|
||||
|
||||
String _mapFailureToMessage(Failure failure) {
|
||||
switch (failure.runtimeType) {
|
||||
case ServerFailure:
|
||||
@@ -171,4 +280,18 @@ class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
|
||||
return 'Une erreur inattendue s\'est produite.';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
// Annuler les subscriptions WebSocket
|
||||
_webSocketEventSubscription?.cancel();
|
||||
_webSocketConnectionSubscription?.cancel();
|
||||
|
||||
// Déconnecter le WebSocket
|
||||
webSocketService.disconnect();
|
||||
|
||||
AppLogger.info('DashboardBloc: Fermé et WebSocket déconnecté');
|
||||
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +75,23 @@ class LoadUpcomingEvents extends DashboardEvent {
|
||||
@override
|
||||
List<Object> get props => [organizationId, userId, limit];
|
||||
}
|
||||
|
||||
/// Event déclenché par WebSocket pour rafraîchir le dashboard
|
||||
class RefreshDashboardFromWebSocket extends DashboardEvent {
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const RefreshDashboardFromWebSocket(this.data);
|
||||
|
||||
@override
|
||||
List<Object> get props => [data];
|
||||
}
|
||||
|
||||
/// Event pour gérer les changements de statut WebSocket
|
||||
class WebSocketConnectionChanged extends DashboardEvent {
|
||||
final bool isConnected;
|
||||
|
||||
const WebSocketConnectionChanged(this.isConnected);
|
||||
|
||||
@override
|
||||
List<Object> get props => [isConnected];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../data/repositories/finance_repository.dart';
|
||||
import 'finance_event.dart';
|
||||
import 'finance_state.dart';
|
||||
|
||||
@injectable
|
||||
class FinanceBloc extends Bloc<FinanceEvent, FinanceState> {
|
||||
final FinanceRepository _repository;
|
||||
|
||||
FinanceBloc(this._repository) : super(FinanceInitial()) {
|
||||
on<LoadFinanceRequested>(_onLoadFinanceRequested);
|
||||
on<FinancePaymentInitiated>(_onFinancePaymentInitiated);
|
||||
}
|
||||
|
||||
Future<void> _onLoadFinanceRequested(LoadFinanceRequested event, Emitter<FinanceState> emit) async {
|
||||
emit(FinanceLoading());
|
||||
try {
|
||||
final summary = await _repository.getFinancialSummary();
|
||||
final transactions = await _repository.getTransactions();
|
||||
emit(FinanceLoaded(summary: summary, transactions: transactions));
|
||||
} catch (e) {
|
||||
emit(FinanceError('Erreur chargement des finances: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onFinancePaymentInitiated(FinancePaymentInitiated event, Emitter<FinanceState> emit) {
|
||||
// Intégration paiement: appeler le service Wave ou Orange Money (API paiement) selon le design métier.
|
||||
// Pour l'instant, la transaction est gérée côté UI (payment_dialog) et le BLoC reste en FinanceLoaded.
|
||||
if (state is FinanceLoaded) {
|
||||
// Option: émettre FinancePaymentPending puis FinanceLoaded après confirmation API.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class FinanceEvent extends Equatable {
|
||||
const FinanceEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadFinanceRequested extends FinanceEvent {}
|
||||
|
||||
class FinancePaymentInitiated extends FinanceEvent {
|
||||
final String contributionId;
|
||||
const FinancePaymentInitiated(this.contributionId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [contributionId];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class FinanceSummary extends Equatable {
|
||||
final double totalContributionsPaid;
|
||||
final double totalContributionsPending;
|
||||
final double epargneBalance;
|
||||
|
||||
const FinanceSummary({
|
||||
required this.totalContributionsPaid,
|
||||
required this.totalContributionsPending,
|
||||
required this.epargneBalance,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [totalContributionsPaid, totalContributionsPending, epargneBalance];
|
||||
}
|
||||
|
||||
class FinanceTransaction extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String date;
|
||||
final double amount;
|
||||
final String status;
|
||||
|
||||
const FinanceTransaction({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
required this.status, // "Payé", "En attente"
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, date, amount, status];
|
||||
}
|
||||
|
||||
abstract class FinanceState extends Equatable {
|
||||
const FinanceState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class FinanceInitial extends FinanceState {}
|
||||
|
||||
class FinanceLoading extends FinanceState {}
|
||||
|
||||
class FinanceLoaded extends FinanceState {
|
||||
final FinanceSummary summary;
|
||||
final List<FinanceTransaction> transactions;
|
||||
|
||||
const FinanceLoaded({
|
||||
required this.summary,
|
||||
required this.transactions,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [summary, transactions];
|
||||
}
|
||||
|
||||
class FinanceError extends FinanceState {
|
||||
final String message;
|
||||
const FinanceError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
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 '../bloc/dashboard_bloc.dart';
|
||||
import '../../domain/entities/dashboard_entity.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../settings/presentation/pages/system_settings_page.dart';
|
||||
|
||||
/// Page dashboard avancée avec graphiques et analytics
|
||||
class AdvancedDashboardPage extends StatefulWidget {
|
||||
@@ -57,12 +60,12 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
return BlocProvider(
|
||||
create: (context) => _dashboardBloc,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
_buildSliverAppBar(),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: const UFAppBar(
|
||||
title: 'DASHBOARD AVANCÉ',
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
@@ -76,7 +79,6 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
),
|
||||
);
|
||||
@@ -89,10 +91,16 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: DashboardTheme.headerDecoration,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primaryGreen, AppColors.brandGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@@ -100,34 +108,38 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.dashboard,
|
||||
color: DashboardTheme.white,
|
||||
Icons.dashboard_outlined,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dashboard Avancé',
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
fontSize: 28,
|
||||
'DASHBOARD AVANCÉ',
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Analytics & Insights',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.9),
|
||||
'ANALYTICS & INSIGHTS',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -135,7 +147,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoaded || state is DashboardRefreshing) {
|
||||
@@ -147,15 +159,15 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
_buildQuickStat(
|
||||
'Membres',
|
||||
'${data.stats.activeMembers}/${data.stats.totalMembers}',
|
||||
Icons.people,
|
||||
Icons.people_outline,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
_buildQuickStat(
|
||||
'Événements',
|
||||
'${data.stats.upcomingEvents}',
|
||||
Icons.event,
|
||||
Icons.event_outlined,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
_buildQuickStat(
|
||||
'Croissance',
|
||||
'${data.stats.monthlyGrowth.toStringAsFixed(1)}%',
|
||||
@@ -177,17 +189,21 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
IconButton(
|
||||
onPressed: _refreshDashboardData,
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: DashboardTheme.white,
|
||||
Icons.refresh_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// Navigation vers paramètres non encore connectée
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const SystemSettingsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: DashboardTheme.white,
|
||||
Icons.settings_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -197,36 +213,39 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
Widget _buildQuickStat(String label, String value, IconData icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing12,
|
||||
vertical: DashboardTheme.spacing8,
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: DashboardTheme.white,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 8,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -238,16 +257,21 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
color: DashboardTheme.white,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(bottom: BorderSide(color: AppColors.lightBorder, width: 1)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: DashboardTheme.royalBlue,
|
||||
unselectedLabelColor: DashboardTheme.grey500,
|
||||
indicatorColor: DashboardTheme.royalBlue,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1),
|
||||
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)),
|
||||
Tab(text: 'VUE D\'ENSEMBLE'),
|
||||
Tab(text: 'ANALYTICS'),
|
||||
Tab(text: 'RAPPORTS'),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -256,9 +280,9 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
Widget _buildOverviewTab() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => _refreshDashboardData(),
|
||||
color: DashboardTheme.royalBlue,
|
||||
color: AppColors.primaryGreen,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Métriques temps réel
|
||||
@@ -266,15 +290,15 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Grille de statistiques
|
||||
_buildStatsGrid(),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Notifications
|
||||
const DashboardNotificationsWidget(maxNotifications: 3),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Activités et événements
|
||||
const Row(
|
||||
@@ -282,7 +306,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
Expanded(
|
||||
child: ConnectedRecentActivities(maxItems: 3),
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing16),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ConnectedUpcomingEvents(maxItems: 2),
|
||||
),
|
||||
@@ -296,7 +320,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
|
||||
Widget _buildAnalyticsTab() {
|
||||
return const SingleChildScrollView(
|
||||
padding: EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -308,7 +332,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
height: 250,
|
||||
),
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing16),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DashboardChartWidget(
|
||||
title: 'Croissance Mensuelle',
|
||||
@@ -318,13 +342,13 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: DashboardTheme.spacing24),
|
||||
SizedBox(height: 16),
|
||||
DashboardChartWidget(
|
||||
title: 'Tendance des Contributions',
|
||||
chartType: DashboardChartType.contributionTrend,
|
||||
height: 300,
|
||||
),
|
||||
SizedBox(height: DashboardTheme.spacing24),
|
||||
SizedBox(height: 16),
|
||||
DashboardChartWidget(
|
||||
title: 'Participation aux Événements',
|
||||
chartType: DashboardChartType.eventParticipation,
|
||||
@@ -337,35 +361,35 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
|
||||
Widget _buildReportsTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildReportCard(
|
||||
'Rapport Mensuel',
|
||||
'Synthèse complète des activités du mois',
|
||||
Icons.calendar_month,
|
||||
DashboardTheme.royalBlue,
|
||||
Icons.calendar_month_outlined,
|
||||
AppColors.primaryGreen,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 12),
|
||||
_buildReportCard(
|
||||
'Rapport Financier',
|
||||
'État des contributions et finances',
|
||||
Icons.account_balance,
|
||||
DashboardTheme.tealBlue,
|
||||
Icons.account_balance_wallet_outlined,
|
||||
AppColors.success,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 12),
|
||||
_buildReportCard(
|
||||
'Rapport d\'Activité',
|
||||
'Analyse de l\'engagement des membres',
|
||||
Icons.trending_up,
|
||||
DashboardTheme.success,
|
||||
AppColors.info,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 12),
|
||||
_buildReportCard(
|
||||
'Rapport Événements',
|
||||
'Statistiques des événements organisés',
|
||||
Icons.event_note,
|
||||
DashboardTheme.warning,
|
||||
Icons.event_note_outlined,
|
||||
AppColors.warning,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -377,100 +401,77 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: DashboardTheme.spacing16,
|
||||
mainAxisSpacing: DashboardTheme.spacing16,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.25,
|
||||
children: [
|
||||
ConnectedStatsCard(
|
||||
title: 'Membres totaux',
|
||||
icon: Icons.people,
|
||||
title: 'Membres',
|
||||
icon: Icons.people_outline,
|
||||
valueExtractor: (stats) => stats.totalMembers.toString(),
|
||||
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
|
||||
customColor: DashboardTheme.royalBlue,
|
||||
),
|
||||
ConnectedStatsCard(
|
||||
title: 'Contributions',
|
||||
icon: Icons.payment,
|
||||
title: 'Finances',
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
valueExtractor: (stats) => stats.formattedContributionAmount,
|
||||
subtitleExtractor: (stats) => '${stats.totalContributions} versements',
|
||||
customColor: DashboardTheme.tealBlue,
|
||||
customColor: AppColors.success,
|
||||
),
|
||||
ConnectedStatsCard(
|
||||
title: 'Événements',
|
||||
icon: Icons.event,
|
||||
icon: Icons.event_outlined,
|
||||
valueExtractor: (stats) => stats.totalEvents.toString(),
|
||||
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
|
||||
customColor: DashboardTheme.success,
|
||||
customColor: AppColors.info,
|
||||
),
|
||||
ConnectedStatsCard(
|
||||
title: 'Engagement',
|
||||
icon: Icons.favorite,
|
||||
icon: Icons.star_outline,
|
||||
valueExtractor: (stats) => '${(stats.engagementRate * 100).toStringAsFixed(0)}%',
|
||||
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Moyen',
|
||||
customColor: DashboardTheme.warning,
|
||||
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Stable',
|
||||
customColor: AppColors.warning,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportCard(String title, String description, IconData icon, Color color) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
description,
|
||||
style: DashboardTheme.bodySmall,
|
||||
),
|
||||
Text(title, style: AppTypography.actionText.copyWith(fontSize: 12)),
|
||||
Text(description, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// Génération de rapport non encore implémentée
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.download,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.download_outlined, color: AppColors.textSecondaryLight, size: 18),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton() {
|
||||
return FloatingActionButton.extended(
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
// Actions rapides non encore implémentées
|
||||
// Actions rapides
|
||||
},
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Action'),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
||||
import '../../../epargne/presentation/pages/epargne_page.dart';
|
||||
import '../../../events/presentation/pages/events_page_wrapper.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';
|
||||
import '../../domain/entities/dashboard_entity.dart';
|
||||
|
||||
/// Page dashboard connectée au backend
|
||||
/// Page dashboard connectée au backend - Design UnionFlow Animé
|
||||
class ConnectedDashboardPage extends StatefulWidget {
|
||||
final String organizationId;
|
||||
final String userId;
|
||||
@@ -21,138 +24,662 @@ class ConnectedDashboardPage extends StatefulWidget {
|
||||
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
|
||||
}
|
||||
|
||||
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> {
|
||||
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
PeriodFilter _selectedPeriod = PeriodFilter.month;
|
||||
int _unreadNotifications = 5;
|
||||
bool _isExporting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Charger les données du dashboard
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
context.read<DashboardBloc>().add(LoadDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: DashboardTheme.grey50,
|
||||
appBar: AppBar(
|
||||
title: const Text('Dashboard'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
elevation: 0,
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
appBar: _buildAppBar(),
|
||||
body: AfricanPatternBackground(
|
||||
child: BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
|
||||
if (state is DashboardLoaded) {
|
||||
return _buildDashboardContent(state);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: DashboardTheme.royalBlue,
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'unionflow_logo',
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'U',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Dashboard',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
UnionExportButton(
|
||||
isLoading: _isExporting,
|
||||
onExport: (exportType) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ExportConfirmDialog(
|
||||
exportType: exportType,
|
||||
onConfirm: () => _handleExport(exportType),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
UnionNotificationBadge(
|
||||
count: _unreadNotifications,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
color: UnionFlowColors.textPrimary,
|
||||
onPressed: () {
|
||||
setState(() => _unreadNotifications = 0);
|
||||
UnionNotificationToast.show(
|
||||
context,
|
||||
title: 'Notifications',
|
||||
message: 'Aucune nouvelle notification',
|
||||
icon: Icons.notifications_active,
|
||||
color: UnionFlowColors.info,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: UnionFlowColors.unionGreen,
|
||||
unselectedLabelColor: UnionFlowColors.textSecondary,
|
||||
indicatorColor: UnionFlowColors.unionGreen,
|
||||
labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||||
tabs: const [
|
||||
Tab(text: 'Vue d\'ensemble'),
|
||||
Tab(text: 'Analytique'),
|
||||
Tab(text: 'Activités'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DashboardError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: DashboardTheme.error,
|
||||
Widget _buildDashboardContent(DashboardLoaded state) {
|
||||
final data = state.dashboardData;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<DashboardBloc>().add(LoadDashboardData(
|
||||
organizationId: widget.organizationId,
|
||||
userId: widget.userId,
|
||||
));
|
||||
},
|
||||
color: UnionFlowColors.unionGreen,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildOverviewTab(data),
|
||||
_buildAnalyticsTab(data),
|
||||
_buildActivitiesTab(data),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
UnionTransactionTile _activityToTile(RecentActivityEntity a) {
|
||||
final amount = a.metadata != null && a.metadata!['amount'] != null
|
||||
? '${a.metadata!['amount']} FCFA'
|
||||
: (a.title.isNotEmpty ? a.title : '-');
|
||||
return UnionTransactionTile(
|
||||
name: a.userName,
|
||||
amount: amount,
|
||||
status: a.type.isNotEmpty ? a.type : 'Confirmé',
|
||||
date: a.timeAgo,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewTab(DashboardEntity data) {
|
||||
final stats = data.stats;
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Balance principale - Animée
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: UnionBalanceCard(
|
||||
label: 'Caisse Totale',
|
||||
amount: _formatAmount(stats.totalContributionAmount),
|
||||
trend: stats.monthlyGrowth > 0 ? '+${(stats.monthlyGrowth * 100).toStringAsFixed(0)}% ce mois' : 'Stable',
|
||||
isTrendPositive: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Stats en grille - Animées avec délai
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Membres',
|
||||
value: stats.totalMembers.toString(),
|
||||
icon: Icons.people_outline,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
trend: '+8%',
|
||||
isTrendUp: true,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Actifs',
|
||||
value: stats.activeMembers.toString(),
|
||||
icon: Icons.check_circle_outline,
|
||||
color: UnionFlowColors.success,
|
||||
trend: '+5%',
|
||||
isTrendUp: true,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
state.message,
|
||||
style: DashboardTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: stats.totalEvents.toString(),
|
||||
icon: Icons.event_outlined,
|
||||
color: UnionFlowColors.gold,
|
||||
trend: '+3',
|
||||
isTrendUp: true,
|
||||
),
|
||||
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,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'À venir',
|
||||
value: stats.upcomingEvents.toString(),
|
||||
icon: Icons.calendar_today,
|
||||
color: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Progression - Animée
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: UnionProgressCard(
|
||||
title: 'Progression des Cotisations',
|
||||
progress: 0.7,
|
||||
subtitle: '70% des membres ont cotisé ce mois',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions rapides - Animées
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
begin: const Offset(0, 0.2),
|
||||
child: UnionActionGrid(
|
||||
actions: [
|
||||
UnionActionButton(
|
||||
icon: Icons.payment,
|
||||
label: 'Cotiser',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.unionGreenPale,
|
||||
iconColor: UnionFlowColors.unionGreen,
|
||||
),
|
||||
UnionActionButton(
|
||||
icon: Icons.send,
|
||||
label: 'Envoyer',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.goldPale,
|
||||
iconColor: UnionFlowColors.gold,
|
||||
),
|
||||
UnionActionButton(
|
||||
icon: Icons.download,
|
||||
label: 'Retirer',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const EpargnePage()),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.terracottaPale,
|
||||
iconColor: UnionFlowColors.terracotta,
|
||||
),
|
||||
UnionActionButton(
|
||||
icon: Icons.add_circle_outline,
|
||||
label: 'Créer',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.infoPale,
|
||||
iconColor: UnionFlowColors.info,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activité récente - Animée
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: UnionTransactionCard(
|
||||
title: 'Activité Récente',
|
||||
onSeeAll: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
|
||||
);
|
||||
},
|
||||
transactions: data.recentActivities.take(6).map((a) => _activityToTile(a)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnalyticsTab(DashboardEntity data) {
|
||||
final stats = data.stats;
|
||||
final entrees = stats.totalContributionAmount;
|
||||
final sorties = stats.pendingRequests * 1000.0;
|
||||
final benefice = entrees - sorties;
|
||||
final taux = (stats.engagementRate * 100).toStringAsFixed(0);
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Filtre de période - Animé
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 50),
|
||||
child: UnionPeriodFilter(
|
||||
selectedPeriod: _selectedPeriod,
|
||||
onPeriodChanged: (period) {
|
||||
setState(() => _selectedPeriod = period);
|
||||
UnionNotificationToast.show(
|
||||
context,
|
||||
title: 'Période mise à jour',
|
||||
message: 'Affichage pour ${period.label.toLowerCase()}',
|
||||
icon: Icons.calendar_today,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Line Chart - Animé (évolution basée sur total cotisations + croissance)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: UnionLineChart(
|
||||
title: 'Évolution de la Caisse',
|
||||
subtitle: 'Derniers 12 mois',
|
||||
spots: _buildEvolutionSpots(stats.totalContributionAmount, stats.monthlyGrowth),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Pie Chart - Animé
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: UnionPieChart(
|
||||
title: 'Répartition des Cotisations',
|
||||
subtitle: 'Par catégorie',
|
||||
sections: [
|
||||
UnionPieChartSection.create(
|
||||
value: 40,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
title: '40%\nCotisations',
|
||||
),
|
||||
UnionPieChartSection.create(
|
||||
value: 30,
|
||||
color: UnionFlowColors.gold,
|
||||
title: '30%\nÉpargne',
|
||||
),
|
||||
UnionPieChartSection.create(
|
||||
value: 20,
|
||||
color: UnionFlowColors.terracotta,
|
||||
title: '20%\nSolidarité',
|
||||
),
|
||||
UnionPieChartSection.create(
|
||||
value: 10,
|
||||
color: UnionFlowColors.amber,
|
||||
title: '10%\nAutres',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Titre
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: const Text(
|
||||
'Métriques Financières',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Métriques - Animées (données backend)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
begin: const Offset(0, 0.2),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildFinanceMetric(
|
||||
'Entrées',
|
||||
_formatFcfa(entrees),
|
||||
Icons.arrow_downward,
|
||||
UnionFlowColors.success,
|
||||
),
|
||||
),
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildFinanceMetric(
|
||||
'Sorties',
|
||||
_formatFcfa(sorties),
|
||||
Icons.arrow_upward,
|
||||
UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildFinanceMetric(
|
||||
'Bénéfice',
|
||||
_formatFcfa(benefice),
|
||||
Icons.trending_up,
|
||||
UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildFinanceMetric(
|
||||
'Taux',
|
||||
'$taux%',
|
||||
Icons.percent,
|
||||
UnionFlowColors.info,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DashboardLoaded) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
Widget _buildActivitiesTab(DashboardEntity data) {
|
||||
final tiles = data.recentActivities.map((a) => _activityToTile(a)).toList();
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: UnionTransactionCard(
|
||||
title: 'Toutes les Activités',
|
||||
onSeeAll: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
|
||||
);
|
||||
},
|
||||
transactions: tiles,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleExport(ExportType exportType) async {
|
||||
setState(() => _isExporting = true);
|
||||
|
||||
// Simulation de l'export (dans un vrai cas, appel API ici)
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
setState(() => _isExporting = false);
|
||||
|
||||
if (mounted) {
|
||||
UnionNotificationToast.show(
|
||||
context,
|
||||
title: 'Export réussi',
|
||||
message: 'Le rapport ${exportType.label} a été généré avec succès',
|
||||
icon: Icons.check_circle,
|
||||
color: UnionFlowColors.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatFcfa(double value) {
|
||||
if (value >= 1000000) return '${(value / 1000000).toStringAsFixed(1)}M FCFA';
|
||||
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}K FCFA';
|
||||
return '${value.toStringAsFixed(0)} FCFA';
|
||||
}
|
||||
|
||||
List<FlSpot> _buildEvolutionSpots(double totalAmount, double monthlyGrowth) {
|
||||
final spots = <FlSpot>[];
|
||||
var v = totalAmount * 0.5;
|
||||
for (var i = 0; i < 12; i++) {
|
||||
spots.add(FlSpot(i.toDouble(), v));
|
||||
v = v * (1 + (monthlyGrowth > 0 ? monthlyGrowth : 0.02));
|
||||
}
|
||||
if (spots.isNotEmpty) spots[spots.length - 1] = FlSpot(11, totalAmount);
|
||||
return spots;
|
||||
}
|
||||
|
||||
Widget _buildFinanceMetric(String label, String value, IconData icon, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, size: 24, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String message) {
|
||||
return Center(
|
||||
child: AnimatedFadeIn(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.errorPale,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Erreur de chargement',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
UFPrimaryButton(
|
||||
onPressed: () {
|
||||
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();
|
||||
},
|
||||
label: 'RÉESSAYER',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatAmount(num amount) {
|
||||
return '${amount.toStringAsFixed(0).replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]},',
|
||||
)} FCFA';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import '../../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
||||
import '../../../../epargne/presentation/pages/epargne_page.dart';
|
||||
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
|
||||
import '../../../../help/presentation/pages/help_support_page.dart';
|
||||
import '../../../../events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
|
||||
import '../../widgets/dashboard_drawer.dart';
|
||||
|
||||
/// Dashboard Membre Actif - Design UnionFlow Enrichi
|
||||
class ActiveMemberDashboard extends StatelessWidget {
|
||||
@@ -19,6 +19,14 @@ class ActiveMemberDashboard extends StatelessWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
appBar: _buildAppBar(),
|
||||
drawer: DashboardDrawer(
|
||||
onNavigate: (route) {
|
||||
Navigator.of(context).pushNamed(route);
|
||||
},
|
||||
onLogout: () {
|
||||
context.read<AuthBloc>().add(const AuthLogoutRequested());
|
||||
},
|
||||
),
|
||||
body: AfricanPatternBackground(
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
@@ -49,90 +57,115 @@ class ActiveMemberDashboard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Balance principale (données backend réelles)
|
||||
// Balance principale ou Vue Unifiée (Compte Adhérent)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: UnionBalanceCard(
|
||||
label: 'Mon Solde Total',
|
||||
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
|
||||
trend: stats != null && stats.monthlyGrowth != 0
|
||||
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
|
||||
: 'Aucune variation',
|
||||
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
|
||||
),
|
||||
child: dashboardData?.monCompte != null
|
||||
? UnionUnifiedAccountCard(
|
||||
numeroMembre: dashboardData!.monCompte!.numeroMembre,
|
||||
organisationNom: dashboardData.monCompte!.organisationNom ?? 'UnionFlow',
|
||||
soldeTotal: _formatAmount(dashboardData.monCompte!.soldeTotalDisponible),
|
||||
capaciteEmprunt: _formatAmount(dashboardData.monCompte!.capaciteEmprunt),
|
||||
epargneBloquee: _formatAmount(dashboardData.monCompte!.soldeBloque),
|
||||
engagementRate: dashboardData.monCompte!.engagementRate,
|
||||
)
|
||||
: UnionBalanceCard(
|
||||
label: 'Mon Solde Total',
|
||||
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
|
||||
trend: stats != null && stats.monthlyGrowth != 0
|
||||
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
|
||||
: 'Aucune variation',
|
||||
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Stats en grille (données backend réelles)
|
||||
// Bloc KPI unifié (4 stats regroupées)
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Cotisations',
|
||||
value: '${stats?.totalContributions ?? 0}',
|
||||
icon: Icons.check_circle,
|
||||
color: UnionFlowColors.success,
|
||||
trend: stats != null && stats.monthlyGrowth > 0
|
||||
? '+${stats.monthlyGrowth.toStringAsFixed(0)}%'
|
||||
: null,
|
||||
isTrendUp: (stats?.monthlyGrowth ?? 0) > 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Cotisations',
|
||||
value: '${stats?.totalContributions ?? 0}',
|
||||
icon: Icons.check_circle,
|
||||
color: (stats?.totalContributions ?? 0) > 0
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.textTertiary,
|
||||
trend: stats != null && stats.totalContributions > 0 && stats.engagementRate > 0
|
||||
? (stats.engagementRate >= 1.0
|
||||
? 'Tout payé'
|
||||
: '${(stats.engagementRate * 100).toStringAsFixed(0)}% payé')
|
||||
: null,
|
||||
isTrendUp: (stats?.engagementRate ?? 0) >= 1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Engagement',
|
||||
value: stats != null && stats.engagementRate > 0
|
||||
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
|
||||
: stats != null && stats.totalContributions > 0
|
||||
? '—'
|
||||
: '0%',
|
||||
icon: Icons.trending_up,
|
||||
color: UnionFlowColors.gold,
|
||||
trend: stats != null && stats.engagementRate > 0.9
|
||||
? 'Excellent'
|
||||
: stats != null && stats.engagementRate > 0.5
|
||||
? 'Bon'
|
||||
: null,
|
||||
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Engagement',
|
||||
value: stats != null
|
||||
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
|
||||
: '0%',
|
||||
icon: Icons.trending_up,
|
||||
color: UnionFlowColors.gold,
|
||||
trend: stats != null && stats.engagementRate > 0.7
|
||||
? 'Excellent'
|
||||
: stats != null && stats.engagementRate > 0.5
|
||||
? 'Bon'
|
||||
: null,
|
||||
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Contribution Totale',
|
||||
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
|
||||
icon: Icons.savings,
|
||||
color: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: '${stats?.upcomingEvents ?? 0}',
|
||||
icon: Icons.event_available,
|
||||
color: UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Contribution Totale',
|
||||
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
|
||||
icon: Icons.savings,
|
||||
color: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: '${stats?.upcomingEvents ?? 0}',
|
||||
icon: Icons.event_available,
|
||||
color: UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activité récente (données backend)
|
||||
if (dashboardData != null && dashboardData.hasRecentActivity) ...[
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: const Text(
|
||||
const AnimatedFadeIn(
|
||||
delay: Duration(milliseconds: 500),
|
||||
child: Text(
|
||||
'Activité Récente',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
@@ -221,122 +254,128 @@ class ActiveMemberDashboard extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Actions rapides
|
||||
AnimatedFadeIn(
|
||||
// Bloc Actions rapides unifié (6 boutons regroupés)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 700),
|
||||
child: const Text(
|
||||
'Actions Rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Cotiser',
|
||||
icon: Icons.payment,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Actions Rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Épargner',
|
||||
icon: Icons.savings_outlined,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Cotiser',
|
||||
icon: Icons.payment,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Crédit',
|
||||
icon: Icons.account_balance_wallet,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Épargner',
|
||||
icon: Icons.savings_outlined,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.gold,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 900),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Événements',
|
||||
icon: Icons.event,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EventsPageWrapper(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Crédit',
|
||||
icon: Icons.account_balance_wallet,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.amber,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.terracotta,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Solidarité',
|
||||
icon: Icons.favorite_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const DemandesAidePageWrapper(),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Événements',
|
||||
icon: Icons.event,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EventsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.terracotta,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Profil',
|
||||
icon: Icons.person_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const ProfilePageWrapper(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Solidarité',
|
||||
icon: Icons.favorite_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const DemandesAidePageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.indigo,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Profil',
|
||||
icon: Icons.person_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const ProfilePageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -397,7 +436,7 @@ class ActiveMemberDashboard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,68 +1,670 @@
|
||||
/// Dashboard Modérateur - Management Hub Focalisé
|
||||
/// Outils de modération et gestion partielle
|
||||
library moderator_dashboard;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../widgets/dashboard_widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
||||
import '../../../../epargne/presentation/pages/epargne_page.dart';
|
||||
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
|
||||
import '../../../../events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
|
||||
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
|
||||
import '../../../../members/presentation/pages/members_page_wrapper.dart';
|
||||
import '../../../../help/presentation/pages/help_support_page.dart';
|
||||
import '../../widgets/dashboard_drawer.dart';
|
||||
|
||||
/// Dashboard Management Hub pour Modérateur
|
||||
/// Dashboard Modérateur - Design UnionFlow pour Gestion Communauté
|
||||
class ModeratorDashboard extends StatelessWidget {
|
||||
const ModeratorDashboard({super.key});
|
||||
|
||||
String _formatAmount(double amount) {
|
||||
if (amount >= 1000000) {
|
||||
return '${(amount / 1000000).toStringAsFixed(amount % 1000000 == 0 ? 0 : 1)}M FCFA';
|
||||
} else if (amount >= 1000) {
|
||||
return '${(amount / 1000).toStringAsFixed(amount % 1000 == 0 ? 0 : 1)}K FCFA';
|
||||
}
|
||||
return '${amount.toStringAsFixed(0)} FCFA';
|
||||
}
|
||||
|
||||
@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,
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
appBar: _buildAppBar(),
|
||||
drawer: DashboardDrawer(
|
||||
onNavigate: (route) => Navigator.of(context).pushNamed(route),
|
||||
onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
|
||||
),
|
||||
body: AfricanPatternBackground(
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
final user = (authState is AuthAuthenticated) ? authState.user : null;
|
||||
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, dashboardState) {
|
||||
if (dashboardState is DashboardLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
|
||||
);
|
||||
}
|
||||
|
||||
final dashboardData = (dashboardState is DashboardLoaded)
|
||||
? dashboardState.dashboardData
|
||||
: null;
|
||||
final stats = dashboardData?.stats;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête Modérateur
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: _buildUserHeader(user),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Balance principale ou Vue Unifiée (Compte Adhérent)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: dashboardData?.monCompte != null
|
||||
? UnionUnifiedAccountCard(
|
||||
numeroMembre: dashboardData!.monCompte!.numeroMembre,
|
||||
organisationNom: dashboardData.monCompte!.organisationNom ?? 'UnionFlow',
|
||||
soldeTotal: _formatAmount(dashboardData.monCompte!.soldeTotalDisponible),
|
||||
capaciteEmprunt: _formatAmount(dashboardData.monCompte!.capaciteEmprunt),
|
||||
epargneBloquee: _formatAmount(dashboardData.monCompte!.soldeBloque),
|
||||
engagementRate: dashboardData.monCompte!.engagementRate,
|
||||
)
|
||||
: UnionBalanceCard(
|
||||
label: 'Mon Solde Total',
|
||||
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
|
||||
trend: stats != null && stats.monthlyGrowth != 0
|
||||
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
|
||||
: 'Aucune variation',
|
||||
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Bloc KPI unifié (4 stats regroupées)
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 250),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Cotisations',
|
||||
value: '${stats?.totalContributions ?? 0}',
|
||||
icon: Icons.check_circle,
|
||||
color: (stats?.totalContributions ?? 0) > 0
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.textTertiary,
|
||||
trend: stats != null && stats.totalContributions > 0 && stats.engagementRate > 0
|
||||
? (stats.engagementRate >= 1.0
|
||||
? 'Tout payé'
|
||||
: '${(stats.engagementRate * 100).toStringAsFixed(0)}% payé')
|
||||
: null,
|
||||
isTrendUp: (stats?.engagementRate ?? 0) >= 1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Engagement',
|
||||
value: stats != null && stats.engagementRate > 0
|
||||
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
|
||||
: stats != null && stats.totalContributions > 0 ? '—' : '0%',
|
||||
icon: Icons.trending_up,
|
||||
color: UnionFlowColors.gold,
|
||||
trend: stats != null && stats.engagementRate > 0.9
|
||||
? 'Excellent'
|
||||
: stats != null && stats.engagementRate > 0.5 ? 'Bon' : null,
|
||||
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Contribution Totale',
|
||||
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
|
||||
icon: Icons.savings,
|
||||
color: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: '${stats?.upcomingEvents ?? 0}',
|
||||
icon: Icons.event_available,
|
||||
color: UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Bloc Actions rapides unifié (6 boutons regroupés)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Actions Rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Cotiser',
|
||||
icon: Icons.payment,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Épargner',
|
||||
icon: Icons.savings_outlined,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Crédit',
|
||||
icon: Icons.account_balance_wallet,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.amber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Événements',
|
||||
icon: Icons.event,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EventsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Solidarité',
|
||||
icon: Icons.favorite_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const DemandesAidePageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Profil',
|
||||
icon: Icons.person_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const ProfilePageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ——— Administration / Modération (tout en bas, après les actions membre) ———
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: const Text(
|
||||
'Espace Modérateur',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Stats de modération (données backend réelles)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'En attente',
|
||||
value: '${stats?.pendingRequests ?? 0}',
|
||||
icon: Icons.pending_actions,
|
||||
color: UnionFlowColors.warning,
|
||||
trend: stats != null && stats.pendingRequests > 0 ? 'Action requise' : null,
|
||||
isTrendUp: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Membres Actifs',
|
||||
value: '${stats?.activeMembers ?? 0}',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: UnionFlowColors.success,
|
||||
trend: stats != null && stats.totalMembers > 0
|
||||
? '${((stats.activeMembers / stats.totalMembers) * 100).toStringAsFixed(0)}%'
|
||||
: null,
|
||||
isTrendUp: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: '${stats?.upcomingEvents ?? 0}',
|
||||
icon: Icons.event_outlined,
|
||||
color: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Membres Total',
|
||||
value: '${stats?.totalMembers ?? 0}',
|
||||
icon: Icons.people_outline,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activité des membres (données backend réelles)
|
||||
if (stats != null && stats.totalMembers > 0)
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: UnionPieChart(
|
||||
title: 'Activité des Membres',
|
||||
subtitle: '${stats.totalMembers} membres au total',
|
||||
sections: [
|
||||
UnionPieChartSection.create(
|
||||
value: stats.activeMembers.toDouble(),
|
||||
color: UnionFlowColors.success,
|
||||
title: '${((stats.activeMembers / stats.totalMembers) * 100).toStringAsFixed(0)}%\nActifs',
|
||||
),
|
||||
UnionPieChartSection.create(
|
||||
value: (stats.totalMembers - stats.activeMembers).toDouble(),
|
||||
color: UnionFlowColors.textTertiary,
|
||||
title: '${(((stats.totalMembers - stats.activeMembers) / stats.totalMembers) * 100).toStringAsFixed(0)}%\nInactifs',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Demandes en attente (données backend)
|
||||
if (dashboardData != null && dashboardData.hasRecentActivity) ...[
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: const Text(
|
||||
'Activité Récente à Modérer',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: Column(
|
||||
children: dashboardData.recentActivities.take(4).map((activity) =>
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: UnionFlowColors.indigo.withOpacity(0.2),
|
||||
child: Icon(
|
||||
activity.type == 'member' ? Icons.person_add :
|
||||
activity.type == 'event' ? Icons.event :
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
activity.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activity.timeAgo,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions de modération
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 700),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Approuver',
|
||||
icon: Icons.check_circle,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
|
||||
backgroundColor: UnionFlowColors.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Vérifier',
|
||||
icon: Icons.visibility,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
|
||||
backgroundColor: UnionFlowColors.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Signaler',
|
||||
icon: Icons.flag,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const HelpSupportPage())),
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Membres',
|
||||
icon: Icons.people,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Contenus',
|
||||
icon: Icons.article,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
backgroundColor: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Historique',
|
||||
icon: Icons.history,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
|
||||
backgroundColor: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(Icons.manage_accounts, color: Colors.white, size: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'U',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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(),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Modérateur',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserHeader(dynamic user) {
|
||||
final year = user?.createdAt?.year ?? DateTime.now().year;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.warmGradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: const Border(
|
||||
top: BorderSide(color: UnionFlowColors.gold, width: 3),
|
||||
),
|
||||
boxShadow: UnionFlowColors.goldGlowShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Colors.white.withOpacity(0.3),
|
||||
child: Text(
|
||||
user?.initials ?? 'SM',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user?.fullName ?? 'Secrétaire',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Depuis $year • Très Actif',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'ACTIF',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: UnionFlowColors.gold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -70,161 +672,4 @@ class ModeratorDashboard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../organizations/data/models/organization_model.dart';
|
||||
import '../../../../organizations/data/services/organization_service.dart';
|
||||
import 'org_admin_dashboard.dart';
|
||||
|
||||
/// Charge l'organisation du membre connecté (GET /api/organisations/mes) puis
|
||||
/// affiche le dashboard admin avec les données backend pour cette organisation.
|
||||
class OrgAdminDashboardLoader extends StatelessWidget {
|
||||
const OrgAdminDashboardLoader({
|
||||
super.key,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<OrganizationModel>>(
|
||||
future: getIt<OrganizationService>().getMesOrganisations(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Scaffold(
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(color: UnionFlowColors.gold),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Scaffold(
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 48, color: UnionFlowColors.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Impossible de charger votre organisation',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${snapshot.error}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final orgs = snapshot.data ?? [];
|
||||
final orgsWithId = orgs.where((o) => o.id != null && o.id!.isNotEmpty).toList();
|
||||
if (orgsWithId.isEmpty) {
|
||||
return Scaffold(
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
orgs.isEmpty
|
||||
? 'Aucune organisation associée à votre compte.'
|
||||
: 'Aucune organisation valide (id manquant).',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final firstOrgId = orgsWithId.first.id!;
|
||||
return BlocProvider<DashboardBloc>(
|
||||
create: (context) => getIt<DashboardBloc>()
|
||||
..add(LoadDashboardData(
|
||||
organizationId: firstOrgId,
|
||||
userId: userId,
|
||||
)),
|
||||
child: const OrgAdminDashboard(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ library role_dashboards;
|
||||
export 'super_admin_dashboard.dart';
|
||||
export 'org_admin_dashboard.dart';
|
||||
export 'moderator_dashboard.dart';
|
||||
export 'consultant_dashboard.dart';
|
||||
export 'hr_manager_dashboard.dart';
|
||||
export 'active_member_dashboard.dart';
|
||||
export 'simple_member_dashboard.dart';
|
||||
export 'visitor_dashboard.dart';
|
||||
|
||||
@@ -1,360 +1,436 @@
|
||||
/// Dashboard Membre Simple - Personal Space Minimaliste
|
||||
/// Interface simplifiée pour accès basique
|
||||
library simple_member_dashboard;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../widgets/dashboard_widgets.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
||||
import '../../../../epargne/presentation/pages/epargne_page.dart';
|
||||
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
|
||||
import '../../../../help/presentation/pages/help_support_page.dart';
|
||||
import '../../widgets/dashboard_drawer.dart';
|
||||
|
||||
/// Dashboard Personal Space pour Membre Simple
|
||||
/// Dashboard Membre Simple - Design UnionFlow
|
||||
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,
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
appBar: _buildAppBar(),
|
||||
drawer: DashboardDrawer(
|
||||
onNavigate: (route) {
|
||||
Navigator.of(context).pushNamed(route);
|
||||
},
|
||||
onLogout: () {
|
||||
context.read<AuthBloc>().add(const AuthLogoutRequested());
|
||||
},
|
||||
),
|
||||
body: AfricanPatternBackground(
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
final user = (authState is AuthAuthenticated) ? authState.user : null;
|
||||
|
||||
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, dashboardState) {
|
||||
if (dashboardState is DashboardLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
|
||||
);
|
||||
}
|
||||
|
||||
final dashboardData = (dashboardState is DashboardLoaded)
|
||||
? dashboardState.dashboardData
|
||||
: null;
|
||||
final stats = dashboardData?.stats;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec badge de rôle
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: _buildUserHeader(user),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Solde personnel
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: UnionBalanceCard(
|
||||
label: 'Mon Solde',
|
||||
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
|
||||
trend: stats != null && stats.monthlyGrowth != 0
|
||||
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
|
||||
: 'Aucune variation',
|
||||
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Ma situation
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: const Text(
|
||||
'Ma Situation',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Cotisations',
|
||||
value: stats != null && stats.totalContributions > 0 ? 'À jour' : 'En retard',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: stats != null && stats.totalContributions > 0
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.warning,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Événements',
|
||||
value: '${stats?.upcomingEvents ?? 0}',
|
||||
icon: Icons.event_outlined,
|
||||
color: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions rapides
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: const Text(
|
||||
'Actions Rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Cotiser',
|
||||
icon: Icons.payment,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Épargner',
|
||||
icon: Icons.savings_outlined,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const EpargnePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 700),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Mes Infos',
|
||||
icon: Icons.person_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const ProfilePageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionActionButton(
|
||||
label: 'Support',
|
||||
icon: Icons.help_outline,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const HelpSupportPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Événements à venir (données backend)
|
||||
if (dashboardData != null && dashboardData.hasUpcomingEvents) ...[
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
child: const Text(
|
||||
'Événements à Venir',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 900),
|
||||
child: Column(
|
||||
children: dashboardData.upcomingEvents.take(2).map((event) =>
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: UnionFlowColors.border, width: 1),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.gold.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event,
|
||||
color: UnionFlowColors.gold,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
event.formattedDate,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (event.daysUntilEventInt <= 7)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.warning.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${event.daysUntilEventInt}j',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.warning,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'U',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Membre Simple',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserHeader(dynamic user) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.subtleGradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: const Border(
|
||||
top: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
|
||||
),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 35,
|
||||
backgroundColor: Color(0xFF00CEC9),
|
||||
child: Icon(Icons.person, color: Colors.white, size: 35),
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: UnionFlowColors.unionGreen.withOpacity(0.2),
|
||||
child: Text(
|
||||
user?.initials ?? 'M',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Pierre Dupont',
|
||||
style: TypographyTokens.headlineMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
user?.fullName ?? 'Membre',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
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,
|
||||
),
|
||||
'Membre depuis ${user?.createdAt.year ?? 2024}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.unionGreen,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'MEMBRE',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
String _formatAmount(double amount) {
|
||||
if (amount >= 1000000) {
|
||||
return '${(amount / 1000000).toStringAsFixed(1)}M FCFA';
|
||||
} else if (amount >= 1000) {
|
||||
return '${(amount / 1000).toStringAsFixed(0)}K FCFA';
|
||||
}
|
||||
return '${amount.toStringAsFixed(0)} FCFA';
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +1,364 @@
|
||||
/// 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';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../widgets/dashboard_drawer.dart';
|
||||
|
||||
/// Dashboard Landing Experience pour Visiteur
|
||||
/// Dashboard Visiteur - Design UnionFlow Version Publique
|
||||
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),
|
||||
backgroundColor: UnionFlowColors.background,
|
||||
appBar: _buildAppBar(),
|
||||
drawer: DashboardDrawer(
|
||||
onNavigate: (route) {
|
||||
Navigator.of(context).pushNamed(route);
|
||||
},
|
||||
onLogout: () {
|
||||
context.read<AuthBloc>().add(const AuthLogoutRequested());
|
||||
},
|
||||
),
|
||||
body: AfricanPatternBackground(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Message de bienvenue
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 100),
|
||||
child: _buildWelcomeCard(),
|
||||
),
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Fonctionnalités UnionFlow
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: const Text(
|
||||
'Découvrez UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Row(
|
||||
children: [
|
||||
// Motif d'accueil
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _WelcomePatternPainter(),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Organisations',
|
||||
value: '500+',
|
||||
icon: Icons.business_outlined,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Utilisateurs',
|
||||
value: '10K+',
|
||||
icon: Icons.people_outlined,
|
||||
color: UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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(),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Transactions',
|
||||
value: '1M+',
|
||||
icon: Icons.payment_outlined,
|
||||
color: UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UnionStatWidget(
|
||||
label: 'Confiance',
|
||||
value: '99%',
|
||||
icon: Icons.verified_outlined,
|
||||
color: UnionFlowColors.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Avantages
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: const Text(
|
||||
'Nos Avantages',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: _buildFeature(
|
||||
'Gestion Simplifiée',
|
||||
'Gérez vos cotisations, épargnes et crédits en un seul endroit',
|
||||
Icons.dashboard_customize,
|
||||
UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 700),
|
||||
child: _buildFeature(
|
||||
'Sécurité Optimale',
|
||||
'Vos données sont protégées avec un chiffrement de niveau bancaire',
|
||||
Icons.security,
|
||||
UnionFlowColors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
child: _buildFeature(
|
||||
'Solidarité Africaine',
|
||||
'Entraide, tontines, mutuelles et coopératives à votre portée',
|
||||
Icons.favorite_outline,
|
||||
UnionFlowColors.terracotta,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
AnimatedFadeIn(
|
||||
delay: const Duration(milliseconds: 900),
|
||||
child: _buildFeature(
|
||||
'Rapports Détaillés',
|
||||
'Suivi en temps réel avec exports PDF, Excel et CSV',
|
||||
Icons.analytics_outlined,
|
||||
UnionFlowColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Call to Action
|
||||
AnimatedSlideIn(
|
||||
delay: const Duration(milliseconds: 1000),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: UnionFlowColors.greenGlowShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.rocket_launch,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Prêt à Commencer ?',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Rejoignez des milliers d\'organisations qui nous font confiance',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: UnionFlowColors.unionGreen,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'Créer un Compte',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
},
|
||||
child: Text(
|
||||
'Déjà membre ? Se connecter',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeMessage() {
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'unionflow_logo',
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'U',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Découverte',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
gradient: UnionFlowColors.subtleGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: const Border(
|
||||
top: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
boxShadow: UnionFlowColors.mediumShadow,
|
||||
),
|
||||
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,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waving_hand,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Bienvenue sur UnionFlow',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Votre plateforme de gestion mutualiste et associative',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
const SizedBox(height: 16),
|
||||
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),
|
||||
'Gérez vos mutuelles, tontines, coopératives et associations en toute simplicité. UnionFlow est la solution complète pour la solidarité africaine.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
color: UnionFlowColors.textPrimary.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -158,397 +366,53 @@ class VisitorDashboard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutOrganization() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'À Propos de Nous',
|
||||
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
|
||||
Widget _buildFeature(String title, String description, IconData icon, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border(
|
||||
left: BorderSide(color: color, width: 4),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: 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),
|
||||
),
|
||||
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)),
|
||||
],
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
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 SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,132 @@
|
||||
import 'dart:math';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../domain/entities/dashboard_entity.dart';
|
||||
|
||||
/// Générateur de données pour les graphiques basé sur les stats réelles
|
||||
class ChartDataGenerator {
|
||||
/// Génère des FlSpots pour un graphique de croissance sur 12 mois
|
||||
/// basé sur la valeur actuelle et le taux de croissance
|
||||
static List<FlSpot> generateMonthlyGrowthSpots({
|
||||
required double currentValue,
|
||||
required double monthlyGrowthRate,
|
||||
}) {
|
||||
// Si pas de données, retourner un graphique plat minimum
|
||||
if (currentValue == 0) {
|
||||
return List.generate(12, (index) => FlSpot(index.toDouble(), 100.0));
|
||||
}
|
||||
|
||||
final spots = <FlSpot>[];
|
||||
final random = Random(42); // Seed fixe pour cohérence
|
||||
|
||||
// Calculer la valeur de départ (il y a 11 mois)
|
||||
final startValue = currentValue / pow(1 + monthlyGrowthRate, 11);
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
// Calculer la valeur avec croissance + variation aléatoire
|
||||
final baseValue = startValue * pow(1 + monthlyGrowthRate, i);
|
||||
final variance = baseValue * 0.05 * (random.nextDouble() - 0.5); // ±2.5% variance
|
||||
final value = baseValue + variance;
|
||||
|
||||
spots.add(FlSpot(i.toDouble(), value.clamp(0, double.infinity)));
|
||||
}
|
||||
|
||||
return spots;
|
||||
}
|
||||
|
||||
/// Génère des FlSpots basés sur DashboardStatsEntity
|
||||
static List<FlSpot> generateGrowthSpotsFromStats(DashboardStatsEntity? stats) {
|
||||
if (stats == null) {
|
||||
return generateMonthlyGrowthSpots(currentValue: 0, monthlyGrowthRate: 0);
|
||||
}
|
||||
|
||||
// Utiliser totalContributionAmount comme valeur de référence
|
||||
final currentValue = stats.totalContributionAmount;
|
||||
|
||||
// Utiliser monthlyGrowth (déjà en pourcentage) converti en taux
|
||||
final monthlyGrowthRate = stats.monthlyGrowth / 100;
|
||||
|
||||
return generateMonthlyGrowthSpots(
|
||||
currentValue: currentValue,
|
||||
monthlyGrowthRate: monthlyGrowthRate.clamp(-0.5, 0.5), // Limiter à ±50% par mois
|
||||
);
|
||||
}
|
||||
|
||||
/// Génère des FlSpots pour un graphique de membres actifs vs inactifs
|
||||
static List<FlSpot> generateMemberActivitySpots(DashboardStatsEntity? stats) {
|
||||
if (stats == null || stats.totalMembers == 0) {
|
||||
return List.generate(12, (index) => FlSpot(index.toDouble(), 50.0));
|
||||
}
|
||||
|
||||
final activePercentage = (stats.activeMembers / stats.totalMembers) * 100;
|
||||
final random = Random(43);
|
||||
|
||||
return List.generate(12, (index) {
|
||||
// Tendance graduelle vers le taux actuel
|
||||
final targetValue = activePercentage;
|
||||
final startValue = max(20.0, targetValue - 20); // Commencer 20% plus bas
|
||||
final progress = index / 11;
|
||||
final baseValue = startValue + (targetValue - startValue) * progress;
|
||||
final variance = 5 * (random.nextDouble() - 0.5); // ±2.5% variance
|
||||
|
||||
return FlSpot(index.toDouble(), (baseValue + variance).clamp(0, 100));
|
||||
});
|
||||
}
|
||||
|
||||
/// Génère des FlSpots pour un graphique d'engagement sur 12 mois
|
||||
static List<FlSpot> generateEngagementSpots(DashboardStatsEntity? stats) {
|
||||
if (stats == null) {
|
||||
return List.generate(12, (index) => FlSpot(index.toDouble(), 50.0));
|
||||
}
|
||||
|
||||
final currentEngagement = stats.engagementRate * 100;
|
||||
final random = Random(44);
|
||||
|
||||
return List.generate(12, (index) {
|
||||
final targetValue = currentEngagement;
|
||||
final startValue = max(30.0, targetValue - 15);
|
||||
final progress = index / 11;
|
||||
final baseValue = startValue + (targetValue - startValue) * progress;
|
||||
final variance = 8 * (random.nextDouble() - 0.5);
|
||||
|
||||
return FlSpot(index.toDouble(), (baseValue + variance).clamp(0, 100));
|
||||
});
|
||||
}
|
||||
|
||||
/// Génère des FlSpots pour un graphique d'événements sur 12 mois
|
||||
static List<FlSpot> generateEventsSpots(DashboardStatsEntity? stats) {
|
||||
if (stats == null || stats.totalEvents == 0) {
|
||||
return List.generate(12, (index) => FlSpot(index.toDouble(), 2.0));
|
||||
}
|
||||
|
||||
final avgEventsPerMonth = stats.totalEvents / 12;
|
||||
final random = Random(45);
|
||||
|
||||
return List.generate(12, (index) {
|
||||
final baseValue = avgEventsPerMonth;
|
||||
final variance = baseValue * 0.4 * (random.nextDouble() - 0.5);
|
||||
final value = baseValue + variance;
|
||||
|
||||
return FlSpot(index.toDouble(), value.clamp(0, double.infinity));
|
||||
});
|
||||
}
|
||||
|
||||
/// Génère des FlSpots pour les contributions sur 12 mois
|
||||
static List<FlSpot> generateContributionSpots(DashboardStatsEntity? stats) {
|
||||
if (stats == null || stats.totalContributionAmount == 0) {
|
||||
return List.generate(12, (index) => FlSpot(index.toDouble(), 1000.0));
|
||||
}
|
||||
|
||||
final avgPerMonth = stats.totalContributionAmount / 12;
|
||||
final random = Random(46);
|
||||
|
||||
return List.generate(12, (index) {
|
||||
// Tendance croissante vers la fin
|
||||
final seasonality = 1 + (index / 11) * 0.3; // +30% croissance sur l'année
|
||||
final baseValue = avgPerMonth * seasonality;
|
||||
final variance = baseValue * 0.25 * (random.nextDouble() - 0.5);
|
||||
final value = baseValue + variance;
|
||||
|
||||
return FlSpot(index.toDouble(), value.clamp(0, double.infinity));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
|
||||
/// Widget de graphique pour le dashboard
|
||||
class DashboardChartWidget extends StatelessWidget {
|
||||
@@ -20,14 +21,13 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: BlocBuilder<DashboardBloc, DashboardState>(
|
||||
@@ -54,23 +54,16 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
Icon(
|
||||
_getChartIcon(),
|
||||
color: AppColors.primaryGreen,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: DashboardTheme.titleMedium,
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -97,22 +90,22 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: DashboardTheme.success,
|
||||
color: AppColors.success,
|
||||
value: stats.activeMembers.toDouble(),
|
||||
title: '${stats.activeMembers}',
|
||||
radius: 50,
|
||||
titleStyle: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
titleStyle: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: DashboardTheme.grey300,
|
||||
color: AppColors.lightBorder,
|
||||
value: (stats.totalMembers - stats.activeMembers).toDouble(),
|
||||
title: '${stats.totalMembers - stats.activeMembers}',
|
||||
radius: 45,
|
||||
titleStyle: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey700,
|
||||
titleStyle: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -130,7 +123,7 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
horizontalInterval: stats.totalContributionAmount / 4,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return const FlLine(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
@@ -149,7 +142,7 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
if (value.toInt() >= 0 && value.toInt() < months.length) {
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
@@ -164,7 +157,7 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toStringAsFixed(0)}K',
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -181,8 +174,8 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
isCurved: true,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
DashboardTheme.tealBlue,
|
||||
DashboardTheme.royalBlue,
|
||||
AppColors.brandGreen,
|
||||
AppColors.primaryGreen,
|
||||
],
|
||||
),
|
||||
barWidth: 3,
|
||||
@@ -192,8 +185,8 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
DashboardTheme.tealBlue.withOpacity(0.3),
|
||||
DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
AppColors.brandGreen.withOpacity(0.3),
|
||||
AppColors.primaryGreen.withOpacity(0.1),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
@@ -228,7 +221,7 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
events[value.toInt()].title.length > 8
|
||||
? '${events[value.toInt()].title.substring(0, 8)}...'
|
||||
: events[value.toInt()].title,
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
@@ -252,10 +245,10 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
BarChartRodData(
|
||||
toY: event.currentParticipants.toDouble(),
|
||||
color: event.isFull
|
||||
? DashboardTheme.error
|
||||
? AppColors.error
|
||||
: event.isAlmostFull
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.success,
|
||||
? AppColors.warning
|
||||
: AppColors.success,
|
||||
width: 16,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
@@ -283,13 +276,13 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
LineChartBarData(
|
||||
spots: _generateGrowthSpots(stats.monthlyGrowth),
|
||||
isCurved: true,
|
||||
color: stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
color: stats.hasGrowth ? AppColors.success : AppColors.error,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: (stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error)
|
||||
color: (stats.hasGrowth ? AppColors.success : AppColors.error)
|
||||
.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
@@ -321,12 +314,13 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
Widget _buildLoadingChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: AppColors.lightBorder.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: DashboardTheme.royalBlue,
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -335,8 +329,8 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
Widget _buildErrorChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: AppColors.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
@@ -344,14 +338,16 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
color: AppColors.error,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
'ERREUR',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -363,23 +359,24 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
Widget _buildEmptyChart() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: AppColors.lightBorder.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.bar_chart,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 32,
|
||||
Icons.bar_chart_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
'AUCUNE DONNÉE',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -391,13 +388,13 @@ class DashboardChartWidget extends StatelessWidget {
|
||||
IconData _getChartIcon() {
|
||||
switch (chartType) {
|
||||
case DashboardChartType.memberActivity:
|
||||
return Icons.pie_chart;
|
||||
return Icons.pie_chart_outline;
|
||||
case DashboardChartType.contributionTrend:
|
||||
return Icons.trending_up;
|
||||
return Icons.trending_up_outlined;
|
||||
case DashboardChartType.eventParticipation:
|
||||
return Icons.bar_chart;
|
||||
return Icons.bar_chart_outlined;
|
||||
case DashboardChartType.monthlyGrowth:
|
||||
return Icons.show_chart;
|
||||
return Icons.show_chart_outlined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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 '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../../shared/widgets/mini_avatar.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';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
|
||||
/// Widget des activités récentes connecté au backend
|
||||
class ConnectedRecentActivities extends StatelessWidget {
|
||||
@@ -21,14 +23,13 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
if (state is DashboardLoading) {
|
||||
@@ -37,7 +38,7 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildActivitiesList(data.recentActivities);
|
||||
return _buildActivitiesList(context, data.recentActivities);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
@@ -52,33 +53,26 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
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 Icon(
|
||||
Icons.history,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Activités récentes',
|
||||
style: DashboardTheme.titleMedium,
|
||||
'ACTIVITÉS RÉCENTES',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
GestureDetector(
|
||||
onTap: onSeeAll,
|
||||
child: Text(
|
||||
'Voir tout',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
'TOUT VOIR',
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -86,7 +80,7 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivitiesList(List<RecentActivityEntity> activities) {
|
||||
Widget _buildActivitiesList(BuildContext context, List<RecentActivityEntity> activities) {
|
||||
if (activities.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
@@ -101,79 +95,60 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildActivityItem(activity),
|
||||
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildActivityItem(context, activity),
|
||||
if (!isLast) const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}).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),
|
||||
Widget _buildActivityItem(BuildContext context, RecentActivityEntity activity) {
|
||||
return InkWell(
|
||||
onTap: () => _navigateForActivity(context, activity),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MiniAvatar(
|
||||
fallbackText: activity.userName.isNotEmpty ? activity.userName[0].toUpperCase() : '?',
|
||||
imageUrl: activity.userAvatar,
|
||||
size: 32,
|
||||
),
|
||||
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(
|
||||
const SizedBox(width: 12),
|
||||
// Contenu
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
style: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
activity.description,
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
activity.userName,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: DashboardTheme.royalBlue,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryGreen,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' • ${activity.timeAgo}',
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -182,15 +157,14 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
),
|
||||
// Action button si disponible
|
||||
if (activity.hasAction)
|
||||
IconButton(
|
||||
onPressed: () => _navigateForActivity(context, activity),
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 14,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,7 +194,7 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
children: List.generate(3, (index) => Column(
|
||||
children: [
|
||||
_buildLoadingItem(),
|
||||
if (index < 2) const SizedBox(height: DashboardTheme.spacing12),
|
||||
if (index < 2) const SizedBox(height: 12),
|
||||
],
|
||||
)),
|
||||
);
|
||||
@@ -233,11 +207,11 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -246,25 +220,25 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
color: AppColors.lightBorder.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
color: AppColors.lightBorder.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
@@ -279,24 +253,9 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -306,24 +265,10 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
const Icon(Icons.history, color: AppColors.textSecondaryLight, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
const Text('AUCUNE ACTIVITÉ', style: AppTypography.subtitleSmall),
|
||||
Text('Les activités apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -349,17 +294,17 @@ class ConnectedRecentActivities extends StatelessWidget {
|
||||
Color _getActivityColor(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'member':
|
||||
return DashboardTheme.success;
|
||||
return AppColors.success;
|
||||
case 'event':
|
||||
return DashboardTheme.info;
|
||||
return AppColors.info;
|
||||
case 'contribution':
|
||||
return DashboardTheme.tealBlue;
|
||||
return AppColors.brandGreen;
|
||||
case 'organization':
|
||||
return DashboardTheme.royalBlue;
|
||||
return AppColors.primaryGreen;
|
||||
case 'system':
|
||||
return DashboardTheme.warning;
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return DashboardTheme.grey500;
|
||||
return AppColors.textSecondaryLight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
|
||||
/// Widget de carte de statistiques connecté au backend
|
||||
class ConnectedStatsCard extends StatelessWidget {
|
||||
@@ -45,157 +46,85 @@ class ConnectedStatsCard extends StatelessWidget {
|
||||
Widget _buildDataCard(DashboardStatsEntity stats) {
|
||||
final value = valueExtractor(stats);
|
||||
final subtitle = subtitleExtractor?.call(stats);
|
||||
final color = customColor ?? DashboardTheme.royalBlue;
|
||||
final color = customColor ?? AppColors.primaryGreen;
|
||||
|
||||
return GestureDetector(
|
||||
return CoreCard(
|
||||
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),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
Container(
|
||||
height: 32,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
return const CoreCard(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// On peut utiliser un Shimmer ici si disponible
|
||||
CircularProgressIndicator(strokeWidth: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorCard(String message) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.error_outline, color: AppColors.error, size: 20),
|
||||
const SizedBox(height: 8),
|
||||
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
|
||||
/// Widget des événements à venir connecté au backend
|
||||
class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
@@ -17,23 +18,22 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<DashboardBloc, DashboardState>(
|
||||
builder: (context, state) {
|
||||
builder: (ctx, 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);
|
||||
return _buildEventsList(context, data.upcomingEvents);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorState(state.message);
|
||||
}
|
||||
@@ -48,33 +48,26 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
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 Icon(
|
||||
Icons.event_outlined,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const Expanded(
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Événements à venir',
|
||||
style: DashboardTheme.titleMedium,
|
||||
'ÉVÉNEMENTS À VENIR',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
GestureDetector(
|
||||
onTap: onSeeAll,
|
||||
child: Text(
|
||||
'Voir tout',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
'TOUT VOIR',
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -82,7 +75,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsList(List<UpcomingEventEntity> events) {
|
||||
Widget _buildEventsList(BuildContext context, List<UpcomingEventEntity> events) {
|
||||
if (events.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
@@ -97,86 +90,62 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildEventCard(event),
|
||||
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
|
||||
_buildEventCard(context, event),
|
||||
if (!isLast) const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}).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),
|
||||
Widget _buildEventCard(BuildContext context, UpcomingEventEntity event) {
|
||||
final statusColor = event.isToday ? AppColors.success : (event.isTomorrow ? AppColors.warning : AppColors.primaryGreen);
|
||||
|
||||
return CoreCard(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Image ou icône
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: event.imageUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
event.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
errorBuilder: (context, error, stackTrace) => Icon(Icons.event_outlined, color: statusColor, size: 20),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.event,
|
||||
color: DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
),
|
||||
: Icon(Icons.event_outlined, color: statusColor, size: 20),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
// Contenu principal
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
style: DashboardTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
style: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
maxLines: 1,
|
||||
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),
|
||||
const Icon(Icons.location_on_outlined, size: 10, color: AppColors.textSecondaryLight),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.location,
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -186,99 +155,47 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Badge de temps
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
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),
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
event.daysUntilEvent,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: event.isToday
|
||||
? DashboardTheme.success
|
||||
: event.isTomorrow
|
||||
? DashboardTheme.warning
|
||||
: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
event.daysUntilEvent.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(color: statusColor, fontSize: 8, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
// Barre de progression des participants
|
||||
Row(
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('PARTICIPANTS', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'${event.currentParticipants}/${event.maxParticipants}',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
value: event.fillPercentage,
|
||||
minHeight: 4,
|
||||
backgroundColor: AppColors.lightBorder,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
event.isFull ? AppColors.error : (event.isAlmostFull ? AppColors.warning : AppColors.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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -289,78 +206,15 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
children: List.generate(2, (index) => Column(
|
||||
children: [
|
||||
_buildLoadingCard(),
|
||||
if (index < 1) const SizedBox(height: DashboardTheme.spacing12),
|
||||
if (index < 1) const SizedBox(height: 12),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return const CoreCard(
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -368,24 +222,9 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -395,24 +234,10 @@ class ConnectedUpcomingEvents extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
const Icon(Icons.event_outlined, color: AppColors.textSecondaryLight, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
const Text('AUCUN ÉVÉNEMENT', style: AppTypography.subtitleSmall),
|
||||
Text('Les événements apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,189 +3,233 @@
|
||||
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';
|
||||
import 'package:flutter_bloc/flutter_bloc.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;
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
|
||||
/// Constructeur du modèle d'élément de menu
|
||||
const DrawerMenuItem({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
|
||||
/// Widget de menu latéral
|
||||
///
|
||||
/// Affiche la navigation principale avec :
|
||||
/// - Header avec profil utilisateur
|
||||
/// - Menu de navigation structuré
|
||||
/// - Actions secondaires
|
||||
/// - Design Material avec gradient
|
||||
import '../../../profile/presentation/pages/profile_page_wrapper.dart';
|
||||
import '../../../notifications/presentation/pages/notifications_page_wrapper.dart';
|
||||
import '../../../help/presentation/pages/help_support_page.dart';
|
||||
import '../../../about/presentation/pages/about_page.dart';
|
||||
|
||||
/// Widget de menu latéral (Drawer / Hamburger)
|
||||
///
|
||||
/// Accessible via le bouton hamburger de l'AppBar.
|
||||
/// Contient uniquement les menus « Mon Espace » :
|
||||
/// - Mon Profil
|
||||
/// - Notifications
|
||||
/// - Aide & Support
|
||||
/// - À propos
|
||||
/// - Déconnexion
|
||||
class DashboardDrawer extends StatelessWidget {
|
||||
/// Callback pour les actions de navigation
|
||||
/// Callback pour les actions de navigation nommée (optionnel, non utilisé en interne)
|
||||
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,
|
||||
return BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
if (authState is! AuthAuthenticated) {
|
||||
return const Drawer();
|
||||
}
|
||||
|
||||
final state = authState;
|
||||
|
||||
return Drawer(
|
||||
backgroundColor: ColorTokens.background,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── En-tête utilisateur (même style que MorePage) ──────────────
|
||||
_buildUserProfile(state),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// ── Section Mon Espace ─────────────────────────────────────────
|
||||
_buildSectionTitle('Mon Espace'),
|
||||
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.person,
|
||||
title: 'Mon Profil',
|
||||
subtitle: 'Modifier mes informations',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.notifications,
|
||||
title: 'Notifications',
|
||||
subtitle: 'Gérer les notifications',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.help,
|
||||
title: 'Aide & Support',
|
||||
subtitle: 'Documentation et support',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const HelpSupportPage()),
|
||||
),
|
||||
),
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.info,
|
||||
title: 'À propos',
|
||||
subtitle: 'Version et informations',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const AboutPage()),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// ── Déconnexion ───────────────────────────────────────────────
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.logout,
|
||||
title: 'Déconnexion',
|
||||
subtitle: 'Se déconnecter de l\'application',
|
||||
color: ColorTokens.error,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.read<AuthBloc>().add(const AuthLogoutRequested());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── Profil utilisateur (idem MorePage._buildUserProfile) ──────────────────
|
||||
Widget _buildUserProfile(AuthAuthenticated state) {
|
||||
return CoreCard(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDrawerHeader(),
|
||||
...mainItems.map((item) => _buildMenuItem(item)),
|
||||
const Divider(),
|
||||
...secondaryItems.map((item) => _buildMenuItem(item)),
|
||||
const Divider(),
|
||||
_buildLogoutItem(),
|
||||
MiniAvatar(
|
||||
fallbackText:
|
||||
state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
|
||||
size: 40,
|
||||
imageUrl: state.user.avatar,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${state.user.firstName} ${state.user.lastName}',
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
Text(
|
||||
state.effectiveRole.displayName.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
state.user.email,
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
// ── Titre de section (idem MorePage._buildSectionTitle) ───────────────────
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 8, left: 4),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tuile d'option (idem MorePage._buildOptionTile) ───────────────────────
|
||||
Widget _buildOptionTile({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
Color? color,
|
||||
}) {
|
||||
final effectiveColor = color ?? AppColors.primaryGreen;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 35,
|
||||
color: ColorTokens.primary,
|
||||
icon,
|
||||
color: effectiveColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Text(
|
||||
'Utilisateur UnionFlow',
|
||||
style: TypographyTokens.titleMedium.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: color ?? AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'admin@unionflow.dev',
|
||||
style: TypographyTokens.bodySmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
|
||||
/// Widget de statistique simple pour les dashboards de rôle
|
||||
class DashboardStat extends StatelessWidget {
|
||||
@@ -18,13 +19,8 @@ class DashboardStat extends StatelessWidget {
|
||||
|
||||
@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,
|
||||
),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -32,22 +28,27 @@ class DashboardStat extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 24,
|
||||
color: color ?? AppColors.primaryGreen,
|
||||
size: 20,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: color ?? AppColors.primaryGreen,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium,
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -72,9 +73,9 @@ class DashboardStatsGrid extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.3,
|
||||
children: stats,
|
||||
);
|
||||
}
|
||||
@@ -95,9 +96,9 @@ class DashboardQuickActionsGrid extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.5,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.4,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
@@ -120,37 +121,34 @@ class DashboardQuickAction extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
return CoreCard(
|
||||
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(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 32,
|
||||
color: color ?? AppColors.primaryGreen,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -167,21 +165,19 @@ class DashboardRecentActivitySection extends StatelessWidget {
|
||||
|
||||
@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,
|
||||
),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Activités récentes',
|
||||
style: DashboardTheme.titleMedium,
|
||||
Text(
|
||||
'ACTIVITÉS RÉCENTES',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
@@ -209,43 +205,45 @@ class DashboardActivity extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: DashboardTheme.spacing12),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color ?? DashboardTheme.royalBlue,
|
||||
size: 16,
|
||||
color: color ?? AppColors.primaryGreen,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
time,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
|
||||
/// Widget de métriques en temps réel avec animations
|
||||
class RealTimeMetricsWidget extends StatefulWidget {
|
||||
@@ -81,13 +82,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.gradientCardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryGreen.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const SizedBox(height: 20),
|
||||
BlocConsumer<DashboardBloc, DashboardState>(
|
||||
listener: (context, state) {
|
||||
if (state is DashboardLoaded) {
|
||||
@@ -122,37 +137,39 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white,
|
||||
size: 24,
|
||||
Icons.speed_outlined,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Métriques Temps Réel',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
'MÉTRIQUES TEMPS RÉEL',
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Mise à jour automatique',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
'Mise à jour automatique (5 min)',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -172,7 +189,7 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -185,15 +202,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing4),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: DashboardTheme.white,
|
||||
size: 16,
|
||||
Icons.refresh_outlined,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -211,27 +228,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Membres Actifs',
|
||||
'MEMBRES ACTIFS',
|
||||
(data.stats.activeMembers * _countAnimation.value).round(),
|
||||
data.stats.totalMembers,
|
||||
Icons.people,
|
||||
DashboardTheme.success,
|
||||
Icons.people_outline,
|
||||
AppColors.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Engagement',
|
||||
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
|
||||
100,
|
||||
Icons.favorite,
|
||||
DashboardTheme.warning,
|
||||
AppColors.warning,
|
||||
suffix: '%',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -240,17 +257,17 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
(data.stats.upcomingEvents * _countAnimation.value).round(),
|
||||
data.stats.totalEvents,
|
||||
Icons.event,
|
||||
DashboardTheme.info,
|
||||
AppColors.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Croissance',
|
||||
(data.stats.monthlyGrowth * _countAnimation.value),
|
||||
null,
|
||||
Icons.trending_up,
|
||||
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
data.stats.hasGrowth ? AppColors.success : AppColors.error,
|
||||
suffix: '%',
|
||||
isDecimal: true,
|
||||
),
|
||||
@@ -280,12 +297,12 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
@@ -296,34 +313,36 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
displayValue,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
if (maxValue != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'sur $maxValue',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.6),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -338,15 +357,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
@@ -357,15 +376,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Widget _buildLoadingMetricItem() {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -375,8 +394,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: AppColors.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
@@ -384,14 +403,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
color: AppColors.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -404,8 +423,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
@@ -413,14 +432,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white.withOpacity(0.5),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.7),
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
import '../../../data/services/dashboard_performance_monitor.dart';
|
||||
|
||||
/// Widget de monitoring des performances en temps réel
|
||||
@@ -127,13 +128,9 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
return _buildLoadingWidget();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.subtleShadow,
|
||||
),
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
@@ -151,27 +148,23 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
}
|
||||
|
||||
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(
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.royalBlue),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
|
||||
),
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Initialisation du monitoring...',
|
||||
style: DashboardTheme.bodyMedium,
|
||||
style: AppTypography.bodyTextSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -185,9 +178,8 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
@@ -213,18 +205,24 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Performances Système',
|
||||
style: DashboardTheme.titleSmall,
|
||||
'PERFORMANCES SYSTÈME',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildQuickMetrics(),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
_isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: DashboardTheme.grey600,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -241,13 +239,13 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickMetric(
|
||||
'CPU',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickMetric(
|
||||
'NET',
|
||||
'${_currentMetrics!.networkLatency}ms',
|
||||
@@ -264,8 +262,8 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
fontSize: 9,
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -283,7 +281,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
|
||||
Widget _buildDetailedMetrics() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMetricRow(
|
||||
@@ -293,37 +291,37 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
|
||||
Icons.memory,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: 12),
|
||||
_buildMetricRow(
|
||||
'Processeur',
|
||||
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
|
||||
_currentMetrics!.cpuUsage / 100,
|
||||
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
|
||||
Icons.speed,
|
||||
Icons.speed_outlined,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: 12),
|
||||
_buildMetricRow(
|
||||
'Réseau',
|
||||
'${_currentMetrics!.networkLatency} ms',
|
||||
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
|
||||
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
|
||||
Icons.wifi,
|
||||
Icons.wifi_outlined,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: 12),
|
||||
_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,
|
||||
Icons.videocam_outlined,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: 12),
|
||||
_buildMetricRow(
|
||||
'Batterie',
|
||||
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
|
||||
_currentMetrics!.batteryLevel / 100,
|
||||
_getBatteryColor(_currentMetrics!.batteryLevel),
|
||||
Icons.battery_std,
|
||||
Icons.battery_std_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -340,23 +338,27 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
backgroundColor: DashboardTheme.grey200,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
backgroundColor: AppColors.lightBorder,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
minHeight: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
@@ -375,15 +377,15 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
|
||||
Widget _buildAlertsSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Alertes Récentes',
|
||||
style: DashboardTheme.titleSmall,
|
||||
Text(
|
||||
'ALERTES RÉCENTES',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, fontSize: 10),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
..._recentAlerts.take(3).map((alert) => _buildAlertItem(alert)),
|
||||
],
|
||||
),
|
||||
@@ -402,13 +404,13 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alert.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: DashboardTheme.grey700,
|
||||
fontSize: 11,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -416,7 +418,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
_formatTime(alert.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey500,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -425,7 +427,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
}
|
||||
|
||||
Color _getOverallHealthColor() {
|
||||
if (_currentMetrics == null) return DashboardTheme.grey400;
|
||||
if (_currentMetrics == null) return AppColors.textSecondaryLight;
|
||||
|
||||
final metrics = _currentMetrics!;
|
||||
|
||||
@@ -438,36 +440,36 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
|
||||
|
||||
switch (issues) {
|
||||
case 0:
|
||||
return DashboardTheme.success;
|
||||
return AppColors.success;
|
||||
case 1:
|
||||
return DashboardTheme.warning;
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return DashboardTheme.error;
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
|
||||
if (value >= errorThreshold) return DashboardTheme.error;
|
||||
if (value >= warningThreshold) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
if (value >= errorThreshold) return AppColors.error;
|
||||
if (value >= warningThreshold) return AppColors.warning;
|
||||
return AppColors.success;
|
||||
}
|
||||
|
||||
Color _getBatteryColor(double batteryLevel) {
|
||||
if (batteryLevel <= 20) return DashboardTheme.error;
|
||||
if (batteryLevel <= 50) return DashboardTheme.warning;
|
||||
return DashboardTheme.success;
|
||||
if (batteryLevel <= 20) return AppColors.error;
|
||||
if (batteryLevel <= 50) return AppColors.warning;
|
||||
return AppColors.success;
|
||||
}
|
||||
|
||||
Color _getAlertColor(AlertSeverity severity) {
|
||||
switch (severity) {
|
||||
case AlertSeverity.info:
|
||||
return DashboardTheme.info;
|
||||
return AppColors.info;
|
||||
case AlertSeverity.warning:
|
||||
return DashboardTheme.warning;
|
||||
return AppColors.warning;
|
||||
case AlertSeverity.error:
|
||||
return DashboardTheme.error;
|
||||
return AppColors.error;
|
||||
case AlertSeverity.critical:
|
||||
return DashboardTheme.error;
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../pages/connected_dashboard_page.dart';
|
||||
import '../../pages/advanced_dashboard_page.dart';
|
||||
import '../../../../settings/presentation/pages/language_settings_page.dart';
|
||||
import '../../../../settings/presentation/pages/system_settings_page.dart';
|
||||
import '../../../../reports/presentation/pages/reports_page_wrapper.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';
|
||||
|
||||
/// Widget de navigation pour les différents types de dashboard
|
||||
class DashboardNavigation extends StatefulWidget {
|
||||
@@ -80,11 +86,11 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: DashboardTheme.grey900.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
@@ -92,10 +98,10 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
child: BottomAppBar(
|
||||
shape: const CircularNotchedRectangle(),
|
||||
notchMargin: 8,
|
||||
color: DashboardTheme.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _tabs.asMap().entries.map((entry) {
|
||||
@@ -121,23 +127,24 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
onTap: () => setState(() => _currentIndex = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: DashboardTheme.spacing12,
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? tab.activeIcon : tab.icon,
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
size: 24,
|
||||
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tab.title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -147,21 +154,14 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
return FloatingActionButton(
|
||||
onPressed: _showQuickActions,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
elevation: 4,
|
||||
child: const Icon(
|
||||
Icons.add_outlined,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -169,9 +169,9 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
Widget _buildReportsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Rapports'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
title: Text('Rapports'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: Center(
|
||||
@@ -179,20 +179,20 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.assessment,
|
||||
size: 64,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.assessment_outlined,
|
||||
size: 48,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const Text(
|
||||
'Page Rapports',
|
||||
style: DashboardTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Fonctionnalité en cours de développement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
'Page Rapports'.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'En cours de développement',
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -204,64 +204,64 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
Widget _buildSettingsPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Paramètres'),
|
||||
backgroundColor: DashboardTheme.royalBlue,
|
||||
foregroundColor: DashboardTheme.white,
|
||||
title: Text('Paramètres'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSettingsSection(
|
||||
'Apparence',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Thème',
|
||||
'Bleu Roi & Pétrole',
|
||||
Icons.palette,
|
||||
() {},
|
||||
'Design System UnionFlow',
|
||||
Icons.palette_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Langue',
|
||||
'Français',
|
||||
Icons.language,
|
||||
() {},
|
||||
Icons.language_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const LanguageSettingsPage())),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(
|
||||
'Notifications',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Notifications push',
|
||||
'Activées',
|
||||
Icons.notifications,
|
||||
() {},
|
||||
Icons.notifications_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Emails',
|
||||
'Quotidien',
|
||||
Icons.email,
|
||||
() {},
|
||||
Icons.email_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing24),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(
|
||||
'Données',
|
||||
[
|
||||
_buildSettingsTile(
|
||||
'Synchronisation',
|
||||
'Automatique',
|
||||
Icons.sync,
|
||||
() {},
|
||||
Icons.sync_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
_buildSettingsTile(
|
||||
'Cache',
|
||||
'Vider le cache',
|
||||
Icons.storage,
|
||||
() {},
|
||||
Icons.storage_outlined,
|
||||
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -275,12 +275,16 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.titleMedium,
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: AppColors.primaryGreen, fontSize: 10),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
),
|
||||
child: Column(children: children),
|
||||
),
|
||||
],
|
||||
@@ -294,12 +298,13 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: DashboardTheme.royalBlue),
|
||||
title: Text(title, style: DashboardTheme.bodyMedium),
|
||||
subtitle: Text(subtitle, style: DashboardTheme.bodySmall),
|
||||
leading: Icon(icon, color: AppColors.primaryGreen, size: 20),
|
||||
title: Text(title, style: AppTypography.actionText.copyWith(fontSize: 13)),
|
||||
subtitle: Text(subtitle, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.chevron_right_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
@@ -310,14 +315,14 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
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),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -325,61 +330,60 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.grey300,
|
||||
color: AppColors.lightBorder,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const Text(
|
||||
'Actions Rapides',
|
||||
style: DashboardTheme.titleMedium,
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'ACTIONS RAPIDES',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const SizedBox(height: 20),
|
||||
GridView.count(
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: DashboardTheme.spacing16,
|
||||
mainAxisSpacing: DashboardTheme.spacing16,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
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),
|
||||
_buildQuickActionItem(context, 'Nouveau\nMembre', Icons.person_add_outlined, AppColors.success, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper()))),
|
||||
_buildQuickActionItem(context, 'Créer\nÉvénement', Icons.event_available_outlined, AppColors.primaryGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()))),
|
||||
_buildQuickActionItem(context, 'Ajouter\nContribution', Icons.account_balance_wallet_outlined, AppColors.brandGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()))),
|
||||
_buildQuickActionItem(context, 'Générer\nRapport', Icons.assessment_outlined, AppColors.info, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper()))),
|
||||
_buildQuickActionItem(context, 'Paramètres', Icons.settings_outlined, AppColors.textSecondaryLight, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage()))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionItem(String title, IconData icon, Color color) {
|
||||
Widget _buildQuickActionItem(BuildContext context, String title, IconData icon, Color color, VoidCallback onNavigate) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// Action rapide non encore connectée
|
||||
onNavigate();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
@@ -2,7 +2,11 @@ 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 '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
|
||||
import '../../../../events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../../../settings/presentation/pages/system_settings_page.dart';
|
||||
|
||||
/// Widget de notifications pour le dashboard
|
||||
class DashboardNotificationsWidget extends StatelessWidget {
|
||||
@@ -15,8 +19,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
return CoreCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -29,7 +33,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
return _buildNotifications(data);
|
||||
return _buildNotifications(context, data);
|
||||
} else if (state is DashboardError) {
|
||||
return _buildErrorNotifications();
|
||||
}
|
||||
@@ -43,35 +47,36 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue.withOpacity(0.1),
|
||||
color: AppColors.primaryGreen.withOpacity(0.05),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadius),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadius),
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.royalBlue,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: AppColors.primaryGreen,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications,
|
||||
color: DashboardTheme.white,
|
||||
size: 20,
|
||||
Icons.notifications_outlined,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Notifications',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
'NOTIFICATIONS',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -81,23 +86,24 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
final data = state is DashboardLoaded
|
||||
? state.dashboardData
|
||||
: (state as DashboardRefreshing).dashboardData;
|
||||
final urgentCount = _getUrgentNotificationsCount(data);
|
||||
final urgentCount = _getUrgentNotificationsCount(context, data);
|
||||
|
||||
if (urgentCount > 0) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: AppColors.error,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
urgentCount.toString(),
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -111,8 +117,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotifications(DashboardEntity data) {
|
||||
final notifications = _generateNotifications(data);
|
||||
Widget _buildNotifications(BuildContext context, DashboardEntity data) {
|
||||
final notifications = _generateNotifications(context, data);
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return _buildEmptyNotifications();
|
||||
@@ -127,11 +133,11 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildNotificationItem(DashboardNotification notification) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
decoration: const BoxDecoration(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -140,18 +146,18 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: notification.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
notification.icon,
|
||||
color: notification.color,
|
||||
size: 20,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -161,7 +167,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
notification.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -169,40 +176,41 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
if (notification.isUrgent) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing6,
|
||||
vertical: DashboardTheme.spacing2,
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: AppColors.error,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: Text(
|
||||
'URGENT',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
fontSize: 7,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
notification.message,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
notification.timeAgo,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
fontSize: 11,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -211,9 +219,10 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
onTap: notification.onAction,
|
||||
child: Text(
|
||||
notification.actionLabel!,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.royalBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -229,76 +238,28 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorNotifications() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
size: 32,
|
||||
color: AppColors.error,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
'Erreur',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -309,28 +270,23 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildEmptyNotifications() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.notifications_none,
|
||||
color: DashboardTheme.grey400,
|
||||
size: 32,
|
||||
Icons.notifications_none_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune notification',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey500,
|
||||
),
|
||||
'AUCUNE NOTIFICATION',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
Text(
|
||||
'Vous êtes à jour !',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -338,20 +294,20 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<DashboardNotification> _generateNotifications(DashboardEntity data) {
|
||||
List<DashboardNotification> _generateNotifications(BuildContext context, 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,
|
||||
message: '${data.stats.pendingRequests} demandes à valider',
|
||||
icon: Icons.pending_actions_outlined,
|
||||
color: AppColors.warning,
|
||||
timeAgo: '2h',
|
||||
isUrgent: data.stats.pendingRequests > 20,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -359,13 +315,13 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
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,
|
||||
message: '${data.todayEventsCount} événement(s) aujourd\'hui',
|
||||
icon: Icons.event_available_outlined,
|
||||
color: AppColors.info,
|
||||
timeAgo: '30min',
|
||||
isUrgent: false,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -373,9 +329,9 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
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,
|
||||
message: 'Progression de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
|
||||
icon: Icons.trending_up_outlined,
|
||||
color: AppColors.success,
|
||||
timeAgo: '1j',
|
||||
isUrgent: false,
|
||||
actionLabel: null,
|
||||
@@ -386,14 +342,14 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
// 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,
|
||||
title: 'Engagement à surveiller',
|
||||
message: 'Taux: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
|
||||
icon: Icons.trending_down_outlined,
|
||||
color: AppColors.error,
|
||||
timeAgo: '3h',
|
||||
isUrgent: data.stats.engagementRate < 0.5,
|
||||
actionLabel: 'Améliorer',
|
||||
onAction: () {},
|
||||
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -401,21 +357,21 @@ class DashboardNotificationsWidget extends StatelessWidget {
|
||||
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,
|
||||
message: '${data.recentActivitiesCount} activités récentes',
|
||||
icon: Icons.fiber_new_outlined,
|
||||
color: AppColors.brandGreen,
|
||||
timeAgo: '15min',
|
||||
isUrgent: false,
|
||||
actionLabel: 'Voir',
|
||||
onAction: () {},
|
||||
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
));
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
int _getUrgentNotificationsCount(DashboardEntity data) {
|
||||
final notifications = _generateNotifications(data);
|
||||
int _getUrgentNotificationsCount(BuildContext context, DashboardEntity data) {
|
||||
final notifications = _generateNotifications(context, data);
|
||||
return notifications.where((n) => n.isUrgent).length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.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 recherche rapide pour le dashboard
|
||||
class DashboardSearchWidget extends StatefulWidget {
|
||||
@@ -26,13 +31,14 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isExpanded = false;
|
||||
List<SearchSuggestion> _filteredSuggestions = [];
|
||||
List<SearchSuggestion>? _defaultSuggestions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_setupListeners();
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
_filteredSuggestions = widget.suggestions ?? [];
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
@@ -71,12 +77,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
void _filterSuggestions(String query) {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
_filteredSuggestions = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
|
||||
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
final filtered = defaultList
|
||||
.where((suggestion) =>
|
||||
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
||||
@@ -89,11 +96,19 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_defaultSuggestions == null) {
|
||||
_defaultSuggestions = _getDefaultSuggestions(context);
|
||||
if (_filteredSuggestions.isEmpty && widget.suggestions == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _filteredSuggestions = _defaultSuggestions!);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestions(),
|
||||
],
|
||||
],
|
||||
@@ -108,9 +123,11 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: _isExpanded
|
||||
? [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4))]
|
||||
: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
@@ -123,12 +140,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'Rechercher...',
|
||||
hintStyle: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
hintStyle: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
Icons.search_outlined,
|
||||
color: _isExpanded ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
@@ -137,30 +155,31 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.close_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: DashboardTheme.royalBlue,
|
||||
width: 2,
|
||||
color: AppColors.primaryGreen,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
vertical: DashboardTheme.spacing12,
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: DashboardTheme.white,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
),
|
||||
style: DashboardTheme.bodyMedium,
|
||||
style: AppTypography.bodyTextSmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -172,9 +191,15 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@@ -196,13 +221,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
suggestion.onTap?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -210,34 +235,36 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: suggestion.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
suggestion.icon,
|
||||
color: suggestion.color,
|
||||
size: 20,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
suggestion.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (suggestion.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing2),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
suggestion.subtitle,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -245,8 +272,8 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.chevron_right_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
@@ -255,42 +282,42 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchSuggestion> _getDefaultSuggestions() {
|
||||
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
|
||||
return [
|
||||
SearchSuggestion(
|
||||
title: 'Membres',
|
||||
subtitle: 'Rechercher des membres',
|
||||
icon: Icons.people,
|
||||
color: DashboardTheme.royalBlue,
|
||||
onTap: () {},
|
||||
icon: Icons.people_outline,
|
||||
color: AppColors.primaryGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Événements',
|
||||
subtitle: 'Trouver des événements',
|
||||
icon: Icons.event,
|
||||
color: DashboardTheme.tealBlue,
|
||||
onTap: () {},
|
||||
icon: Icons.event_outlined,
|
||||
color: AppColors.brandGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Contributions',
|
||||
subtitle: 'Historique des paiements',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.success,
|
||||
onTap: () {},
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
color: AppColors.success,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Rapports',
|
||||
subtitle: 'Consulter les rapports',
|
||||
icon: Icons.assessment,
|
||||
color: DashboardTheme.warning,
|
||||
onTap: () {},
|
||||
icon: Icons.assessment_outlined,
|
||||
color: AppColors.warning,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Paramètres',
|
||||
subtitle: 'Configuration système',
|
||||
icon: Icons.settings,
|
||||
color: DashboardTheme.grey600,
|
||||
onTap: () {},
|
||||
icon: Icons.settings_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../../shared/design_system/tokens/app_typography.dart';
|
||||
import '../../../../../shared/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../../shared/design_system/tokens/radius_tokens.dart';
|
||||
import '../../../../../shared/widgets/core_card.dart';
|
||||
|
||||
/// Widget de sélection de thème pour le Dashboard
|
||||
class ThemeSelectorWidget extends StatefulWidget {
|
||||
@@ -27,13 +31,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
|
||||
@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,
|
||||
),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -41,17 +40,17 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
children: [
|
||||
Icon(
|
||||
Icons.palette,
|
||||
color: DashboardTheme.royalBlue,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: DashboardTheme.spacing8),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Thème de l\'interface',
|
||||
style: DashboardTheme.titleMedium,
|
||||
style: AppTypography.headerSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
|
||||
// Grille des thèmes
|
||||
GridView.builder(
|
||||
@@ -59,8 +58,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemCount: DashboardThemeManager.availableThemes.length,
|
||||
@@ -72,7 +71,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
|
||||
// Aperçu du thème sélectionné
|
||||
_buildThemePreview(),
|
||||
@@ -87,11 +86,11 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? themeOption.theme.primaryColor
|
||||
: DashboardTheme.grey300,
|
||||
: const Color(0xFFD1D5DB),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
@@ -102,7 +101,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: DashboardTheme.subtleShadow,
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -121,8 +120,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
topRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
topLeft: Radius.circular(RadiusTokens.lg - 1),
|
||||
topRight: Radius.circular(RadiusTokens.lg - 1),
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
@@ -140,12 +139,12 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
flex: 1,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: themeOption.theme.cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
bottomRight: Radius.circular(DashboardTheme.borderRadius - 1),
|
||||
bottomLeft: Radius.circular(7),
|
||||
bottomRight: Radius.circular(7),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
@@ -172,11 +171,11 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
.firstWhere((theme) => theme.key == _selectedTheme);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
border: Border.all(color: DashboardTheme.grey300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFD1D5DB)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -189,15 +188,15 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
color: currentTheme.theme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
|
||||
// Exemple de carte avec le thème
|
||||
// Aperçu de carte avec le thème
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: currentTheme.theme.primaryColor.withOpacity(0.1),
|
||||
@@ -221,7 +220,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: SpacingTokens.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -236,7 +235,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Exemple avec ce thème',
|
||||
'Aperçu',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: currentTheme.theme.textSecondary,
|
||||
@@ -247,12 +246,12 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing8,
|
||||
vertical: DashboardTheme.spacing4,
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTheme.theme.success.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Actif',
|
||||
@@ -267,17 +266,17 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DashboardTheme.spacing12),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
|
||||
// Palette de couleurs
|
||||
Row(
|
||||
children: [
|
||||
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
_buildColorSwatch('Succès', currentTheme.theme.success),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
_buildColorSwatch('Attention', currentTheme.theme.warning),
|
||||
],
|
||||
),
|
||||
@@ -295,7 +294,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -303,7 +302,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: DashboardTheme.grey600,
|
||||
color: Color(0xFF4B5563),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../../shared/widgets/core_card.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';
|
||||
@@ -22,14 +23,13 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
final shortcuts = customShortcuts ?? _getDefaultShortcuts(context);
|
||||
final displayShortcuts = shortcuts.take(maxShortcuts).toList();
|
||||
|
||||
return Container(
|
||||
decoration: DashboardTheme.cardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
_buildShortcutsGrid(displayShortcuts),
|
||||
],
|
||||
),
|
||||
@@ -39,36 +39,18 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
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 Icon(
|
||||
Icons.flash_on_outlined,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Actions Rapides',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
'ACTIONS RAPIDES',
|
||||
style: AppTypography.subtitleSmall.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,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -82,9 +64,9 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: DashboardTheme.spacing12,
|
||||
mainAxisSpacing: DashboardTheme.spacing12,
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 0.9,
|
||||
),
|
||||
itemCount: shortcuts.length,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -96,64 +78,34 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
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(12),
|
||||
decoration: BoxDecoration(
|
||||
color: shortcut.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
shortcut.icon,
|
||||
color: shortcut.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
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: 8),
|
||||
Text(
|
||||
shortcut.title,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -162,8 +114,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
return [
|
||||
DashboardShortcut(
|
||||
title: 'Nouveau\nMembre',
|
||||
icon: Icons.person_add,
|
||||
color: DashboardTheme.success,
|
||||
icon: Icons.person_add_outlined,
|
||||
color: AppColors.success,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -174,8 +126,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Créer\nÉvénement',
|
||||
icon: Icons.event_available,
|
||||
color: DashboardTheme.royalBlue,
|
||||
icon: Icons.event_available_outlined,
|
||||
color: AppColors.primaryGreen,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -186,32 +138,20 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Ajouter\nContribution',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.tealBlue,
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
color: AppColors.brandGreen,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ContributionsPageWrapper(),
|
||||
builder: (context) => const CotisationsPageWrapper(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
icon: Icons.assessment_outlined,
|
||||
color: AppColors.info,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -222,8 +162,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
|
||||
),
|
||||
DashboardShortcut(
|
||||
title: 'Paramètres',
|
||||
icon: Icons.settings,
|
||||
color: DashboardTheme.grey600,
|
||||
icon: Icons.settings_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
||||
Reference in New Issue
Block a user