import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:open_filex/open_filex.dart'; import 'package:universal_html/html.dart' as html; import '../models/user_profile.dart'; import '../models/cycle_entry.dart'; import '../theme/app_theme.dart'; class PdfService { static Future generateCycleReport(UserProfile user, List entries) async { final pdf = pw.Document(); final primaryColor = PdfColor.fromInt(AppColors.sageGreen.value); final accentColor = PdfColor.fromInt(AppColors.rose.value); final textColor = PdfColor.fromInt(AppColors.charcoal.value); // Sort entries by date for the report entries.sort((a, b) => a.date.compareTo(b.date)); pdf.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, build: (pw.Context context) => [ _buildHeader(user, primaryColor, accentColor, textColor), pw.SizedBox(height: 20), _buildCycleSummary(user, textColor), pw.SizedBox(height: 20), _buildEntriesTable(entries, primaryColor, textColor), ], ), ); final String fileName = 'cycle_report_${DateFormat('yyyyMMdd').format(DateTime.now())}.pdf'; if (kIsWeb) { // Web platform final bytes = await pdf.save(); final blob = html.Blob([bytes], 'application/pdf'); final url = html.Url.createObjectUrlFromBlob(blob); html.AnchorElement(href: url) ..setAttribute('download', fileName) ..click(); html.Url.revokeObjectUrl(url); } else { // Mobile platform final directory = await getApplicationDocumentsDirectory(); final filePath = '${directory.path}/$fileName'; final file = File(filePath); await file.writeAsBytes(await pdf.save()); await OpenFilex.open(filePath); } } static pw.Widget _buildHeader(UserProfile user, PdfColor primaryColor, PdfColor accentColor, PdfColor textColor) { return pw.Container( padding: pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( color: primaryColor.lighter(30), borderRadius: pw.BorderRadius.circular(8), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'Cycle Report for ${user.name}', style: pw.TextStyle( fontSize: 24, fontWeight: pw.FontWeight.bold, color: primaryColor.isLight ? primaryColor.darker(50) : PdfColors.white, ), ), pw.SizedBox(height: 5), pw.Text( 'Generated on: ${DateFormat('MMMM d, yyyy').format(DateTime.now())}', style: pw.TextStyle(fontSize: 12, color: primaryColor.isLight ? primaryColor.darker(30) : PdfColors.white.darker(10)), ), ], ), ); } static pw.Widget _buildCycleSummary(UserProfile user, PdfColor textColor) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'Summary', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold, color: textColor), ), pw.SizedBox(height: 10), pw.Text('Average Cycle Length: ${user.averageCycleLength} days', style: pw.TextStyle(color: textColor)), pw.Text('Average Period Length: ${user.averagePeriodLength} days', style: pw.TextStyle(color: textColor)), if (user.lastPeriodStartDate != null) pw.Text('Last Period Start: ${DateFormat.yMMMMd().format(user.lastPeriodStartDate!)}', style: pw.TextStyle(color: textColor)), pw.Text('Irregular Cycle: ${user.isIrregularCycle ? 'Yes' : 'No'}', style: pw.TextStyle(color: textColor)), ], ); } static pw.Widget _buildEntriesTable(List entries, PdfColor primaryColor, PdfColor textColor) { final headers = ['Date', 'Period', 'Mood', 'Symptoms', 'Notes']; return pw.Table.fromTextArray( headers: headers, data: entries.map((entry) { return [ DateFormat.yMMMd().format(entry.date), entry.isPeriodDay ? 'Yes' : 'No', entry.mood != null ? entry.mood!.label : 'N/A', entry.hasSymptoms ? entry.symptomCount.toString() : 'No', entry.notes != null && entry.notes!.isNotEmpty ? entry.notes! : 'N/A', ]; }).toList(), border: pw.TableBorder.all(color: primaryColor.lighter(10)), headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, color: primaryColor), cellStyle: pw.TextStyle(color: textColor), cellAlignment: pw.Alignment.centerLeft, headerDecoration: pw.BoxDecoration(color: primaryColor.lighter(20)), rowDecoration: pw.BoxDecoration(color: PdfColors.grey100), tableWidth: pw.TableWidth.min, ); } } // Extension to determine if a color is light or dark for text contrast extension on PdfColor { bool get isLight { // Calculate luminance (Y from YIQ) // Formula: Y = (R*299 + G*587 + B*114) / 1000 final r = red * 255; final g = green * 255; final b = blue * 255; final luminance = (r * 299 + g * 587 + b * 114) / 1000; return luminance > 128; // Using 128 as a threshold } PdfColor lighter(int amount) { double factor = 1 + (amount / 100.0); return PdfColor( red * factor > 1.0 ? 1.0 : red * factor, green * factor > 1.0 ? 1.0 : green * factor, blue * factor > 1.0 ? 1.0 : blue * factor, ); } PdfColor darker(int amount) { double factor = 1 - (amount / 100.0); return PdfColor( red * factor < 0.0 ? 0.0 : red * factor, green * factor < 0.0 ? 0.0 : green * factor, blue * factor < 0.0 ? 0.0 : blue * factor, ); } }