1731 lines
64 KiB
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);
|
|
}
|
|
}
|