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