652 lines
27 KiB
Dart
652 lines
27 KiB
Dart
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<File> generatePdf({
|
|
required String title,
|
|
required Map<String, dynamic> harvestData,
|
|
Map<String, dynamic>? scheduleData,
|
|
List<Map<String, dynamic>>? 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<void> 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<void> 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<File> 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<String> 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(),
|
|
],
|
|
);
|
|
}
|
|
} |