MIF_E31222656/lib/utils/pdf_generator.dart

1731 lines
64 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) {
// 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<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)),
],
),
),
),
],
);
}
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<Map<String, dynamic>> 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<Map<String, dynamic>>? 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>>? 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<Map<String, dynamic>>? 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);
}
}