Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
import 'dart:io';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
|
||||
/// Service d'export de rapports PDF pour le Dashboard
|
||||
class DashboardExportService {
|
||||
static const String _reportsFolder = 'UnionFlow_Reports';
|
||||
|
||||
/// Exporte un rapport complet du dashboard en PDF
|
||||
Future<String> exportDashboardReport({
|
||||
required DashboardDataModel dashboardData,
|
||||
required String organizationName,
|
||||
required String reportTitle,
|
||||
bool includeCharts = true,
|
||||
bool includeActivities = true,
|
||||
bool includeEvents = true,
|
||||
}) async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
// Charger les polices personnalisées si disponibles
|
||||
final font = await _loadFont();
|
||||
|
||||
// Page 1: Couverture et statistiques principales
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: _createTheme(font),
|
||||
build: (context) => [
|
||||
_buildHeader(organizationName, reportTitle),
|
||||
pw.SizedBox(height: 20),
|
||||
_buildStatsSection(dashboardData.stats),
|
||||
pw.SizedBox(height: 20),
|
||||
_buildSummarySection(dashboardData),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Page 2: Activités récentes (si incluses)
|
||||
if (includeActivities && dashboardData.recentActivities.isNotEmpty) {
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: _createTheme(font),
|
||||
build: (context) => [
|
||||
_buildSectionTitle('Activités Récentes'),
|
||||
pw.SizedBox(height: 10),
|
||||
_buildActivitiesSection(dashboardData.recentActivities),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Page 3: Événements à venir (si inclus)
|
||||
if (includeEvents && dashboardData.upcomingEvents.isNotEmpty) {
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: _createTheme(font),
|
||||
build: (context) => [
|
||||
_buildSectionTitle('Événements à Venir'),
|
||||
pw.SizedBox(height: 10),
|
||||
_buildEventsSection(dashboardData.upcomingEvents),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Page 4: Graphiques et analyses (si inclus)
|
||||
if (includeCharts) {
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: _createTheme(font),
|
||||
build: (context) => [
|
||||
_buildSectionTitle('Analyses et Tendances'),
|
||||
pw.SizedBox(height: 10),
|
||||
_buildAnalyticsSection(dashboardData.stats),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Sauvegarder le PDF
|
||||
final fileName = _generateFileName(reportTitle);
|
||||
final filePath = await _savePdf(pdf, fileName);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// Exporte uniquement les statistiques en PDF
|
||||
Future<String> exportStatsReport({
|
||||
required DashboardStatsModel stats,
|
||||
required String organizationName,
|
||||
String? customTitle,
|
||||
}) async {
|
||||
final pdf = pw.Document();
|
||||
final font = await _loadFont();
|
||||
final title = customTitle ?? 'Rapport Statistiques - ${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year}';
|
||||
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: _createTheme(font),
|
||||
build: (context) => [
|
||||
_buildHeader(organizationName, title),
|
||||
pw.SizedBox(height: 30),
|
||||
_buildStatsSection(stats),
|
||||
pw.SizedBox(height: 30),
|
||||
_buildStatsAnalysis(stats),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final fileName = _generateFileName('Stats_${DateTime.now().millisecondsSinceEpoch}');
|
||||
final filePath = await _savePdf(pdf, fileName);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// Charge une police personnalisée
|
||||
Future<pw.Font?> _loadFont() async {
|
||||
try {
|
||||
final fontData = await rootBundle.load('assets/fonts/Inter-Regular.ttf');
|
||||
return pw.Font.ttf(fontData);
|
||||
} catch (e) {
|
||||
// Police par défaut si la police personnalisée n'est pas disponible
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée le thème PDF
|
||||
pw.ThemeData _createTheme(pw.Font? font) {
|
||||
return pw.ThemeData.withFont(
|
||||
base: font ?? pw.Font.helvetica(),
|
||||
bold: font ?? pw.Font.helveticaBold(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'en-tête du rapport
|
||||
pw.Widget _buildHeader(String organizationName, String reportTitle) {
|
||||
return pw.Container(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(20),
|
||||
decoration: pw.BoxDecoration(
|
||||
gradient: pw.LinearGradient(
|
||||
colors: [
|
||||
PdfColor.fromHex('#4169E1'), // Bleu Roi
|
||||
PdfColor.fromHex('#008B8B'), // Bleu Pétrole
|
||||
],
|
||||
),
|
||||
borderRadius: pw.BorderRadius.circular(10),
|
||||
),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
organizationName,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.white,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Text(
|
||||
reportTitle,
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 16,
|
||||
color: PdfColors.white,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text(
|
||||
'Généré le ${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year} à ${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}',
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 12,
|
||||
color: PdfColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section des statistiques
|
||||
pw.Widget _buildStatsSection(DashboardStatsModel stats) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Statistiques Principales'),
|
||||
pw.SizedBox(height: 15),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Membres Total', stats.totalMembers.toString(), PdfColor.fromHex('#4169E1')),
|
||||
),
|
||||
pw.SizedBox(width: 10),
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Membres Actifs', stats.activeMembers.toString(), PdfColor.fromHex('#10B981')),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Événements', stats.totalEvents.toString(), PdfColor.fromHex('#008B8B')),
|
||||
),
|
||||
pw.SizedBox(width: 10),
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Contributions', stats.formattedContributionAmount, PdfColor.fromHex('#F59E0B')),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Croissance', '${stats.monthlyGrowth.toStringAsFixed(1)}%',
|
||||
stats.hasGrowth ? PdfColor.fromHex('#10B981') : PdfColor.fromHex('#EF4444')),
|
||||
),
|
||||
pw.SizedBox(width: 10),
|
||||
pw.Expanded(
|
||||
child: _buildStatCard('Engagement', '${(stats.engagementRate * 100).toStringAsFixed(1)}%',
|
||||
stats.isHighEngagement ? PdfColor.fromHex('#10B981') : PdfColor.fromHex('#F59E0B')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte de statistique
|
||||
pw.Widget _buildStatCard(String title, String value, PdfColor color) {
|
||||
return pw.Container(
|
||||
padding: const pw.EdgeInsets.all(15),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: color, width: 2),
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
title,
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 12,
|
||||
color: PdfColors.grey700,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Text(
|
||||
value,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un titre de section
|
||||
pw.Widget _buildSectionTitle(String title) {
|
||||
return pw.Text(
|
||||
title,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('#1F2937'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section de résumé
|
||||
pw.Widget _buildSummarySection(DashboardDataModel data) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Résumé Exécutif'),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text(
|
||||
'Ce rapport présente un aperçu complet de l\'activité de l\'organisation. '
|
||||
'Avec ${data.stats.totalMembers} membres dont ${data.stats.activeMembers} actifs '
|
||||
'(${data.stats.activeMemberPercentage.toStringAsFixed(1)}%), l\'organisation maintient '
|
||||
'un niveau d\'engagement de ${(data.stats.engagementRate * 100).toStringAsFixed(1)}%.',
|
||||
style: const pw.TextStyle(fontSize: 12, lineSpacing: 1.5),
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text(
|
||||
'La croissance mensuelle de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% '
|
||||
'${data.stats.hasGrowth ? 'indique une tendance positive' : 'nécessite une attention particulière'}. '
|
||||
'Les contributions totales s\'élèvent à ${data.stats.formattedContributionAmount} XOF.',
|
||||
style: const pw.TextStyle(fontSize: 12, lineSpacing: 1.5),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section des activités
|
||||
pw.Widget _buildActivitiesSection(List<RecentActivityModel> activities) {
|
||||
return pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
children: [
|
||||
// En-tête
|
||||
pw.TableRow(
|
||||
decoration: pw.BoxDecoration(color: PdfColor.fromHex('#F3F4F6')),
|
||||
children: [
|
||||
_buildTableHeader('Type'),
|
||||
_buildTableHeader('Description'),
|
||||
_buildTableHeader('Utilisateur'),
|
||||
_buildTableHeader('Date'),
|
||||
],
|
||||
),
|
||||
// Données
|
||||
...activities.take(10).map((activity) => pw.TableRow(
|
||||
children: [
|
||||
_buildTableCell(activity.type),
|
||||
_buildTableCell(activity.title),
|
||||
_buildTableCell(activity.userName),
|
||||
_buildTableCell(activity.timeAgo),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section des événements
|
||||
pw.Widget _buildEventsSection(List<UpcomingEventModel> events) {
|
||||
return pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
children: [
|
||||
// En-tête
|
||||
pw.TableRow(
|
||||
decoration: pw.BoxDecoration(color: PdfColor.fromHex('#F3F4F6')),
|
||||
children: [
|
||||
_buildTableHeader('Événement'),
|
||||
_buildTableHeader('Date'),
|
||||
_buildTableHeader('Lieu'),
|
||||
_buildTableHeader('Participants'),
|
||||
],
|
||||
),
|
||||
// Données
|
||||
...events.take(10).map((event) => pw.TableRow(
|
||||
children: [
|
||||
_buildTableCell(event.title),
|
||||
_buildTableCell('${event.startDate.day}/${event.startDate.month}'),
|
||||
_buildTableCell(event.location),
|
||||
_buildTableCell('${event.currentParticipants}/${event.maxParticipants}'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'en-tête de tableau
|
||||
pw.Widget _buildTableHeader(String text) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(
|
||||
text,
|
||||
style: pw.TextStyle(
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une cellule de tableau
|
||||
pw.Widget _buildTableCell(String text) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(
|
||||
text,
|
||||
style: const pw.TextStyle(fontSize: 9),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section d'analyse des statistiques
|
||||
pw.Widget _buildStatsAnalysis(DashboardStatsModel stats) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Analyse des Performances'),
|
||||
pw.SizedBox(height: 10),
|
||||
_buildAnalysisPoint('Taux d\'activité des membres',
|
||||
'${stats.activeMemberPercentage.toStringAsFixed(1)}%',
|
||||
stats.activeMemberPercentage > 70 ? 'Excellent' : 'À améliorer'),
|
||||
_buildAnalysisPoint('Croissance mensuelle',
|
||||
'${stats.monthlyGrowth.toStringAsFixed(1)}%',
|
||||
stats.hasGrowth ? 'Positive' : 'Négative'),
|
||||
_buildAnalysisPoint('Niveau d\'engagement',
|
||||
'${(stats.engagementRate * 100).toStringAsFixed(1)}%',
|
||||
stats.isHighEngagement ? 'Élevé' : 'Modéré'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un point d'analyse
|
||||
pw.Widget _buildAnalysisPoint(String metric, String value, String assessment) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 5),
|
||||
child: pw.Row(
|
||||
children: [
|
||||
pw.Expanded(flex: 2, child: pw.Text(metric, style: const pw.TextStyle(fontSize: 11))),
|
||||
pw.Expanded(flex: 1, child: pw.Text(value, style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold))),
|
||||
pw.Expanded(flex: 1, child: pw.Text(assessment, style: const pw.TextStyle(fontSize: 11))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la section d'analytics
|
||||
pw.Widget _buildAnalyticsSection(DashboardStatsModel stats) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Tendances et Projections', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 15),
|
||||
pw.Text('Basé sur les données actuelles, voici les principales tendances observées:', style: const pw.TextStyle(fontSize: 11)),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Bullet(text: 'Évolution du nombre de membres: ${stats.hasGrowth ? 'Croissance' : 'Déclin'} de ${stats.monthlyGrowth.abs().toStringAsFixed(1)}% ce mois'),
|
||||
pw.Bullet(text: 'Participation aux événements: ${stats.upcomingEvents} événements programmés'),
|
||||
pw.Bullet(text: 'Volume des contributions: ${stats.formattedContributionAmount} XOF collectés'),
|
||||
pw.Bullet(text: 'Demandes en attente: ${stats.pendingRequests} nécessitent un traitement'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Génère un nom de fichier unique
|
||||
String _generateFileName(String baseName) {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final cleanName = baseName.replaceAll(RegExp(r'[^\w\s-]'), '').replaceAll(' ', '_');
|
||||
return '${cleanName}_$timestamp.pdf';
|
||||
}
|
||||
|
||||
/// Sauvegarde le PDF et retourne le chemin
|
||||
Future<String> _savePdf(pw.Document pdf, String fileName) async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final reportsDir = Directory('${directory.path}/$_reportsFolder');
|
||||
|
||||
if (!await reportsDir.exists()) {
|
||||
await reportsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final file = File('${reportsDir.path}/$fileName');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
/// Partage un rapport PDF
|
||||
Future<void> shareReport(String filePath, {String? subject}) async {
|
||||
await Share.shareXFiles(
|
||||
[XFile(filePath)],
|
||||
subject: subject ?? 'Rapport Dashboard UnionFlow',
|
||||
text: 'Rapport généré par l\'application UnionFlow',
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la liste des rapports sauvegardés
|
||||
Future<List<File>> getSavedReports() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final reportsDir = Directory('${directory.path}/$_reportsFolder');
|
||||
|
||||
if (!await reportsDir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = await reportsDir.list().where((entity) =>
|
||||
entity is File && entity.path.endsWith('.pdf')).cast<File>().toList();
|
||||
|
||||
// Trier par date de modification (plus récent en premier)
|
||||
files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/// Supprime un rapport
|
||||
Future<void> deleteReport(String filePath) async {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime tous les rapports anciens (plus de 30 jours)
|
||||
Future<void> cleanupOldReports() async {
|
||||
final reports = await getSavedReports();
|
||||
final cutoffDate = DateTime.now().subtract(const Duration(days: 30));
|
||||
|
||||
for (final report in reports) {
|
||||
final lastModified = await report.lastModified();
|
||||
if (lastModified.isBefore(cutoffDate)) {
|
||||
await report.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../../config/dashboard_config.dart';
|
||||
|
||||
/// Service de notifications temps réel pour le Dashboard
|
||||
class DashboardNotificationService {
|
||||
static const String _wsEndpoint = 'ws://localhost:8080/ws/dashboard';
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _subscription;
|
||||
Timer? _reconnectTimer;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
bool _isConnected = false;
|
||||
bool _shouldReconnect = true;
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
static const Duration _reconnectDelay = Duration(seconds: 5);
|
||||
static const Duration _heartbeatInterval = Duration(seconds: 30);
|
||||
|
||||
// Streams pour les différents types de notifications
|
||||
final StreamController<DashboardStatsModel> _statsController =
|
||||
StreamController<DashboardStatsModel>.broadcast();
|
||||
final StreamController<RecentActivityModel> _activityController =
|
||||
StreamController<RecentActivityModel>.broadcast();
|
||||
final StreamController<UpcomingEventModel> _eventController =
|
||||
StreamController<UpcomingEventModel>.broadcast();
|
||||
final StreamController<DashboardNotification> _notificationController =
|
||||
StreamController<DashboardNotification>.broadcast();
|
||||
final StreamController<ConnectionStatus> _connectionController =
|
||||
StreamController<ConnectionStatus>.broadcast();
|
||||
|
||||
// Getters pour les streams
|
||||
Stream<DashboardStatsModel> get statsStream => _statsController.stream;
|
||||
Stream<RecentActivityModel> get activityStream => _activityController.stream;
|
||||
Stream<UpcomingEventModel> get eventStream => _eventController.stream;
|
||||
Stream<DashboardNotification> get notificationStream => _notificationController.stream;
|
||||
Stream<ConnectionStatus> get connectionStream => _connectionController.stream;
|
||||
|
||||
/// Initialise le service de notifications
|
||||
Future<void> initialize(String organizationId, String userId) async {
|
||||
if (!DashboardConfig.enableNotifications) {
|
||||
debugPrint('📱 Notifications désactivées dans la configuration');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('📱 Initialisation du service de notifications...');
|
||||
await _connect(organizationId, userId);
|
||||
}
|
||||
|
||||
/// Établit la connexion WebSocket
|
||||
Future<void> _connect(String organizationId, String userId) async {
|
||||
if (_isConnected) return;
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('$_wsEndpoint?orgId=$organizationId&userId=$userId');
|
||||
_channel = WebSocketChannel.connect(uri);
|
||||
|
||||
debugPrint('📱 Connexion WebSocket en cours...');
|
||||
_connectionController.add(ConnectionStatus.connecting);
|
||||
|
||||
// Écouter les messages
|
||||
_subscription = _channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDisconnection,
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
_reconnectAttempts = 0;
|
||||
_connectionController.add(ConnectionStatus.connected);
|
||||
|
||||
// Démarrer le heartbeat
|
||||
_startHeartbeat();
|
||||
|
||||
debugPrint('✅ Connexion WebSocket établie');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur de connexion WebSocket: $e');
|
||||
_connectionController.add(ConnectionStatus.error);
|
||||
_scheduleReconnect(organizationId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les messages reçus
|
||||
void _handleMessage(dynamic message) {
|
||||
try {
|
||||
final data = jsonDecode(message as String);
|
||||
final type = data['type'] as String?;
|
||||
final payload = data['payload'];
|
||||
|
||||
debugPrint('📨 Message reçu: $type');
|
||||
|
||||
switch (type) {
|
||||
case 'stats_update':
|
||||
final stats = DashboardStatsModel.fromJson(payload);
|
||||
_statsController.add(stats);
|
||||
break;
|
||||
|
||||
case 'new_activity':
|
||||
final activity = RecentActivityModel.fromJson(payload);
|
||||
_activityController.add(activity);
|
||||
break;
|
||||
|
||||
case 'event_update':
|
||||
final event = UpcomingEventModel.fromJson(payload);
|
||||
_eventController.add(event);
|
||||
break;
|
||||
|
||||
case 'notification':
|
||||
final notification = DashboardNotification.fromJson(payload);
|
||||
_notificationController.add(notification);
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// Réponse au heartbeat
|
||||
debugPrint('💓 Heartbeat reçu');
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint('⚠️ Type de message inconnu: $type');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur de parsing du message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs de connexion
|
||||
void _handleError(error) {
|
||||
debugPrint('❌ Erreur WebSocket: $error');
|
||||
_isConnected = false;
|
||||
_connectionController.add(ConnectionStatus.error);
|
||||
}
|
||||
|
||||
/// Gère la déconnexion
|
||||
void _handleDisconnection() {
|
||||
debugPrint('🔌 Connexion WebSocket fermée');
|
||||
_isConnected = false;
|
||||
_connectionController.add(ConnectionStatus.disconnected);
|
||||
|
||||
if (_shouldReconnect) {
|
||||
// Programmer une reconnexion
|
||||
_scheduleReconnect('', ''); // Les IDs seront récupérés du contexte
|
||||
}
|
||||
}
|
||||
|
||||
/// Programme une tentative de reconnexion
|
||||
void _scheduleReconnect(String organizationId, String userId) {
|
||||
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
||||
debugPrint('❌ Nombre maximum de tentatives de reconnexion atteint');
|
||||
_connectionController.add(ConnectionStatus.failed);
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnectAttempts++;
|
||||
final delay = _reconnectDelay * _reconnectAttempts;
|
||||
|
||||
debugPrint('🔄 Reconnexion programmée dans ${delay.inSeconds}s (tentative $_reconnectAttempts)');
|
||||
|
||||
_reconnectTimer = Timer(delay, () {
|
||||
if (_shouldReconnect) {
|
||||
_connect(organizationId, userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Démarre le heartbeat
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer = Timer.periodic(_heartbeatInterval, (timer) {
|
||||
if (_isConnected && _channel != null) {
|
||||
try {
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'ping',
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
}));
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'envoi du heartbeat: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Envoie une demande de rafraîchissement
|
||||
void requestRefresh(String organizationId, String userId) {
|
||||
if (_isConnected && _channel != null) {
|
||||
try {
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'refresh_request',
|
||||
'payload': {
|
||||
'organizationId': organizationId,
|
||||
'userId': userId,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
},
|
||||
}));
|
||||
debugPrint('📤 Demande de rafraîchissement envoyée');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'envoi de la demande: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// S'abonne aux notifications pour un type spécifique
|
||||
void subscribeToNotifications(List<String> notificationTypes) {
|
||||
if (_isConnected && _channel != null) {
|
||||
try {
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'subscribe',
|
||||
'payload': {
|
||||
'notificationTypes': notificationTypes,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
},
|
||||
}));
|
||||
debugPrint('📋 Abonnement aux notifications: $notificationTypes');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'abonnement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Se désabonne des notifications
|
||||
void unsubscribeFromNotifications(List<String> notificationTypes) {
|
||||
if (_isConnected && _channel != null) {
|
||||
try {
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'unsubscribe',
|
||||
'payload': {
|
||||
'notificationTypes': notificationTypes,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
},
|
||||
}));
|
||||
debugPrint('📋 Désabonnement des notifications: $notificationTypes');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du désabonnement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le statut de la connexion
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
/// Obtient le nombre de tentatives de reconnexion
|
||||
int get reconnectAttempts => _reconnectAttempts;
|
||||
|
||||
/// Force une reconnexion
|
||||
Future<void> reconnect(String organizationId, String userId) async {
|
||||
await disconnect();
|
||||
_reconnectAttempts = 0;
|
||||
await _connect(organizationId, userId);
|
||||
}
|
||||
|
||||
/// Déconnecte le service
|
||||
Future<void> disconnect() async {
|
||||
_shouldReconnect = false;
|
||||
|
||||
_reconnectTimer?.cancel();
|
||||
_heartbeatTimer?.cancel();
|
||||
|
||||
if (_channel != null) {
|
||||
await _channel!.sink.close();
|
||||
_channel = null;
|
||||
}
|
||||
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
_isConnected = false;
|
||||
_connectionController.add(ConnectionStatus.disconnected);
|
||||
|
||||
debugPrint('🔌 Service de notifications déconnecté');
|
||||
}
|
||||
|
||||
/// Libère les ressources
|
||||
void dispose() {
|
||||
disconnect();
|
||||
|
||||
_statsController.close();
|
||||
_activityController.close();
|
||||
_eventController.close();
|
||||
_notificationController.close();
|
||||
_connectionController.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Statut de la connexion
|
||||
enum ConnectionStatus {
|
||||
disconnected,
|
||||
connecting,
|
||||
connected,
|
||||
error,
|
||||
failed,
|
||||
}
|
||||
|
||||
/// Notification du dashboard
|
||||
class DashboardNotification {
|
||||
final String id;
|
||||
final String type;
|
||||
final String title;
|
||||
final String message;
|
||||
final NotificationPriority priority;
|
||||
final DateTime timestamp;
|
||||
final Map<String, dynamic>? data;
|
||||
final String? actionUrl;
|
||||
final bool isRead;
|
||||
|
||||
const DashboardNotification({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.priority,
|
||||
required this.timestamp,
|
||||
this.data,
|
||||
this.actionUrl,
|
||||
this.isRead = false,
|
||||
});
|
||||
|
||||
factory DashboardNotification.fromJson(Map<String, dynamic> json) {
|
||||
return DashboardNotification(
|
||||
id: json['id'] as String,
|
||||
type: json['type'] as String,
|
||||
title: json['title'] as String,
|
||||
message: json['message'] as String,
|
||||
priority: NotificationPriority.values.firstWhere(
|
||||
(p) => p.name == json['priority'],
|
||||
orElse: () => NotificationPriority.normal,
|
||||
),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
data: json['data'] as Map<String, dynamic>?,
|
||||
actionUrl: json['actionUrl'] as String?,
|
||||
isRead: json['isRead'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'priority': priority.name,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'data': data,
|
||||
'actionUrl': actionUrl,
|
||||
'isRead': isRead,
|
||||
};
|
||||
}
|
||||
|
||||
/// Obtient l'icône pour le type de notification
|
||||
String get icon {
|
||||
switch (type) {
|
||||
case 'new_member':
|
||||
return '👤';
|
||||
case 'new_event':
|
||||
return '📅';
|
||||
case 'contribution':
|
||||
return '💰';
|
||||
case 'urgent':
|
||||
return '🚨';
|
||||
case 'system':
|
||||
return '⚙️';
|
||||
default:
|
||||
return '📢';
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la couleur pour la priorité
|
||||
String get priorityColor {
|
||||
switch (priority) {
|
||||
case NotificationPriority.low:
|
||||
return '#6B7280';
|
||||
case NotificationPriority.normal:
|
||||
return '#3B82F6';
|
||||
case NotificationPriority.high:
|
||||
return '#F59E0B';
|
||||
case NotificationPriority.urgent:
|
||||
return '#EF4444';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Priorité des notifications
|
||||
enum NotificationPriority {
|
||||
low,
|
||||
normal,
|
||||
high,
|
||||
urgent,
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../cache/dashboard_cache_manager.dart';
|
||||
|
||||
/// Service de mode hors ligne avec synchronisation pour le Dashboard
|
||||
class DashboardOfflineService {
|
||||
static const String _offlineQueueKey = 'dashboard_offline_queue';
|
||||
static const String _lastSyncKey = 'dashboard_last_sync';
|
||||
static const String _offlineModeKey = 'dashboard_offline_mode';
|
||||
|
||||
final DashboardCacheManager _cacheManager;
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||
Timer? _syncTimer;
|
||||
|
||||
final StreamController<OfflineStatus> _statusController =
|
||||
StreamController<OfflineStatus>.broadcast();
|
||||
final StreamController<SyncProgress> _syncController =
|
||||
StreamController<SyncProgress>.broadcast();
|
||||
|
||||
final List<OfflineAction> _pendingActions = [];
|
||||
bool _isOnline = true;
|
||||
bool _isSyncing = false;
|
||||
DateTime? _lastSyncTime;
|
||||
|
||||
// Streams publics
|
||||
Stream<OfflineStatus> get statusStream => _statusController.stream;
|
||||
Stream<SyncProgress> get syncStream => _syncController.stream;
|
||||
|
||||
DashboardOfflineService(this._cacheManager);
|
||||
|
||||
/// Initialise le service hors ligne
|
||||
Future<void> initialize() async {
|
||||
debugPrint('📱 Initialisation du service hors ligne...');
|
||||
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Charger les actions en attente
|
||||
await _loadPendingActions();
|
||||
|
||||
// Charger la dernière synchronisation
|
||||
_loadLastSyncTime();
|
||||
|
||||
// Vérifier la connectivité initiale
|
||||
final connectivityResult = await _connectivity.checkConnectivity();
|
||||
_updateConnectivityStatus(connectivityResult);
|
||||
|
||||
// Écouter les changements de connectivité
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
|
||||
(List<ConnectivityResult> results) => _updateConnectivityStatus(results),
|
||||
);
|
||||
|
||||
// Démarrer la synchronisation automatique
|
||||
_startAutoSync();
|
||||
|
||||
debugPrint('✅ Service hors ligne initialisé');
|
||||
}
|
||||
|
||||
/// Met à jour le statut de connectivité
|
||||
void _updateConnectivityStatus(dynamic result) {
|
||||
final wasOnline = _isOnline;
|
||||
if (result is List<ConnectivityResult>) {
|
||||
_isOnline = result.any((r) => r != ConnectivityResult.none);
|
||||
} else if (result is ConnectivityResult) {
|
||||
_isOnline = result != ConnectivityResult.none;
|
||||
} else {
|
||||
_isOnline = false;
|
||||
}
|
||||
|
||||
debugPrint('🌐 Connectivité: ${_isOnline ? 'En ligne' : 'Hors ligne'}');
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
// Si on revient en ligne, synchroniser
|
||||
if (!wasOnline && _isOnline && _pendingActions.isNotEmpty) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
}
|
||||
|
||||
/// Démarre la synchronisation automatique
|
||||
void _startAutoSync() {
|
||||
_syncTimer = Timer.periodic(
|
||||
const Duration(minutes: 5),
|
||||
(_) {
|
||||
if (_isOnline && _pendingActions.isNotEmpty) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Ajoute une action à la queue hors ligne
|
||||
Future<void> queueAction(OfflineAction action) async {
|
||||
_pendingActions.add(action);
|
||||
await _savePendingActions();
|
||||
|
||||
debugPrint('📝 Action mise en queue: ${action.type} (${_pendingActions.length} en attente)');
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
// Si en ligne, essayer de synchroniser immédiatement
|
||||
if (_isOnline) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronise les actions en attente
|
||||
Future<void> _syncPendingActions() async {
|
||||
if (_isSyncing || _pendingActions.isEmpty || !_isOnline) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isSyncing = true;
|
||||
debugPrint('🔄 Début de la synchronisation (${_pendingActions.length} actions)');
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: true,
|
||||
totalActions: _pendingActions.length,
|
||||
completedActions: 0,
|
||||
currentAction: _pendingActions.first.type.toString(),
|
||||
));
|
||||
|
||||
final actionsToSync = List<OfflineAction>.from(_pendingActions);
|
||||
int completedCount = 0;
|
||||
|
||||
for (final action in actionsToSync) {
|
||||
try {
|
||||
await _executeAction(action);
|
||||
_pendingActions.remove(action);
|
||||
completedCount++;
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: true,
|
||||
totalActions: actionsToSync.length,
|
||||
completedActions: completedCount,
|
||||
currentAction: completedCount < actionsToSync.length
|
||||
? actionsToSync[completedCount].type.toString()
|
||||
: null,
|
||||
));
|
||||
|
||||
debugPrint('✅ Action synchronisée: ${action.type}');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la synchronisation de ${action.type}: $e');
|
||||
|
||||
// Marquer l'action comme échouée si trop de tentatives
|
||||
action.retryCount++;
|
||||
if (action.retryCount >= 3) {
|
||||
_pendingActions.remove(action);
|
||||
debugPrint('🗑️ Action abandonnée après 3 tentatives: ${action.type}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _savePendingActions();
|
||||
_lastSyncTime = DateTime.now();
|
||||
await _saveLastSyncTime();
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: false,
|
||||
totalActions: actionsToSync.length,
|
||||
completedActions: completedCount,
|
||||
currentAction: null,
|
||||
));
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
_isSyncing = false;
|
||||
debugPrint('✅ Synchronisation terminée ($completedCount/${actionsToSync.length} réussies)');
|
||||
}
|
||||
|
||||
/// Exécute une action spécifique
|
||||
Future<void> _executeAction(OfflineAction action) async {
|
||||
switch (action.type) {
|
||||
case OfflineActionType.refreshDashboard:
|
||||
await _syncDashboardData(action);
|
||||
break;
|
||||
case OfflineActionType.updatePreferences:
|
||||
await _syncUserPreferences(action);
|
||||
break;
|
||||
case OfflineActionType.markActivityRead:
|
||||
await _syncActivityRead(action);
|
||||
break;
|
||||
case OfflineActionType.joinEvent:
|
||||
await _syncEventJoin(action);
|
||||
break;
|
||||
case OfflineActionType.exportReport:
|
||||
await _syncReportExport(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronise les données du dashboard
|
||||
Future<void> _syncDashboardData(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation des données
|
||||
await Future.delayed(const Duration(milliseconds: 500)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise les préférences utilisateur
|
||||
Future<void> _syncUserPreferences(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation des préférences
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise le marquage d'activité comme lue
|
||||
Future<void> _syncActivityRead(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation du marquage
|
||||
await Future.delayed(const Duration(milliseconds: 200)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise l'inscription à un événement
|
||||
Future<void> _syncEventJoin(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation d'inscription
|
||||
await Future.delayed(const Duration(milliseconds: 400)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise l'export de rapport
|
||||
Future<void> _syncReportExport(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation d'export
|
||||
await Future.delayed(const Duration(milliseconds: 800)); // Simulation
|
||||
}
|
||||
|
||||
/// Sauvegarde les actions en attente
|
||||
Future<void> _savePendingActions() async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final actionsJson = _pendingActions
|
||||
.map((action) => action.toJson())
|
||||
.toList();
|
||||
|
||||
await _prefs!.setString(_offlineQueueKey, jsonEncode(actionsJson));
|
||||
}
|
||||
|
||||
/// Charge les actions en attente
|
||||
Future<void> _loadPendingActions() async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final actionsJsonString = _prefs!.getString(_offlineQueueKey);
|
||||
if (actionsJsonString != null) {
|
||||
try {
|
||||
final actionsJson = jsonDecode(actionsJsonString) as List;
|
||||
_pendingActions.clear();
|
||||
_pendingActions.addAll(
|
||||
actionsJson.map((json) => OfflineAction.fromJson(json)),
|
||||
);
|
||||
|
||||
debugPrint('📋 ${_pendingActions.length} actions chargées depuis le cache');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du chargement des actions: $e');
|
||||
await _prefs!.remove(_offlineQueueKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde l'heure de dernière synchronisation
|
||||
Future<void> _saveLastSyncTime() async {
|
||||
if (_prefs == null || _lastSyncTime == null) return;
|
||||
|
||||
await _prefs!.setInt(_lastSyncKey, _lastSyncTime!.millisecondsSinceEpoch);
|
||||
}
|
||||
|
||||
/// Charge l'heure de dernière synchronisation
|
||||
void _loadLastSyncTime() {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final lastSyncMs = _prefs!.getInt(_lastSyncKey);
|
||||
if (lastSyncMs != null) {
|
||||
_lastSyncTime = DateTime.fromMillisecondsSinceEpoch(lastSyncMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Force une synchronisation manuelle
|
||||
Future<void> forcSync() async {
|
||||
if (!_isOnline) {
|
||||
throw Exception('Impossible de synchroniser hors ligne');
|
||||
}
|
||||
|
||||
await _syncPendingActions();
|
||||
}
|
||||
|
||||
/// Obtient les données en mode hors ligne
|
||||
Future<DashboardDataModel?> getOfflineData(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
return await _cacheManager.getCachedDashboardData(organizationId, userId);
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles hors ligne
|
||||
Future<bool> hasOfflineData(String organizationId, String userId) async {
|
||||
final data = await getOfflineData(organizationId, userId);
|
||||
return data != null;
|
||||
}
|
||||
|
||||
/// Obtient les statistiques du mode hors ligne
|
||||
OfflineStats getStats() {
|
||||
return OfflineStats(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
isSyncing: _isSyncing,
|
||||
cacheStats: _cacheManager.getCacheStats(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Nettoie les anciennes actions
|
||||
Future<void> cleanupOldActions() async {
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(days: 7));
|
||||
|
||||
_pendingActions.removeWhere((action) =>
|
||||
action.timestamp.isBefore(cutoffTime));
|
||||
|
||||
await _savePendingActions();
|
||||
}
|
||||
|
||||
/// Libère les ressources
|
||||
void dispose() {
|
||||
_connectivitySubscription?.cancel();
|
||||
_syncTimer?.cancel();
|
||||
_statusController.close();
|
||||
_syncController.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Action hors ligne
|
||||
class OfflineAction {
|
||||
final String id;
|
||||
final OfflineActionType type;
|
||||
final Map<String, dynamic> data;
|
||||
final DateTime timestamp;
|
||||
int retryCount;
|
||||
|
||||
OfflineAction({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.timestamp,
|
||||
this.retryCount = 0,
|
||||
});
|
||||
|
||||
factory OfflineAction.fromJson(Map<String, dynamic> json) {
|
||||
return OfflineAction(
|
||||
id: json['id'] as String,
|
||||
type: OfflineActionType.values.firstWhere(
|
||||
(t) => t.name == json['type'],
|
||||
),
|
||||
data: json['data'] as Map<String, dynamic>,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.name,
|
||||
'data': data,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'retryCount': retryCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Types d'actions hors ligne
|
||||
enum OfflineActionType {
|
||||
refreshDashboard,
|
||||
updatePreferences,
|
||||
markActivityRead,
|
||||
joinEvent,
|
||||
exportReport,
|
||||
}
|
||||
|
||||
/// Statut hors ligne
|
||||
class OfflineStatus {
|
||||
final bool isOnline;
|
||||
final int pendingActionsCount;
|
||||
final DateTime? lastSyncTime;
|
||||
|
||||
const OfflineStatus({
|
||||
required this.isOnline,
|
||||
required this.pendingActionsCount,
|
||||
this.lastSyncTime,
|
||||
});
|
||||
|
||||
String get statusText {
|
||||
if (isOnline) {
|
||||
if (pendingActionsCount > 0) {
|
||||
return 'En ligne - $pendingActionsCount actions en attente';
|
||||
} else {
|
||||
return 'En ligne - Synchronisé';
|
||||
}
|
||||
} else {
|
||||
return 'Hors ligne - Mode cache activé';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Progression de synchronisation
|
||||
class SyncProgress {
|
||||
final bool isActive;
|
||||
final int totalActions;
|
||||
final int completedActions;
|
||||
final String? currentAction;
|
||||
|
||||
const SyncProgress({
|
||||
required this.isActive,
|
||||
required this.totalActions,
|
||||
required this.completedActions,
|
||||
this.currentAction,
|
||||
});
|
||||
|
||||
double get progress {
|
||||
if (totalActions == 0) return 1.0;
|
||||
return completedActions / totalActions;
|
||||
}
|
||||
|
||||
String get progressText {
|
||||
if (!isActive) return 'Synchronisation terminée';
|
||||
if (currentAction != null) {
|
||||
return 'Synchronisation: $currentAction ($completedActions/$totalActions)';
|
||||
}
|
||||
return 'Synchronisation en cours... ($completedActions/$totalActions)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques du mode hors ligne
|
||||
class OfflineStats {
|
||||
final bool isOnline;
|
||||
final int pendingActionsCount;
|
||||
final DateTime? lastSyncTime;
|
||||
final bool isSyncing;
|
||||
final Map<String, dynamic> cacheStats;
|
||||
|
||||
const OfflineStats({
|
||||
required this.isOnline,
|
||||
required this.pendingActionsCount,
|
||||
this.lastSyncTime,
|
||||
required this.isSyncing,
|
||||
required this.cacheStats,
|
||||
});
|
||||
|
||||
String get lastSyncText {
|
||||
if (lastSyncTime == null) return 'Jamais synchronisé';
|
||||
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSyncTime!);
|
||||
|
||||
if (diff.inMinutes < 1) return 'Synchronisé à l\'instant';
|
||||
if (diff.inMinutes < 60) return 'Synchronisé il y a ${diff.inMinutes}min';
|
||||
if (diff.inHours < 24) return 'Synchronisé il y a ${diff.inHours}h';
|
||||
return 'Synchronisé il y a ${diff.inDays}j';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../config/dashboard_config.dart';
|
||||
|
||||
/// Moniteur de performances avancé pour le Dashboard
|
||||
class DashboardPerformanceMonitor {
|
||||
static const String _channelName = 'dashboard_performance';
|
||||
static const MethodChannel _channel = MethodChannel(_channelName);
|
||||
|
||||
Timer? _monitoringTimer;
|
||||
Timer? _reportTimer;
|
||||
final List<PerformanceSnapshot> _snapshots = [];
|
||||
final StreamController<PerformanceMetrics> _metricsController =
|
||||
StreamController<PerformanceMetrics>.broadcast();
|
||||
final StreamController<PerformanceAlert> _alertController =
|
||||
StreamController<PerformanceAlert>.broadcast();
|
||||
|
||||
bool _isMonitoring = false;
|
||||
DateTime _startTime = DateTime.now();
|
||||
|
||||
// Seuils d'alerte configurables
|
||||
final double _memoryThreshold = DashboardConfig.getAlertThreshold('memoryUsage');
|
||||
final double _cpuThreshold = DashboardConfig.getAlertThreshold('cpuUsage');
|
||||
final int _networkLatencyThreshold = DashboardConfig.getAlertThreshold('networkLatency').toInt();
|
||||
final double _frameRateThreshold = DashboardConfig.getAlertThreshold('frameRate');
|
||||
|
||||
// Streams publics
|
||||
Stream<PerformanceMetrics> get metricsStream => _metricsController.stream;
|
||||
Stream<PerformanceAlert> get alertStream => _alertController.stream;
|
||||
|
||||
/// Démarre le monitoring des performances
|
||||
Future<void> startMonitoring() async {
|
||||
if (_isMonitoring) return;
|
||||
|
||||
debugPrint('🔍 Démarrage du monitoring des performances...');
|
||||
|
||||
_isMonitoring = true;
|
||||
_startTime = DateTime.now();
|
||||
|
||||
// Timer pour collecter les métriques
|
||||
_monitoringTimer = Timer.periodic(
|
||||
DashboardConfig.performanceCheckInterval,
|
||||
(_) => _collectMetrics(),
|
||||
);
|
||||
|
||||
// Timer pour générer les rapports
|
||||
_reportTimer = Timer.periodic(
|
||||
const Duration(minutes: 5),
|
||||
(_) => _generateReport(),
|
||||
);
|
||||
|
||||
// Collecte initiale
|
||||
await _collectMetrics();
|
||||
|
||||
debugPrint('✅ Monitoring des performances démarré');
|
||||
}
|
||||
|
||||
/// Arrête le monitoring
|
||||
void stopMonitoring() {
|
||||
if (!_isMonitoring) return;
|
||||
|
||||
_isMonitoring = false;
|
||||
_monitoringTimer?.cancel();
|
||||
_reportTimer?.cancel();
|
||||
|
||||
debugPrint('🛑 Monitoring des performances arrêté');
|
||||
}
|
||||
|
||||
/// Collecte les métriques de performance
|
||||
Future<void> _collectMetrics() async {
|
||||
try {
|
||||
final metrics = await _gatherMetrics();
|
||||
final snapshot = PerformanceSnapshot(
|
||||
timestamp: DateTime.now(),
|
||||
metrics: metrics,
|
||||
);
|
||||
|
||||
_snapshots.add(snapshot);
|
||||
|
||||
// Garder seulement les 1000 derniers snapshots
|
||||
if (_snapshots.length > 1000) {
|
||||
_snapshots.removeAt(0);
|
||||
}
|
||||
|
||||
// Émettre les métriques
|
||||
_metricsController.add(metrics);
|
||||
|
||||
// Vérifier les seuils d'alerte
|
||||
_checkAlerts(metrics);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la collecte des métriques: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Rassemble toutes les métriques
|
||||
Future<PerformanceMetrics> _gatherMetrics() async {
|
||||
final memoryUsage = await _getMemoryUsage();
|
||||
final cpuUsage = await _getCpuUsage();
|
||||
final networkLatency = await _getNetworkLatency();
|
||||
final frameRate = await _getFrameRate();
|
||||
final batteryLevel = await _getBatteryLevel();
|
||||
final diskUsage = await _getDiskUsage();
|
||||
final networkUsage = await _getNetworkUsage();
|
||||
|
||||
return PerformanceMetrics(
|
||||
timestamp: DateTime.now(),
|
||||
memoryUsage: memoryUsage,
|
||||
cpuUsage: cpuUsage,
|
||||
networkLatency: networkLatency,
|
||||
frameRate: frameRate,
|
||||
batteryLevel: batteryLevel,
|
||||
diskUsage: diskUsage,
|
||||
networkUsage: networkUsage,
|
||||
uptime: DateTime.now().difference(_startTime),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient l'utilisation mémoire
|
||||
Future<double> _getMemoryUsage() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getMemoryUsage');
|
||||
return (result as num).toDouble();
|
||||
} else {
|
||||
// Simulation pour les autres plateformes
|
||||
return _simulateMemoryUsage();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateMemoryUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient l'utilisation CPU
|
||||
Future<double> _getCpuUsage() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getCpuUsage');
|
||||
return (result as num).toDouble();
|
||||
} else {
|
||||
return _simulateCpuUsage();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateCpuUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la latence réseau
|
||||
Future<int> _getNetworkLatency() async {
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// Ping vers le serveur de l'API
|
||||
final socket = await Socket.connect('localhost', 8080)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
|
||||
stopwatch.stop();
|
||||
await socket.close();
|
||||
|
||||
return stopwatch.elapsedMilliseconds;
|
||||
} catch (e) {
|
||||
return _simulateNetworkLatency();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le frame rate
|
||||
Future<double> _getFrameRate() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getFrameRate');
|
||||
return (result as num).toDouble();
|
||||
} else {
|
||||
return _simulateFrameRate();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateFrameRate();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le niveau de batterie
|
||||
Future<double> _getBatteryLevel() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getBatteryLevel');
|
||||
return (result as num).toDouble();
|
||||
} else {
|
||||
return _simulateBatteryLevel();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateBatteryLevel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient l'utilisation disque
|
||||
Future<double> _getDiskUsage() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getDiskUsage');
|
||||
return (result as num).toDouble();
|
||||
} else {
|
||||
return _simulateDiskUsage();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateDiskUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient l'utilisation réseau
|
||||
Future<NetworkUsage> _getNetworkUsage() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final result = await _channel.invokeMethod('getNetworkUsage');
|
||||
return NetworkUsage(
|
||||
bytesReceived: (result['bytesReceived'] as num).toDouble(),
|
||||
bytesSent: (result['bytesSent'] as num).toDouble(),
|
||||
);
|
||||
} else {
|
||||
return _simulateNetworkUsage();
|
||||
}
|
||||
} catch (e) {
|
||||
return _simulateNetworkUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie les seuils d'alerte
|
||||
void _checkAlerts(PerformanceMetrics metrics) {
|
||||
// Alerte mémoire
|
||||
if (metrics.memoryUsage > _memoryThreshold) {
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.memory,
|
||||
severity: AlertSeverity.warning,
|
||||
message: 'Utilisation mémoire élevée: ${metrics.memoryUsage.toStringAsFixed(1)}MB',
|
||||
value: metrics.memoryUsage,
|
||||
threshold: _memoryThreshold,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
// Alerte CPU
|
||||
if (metrics.cpuUsage > _cpuThreshold) {
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.cpu,
|
||||
severity: AlertSeverity.warning,
|
||||
message: 'Utilisation CPU élevée: ${metrics.cpuUsage.toStringAsFixed(1)}%',
|
||||
value: metrics.cpuUsage,
|
||||
threshold: _cpuThreshold,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
// Alerte latence réseau
|
||||
if (metrics.networkLatency > _networkLatencyThreshold) {
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.network,
|
||||
severity: AlertSeverity.error,
|
||||
message: 'Latence réseau élevée: ${metrics.networkLatency}ms',
|
||||
value: metrics.networkLatency.toDouble(),
|
||||
threshold: _networkLatencyThreshold.toDouble(),
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
// Alerte frame rate
|
||||
if (metrics.frameRate < _frameRateThreshold) {
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.performance,
|
||||
severity: AlertSeverity.warning,
|
||||
message: 'Frame rate faible: ${metrics.frameRate.toStringAsFixed(1)}fps',
|
||||
value: metrics.frameRate,
|
||||
threshold: _frameRateThreshold,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère un rapport de performance
|
||||
void _generateReport() {
|
||||
if (_snapshots.isEmpty) return;
|
||||
|
||||
final recentSnapshots = _snapshots.where((snapshot) =>
|
||||
DateTime.now().difference(snapshot.timestamp).inMinutes <= 5).toList();
|
||||
|
||||
if (recentSnapshots.isEmpty) return;
|
||||
|
||||
final report = PerformanceReport.fromSnapshots(recentSnapshots);
|
||||
|
||||
debugPrint('📊 RAPPORT DE PERFORMANCE (5 min)');
|
||||
debugPrint('Mémoire: ${report.averageMemoryUsage.toStringAsFixed(1)}MB (max: ${report.maxMemoryUsage.toStringAsFixed(1)}MB)');
|
||||
debugPrint('CPU: ${report.averageCpuUsage.toStringAsFixed(1)}% (max: ${report.maxCpuUsage.toStringAsFixed(1)}%)');
|
||||
debugPrint('Latence: ${report.averageNetworkLatency.toStringAsFixed(0)}ms (max: ${report.maxNetworkLatency.toStringAsFixed(0)}ms)');
|
||||
debugPrint('FPS: ${report.averageFrameRate.toStringAsFixed(1)}fps (min: ${report.minFrameRate.toStringAsFixed(1)}fps)');
|
||||
}
|
||||
|
||||
/// Obtient les statistiques de performance
|
||||
PerformanceStats getStats() {
|
||||
if (_snapshots.isEmpty) {
|
||||
return PerformanceStats.empty();
|
||||
}
|
||||
|
||||
return PerformanceStats.fromSnapshots(_snapshots);
|
||||
}
|
||||
|
||||
/// Méthodes de simulation pour le développement
|
||||
double _simulateMemoryUsage() {
|
||||
const base = 200.0;
|
||||
final variation = 100.0 * (DateTime.now().millisecond / 1000.0);
|
||||
return base + variation;
|
||||
}
|
||||
|
||||
double _simulateCpuUsage() {
|
||||
const base = 30.0;
|
||||
final variation = 40.0 * (DateTime.now().second / 60.0);
|
||||
return (base + variation).clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
int _simulateNetworkLatency() {
|
||||
const base = 150;
|
||||
final variation = (200 * (DateTime.now().millisecond / 1000.0)).round();
|
||||
return base + variation;
|
||||
}
|
||||
|
||||
double _simulateFrameRate() {
|
||||
const base = 58.0;
|
||||
final variation = 5.0 * (DateTime.now().millisecond / 1000.0);
|
||||
return (base + variation).clamp(30.0, 60.0);
|
||||
}
|
||||
|
||||
double _simulateBatteryLevel() {
|
||||
final elapsed = DateTime.now().difference(_startTime).inMinutes;
|
||||
return (100.0 - elapsed * 0.1).clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
double _simulateDiskUsage() {
|
||||
return 45.0 + (10.0 * (DateTime.now().millisecond / 1000.0));
|
||||
}
|
||||
|
||||
NetworkUsage _simulateNetworkUsage() {
|
||||
const base = 1024.0;
|
||||
final variation = 512.0 * (DateTime.now().millisecond / 1000.0);
|
||||
return NetworkUsage(
|
||||
bytesReceived: base + variation,
|
||||
bytesSent: (base + variation) * 0.3,
|
||||
);
|
||||
}
|
||||
|
||||
/// Libère les ressources
|
||||
void dispose() {
|
||||
stopMonitoring();
|
||||
_metricsController.close();
|
||||
_alertController.close();
|
||||
_snapshots.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Métriques de performance
|
||||
class PerformanceMetrics {
|
||||
final DateTime timestamp;
|
||||
final double memoryUsage; // MB
|
||||
final double cpuUsage; // %
|
||||
final int networkLatency; // ms
|
||||
final double frameRate; // fps
|
||||
final double batteryLevel; // %
|
||||
final double diskUsage; // %
|
||||
final NetworkUsage networkUsage;
|
||||
final Duration uptime;
|
||||
|
||||
const PerformanceMetrics({
|
||||
required this.timestamp,
|
||||
required this.memoryUsage,
|
||||
required this.cpuUsage,
|
||||
required this.networkLatency,
|
||||
required this.frameRate,
|
||||
required this.batteryLevel,
|
||||
required this.diskUsage,
|
||||
required this.networkUsage,
|
||||
required this.uptime,
|
||||
});
|
||||
}
|
||||
|
||||
/// Utilisation réseau
|
||||
class NetworkUsage {
|
||||
final double bytesReceived;
|
||||
final double bytesSent;
|
||||
|
||||
const NetworkUsage({
|
||||
required this.bytesReceived,
|
||||
required this.bytesSent,
|
||||
});
|
||||
|
||||
double get totalBytes => bytesReceived + bytesSent;
|
||||
}
|
||||
|
||||
/// Snapshot de performance
|
||||
class PerformanceSnapshot {
|
||||
final DateTime timestamp;
|
||||
final PerformanceMetrics metrics;
|
||||
|
||||
const PerformanceSnapshot({
|
||||
required this.timestamp,
|
||||
required this.metrics,
|
||||
});
|
||||
}
|
||||
|
||||
/// Alerte de performance
|
||||
class PerformanceAlert {
|
||||
final AlertType type;
|
||||
final AlertSeverity severity;
|
||||
final String message;
|
||||
final double value;
|
||||
final double threshold;
|
||||
final DateTime timestamp;
|
||||
|
||||
const PerformanceAlert({
|
||||
required this.type,
|
||||
required this.severity,
|
||||
required this.message,
|
||||
required this.value,
|
||||
required this.threshold,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/// Type d'alerte
|
||||
enum AlertType { memory, cpu, network, performance, battery, disk }
|
||||
|
||||
/// Sévérité d'alerte
|
||||
enum AlertSeverity { info, warning, error, critical }
|
||||
|
||||
/// Rapport de performance
|
||||
class PerformanceReport {
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final double averageMemoryUsage;
|
||||
final double maxMemoryUsage;
|
||||
final double averageCpuUsage;
|
||||
final double maxCpuUsage;
|
||||
final double averageNetworkLatency;
|
||||
final double maxNetworkLatency;
|
||||
final double averageFrameRate;
|
||||
final double minFrameRate;
|
||||
|
||||
const PerformanceReport({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.averageMemoryUsage,
|
||||
required this.maxMemoryUsage,
|
||||
required this.averageCpuUsage,
|
||||
required this.maxCpuUsage,
|
||||
required this.averageNetworkLatency,
|
||||
required this.maxNetworkLatency,
|
||||
required this.averageFrameRate,
|
||||
required this.minFrameRate,
|
||||
});
|
||||
|
||||
factory PerformanceReport.fromSnapshots(List<PerformanceSnapshot> snapshots) {
|
||||
if (snapshots.isEmpty) {
|
||||
throw ArgumentError('Cannot create report from empty snapshots');
|
||||
}
|
||||
|
||||
final metrics = snapshots.map((s) => s.metrics).toList();
|
||||
|
||||
return PerformanceReport(
|
||||
startTime: snapshots.first.timestamp,
|
||||
endTime: snapshots.last.timestamp,
|
||||
averageMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a + b) / metrics.length,
|
||||
maxMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a > b ? a : b),
|
||||
averageCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a + b) / metrics.length,
|
||||
maxCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a > b ? a : b),
|
||||
averageNetworkLatency: metrics.map((m) => m.networkLatency.toDouble()).reduce((a, b) => a + b) / metrics.length,
|
||||
maxNetworkLatency: metrics.map((m) => m.networkLatency.toDouble()).reduce((a, b) => a > b ? a : b),
|
||||
averageFrameRate: metrics.map((m) => m.frameRate).reduce((a, b) => a + b) / metrics.length,
|
||||
minFrameRate: metrics.map((m) => m.frameRate).reduce((a, b) => a < b ? a : b),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques de performance
|
||||
class PerformanceStats {
|
||||
final int totalSnapshots;
|
||||
final Duration totalUptime;
|
||||
final double averageMemoryUsage;
|
||||
final double peakMemoryUsage;
|
||||
final double averageCpuUsage;
|
||||
final double peakCpuUsage;
|
||||
final int alertsGenerated;
|
||||
|
||||
const PerformanceStats({
|
||||
required this.totalSnapshots,
|
||||
required this.totalUptime,
|
||||
required this.averageMemoryUsage,
|
||||
required this.peakMemoryUsage,
|
||||
required this.averageCpuUsage,
|
||||
required this.peakCpuUsage,
|
||||
required this.alertsGenerated,
|
||||
});
|
||||
|
||||
factory PerformanceStats.empty() {
|
||||
return const PerformanceStats(
|
||||
totalSnapshots: 0,
|
||||
totalUptime: Duration.zero,
|
||||
averageMemoryUsage: 0.0,
|
||||
peakMemoryUsage: 0.0,
|
||||
averageCpuUsage: 0.0,
|
||||
peakCpuUsage: 0.0,
|
||||
alertsGenerated: 0,
|
||||
);
|
||||
}
|
||||
|
||||
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots) {
|
||||
if (snapshots.isEmpty) return PerformanceStats.empty();
|
||||
|
||||
final metrics = snapshots.map((s) => s.metrics).toList();
|
||||
|
||||
return PerformanceStats(
|
||||
totalSnapshots: snapshots.length,
|
||||
totalUptime: snapshots.last.timestamp.difference(snapshots.first.timestamp),
|
||||
averageMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a + b) / metrics.length,
|
||||
peakMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a > b ? a : b),
|
||||
averageCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a + b) / metrics.length,
|
||||
peakCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a > b ? a : b),
|
||||
alertsGenerated: 0, // À implémenter si nécessaire
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user