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) { 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), ], ), ), 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']))), ], ); }).toList(), ], ), ], ]; }, ), ); // 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)), ], ), )).toList(), ], ); } }