1635 lines
53 KiB
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),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|