MIF_E31222656/lib/screens/panen/analisis_hasil_screen.dart

1635 lines
53 KiB
Dart

import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:tugas_akhir_supabase/utils/pdf_generator.dart';
import 'dart:io';
import 'package:tugas_akhir_supabase/screens/panen/analisis_chart_screen.dart';
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
class HarvestResultScreen extends StatefulWidget {
final String userId;
final Map<String, dynamic>? harvestData;
final Map<String, dynamic>? scheduleData;
const HarvestResultScreen({
super.key,
required this.userId,
this.harvestData,
this.scheduleData,
});
@override
State<HarvestResultScreen> createState() => _HarvestResultScreenState();
}
class _HarvestResultScreenState extends State<HarvestResultScreen> {
final supabase = Supabase.instance.client;
final currency = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ');
// Tab index
int _selectedTabIndex = 0;
// Data fields from the previous analysis
double? _produktivitasPerHektar;
double? _totalBiayaProduksi;
double? _pendapatanKotor;
double? _keuntunganBersih;
double? _rasioKeuntungan;
String? _statusPanen;
// Data from harvestData
Map<String, dynamic>? get _harvestData => widget.harvestData;
Map<String, dynamic>? get _selectedSchedule => widget.scheduleData;
// GlobalKey for capturing chart view as image
final GlobalKey _chartKey = GlobalKey();
@override
void initState() {
super.initState();
// Gunakan Future.microtask untuk menghindari setState selama build
Future.microtask(() => _loadData());
}
void _loadData() {
try {
if (widget.harvestData != null) {
final data = widget.harvestData!;
setState(() {
_produktivitasPerHektar = data['productivity'];
_totalBiayaProduksi = data['cost'];
_pendapatanKotor = data['income'];
_keuntunganBersih = data['profit'];
_rasioKeuntungan = data['profit_margin']?.toDouble();
_statusPanen = data['status'];
});
// Debug untuk memastikan data konsisten
debugPrint('=== HASIL SCREEN DATA VALIDATION ===');
debugPrint('Cost: $_totalBiayaProduksi');
debugPrint('Income: $_pendapatanKotor');
debugPrint('Profit: $_keuntunganBersih');
debugPrint('Profit Margin: $_rasioKeuntungan%');
debugPrint('Status: $_statusPanen');
}
} catch (e) {
debugPrint('Error loading harvest data: $e');
// Handle error gracefully
setState(() {
_produktivitasPerHektar = 0;
_totalBiayaProduksi = 0;
_pendapatanKotor = 0;
_keuntunganBersih = 0;
_rasioKeuntungan = 0;
_statusPanen = 'Tidak diketahui';
});
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
// Hapus unfocus otomatis yang menyebabkan masalah keyboard
// onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
body: SafeArea(child: _buildBody()),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Hapus unfocus yang mungkin menyebabkan masalah keyboard
// FocusScope.of(context).unfocus();
// Small delay to ensure UI is responsive
Future.delayed(const Duration(milliseconds: 50), () {
if (mounted) {
_exportToPdf();
}
});
},
backgroundColor: Colors.green.shade700,
tooltip: 'Ekspor PDF',
child: const Icon(Icons.picture_as_pdf),
),
),
);
}
Widget _buildBody() {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Analisis Panen'),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh Data',
onPressed: () {
setState(() {
// Reset data
_produktivitasPerHektar = null;
_totalBiayaProduksi = null;
_pendapatanKotor = null;
_keuntunganBersih = null;
_rasioKeuntungan = null;
_statusPanen = null;
});
// Reload data
_loadData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Data berhasil diperbarui')),
);
},
),
IconButton(
icon: const Icon(Icons.help_outline),
onPressed: () {
// Hapus unfocus yang mungkin menyebabkan masalah keyboard
// FocusScope.of(context).unfocus();
// Add small delay to ensure UI responsiveness
Future.delayed(const Duration(milliseconds: 50), () {
if (mounted) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Tentang Analisis Panen'),
content: const Text(
'Analisis panen mengukur produktivitas, efisiensi biaya, dan profitabilitas tanaman Anda. '
'Status "Baik" menunjukkan produktivitas dan keuntungan optimal.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
],
),
);
}
});
},
),
],
bottom: TabBar(
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white60,
isScrollable: true,
onTap: (index) {
setState(() {
_selectedTabIndex = index;
});
},
tabs: const [
Tab(icon: Icon(Icons.analytics, size: 20), text: 'Ringkasan'),
Tab(icon: Icon(Icons.pie_chart, size: 20), text: 'Grafik'),
Tab(icon: Icon(Icons.assessment, size: 20), text: 'Detail'),
],
),
),
body: Column(
children: [
// Status header - More compact and modern
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
color: Colors.white,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _getStatusColor(_statusPanen),
shape: BoxShape.circle,
),
child: Icon(
_getStatusIcon(_statusPanen),
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: $_statusPanen',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _getStatusColor(_statusPanen),
),
),
Text(
_getStatusDescription(_statusPanen),
style: TextStyle(
color: Colors.grey.shade800,
fontSize: 12,
),
),
],
),
),
],
),
),
// TabBarView wrapped in Expanded to avoid overflow
Expanded(
child: TabBarView(
children: [
_buildSummaryTab(),
_buildChartTab(),
_buildDetailTab(),
],
),
),
],
),
),
);
}
Widget _buildSummaryTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// Top metrics in a grid - Consistent sizing
GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.8,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildMetricCard(
'Produktivitas',
'${_produktivitasPerHektar?.toStringAsFixed(2) ?? "0"} kilogram/ha',
Icons.park_outlined,
Colors.green.shade700,
),
_buildMetricCard(
'Keuntungan',
currency.format(_keuntunganBersih ?? 0),
Icons.show_chart,
Colors.blue.shade700,
),
_buildMetricCard(
'R/C Ratio',
_getRcRatio().toStringAsFixed(2),
Icons.analytics,
Colors.orange.shade700,
),
_buildMetricCard(
'Pendapatan',
currency.format(_pendapatanKotor ?? 0),
Icons.attach_money,
Colors.purple.shade700,
),
],
),
const SizedBox(height: 12),
// Income vs Cost - Modern Chart
Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pendapatan vs Biaya',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
// Income and Cost comparison
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
const Text(
'Pendapatan',
style: TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
currency.format(_pendapatanKotor ?? 0),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
const Text(
'Biaya',
style: TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
currency.format(_totalBiayaProduksi ?? 0),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Bar Chart - Simplified
SizedBox(
height: 140,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.center,
groupsSpace: 40,
maxY:
(_pendapatanKotor ?? 0) > (_totalBiayaProduksi ?? 0)
? (_pendapatanKotor ?? 0) * 1.2
: (_totalBiayaProduksi ?? 0) * 1.2,
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 24,
getTitlesWidget: (value, meta) {
String text = '';
if (value == 0) text = 'Pendapatan';
if (value == 1) text = 'Biaya';
return Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Text(
text,
style: const TextStyle(fontSize: 10),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(show: false),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: _pendapatanKotor ?? 0,
color: Colors.green,
width: 20,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(6),
),
),
],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(
toY: _totalBiayaProduksi ?? 0,
color: Colors.red,
width: 20,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(6),
),
),
],
),
],
),
),
),
],
),
),
),
const SizedBox(height: 12),
// Recommendation card - More concise
Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.lightbulb, color: Colors.amber, size: 18),
SizedBox(width: 6),
Text(
'Rekomendasi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
_getRecommendation(_statusPanen),
style: const TextStyle(fontSize: 13),
),
],
),
),
),
const SizedBox(height: 16),
// Action buttons in a row - Cleaner
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back, size: 16),
label: const Text('Kembali', style: TextStyle(fontSize: 13)),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.green.shade700,
padding: const EdgeInsets.symmetric(vertical: 10),
side: BorderSide(color: Colors.green.shade700),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
// Dismiss keyboard before action
FocusScope.of(context).unfocus();
_exportToPdf();
},
icon: const Icon(Icons.download, size: 16),
label: const Text(
'Unduh Laporan',
style: TextStyle(fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
],
),
],
);
}
Widget _buildChartTab() {
return RepaintBoundary(
key: _chartKey,
child: HarvestAnalysisChart(
userId: widget.userId,
scheduleData: _selectedSchedule,
harvestData: _harvestData,
isManualInput: _selectedSchedule == null,
),
);
}
Widget _buildDetailTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// Cost breakdown card
Card(
elevation: 2,
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.money_off, color: Colors.red.shade700, size: 16),
const SizedBox(width: 6),
const Text(
'Rincian Biaya Produksi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
// Pie chart and legend in a row
SizedBox(
height: 160,
child: Row(
children: [
// Pie chart
Expanded(
flex: 3,
child: PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 25,
sections: _getCostPieSections(),
),
),
),
// Legend
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLegendItem('Bibit', Colors.green.shade800),
_buildLegendItem('Pupuk', Colors.brown.shade600),
_buildLegendItem(
'Pestisida',
Colors.purple.shade700,
),
_buildLegendItem(
'Tenaga Kerja',
Colors.blue.shade700,
),
_buildLegendItem('Irigasi', Colors.cyan.shade700),
],
),
),
],
),
),
const Divider(height: 20),
// Cost items in a more compact list
_buildCostItem(
'Bibit',
_harvestData?['seed_cost'] ?? 0,
Colors.green.shade800,
),
_buildCostItem(
'Pupuk',
_harvestData?['fertilizer_cost'] ?? 0,
Colors.brown.shade600,
),
_buildCostItem(
'Pestisida',
_harvestData?['pesticide_cost'] ?? 0,
Colors.purple.shade700,
),
_buildCostItem(
'Tenaga Kerja',
_harvestData?['labor_cost'] ?? 0,
Colors.blue.shade700,
),
_buildCostItem(
'Irigasi',
_harvestData?['irrigation_cost'] ?? 0,
Colors.cyan.shade700,
),
const Divider(height: 20),
// Total cost
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total Biaya',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
currency.format(_totalBiayaProduksi ?? 0),
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
],
),
),
),
// Financial ratios card
Card(
elevation: 2,
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.analytics, color: AppColors.primary, size: 16),
const SizedBox(width: 6),
const Text(
'Analisis Kelayakan Usaha Tani',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
// Rasio-rasio keuangan
_buildRatioItem(
'R/C Ratio',
_getRcRatio(),
'Pendapatan/Biaya',
1.0,
1.5,
_getRcRatioColor(_getRcRatio()),
),
const SizedBox(height: 12),
_buildRatioItem(
'B/C Ratio',
_getBcRatio(),
'Keuntungan/Biaya',
0.0,
1.0,
_getBcRatioColor(_getBcRatio()),
),
const SizedBox(height: 12),
_buildRatioItem(
'Profit Margin',
_getProfitMargin(),
'%',
0.0,
15.0,
_getProfitMarginColor(_getProfitMargin()),
),
const SizedBox(height: 16),
// Penjelasan
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Keterangan:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.blue.shade800,
),
),
const SizedBox(height: 4),
Text(
'• R/C Ratio > 1: Usaha tani layak secara ekonomi\n'
'• B/C Ratio > 0: Usaha tani menguntungkan\n'
'• Profit Margin: Persentase keuntungan dari pendapatan',
style: TextStyle(
fontSize: 11,
color: Colors.blue.shade900,
),
),
],
),
),
],
),
),
),
// Productivity analysis card
Card(
elevation: 2,
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.insights, color: AppColors.primary, size: 16),
const SizedBox(width: 6),
const Text(
'Analisis Produktivitas',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
// Row 1: Luas Lahan & Total Panen
Row(
children: [
Expanded(
child: _buildSimpleInfoItem(
'Plot',
'${widget.scheduleData?['plot'] ?? "Tidak diketahui"}',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSimpleInfoItem(
'Total Panen',
'${_harvestData?['quantity']?.toString() ?? "0"} kilogram',
),
),
],
),
const SizedBox(height: 6),
// Row 2: Produktivitas & Harga Jual
Row(
children: [
Expanded(
child: _buildSimpleInfoItem(
'Produktivitas',
'${_produktivitasPerHektar?.toStringAsFixed(2) ?? "0"} kilogram/ha',
isHighlighted: true,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSimpleInfoItem(
'Harga Jual',
'${currency.format((_harvestData?['income'] ?? 0) / ((_harvestData?['quantity'] ?? 1) * 100))}/kg',
),
),
],
),
const SizedBox(height: 6),
// Row 3: Pendapatan & Keuntungan
Row(
children: [
Expanded(
child: _buildSimpleInfoItem(
'Pendapatan',
currency.format(_pendapatanKotor ?? 0),
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSimpleInfoItem(
'Keuntungan',
currency.format(_keuntunganBersih ?? 0),
isHighlighted: true,
),
),
],
),
const SizedBox(height: 6),
// Benchmark visualization
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Benchmark Panen',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
fontSize: 13,
),
),
const SizedBox(height: 8),
_buildBenchmarkItem(
'Produktivitas',
_produktivitasPerHektar ?? 0,
3000.0,
'kilogram/ha',
5000.0,
),
const SizedBox(height: 10),
_buildBenchmarkItem(
'R/C Ratio',
_getRcRatio(),
1.0,
'',
1.5,
),
],
),
],
),
),
),
],
);
}
Widget _buildSimpleInfoItem(
String label,
String value, {
bool isHighlighted = false,
Color? valueColor,
}) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: isHighlighted ? AppColors.lightGreen : Colors.grey.shade50,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(fontSize: 10, color: Colors.grey.shade700),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: valueColor,
fontSize: 11,
),
maxLines: 1,
),
),
],
),
);
}
Widget _buildMetricCard(
String title,
String value,
IconData icon,
Color color,
) {
return Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 4),
Expanded(
child: Text(
title,
style: TextStyle(
color: color,
fontWeight: FontWeight.w500,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
Widget _buildCostItem(String title, double value, Color iconColor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: iconColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
Text(title, style: const TextStyle(fontSize: 12)),
],
),
Text(currency.format(value), style: const TextStyle(fontSize: 12)),
],
),
);
}
Widget _buildLegendItem(String title, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
title,
style: const TextStyle(fontSize: 10),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildBenchmarkItem(
String label,
double value,
double benchmark,
String unit,
double excellent,
) {
final double percentage = value / excellent * 100;
Color progressColor;
if (value >= excellent) {
progressColor = AppColors.primary;
} else if (value >= benchmark) {
progressColor = Colors.orange.shade600;
} else {
progressColor = Colors.red.shade600;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
),
Text(
unit == 'ton/ha'
? '${value.toStringAsFixed(2)} kilogram/ha'
: '${value.toStringAsFixed(2)} $unit',
style: TextStyle(
fontWeight: FontWeight.bold,
color: progressColor,
fontSize: 12,
),
),
],
),
const SizedBox(height: 6),
Stack(
children: [
Container(
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(3),
),
),
Container(
height: 5,
width: ((MediaQuery.of(context).size.width - 70) *
percentage /
100)
.clamp(0.0, double.infinity),
decoration: BoxDecoration(
color: progressColor,
borderRadius: BorderRadius.circular(3),
),
),
],
),
const SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Min: 0',
style: TextStyle(fontSize: 9, color: Colors.grey.shade600),
),
Text(
'Target: $benchmark',
style: TextStyle(fontSize: 9, color: Colors.orange.shade600),
),
Text(
'Optimal: $excellent',
style: TextStyle(fontSize: 9, color: Colors.green.shade600),
),
],
),
],
);
}
List<PieChartSectionData> _getCostPieSections() {
final total = _totalBiayaProduksi ?? 1; // avoid division by zero
final seedCost = _harvestData?['seed_cost'] ?? 0;
final fertilizerCost = _harvestData?['fertilizer_cost'] ?? 0;
final pesticideCost = _harvestData?['pesticide_cost'] ?? 0;
final laborCost = _harvestData?['labor_cost'] ?? 0;
final irrigationCost = _harvestData?['irrigation_cost'] ?? 0;
return [
if (seedCost > 0)
PieChartSectionData(
value: seedCost,
title: '${((seedCost / total) * 100).toStringAsFixed(0)}%',
color: AppColors.primary,
radius: 45,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
if (fertilizerCost > 0)
PieChartSectionData(
value: fertilizerCost,
title: '${((fertilizerCost / total) * 100).toStringAsFixed(0)}%',
color: Colors.brown.shade600,
radius: 45,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
if (pesticideCost > 0)
PieChartSectionData(
value: pesticideCost,
title: '${((pesticideCost / total) * 100).toStringAsFixed(0)}%',
color: Colors.purple.shade700,
radius: 45,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
if (laborCost > 0)
PieChartSectionData(
value: laborCost,
title: '${((laborCost / total) * 100).toStringAsFixed(0)}%',
color: Colors.blue.shade700,
radius: 45,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
if (irrigationCost > 0)
PieChartSectionData(
value: irrigationCost,
title: '${((irrigationCost / total) * 100).toStringAsFixed(0)}%',
color: Colors.cyan.shade700,
radius: 45,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
];
}
IconData _getStatusIcon(String? status) {
switch (status) {
case 'Baik':
return Icons.check_circle;
case 'Cukup':
return Icons.thumbs_up_down;
case 'Kurang':
return Icons.warning;
default:
return Icons.help_outline;
}
}
Color _getStatusColor(String? status) {
switch (status) {
case 'Baik':
return Colors.green.shade600;
case 'Cukup':
return Colors.orange.shade600;
case 'Kurang':
return Colors.red.shade600;
default:
return Colors.grey;
}
}
String _getStatusDescription(String? status) {
final rcRatio = _getRcRatio();
switch (status) {
case 'Baik':
if (rcRatio >= 1.5) {
return 'R/C Ratio ${rcRatio.toStringAsFixed(2)} - Usaha tani sangat layak secara ekonomi';
} else {
return 'Produktivitas dan efisiensi usaha tani optimal';
}
case 'Cukup':
if (rcRatio >= 1.0) {
return 'R/C Ratio ${rcRatio.toStringAsFixed(2)} - Usaha tani cukup layak secara ekonomi';
} else {
return 'Produktivitas baik namun efisiensi biaya perlu ditingkatkan';
}
case 'Kurang':
if (rcRatio < 1.0) {
return 'R/C Ratio ${rcRatio.toStringAsFixed(2)} - Usaha tani tidak layak secara ekonomi';
} else {
return 'Produktivitas dan profitabilitas perlu ditingkatkan';
}
default:
return '';
}
}
String _getRecommendation(String? status) {
// Ambil R/C Ratio untuk analisis lebih spesifik
final rcRatio = _getRcRatio();
switch (status) {
case 'Baik':
if (rcRatio >= 2.0) {
return 'Usaha tani sangat layak dan menguntungkan (R/C Ratio ${rcRatio.toStringAsFixed(2)}). Pertahankan praktik pertanian yang sudah baik dan pertimbangkan untuk memperluas area tanam atau meningkatkan produksi.';
} else {
return 'Pertahankan praktik pertanian yang sudah baik. Tingkatkan efisiensi biaya untuk meningkatkan R/C Ratio. Pertimbangkan untuk mencoba varietas unggulan untuk produktivitas lebih tinggi.';
}
case 'Cukup':
if (rcRatio < 1.2) {
return 'Usaha tani cukup layak (R/C Ratio ${rcRatio.toStringAsFixed(2)}) namun berisiko. Tingkatkan efisiensi biaya produksi, terutama pada komponen biaya terbesar untuk meningkatkan keuntungan.';
} else {
return 'Fokus pada peningkatan produktivitas, karena R/C Ratio sudah cukup baik (${rcRatio.toStringAsFixed(2)}). Optimalkan penggunaan input dan teknik budidaya untuk hasil panen lebih banyak.';
}
case 'Kurang':
if (rcRatio < 1.0) {
return 'Usaha tani tidak layak secara ekonomi (R/C Ratio ${rcRatio.toStringAsFixed(2)} < 1). Evaluasi ulang seluruh struktur biaya dan teknik budidaya. Pertimbangkan untuk beralih ke komoditas lain yang lebih sesuai.';
} else {
return 'Evaluasi ulang teknik budidaya yang diterapkan untuk meningkatkan produktivitas. Pastikan pemilihan varietas yang tepat, perbaiki teknik pemupukan, dan kendalikan hama penyakit secara terpadu.';
}
default:
return 'Belum dapat memberikan rekomendasi spesifik.';
}
}
// Fungsi untuk mengekspor data ke PDF
Future<void> _exportToPdf() async {
try {
// Show loading indicator
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
// Capture chart view as image if available
Uint8List? chartImageBytes;
if (_selectedTabIndex == 1) {
// Switch to chart tab if not already on it
setState(() {
_selectedTabIndex = 1;
});
// Wait for the UI to update
await Future.delayed(const Duration(milliseconds: 300));
// Try to capture the chart
try {
chartImageBytes = await _captureChartAsImage();
} catch (e) {
debugPrint('Failed to capture chart image: $e');
}
}
// Get daily logs data for more comprehensive report
List<Map<String, dynamic>>? dailyLogs;
if (widget.scheduleData != null) {
try {
final scheduleId = widget.scheduleData!['id'];
final res = await supabase
.from('daily_logs')
.select()
.eq('schedule_id', scheduleId)
.order('date', ascending: true);
if (res.isNotEmpty) {
dailyLogs = List<Map<String, dynamic>>.from(res);
}
} catch (e) {
debugPrint('Error fetching daily logs for PDF: $e');
}
}
// Generate PDF using the HarvestPdfGenerator
final pdfGenerator = HarvestPdfGenerator();
final pdfFile = await pdfGenerator.generatePdf(
title: 'Laporan Analisis Panen',
harvestData: _harvestData ?? {},
scheduleData: widget.scheduleData,
dailyLogs: dailyLogs,
chartImageBytes: chartImageBytes,
);
// Close loading dialog
if (!context.mounted) return;
Navigator.pop(context);
// Show success dialog with options
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('PDF Berhasil Dibuat'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Laporan PDF analisis panen telah berhasil dibuat.',
),
const SizedBox(height: 8),
Text(
'Lokasi: ${pdfFile.path}',
style: const TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Tutup'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_sharePdf(pdfFile);
},
child: const Text('Bagikan'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_openPdf(pdfFile);
},
child: const Text('Buka'),
),
],
),
);
} catch (e) {
// Close loading dialog if open
if (context.mounted) {
Navigator.pop(context);
}
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal membuat PDF: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
// Function to capture chart as image
Future<Uint8List?> _captureChartAsImage() async {
try {
// Find the RenderRepaintBoundary object associated with the key
RenderRepaintBoundary? boundary =
_chartKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) {
debugPrint('Could not find chart boundary');
return null;
}
// Capture the image
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData = await image.toByteData(
format: ui.ImageByteFormat.png,
);
if (byteData == null) {
debugPrint('Failed to convert image to bytes');
return null;
}
return byteData.buffer.asUint8List();
} catch (e) {
debugPrint('Error capturing chart image: $e');
return null;
}
}
// Helper function to open the PDF
Future<void> _openPdf(File file) async {
try {
final pdfGenerator = HarvestPdfGenerator();
await pdfGenerator.openPdf(file);
} catch (e) {
if (!context.mounted) return;
// If opening fails, show dialog with options
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Gagal Membuka PDF'),
content: const Text(
'Tidak dapat membuka file PDF secara langsung. '
'Silakan bagikan file untuk dibuka dengan aplikasi lain.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_sharePdf(file);
},
child: const Text('Bagikan'),
),
],
),
);
}
}
// Helper function to share the PDF
Future<void> _sharePdf(File file) async {
try {
final pdfGenerator = HarvestPdfGenerator();
await pdfGenerator.sharePdf(file);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal membagikan PDF: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
// Helper method untuk mendapatkan R/C Ratio
double _getRcRatio() {
final cost = _totalBiayaProduksi ?? 1.0;
final income = _pendapatanKotor ?? 0.0;
return cost > 0 ? income / cost : 0.0;
}
// Helper method untuk mendapatkan B/C Ratio
double _getBcRatio() {
final cost = _totalBiayaProduksi ?? 1.0;
final profit = _keuntunganBersih ?? 0.0;
return cost > 0 ? profit / cost : 0.0;
}
// Helper method untuk mendapatkan Profit Margin
double _getProfitMargin() {
final income = _pendapatanKotor ?? 1.0;
final profit = _keuntunganBersih ?? 0.0;
return income > 0 ? (profit / income) * 100 : 0.0;
}
// Warna untuk R/C Ratio
Color _getRcRatioColor(double value) {
if (value >= 1.5) return Colors.green.shade600;
if (value >= 1.0) return Colors.orange.shade600;
return Colors.red.shade600;
}
// Warna untuk B/C Ratio
Color _getBcRatioColor(double value) {
if (value >= 1.0) return Colors.green.shade600;
if (value >= 0.0) return Colors.orange.shade600;
return Colors.red.shade600;
}
// Warna untuk Profit Margin
Color _getProfitMarginColor(double value) {
if (value >= 15.0) return Colors.green.shade600;
if (value >= 0.0) return Colors.orange.shade600;
return Colors.red.shade600;
}
// Widget untuk menampilkan item ratio
Widget _buildRatioItem(
String label,
double value,
String unit,
double minThreshold,
double goodThreshold,
Color valueColor,
) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: valueColor.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
Text(
'${value.toStringAsFixed(2)}${unit.isNotEmpty ? ' $unit' : ''}',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor,
),
),
],
),
const SizedBox(height: 6),
Stack(
children: [
Container(
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(2),
),
),
Container(
height: 4,
width:
value <= 0
? 0
: (value > goodThreshold * 2
? 1.0
: value / (goodThreshold * 2)) *
MediaQuery.of(context).size.width *
0.7,
decoration: BoxDecoration(
color: valueColor,
borderRadius: BorderRadius.circular(2),
),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
minThreshold.toStringAsFixed(1),
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
Text(
goodThreshold.toStringAsFixed(1),
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
],
),
],
),
);
}
}