import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:open_file/open_file.dart'; import 'package:share_plus/share_plus.dart'; import 'package:cross_file/cross_file.dart'; import 'package:tugas_akhir_supabase/data/models/diagnosis_result_model.dart'; // Pastikan path ini benar import 'package:tugas_akhir_supabase/utils/web_pdf_helper.dart' if (dart.library.io) 'package:tugas_akhir_supabase/utils/mobile_pdf_helper.dart'; // Conditionally import dart:html for web // This is needed because dart:html is not available on mobile platforms // The following line will be ignored when compiling for mobile // @dart=2.9 class HarvestPdfGenerator { final currency = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp '); /// Membuat PDF dari data analisis panen Future generatePdf({ required String title, required Map harvestData, Map? scheduleData, List>? dailyLogs, Uint8List? chartImageBytes, }) async { // Buat dokumen PDF final pdf = pw.Document(); // Tanggal laporan final now = DateTime.now(); final formattedDate = DateFormat('dd MMMM yyyy, HH:mm').format(now); final fileName = 'laporan_panen_${DateFormat('yyyyMMdd_HHmmss').format(now)}.pdf'; // Ekstrak data dengan penanganan tipe data yang aman final cropName = _safeToString( scheduleData?['crop_name'] ?? harvestData['crop_name'] ?? 'Tidak diketahui', ); final productivity = _safeToDouble(harvestData['productivity']); final totalCost = _safeToDouble(harvestData['cost']); final income = _safeToDouble(harvestData['income']); final profit = _safeToDouble(harvestData['profit']); final profitMargin = _safeToDouble(harvestData['profit_margin']); final status = _safeToString(harvestData['status'] ?? 'Tidak diketahui'); // Tambahkan halaman ke PDF pdf.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, header: (pw.Context context) { return pw.Center( child: pw.Text( 'LAPORAN ANALISIS PANEN', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold), ), ); }, footer: (pw.Context context) { return pw.Center( child: pw.Text( 'Halaman ${context.pageNumber} dari ${context.pagesCount}', style: const pw.TextStyle(fontSize: 10), ), ); }, build: (pw.Context context) { // Ambil semua data penting final cropName = _safeToString( scheduleData?['crop_name'] ?? harvestData['crop_name'] ?? 'Tidak diketahui', ); final fieldName = _safeToString( scheduleData?['field_name'] ?? harvestData['field_name'] ?? '-', ); final plot = _safeToString( scheduleData?['plot'] ?? harvestData['plot'] ?? '-', ); final startDate = _formatDate( _safeToString( scheduleData?['start_date'] ?? harvestData['start_date'], ), ); final endDate = _formatDate( _safeToString(scheduleData?['end_date'] ?? harvestData['end_date']), ); final area = _safeToDouble(harvestData['area']); final productivity = _safeToDouble(harvestData['productivity']); final quantity = _safeToDouble(harvestData['quantity']); final totalCost = _safeToDouble(harvestData['cost']); final directCost = _safeToDouble(harvestData['direct_cost']); final indirectCost = _safeToDouble(harvestData['indirect_cost']); final income = _safeToDouble(harvestData['income']); final profit = _safeToDouble(harvestData['profit']); final profitMargin = _safeToDouble(harvestData['profit_margin']); final rcRatio = totalCost > 0 ? income / totalCost : 0; final bcRatio = totalCost > 0 ? profit / totalCost : 0; final roi = totalCost > 0 ? (profit / totalCost) * 100 : 0; final status = _safeToString( harvestData['status'] ?? 'Tidak diketahui', ); final pricePerKg = _safeToDouble(harvestData['price_per_kg']); final bepPrice = _safeToDouble(harvestData['bep_price']); final bepProduction = _safeToDouble(harvestData['bep_production']); final productionCostPerKg = _safeToDouble( harvestData['production_cost_per_kg'], ); final weatherCondition = _safeToString( harvestData['weather_condition'], ); final irrigationType = _safeToString(harvestData['irrigation_type']); final soilType = _safeToString(harvestData['soil_type']); final fertilizerType = _safeToString(harvestData['fertilizer_type']); // Komposisi biaya lengkap final costBreakdown = [ { 'name': 'Bibit', 'cost': _safeToDouble(harvestData['seed_cost']), }, { 'name': 'Pupuk', 'cost': _safeToDouble(harvestData['fertilizer_cost']), }, { 'name': 'Pestisida', 'cost': _safeToDouble(harvestData['pesticide_cost']), }, { 'name': 'Tenaga Kerja', 'cost': _safeToDouble(harvestData['labor_cost']), }, { 'name': 'Irigasi', 'cost': _safeToDouble(harvestData['irrigation_cost']), }, { 'name': 'Persiapan Lahan', 'cost': _safeToDouble(harvestData['land_preparation_cost']), }, { 'name': 'Alat & Peralatan', 'cost': _safeToDouble(harvestData['tools_equipment_cost']), }, { 'name': 'Transportasi', 'cost': _safeToDouble(harvestData['transportation_cost']), }, { 'name': 'Pasca Panen', 'cost': _safeToDouble(harvestData['post_harvest_cost']), }, { 'name': 'Lain-lain', 'cost': _safeToDouble(harvestData['other_cost']), }, ].where((item) => ((item['cost'] as double? ?? 0) > 0)).toList(); return [ pw.Center( child: pw.Text( formattedDate, style: const pw.TextStyle(fontSize: 12), ), ), pw.SizedBox(height: 20), // Informasi Tanaman pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'INFORMASI TANAMAN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildInfoRowForHarvest('Jenis Tanaman', cropName), if (scheduleData != null) ...[ _buildInfoRowForHarvest( 'Lahan', _safeToString(scheduleData['field_name'] ?? '-'), ), _buildInfoRowForHarvest( 'Plot', _safeToString(scheduleData['plot'] ?? '-'), ), _buildInfoRowForHarvest( 'Periode Tanam', '${_formatDate(_safeToString(scheduleData['start_date']))} - ${_formatDate(_safeToString(scheduleData['end_date']))}', ), _buildInfoRowForHarvest( 'Plot', _safeToString(scheduleData['plot']), ), ], ], ), ), pw.SizedBox(height: 15), // Status Panen pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'STATUS PANEN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildInfoRowForHarvest('Status', status), _buildInfoRowForHarvest( 'Keterangan', _getStatusDescription(status), ), ], ), ), pw.SizedBox(height: 15), // Ringkasan Keuangan pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'RINGKASAN KEUANGAN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildInfoRowForHarvest( 'Total Biaya Produksi', currency.format(totalCost), ), _buildInfoRowForHarvest( 'Pendapatan Kotor', currency.format(income), ), _buildInfoRowForHarvest( 'Keuntungan Bersih', currency.format(profit), ), _buildInfoRowForHarvest( 'Rasio Keuntungan', '${profitMargin.toStringAsFixed(2)}%', ), _buildInfoRowForHarvest( 'Produktivitas', '${productivity.toStringAsFixed(2)} kilogram/ha', ), // Add RC Ratio & BC Ratio if (totalCost > 0) ...[ _buildInfoRowForHarvest( 'R/C Ratio', (income / totalCost).toStringAsFixed(2), ), _buildInfoRowForHarvest( 'B/C Ratio', (profit / totalCost).toStringAsFixed(2), ), ], ], ), ), // If chart image is available, add it to the PDF if (chartImageBytes != null) ...[ pw.SizedBox(height: 15), pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'GRAFIK ANALISIS', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.SizedBox(height: 5), pw.Center( child: pw.Image( pw.MemoryImage(chartImageBytes), width: 400, height: 200, fit: pw.BoxFit.contain, ), ), ], ), ), ], pw.SizedBox(height: 15), // Rincian Biaya pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'RINCIAN BIAYA PRODUKSI', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildInfoRowForHarvest( 'Bibit', currency.format(_safeToDouble(harvestData['seed_cost'])), ), _buildInfoRowForHarvest( 'Pupuk', currency.format( _safeToDouble(harvestData['fertilizer_cost']), ), ), _buildInfoRowForHarvest( 'Pestisida', currency.format( _safeToDouble(harvestData['pesticide_cost']), ), ), _buildInfoRowForHarvest( 'Tenaga Kerja', currency.format(_safeToDouble(harvestData['labor_cost'])), ), _buildInfoRowForHarvest( 'Irigasi', currency.format( _safeToDouble(harvestData['irrigation_cost']), ), ), pw.Divider(), _buildInfoRowForHarvest( 'Total', currency.format(totalCost), isBold: true, ), ], ), ), // 1. RINGKASAN & BENCHMARK pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'RINGKASAN & BENCHMARK', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildInfoRowForHarvest( 'Total Panen', '${quantity.toStringAsFixed(2)} kg', ), _buildInfoRowForHarvest( 'Produktivitas', '${productivity.toStringAsFixed(2)} kg/ha', ), _buildInfoRowForHarvest( 'Harga Jual', '${currency.format(pricePerKg)}/kg', ), _buildInfoRowForHarvest( 'Pendapatan', currency.format(income), ), _buildInfoRowForHarvest( 'Keuntungan', currency.format(profit), ), _buildInfoRowForHarvest( 'Margin Keuntungan', '${profitMargin.toStringAsFixed(2)}%', ), _buildInfoRowForHarvest( 'R/C Ratio', rcRatio.toStringAsFixed(2), ), _buildInfoRowForHarvest( 'B/C Ratio', bcRatio.toStringAsFixed(2), ), _buildInfoRowForHarvest('ROI', '${roi.toStringAsFixed(2)}%'), _buildInfoRowForHarvest('Status', status), pw.SizedBox(height: 8), pw.Text( 'Benchmark Panen:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), pw.Bullet( text: 'Produktivitas: ${productivity.toStringAsFixed(2)} kg/ha', ), pw.Bullet(text: 'R/C Ratio: ${rcRatio.toStringAsFixed(2)}'), pw.Bullet(text: 'B/C Ratio: ${bcRatio.toStringAsFixed(2)}'), pw.Bullet(text: 'ROI: ${roi.toStringAsFixed(2)}%'), ], ), ), // 2. KOMPOSISI BIAYA (PIE CHART) if (costBreakdown.isNotEmpty) ...[ pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'KOMPOSISI BIAYA (PIE CHART)', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildPieChart(costBreakdown), pw.SizedBox(height: 8), ...costBreakdown.map( (item) => _buildInfoRowForHarvest( item['name'].toString(), currency.format(_safeToDouble(item['cost'])), ), ), ], ), ), ], // 3. PERBANDINGAN KEUANGAN (BAR CHART) pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'PERBANDINGAN KEUANGAN (BAR CHART)', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildBarChart(totalCost, income, profit), pw.SizedBox(height: 8), _buildInfoRowForHarvest( 'Total Biaya', currency.format(totalCost), ), _buildInfoRowForHarvest( 'Pendapatan', currency.format(income), ), _buildInfoRowForHarvest( 'Keuntungan', currency.format(profit), ), ], ), ), // 4. TREN PENGELUARAN HARIAN (LINE CHART) if (dailyLogs != null && dailyLogs.isNotEmpty) ...[ pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'TREN PENGELUARAN HARIAN (LINE CHART)', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), _buildLineChart(dailyLogs), pw.SizedBox(height: 8), _buildExpenseTrendSummary(dailyLogs), ], ), ), ], // Analisis Profitabilitas pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS PROFITABILITAS', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text( _getProfitabilityAnalysis( cropName, profit, totalCost, income, profitMargin, productivity, ), ), ], ), ), // Analisis RC/BC/ROI pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS RASIO KEUANGAN (RC/BC/ROI)', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text( _getRatioAnalysis( rcRatio.toDouble(), bcRatio.toDouble(), roi.toDouble(), cropName, ), ), ], ), ), // Analisis BEP Harga & Produksi pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS BREAK EVEN POINT (BEP)', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text( _getBepAnalysis( bepPrice, pricePerKg, bepProduction, quantity, cropName, ), ), ], ), ), // Analisis Produktivitas pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS PRODUKTIVITAS', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text(_getProductivityAnalysis(productivity, cropName)), ], ), ), // Analisis Struktur Biaya if (costBreakdown.isNotEmpty) pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS STRUKTUR BIAYA', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text(_getCostStructureAnalysis(costBreakdown, cropName)), ], ), ), // Analisis Tren Pengeluaran Harian if (dailyLogs != null && dailyLogs.length > 1) pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS TREN PENGELUARAN HARIAN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text(_getExpenseTrendAnalysis(dailyLogs)), ], ), ), // Section: LAPORAN DETAIL DATA pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'LAPORAN DETAIL DATA', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text('Semua variabel dari harvestData:'), pw.SizedBox(height: 6), ...harvestData.entries.map( (e) => pw.Text('${e.key}: ${e.value}'), ), if (scheduleData != null) ...[ pw.SizedBox(height: 12), pw.Text('Semua variabel dari scheduleData:'), pw.SizedBox(height: 6), ...scheduleData.entries.map( (e) => pw.Text('${e.key}: ${e.value}'), ), ], ], ), ), // Section: RIWAYAT DATA MENTAH CATATAN HARIAN if (dailyLogs != null && dailyLogs.isNotEmpty) pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'RIWAYAT DATA MENTAH CATATAN HARIAN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text('Seluruh data mentah dari dailyLogs:'), pw.SizedBox(height: 6), pw.Table( border: pw.TableBorder.all( width: 0.5, color: PdfColors.grey400, ), children: [ // Header pw.TableRow( children: [ ...dailyLogs.first.keys.map( (k) => pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text( k.toString(), style: pw.TextStyle( fontWeight: pw.FontWeight.bold, ), ), ), ), ], ), // Data rows ...dailyLogs.map( (row) => pw.TableRow( children: [ ...row.values.map( (v) => pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text( v != null ? v.toString() : '-', ), ), ), ], ), ), ], ), ], ), ), pw.SizedBox(height: 15), // Analisis dan Rekomendasi pw.Container( padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(), borderRadius: pw.BorderRadius.circular(5), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'ANALISIS & REKOMENDASI', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.Divider(), pw.Text(_getAnalysisText(productivity, profitMargin)), pw.SizedBox(height: 10), pw.Text( 'Rekomendasi:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), pw.SizedBox(height: 5), pw.Text(_getRecommendation(status)), ], ), ), // Jika ada catatan harian, tambahkan tabel if (dailyLogs != null && dailyLogs.isNotEmpty) ...[ pw.SizedBox(height: 15), pw.Text( 'CATATAN HARIAN', style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.SizedBox(height: 5), pw.Table( border: pw.TableBorder.all(), children: [ // Header pw.TableRow( children: [ _buildTableCell('Tanggal', isHeader: true), _buildTableCell('Catatan', isHeader: true), _buildTableCell('Biaya', isHeader: true), ], ), // Data rows ...dailyLogs.map((log) { String dateStr; try { final date = DateTime.parse(_safeToString(log['date'])); dateStr = DateFormat('dd/MM/yyyy').format(date); } catch (e) { dateStr = '-'; } return pw.TableRow( children: [ _buildTableCell(dateStr), _buildTableCell(_safeToString(log['note'] ?? '-')), _buildTableCell( currency.format(_safeToDouble(log['cost'])), ), ], ); }), ], ), ], ]; }, ), ); // Simpan PDF ke direktori aplikasi (tidak memerlukan izin khusus) final directory = await getApplicationDocumentsDirectory(); final filePath = '${directory.path}/$fileName'; final file = File(filePath); await file.writeAsBytes(await pdf.save()); debugPrint('PDF saved to: $filePath'); return file; } pw.Widget _buildInfoRowForHarvest( String label, String value, { bool isBold = false, }) { return pw.Padding( padding: const pw.EdgeInsets.symmetric(vertical: 3), child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.SizedBox(width: 150, child: pw.Text(label)), pw.Text(': '), pw.Expanded( child: pw.Text( value, style: isBold ? pw.TextStyle(fontWeight: pw.FontWeight.bold) : null, ), ), ], ), ); } pw.Widget _buildTableCell(String text, {bool isHeader = false}) { return pw.Padding( padding: const pw.EdgeInsets.all(5), child: pw.Text( text, style: isHeader ? pw.TextStyle(fontWeight: pw.FontWeight.bold) : null, ), ); } String _formatDate(String? dateStr) { if (dateStr == null || dateStr.isEmpty) return '-'; try { final date = DateTime.parse(dateStr); return DateFormat('dd MMMM yyyy', 'id_ID').format(date); } catch (e) { return dateStr; // Kembalikan string asli jika parsing gagal } } // Fungsi untuk mengamankan konversi nilai ke string String _safeToString(dynamic value, {String defaultValue = '-'}) { if (value == null) return defaultValue; if (value is List && value.isEmpty) return defaultValue; if (value is String && value.isEmpty) return defaultValue; return value.toString(); } // Fungsi untuk mengamankan konversi nilai ke double double _safeToDouble(dynamic value, {double defaultValue = 0.0}) { if (value == null) return defaultValue; if (value is int) return value.toDouble(); if (value is double) return value; if (value is String) { try { return double.parse(value); } catch (e) { return defaultValue; } } return defaultValue; } String _getStatusDescription(String? status) { switch (status) { case 'Baik': return 'Produktivitas dan profitabilitas optimal'; case 'Cukup': return 'Performa yang cukup baik, masih dapat ditingkatkan'; case 'Kurang': return 'Produktivitas dan profitabilitas perlu ditingkatkan'; default: return ''; } } String _getAnalysisText(double productivity, double profitMargin) { String productivityText; if (productivity > 8000) { productivityText = 'Produktivitas lahan sangat tinggi (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang sangat baik.'; } else if (productivity > 5000) { productivityText = 'Produktivitas lahan baik (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang efektif.'; } else { productivityText = 'Produktivitas lahan kurang optimal (${productivity.toStringAsFixed(2)} kilogram/ha), ada ruang untuk peningkatan praktik budidaya.'; } String profitText; if (profitMargin >= 30) { profitText = 'Rasio keuntungan sangat baik (${profitMargin.toStringAsFixed(2)}%), menunjukkan efisiensi biaya produksi yang tinggi.'; } else if (profitMargin >= 15) { profitText = 'Rasio keuntungan cukup baik (${profitMargin.toStringAsFixed(2)}%), namun masih ada ruang untuk peningkatan efisiensi.'; } else if (profitMargin > 0) { profitText = 'Rasio keuntungan minimal (${profitMargin.toStringAsFixed(2)}%), perlu evaluasi struktur biaya produksi.'; } else { profitText = 'Mengalami kerugian dengan rasio (${profitMargin.toStringAsFixed(2)}%), memerlukan perubahan signifikan pada struktur biaya dan teknik produksi.'; } return '$productivityText\n\n$profitText'; } String _getRecommendation(String? status) { switch (status) { case 'Baik': return 'Pertahankan praktik pertanian yang sudah baik. Pertimbangkan untuk memperluas area tanam atau mencoba varietas unggulan untuk meningkatkan keuntungan lebih lanjut.'; case 'Cukup': return 'Tingkatkan efisiensi biaya produksi, terutama pada komponen biaya terbesar. Pertimbangkan untuk mengoptimalkan penggunaan pupuk dan pestisida agar lebih tepat sasaran.'; case 'Kurang': return 'Evaluasi ulang teknik budidaya yang diterapkan. Pastikan pemilihan varietas yang tepat, perbaiki teknik pemupukan, dan kendalikan hama penyakit secara terpadu untuk meningkatkan produktivitas.'; default: return 'Belum dapat memberikan rekomendasi spesifik.'; } } /// Buka file PDF Future openPdf(File file) async { try { // Try using open_file package first final result = await OpenFile.open(file.path); if (result.type != ResultType.done) { throw Exception('Tidak dapat membuka file: ${result.message}'); } } catch (e) { // If open_file fails, try an alternative approach debugPrint('Error opening PDF with OpenFile: $e'); throw Exception( 'Gagal membuka PDF. Silakan coba bagikan file dan buka dengan aplikasi PDF lain.', ); } } /// Bagikan file PDF Future sharePdf(File file) async { try { await Share.shareXFiles([ XFile(file.path), ], text: 'Laporan Analisis Panen'); } catch (e) { debugPrint('Error sharing PDF: $e'); throw Exception('Gagal membagikan PDF. Silakan coba lagi nanti.'); } } Future generateDiagnosisReportPdf({ required DiagnosisResultModel diagnosisResult, required Uint8List? imageBytes, }) async { final pdf = pw.Document(); final now = DateTime.now(); final formattedDate = DateFormat( 'dd MMMM yyyy, HH:mm', 'id_ID', ).format(now); final fileName = 'laporan_diagnosis_${DateFormat('yyyyMMdd_HHmmss').format(now)}.pdf'; final pw.TextStyle headingStyle = pw.TextStyle( fontSize: 22, fontWeight: pw.FontWeight.bold, color: PdfColors.green800, ); final pw.TextStyle subheadingStyle = pw.TextStyle( fontSize: 12, color: PdfColors.grey600, ); final pw.TextStyle sectionTitleStyle = pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, color: PdfColors.green700, ); final pw.TextStyle boldStyle = pw.TextStyle(fontWeight: pw.FontWeight.bold); final plantImage = imageBytes != null ? pw.Image( pw.MemoryImage(imageBytes), fit: pw.BoxFit.contain, height: 150, ) : pw.Container(); pdf.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4.copyWith( marginLeft: 1.5 * PdfPageFormat.cm, marginRight: 1.5 * PdfPageFormat.cm, marginTop: 1.5 * PdfPageFormat.cm, marginBottom: 1.5 * PdfPageFormat.cm, ), header: (pw.Context context) { return pw.Column( children: [ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Laporan Diagnosis Tanaman', style: headingStyle), pw.Text(formattedDate, style: subheadingStyle), ], ), pw.Divider(height: 20, thickness: 1.5, color: PdfColors.green800), ], ); }, footer: (pw.Context context) { return pw.Center( child: pw.Text( 'Halaman ${context.pageNumber} dari ${context.pagesCount} - Dihasilkan oleh TaniSM4RT', style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey500), ), ); }, build: (pw.Context context) => [ // Plant Image if (imageBytes != null) pw.Center( child: pw.Container( margin: const pw.EdgeInsets.only(bottom: 20), padding: const pw.EdgeInsets.all(5), decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.grey300, width: 1), borderRadius: pw.BorderRadius.circular(5), ), child: plantImage, ), ), // Plant Identification _buildDiagnosisSectionTitle('Identifikasi Tanaman'), _buildDiagnosisInfoRow( 'Spesies Tanaman', _safeToString(diagnosisResult.plantSpecies), ), _buildDiagnosisInfoRow( 'Tahap Pertumbuhan', _safeToString(diagnosisResult.plantData['growthStage']), ), _buildDiagnosisInfoRow( 'Status Kesehatan', diagnosisResult.isHealthy ? 'Sehat' : 'Tidak Sehat / Terindikasi Penyakit', valueStyle: pw.TextStyle( fontWeight: pw.FontWeight.bold, color: diagnosisResult.isHealthy ? PdfColors.green600 : PdfColors.orange600, ), ), // Diagnosis Details (if not healthy) if (!diagnosisResult.isHealthy) ...[ _buildDiagnosisSectionTitle('Detail Diagnosis Penyakit'), _buildDiagnosisInfoRow( 'Nama Penyakit', _safeToString(diagnosisResult.diseaseName), ), _buildDiagnosisInfoRow( 'Nama Ilmiah', _safeToString(diagnosisResult.scientificName), valueStyle: pw.TextStyle(fontStyle: pw.FontStyle.italic), ), _buildDiagnosisInfoRow( 'Bagian Terdampak', _safeToString( diagnosisResult.additionalInfo.affectedParts.join(', '), ), ), _buildDiagnosisInfoRow( 'Kondisi Lingkungan Pemicu', _safeToString( diagnosisResult.additionalInfo.environmentalConditions, ), ), if (diagnosisResult.plantData['diseaseSeverity'] != null || diagnosisResult.plantData['infectedArea'] != null) _buildDiagnosisSectionTitle( 'Tingkat Keparahan & Dampak', fontSize: 14, ), if (diagnosisResult.plantData['diseaseSeverity'] != null) _buildDiagnosisInfoRow( 'Tingkat Keparahan', '${(_safeToDouble(diagnosisResult.plantData['diseaseSeverity']) * 100).toStringAsFixed(0)}%', ), if (diagnosisResult.plantData['infectedArea'] != null) _buildDiagnosisInfoRow( 'Estimasi Area Terinfeksi', '${(_safeToDouble(diagnosisResult.plantData['infectedArea']) * 100).toStringAsFixed(0)}%', ), if (diagnosisResult.economicImpact['estimatedLoss'] != null && _safeToString( diagnosisResult.economicImpact['estimatedLoss'], ).isNotEmpty) _buildDiagnosisInfoRow( 'Potensi Kerugian Ekonomi', _safeToString( diagnosisResult.economicImpact['estimatedLoss'], ), ), _buildDiagnosisListSection( 'Gejala yang Teramati', _safeToString(diagnosisResult.symptoms).split('\n'), ), _buildDiagnosisListSection( 'Kemungkinan Penyebab', _safeToString(diagnosisResult.causes).split('\n'), ), ], // Treatment and Prevention (if not healthy) if (!diagnosisResult.isHealthy) ...[ _buildDiagnosisSectionTitle( 'Rekomendasi Penanganan & Pencegahan', ), _buildDiagnosisListSection( 'Penanganan Organik', _safeToString(diagnosisResult.organicTreatment).split('\n'), ), _buildDiagnosisListSection( 'Penanganan Kimiawi', _safeToString(diagnosisResult.chemicalTreatment).split('\n'), ), _buildDiagnosisListSection( 'Langkah Pencegahan', _safeToString(diagnosisResult.preventionMeasures).split('\n'), ), ], // Environmental Data if (diagnosisResult.environmentalData.isNotEmpty && diagnosisResult.environmentalData.values.any( (v) => _safeToDouble(v) != 0.0, )) ...[ _buildDiagnosisSectionTitle( 'Data Lingkungan Saat Pengambilan Gambar', ), if (_safeToDouble( diagnosisResult.environmentalData['temperature'], ) != 0.0) _buildDiagnosisInfoRow( 'Suhu Udara', '${_safeToDouble(diagnosisResult.environmentalData['temperature']).toStringAsFixed(1)} °C', ), if (_safeToDouble( diagnosisResult.environmentalData['humidity'], ) != 0.0) _buildDiagnosisInfoRow( 'Kelembaban Udara', '${_safeToDouble(diagnosisResult.environmentalData['humidity']).toStringAsFixed(0)} %', ), if (_safeToDouble( diagnosisResult.environmentalData['lightIntensity'], ) != 0.0) _buildDiagnosisInfoRow( 'Intensitas Cahaya', '${_safeToDouble(diagnosisResult.environmentalData['lightIntensity']).toStringAsFixed(0)} lux', ), ], pw.SizedBox(height: 30), pw.Text( 'Catatan: Laporan ini dihasilkan berdasarkan analisis gambar dan data yang diberikan. Validasi lapangan oleh ahli pertanian mungkin diperlukan untuk diagnosis yang lebih akurat dan tindakan yang lebih tepat.', style: pw.TextStyle( fontSize: 9, fontStyle: pw.FontStyle.italic, color: PdfColors.grey700, ), textAlign: pw.TextAlign.justify, ), ], ), ); // Save the PDF using the appropriate helper based on platform final bytes = await pdf.save(); return savePdfFile(fileName, bytes); } pw.Widget _buildDiagnosisSectionTitle( String title, { PdfColor color = PdfColors.green800, double fontSize = 16, }) { return pw.Padding( padding: const pw.EdgeInsets.only(top: 15, bottom: 8), child: pw.Text( title.toUpperCase(), style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: fontSize, color: color, ), ), ); } pw.Widget _buildDiagnosisInfoRow( String label, String value, { bool isBoldValue = false, pw.TextStyle? valueStyle, }) { return pw.Padding( padding: const pw.EdgeInsets.symmetric(vertical: 2.5), child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.SizedBox( width: 130, child: pw.Text( label, style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), ), pw.Text(': ', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), pw.Expanded( child: pw.Text( value, style: valueStyle ?? (isBoldValue ? pw.TextStyle(fontWeight: pw.FontWeight.bold) : null), ), ), ], ), ); } pw.Widget _buildDiagnosisListSection( String title, List items, { PdfColor iconColor = PdfColors.green700, }) { if (items.isEmpty || items.every((item) => item.trim().isEmpty || item == '-')) { return pw.Container(); } return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ _buildDiagnosisSectionTitle(title, fontSize: 14), pw.SizedBox(height: 4), ...items.map( (item) => pw.Padding( padding: const pw.EdgeInsets.only(left: 10, bottom: 4), child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( '• ', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, color: iconColor, ), ), pw.Expanded(child: pw.Text(item)), ], ), ), ), ], ); } String _getProfitabilityAnalysis( String cropName, double profit, double totalCost, double income, double profitMargin, double productivity, ) { String productivityText; if (productivity > 8000) { productivityText = 'Produktivitas lahan sangat tinggi (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang sangat baik.'; } else if (productivity > 5000) { productivityText = 'Produktivitas lahan baik (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang efektif.'; } else { productivityText = 'Produktivitas lahan kurang optimal (${productivity.toStringAsFixed(2)} kilogram/ha), ada ruang untuk peningkatan praktik budidaya.'; } String profitText; if (profitMargin >= 30) { profitText = 'Rasio keuntungan sangat baik (${profitMargin.toStringAsFixed(2)}%), menunjukkan efisiensi biaya produksi yang tinggi.'; } else if (profitMargin >= 15) { profitText = 'Rasio keuntungan cukup baik (${profitMargin.toStringAsFixed(2)}%), namun masih ada ruang untuk peningkatan efisiensi.'; } else if (profitMargin > 0) { profitText = 'Rasio keuntungan minimal (${profitMargin.toStringAsFixed(2)}%), perlu evaluasi struktur biaya produksi.'; } else { profitText = 'Mengalami kerugian dengan rasio (${profitMargin.toStringAsFixed(2)}%), memerlukan perubahan signifikan pada struktur biaya dan teknik produksi.'; } return '$productivityText\n\n$profitText'; } String _getRatioAnalysis( double rcRatio, double bcRatio, double roi, String cropName, ) { String rcRatioText; if (rcRatio > 1.5) { rcRatioText = 'R/C Ratio (${rcRatio.toStringAsFixed(2)}) sangat baik, menunjukkan bahwa pendapatan lebih besar dari biaya produksi. Ini menunjukkan efisiensi yang sangat baik dalam pengelolaan lahan $cropName.'; } else if (rcRatio > 1.0) { rcRatioText = 'R/C Ratio (${rcRatio.toStringAsFixed(2)}) cukup baik, namun masih ada ruang untuk peningkatan efisiensi. Pendapatan hampir sama dengan biaya produksi.'; } else { rcRatioText = 'R/C Ratio (${rcRatio.toStringAsFixed(2)}) kurang baik, menunjukkan bahwa biaya produksi lebih besar dari pendapatan. Ini menunjukkan adanya masalah dalam efisiensi pengelolaan lahan $cropName.'; } String bcRatioText; if (bcRatio > 1.5) { bcRatioText = 'B/C Ratio (${bcRatio.toStringAsFixed(2)}) sangat baik, menunjukkan bahwa keuntungan lebih besar dari biaya produksi. Ini menunjukkan efisiensi yang sangat baik dalam pengelolaan lahan $cropName.'; } else if (bcRatio > 1.0) { bcRatioText = 'B/C Ratio (${bcRatio.toStringAsFixed(2)}) cukup baik, namun masih ada ruang untuk peningkatan efisiensi. Keuntungan hampir sama dengan biaya produksi.'; } else { bcRatioText = 'B/C Ratio (${bcRatio.toStringAsFixed(2)}) kurang baik, menunjukkan bahwa biaya produksi lebih besar dari keuntungan. Ini menunjukkan adanya masalah dalam efisiensi pengelolaan lahan $cropName.'; } String roiText; if (roi > 100) { roiText = 'ROI (${roi.toStringAsFixed(2)}%) sangat baik, menunjukkan bahwa investasi dalam lahan $cropName menghasilkan keuntungan yang sangat besar.'; } else if (roi > 50) { roiText = 'ROI (${roi.toStringAsFixed(2)}%) cukup baik, namun masih ada ruang untuk peningkatan efisiensi. Investasi menghasilkan keuntungan yang cukup besar.'; } else { roiText = 'ROI (${roi.toStringAsFixed(2)}%) kurang baik, menunjukkan bahwa investasi dalam lahan $cropName menghasilkan keuntungan yang rendah. Ini menunjukkan adanya masalah dalam efisiensi pengelolaan lahan $cropName.'; } return '$rcRatioText\n\n$bcRatioText\n\n$roiText'; } String _getBepAnalysis( double bepPrice, double pricePerKg, double bepProduction, double quantity, String cropName, ) { String bepPriceText; if (pricePerKg < bepPrice) { bepPriceText = 'Harga per kilogram (${pricePerKg.toStringAsFixed(2)}) lebih rendah dari BEP (${bepPrice.toStringAsFixed(2)}). Ini menunjukkan bahwa harga jual $cropName saat ini lebih rendah dari harga yang diperlukan untuk mencapai titik impas. Pertimbangkan untuk meningkatkan harga jual atau mengoptimalkan biaya produksi.'; } else if (pricePerKg > bepPrice) { bepPriceText = 'Harga per kilogram (${pricePerKg.toStringAsFixed(2)}) lebih tinggi dari BEP (${bepPrice.toStringAsFixed(2)}). Ini menunjukkan bahwa harga jual $cropName saat ini lebih tinggi dari harga yang diperlukan untuk mencapai titik impas. Pertimbangkan untuk mempertahankan harga jual atau mengoptimalkan biaya produksi.'; } else { bepPriceText = 'Harga per kilogram (${pricePerKg.toStringAsFixed(2)}) sama dengan BEP (${bepPrice.toStringAsFixed(2)}). Ini menunjukkan bahwa harga jual $cropName saat ini sudah mencapai titik impas. Pertimbangkan untuk mempertahankan harga jual atau mengoptimalkan biaya produksi.'; } String bepProductionText; if (quantity < bepProduction) { bepProductionText = 'Produksi (${quantity.toStringAsFixed(2)} kilogram) lebih rendah dari BEP (${bepProduction.toStringAsFixed(2)} kilogram). Ini menunjukkan bahwa produksi $cropName saat ini lebih rendah dari produksi yang diperlukan untuk mencapai titik impas. Pertimbangkan untuk meningkatkan produksi atau mengoptimalkan biaya produksi.'; } else if (quantity > bepProduction) { bepProductionText = 'Produksi (${quantity.toStringAsFixed(2)} kilogram) lebih tinggi dari BEP (${bepProduction.toStringAsFixed(2)} kilogram). Ini menunjukkan bahwa produksi $cropName saat ini lebih tinggi dari produksi yang diperlukan untuk mencapai titik impas. Pertimbangkan untuk mempertahankan produksi atau mengoptimalkan biaya produksi.'; } else { bepProductionText = 'Produksi (${quantity.toStringAsFixed(2)} kilogram) sama dengan BEP (${bepProduction.toStringAsFixed(2)} kilogram). Ini menunjukkan bahwa produksi $cropName saat ini sudah mencapai titik impas. Pertimbangkan untuk mempertahankan produksi atau mengoptimalkan biaya produksi.'; } return '$bepPriceText\n\n$bepProductionText'; } String _getProductivityAnalysis(double productivity, String cropName) { String productivityText; if (productivity > 8000) { productivityText = 'Produktivitas lahan sangat tinggi (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang sangat baik.'; } else if (productivity > 5000) { productivityText = 'Produktivitas lahan baik (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang efektif.'; } else { productivityText = 'Produktivitas lahan kurang optimal (${productivity.toStringAsFixed(2)} kilogram/ha), ada ruang untuk peningkatan praktik budidaya.'; } return productivityText; } String _getCostStructureAnalysis( List> costBreakdown, String cropName, ) { String costStructureText = 'Struktur biaya untuk lahan $cropName menunjukkan bahwa biaya produksi terdiri dari beberapa komponen. '; if (costBreakdown.length == 1) { costStructureText += 'Hanya ada satu komponen biaya, yaitu ${costBreakdown.first['name']}. Ini menunjukkan bahwa pengelolaan lahan $cropName sangat sederhana dan efisien.'; } else { costStructureText += 'Beberapa komponen biaya dominan, termasuk:'; for (var item in costBreakdown) { costStructureText += '\n- ${item['name']} (${item['cost'].toStringAsFixed(0)}), yang merupakan komponen biaya terbesar.'; } costStructureText += '\n\nIni menunjukkan bahwa pengelolaan lahan $cropName memerlukan pengeluaran yang cukup besar untuk beberapa aspek, seperti bibit, pupuk, dan pestisida. Pertimbangkan untuk mengoptimalkan penggunaan pupuk dan pestisida agar lebih tepat sasaran.'; } return costStructureText; } String _getExpenseTrendAnalysis(List>? dailyLogs) { if (dailyLogs == null || dailyLogs.isEmpty) { return 'Tidak ada data pengeluaran harian yang tersedia untuk analisis tren.'; } double totalDailyCost = 0; for (var log in dailyLogs) { totalDailyCost += _safeToDouble(log['cost'] ?? 0); } double averageDailyCost = totalDailyCost / dailyLogs.length; String trendText = 'Analisis tren pengeluaran harian menunjukkan bahwa rata-rata pengeluaran harian untuk lahan ini adalah ${currency.format(averageDailyCost)}. '; if (averageDailyCost > 100000) { trendText += 'Ini menunjukkan bahwa pengeluaran harian untuk lahan ini cukup tinggi, yang dapat mempengaruhi profitabilitas. Pertimbangkan untuk melakukan evaluasi ulang pada biaya-biaya tersebut dan mencari cara untuk mengoptimalkannya.'; } else if (averageDailyCost > 50000) { trendText += 'Ini menunjukkan bahwa pengeluaran harian untuk lahan ini cukup baik, namun masih ada ruang untuk peningkatan efisiensi.'; } else { trendText += 'Ini menunjukkan bahwa pengeluaran harian untuk lahan ini sangat efisien, dengan rata-rata biaya yang rendah.'; } return trendText; } // Helper for pie chart (visual proporsi biaya) pw.Widget _buildPieChart(List> costBreakdown) { if (costBreakdown.isEmpty) { return pw.Text('Tidak ada data biaya untuk grafik.'); } double totalCost = 0; for (var item in costBreakdown) { totalCost += _safeToDouble(item['cost']); } return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: costBreakdown.map((item) { final double value = _safeToDouble(item['cost']); final double percentage = totalCost > 0 ? (value / totalCost) * 100 : 0; final int barLength = (percentage / 5).round(); return pw.Row( children: [ pw.SizedBox(width: 80, child: pw.Text(item['name'])), pw.Text(': '), pw.Text('${percentage.toStringAsFixed(1)}%'), pw.SizedBox(width: 8), pw.Text('|', style: pw.TextStyle(color: PdfColors.grey)), pw.Text( ''.padRight(barLength, '█'), style: pw.TextStyle(color: PdfColors.green800), ), ], ); }).toList(), ); } // Helper for bar chart (visual perbandingan keuangan) pw.Widget _buildBarChart(double totalCost, double income, double profit) { final maxVal = [ _safeToDouble(totalCost), _safeToDouble(income), _safeToDouble(profit), ].reduce((a, b) => a > b ? a : b); List> bars = [ { 'label': 'Total Biaya', 'value': _safeToDouble(totalCost), 'color': PdfColors.red800, }, { 'label': 'Pendapatan', 'value': _safeToDouble(income), 'color': PdfColors.green800, }, { 'label': 'Keuntungan', 'value': _safeToDouble(profit), 'color': PdfColors.blue800, }, ]; return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: bars.map((bar) { final int barLength = maxVal > 0 ? ((_safeToDouble(bar['value']) / maxVal) * 30).round() : 0; return pw.Row( children: [ pw.SizedBox(width: 90, child: pw.Text(bar['label'])), pw.Text(': '), pw.Text(currency.format(_safeToDouble(bar['value']))), pw.SizedBox(width: 8), pw.Text('|', style: pw.TextStyle(color: PdfColors.grey)), pw.Text( ''.padRight(barLength, '█'), style: pw.TextStyle(color: bar['color']), ), ], ); }).toList(), ); } // Helper for line chart (tabel tren pengeluaran harian) pw.Widget _buildLineChart(List>? dailyLogs) { if (dailyLogs == null || dailyLogs.isEmpty) { return pw.Text('Tidak ada data pengeluaran harian untuk grafik.'); } return pw.Table( border: pw.TableBorder.all(width: 0.5, color: PdfColors.grey400), children: [ pw.TableRow( children: [ pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text( 'Tanggal', style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text( 'Biaya', style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), ), ], ), ...dailyLogs.map((log) { String dateStr; try { final date = DateTime.parse(_safeToString(log['date'])); dateStr = DateFormat('dd/MM/yyyy').format(date); } catch (e) { dateStr = '-'; } return pw.TableRow( children: [ pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text(dateStr), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text(currency.format(_safeToDouble(log['cost']))), ), ], ); }), ], ); } // Helper for expense trend summary pw.Widget _buildExpenseTrendSummary(List>? dailyLogs) { if (dailyLogs == null || dailyLogs.isEmpty) { return pw.Text('Tidak ada data pengeluaran harian untuk ringkasan.'); } double totalDailyCost = 0; for (var log in dailyLogs) { totalDailyCost += _safeToDouble(log['cost'] ?? 0); } double averageDailyCost = totalDailyCost / dailyLogs.length; String summaryText = 'Ringkasan Tren Pengeluaran Harian:\n'; summaryText += 'Rata-rata Pengeluaran Harian: ${currency.format(averageDailyCost)}\n'; summaryText += 'Total Pengeluaran Seluruh Periode: ${currency.format(totalDailyCost)}\n'; return pw.Text(summaryText); } }