Files
unionflow-mobile-apps/lib/features/dashboard/data/services/dashboard_export_service.dart
dahoud d094d6db9c Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
2026-03-15 16:30:08 +00:00

508 lines
17 KiB
Dart

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