MIF_E31222656/lib/screens/panen/analisis_chart_screen.dart

1765 lines
64 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class HarvestAnalysisChart extends StatefulWidget {
final String userId;
final Map<String, dynamic>? scheduleData;
final Map<String, dynamic>? harvestData;
final bool isManualInput;
const HarvestAnalysisChart({
super.key,
required this.userId,
this.scheduleData,
this.harvestData,
this.isManualInput = false,
});
@override
State<HarvestAnalysisChart> createState() => _HarvestAnalysisChartState();
}
class _HarvestAnalysisChartState extends State<HarvestAnalysisChart>
with SingleTickerProviderStateMixin {
final supabase = Supabase.instance.client;
final currency = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ');
late TabController _tabController;
bool _isLoading = true;
// Data untuk grafik
List<Map<String, dynamic>> _dailyLogs = [];
List<Map<String, dynamic>> _costBreakdown = [];
Map<String, dynamic> _financialSummary = {};
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_loadData();
}
Future<void> _loadData() async {
if (!mounted) return;
try {
debugPrint('Loading data for harvest analysis...');
// Check if we need to use manual data
if (widget.isManualInput || widget.scheduleData == null) {
debugPrint('Using manual input data');
// Process manual data (sync operation)
_processManualData();
} else {
debugPrint('Using automatic analysis from daily logs');
// Fetch data with proper error handling
await _fetchDailyLogs();
// Check if still mounted before processing
if (!mounted) return;
_processDailyLogs();
}
} catch (e) {
debugPrint('Error loading chart data: $e');
// Handle error state
if (mounted) {
// Set empty data structures to avoid null errors
_dailyLogs = [];
_costBreakdown = [];
_financialSummary = {
'total_cost': 0,
'income': 0,
'profit': 0,
'profit_margin': 0,
'rc_ratio': 0,
'bc_ratio': 0,
'roi': 0,
'productivity': 0,
'status': 'N/A',
};
}
} finally {
// Only update state if widget is still mounted
if (mounted) {
setState(() => _isLoading = false);
}
}
}
void _processManualData() {
if (widget.harvestData == null) return;
final data = widget.harvestData!;
debugPrint('=== MANUAL INPUT CALCULATION ===');
debugPrint('- Income: ${data['income']}');
debugPrint('- Cost: ${data['cost']}');
debugPrint('- Profit: ${data['profit']}');
debugPrint('- Profit Margin: ${data['profit_margin']}');
debugPrint('- Quantity: ${data['quantity']} kilogram');
debugPrint('- Price per kg: ${data['price_per_kg']}');
debugPrint('- BEP Price: ${data['bep_price']}');
debugPrint('- BEP Production: ${data['bep_production']}');
debugPrint('- Production Cost per kg: ${data['production_cost_per_kg']}');
debugPrint('- Crop Name: ${data['crop_name']}');
debugPrint('- Area: ${data['area']}');
debugPrint('- Field: ${data['field_name']}');
debugPrint('- Plot: ${data['plot']}');
// Memproses data biaya dengan semua komponen yang tersedia
_costBreakdown = [
{
'name': 'Bibit',
'cost': data['seed_cost'] ?? 0,
'color': Colors.green.shade700,
},
{
'name': 'Pupuk',
'cost': data['fertilizer_cost'] ?? 0,
'color': Colors.blue.shade700,
},
{
'name': 'Pestisida',
'cost': data['pesticide_cost'] ?? 0,
'color': Colors.red.shade700,
},
{
'name': 'Tenaga Kerja',
'cost': data['labor_cost'] ?? 0,
'color': Colors.orange.shade700,
},
{
'name': 'Irigasi',
'cost': data['irrigation_cost'] ?? 0,
'color': Colors.purple.shade700,
},
{
'name': 'Persiapan Lahan',
'cost': data['land_preparation_cost'] ?? 0,
'color': Colors.brown.shade700,
},
{
'name': 'Alat & Peralatan',
'cost': data['tools_equipment_cost'] ?? 0,
'color': Colors.grey.shade700,
},
{
'name': 'Transportasi',
'cost': data['transportation_cost'] ?? 0,
'color': Colors.indigo.shade700,
},
{
'name': 'Pasca Panen',
'cost': data['post_harvest_cost'] ?? 0,
'color': Colors.teal.shade700,
},
{
'name': 'Lain-lain',
'cost': data['other_cost'] ?? 0,
'color': Colors.amber.shade700,
},
];
// Filter untuk menghapus komponen biaya yang nilainya 0
_costBreakdown =
_costBreakdown.where((item) => (item['cost'] as double) > 0).toList();
// Membuat ringkasan keuangan dengan metrik standar pertanian Indonesia
_financialSummary = {
'total_cost': data['cost'] ?? 0,
'direct_cost': data['direct_cost'] ?? 0,
'indirect_cost': data['indirect_cost'] ?? 0,
'income': data['income'] ?? 0,
'profit': data['profit'] ?? 0,
'profit_margin': data['profit_margin'] ?? 0, // % dari pendapatan
'rc_ratio': data['rc_ratio'] ?? 1.0, // Revenue/Cost ratio
'bc_ratio': data['bc_ratio'] ?? 0, // Benefit/Cost ratio
'bep_price': data['bep_price'] ?? 0, // BEP Harga
'bep_production': data['bep_production'] ?? 0, // BEP Produksi
'production_cost_per_kg':
data['production_cost_per_kg'] ?? 0, // Biaya Pokok Produksi
'roi': data['roi'] ?? 0, // Return on Investment (%)
'productivity': data['productivity'] ?? 0, // Produktivitas (kilogram/ha)
'status': _determineStatus(data),
'quantity': data['quantity'] ?? 0, // Total panen (kilogram)
'area': data['area'] ?? 0, // Luas lahan (m²)
'price_per_kg': data['price_per_kg'] ?? 0, // Harga jual per kg
'weather_condition': data['weather_condition'] ?? 'Normal',
'irrigation_type': data['irrigation_type'] ?? 'Irigasi Teknis',
'soil_type': data['soil_type'] ?? 'Lempung',
'fertilizer_type': data['fertilizer_type'] ?? 'NPK',
'crop_name': data['crop_name'] ?? 'Tanaman',
'field_name': data['field_name'] ?? 'Lahan',
'plot': data['plot'] ?? 'Plot',
'start_date': data['start_date'],
'end_date': data['end_date'],
};
debugPrint('=== FINANCIAL SUMMARY (MANUAL) ===');
debugPrint('Financial summary: $_financialSummary');
}
// Menentukan status berdasarkan metrik pertanian yang lebih komprehensif
String _determineStatus(Map<String, dynamic> data) {
final rcRatio = data['rc_ratio'] ?? 0.0;
final profitMargin = data['profit_margin'] ?? 0.0;
final productivity = data['productivity'] ?? 0.0;
final cropName = data['crop_name']?.toString().toLowerCase() ?? '';
// Mendapatkan target produktivitas berdasarkan jenis tanaman
double targetProductivity = 0.0;
if (cropName.contains('padi')) {
targetProductivity = 5500;
} else if (cropName.contains('jagung')) {
targetProductivity = 5200;
} else if (cropName.contains('kedelai')) {
targetProductivity = 1500;
} else if (cropName.contains('bawang')) {
targetProductivity = 9500;
} else if (cropName.contains('cabai') || cropName.contains('cabe')) {
targetProductivity = 8000;
} else if (cropName.contains('tomat')) {
targetProductivity = 16000;
} else if (cropName.contains('kentang')) {
targetProductivity = 17000;
} else if (cropName.contains('kopi')) {
targetProductivity = 700;
} else if (cropName.contains('kakao') || cropName.contains('coklat')) {
targetProductivity = 800;
} else if (cropName.contains('tebu')) {
targetProductivity = 70000;
} else if (cropName.contains('kelapa sawit') ||
cropName.contains('sawit')) {
targetProductivity = 20000;
} else {
targetProductivity = 4000;
}
// Menghitung rasio produktivitas terhadap target
final productivityRatio = productivity / targetProductivity;
// Menggunakan standar Kementerian Pertanian untuk kelayakan usaha tani
if (rcRatio >= 2.0) {
return 'Sangat Layak';
} else if (rcRatio >= 1.5) {
return 'Layak';
} else if (rcRatio >= 1.0) {
return 'Cukup Layak';
} else {
return 'Tidak Layak';
}
}
Future<void> _fetchDailyLogs() async {
if (widget.scheduleData == null || !mounted) return;
final scheduleId = widget.scheduleData!['id'];
try {
debugPrint(
'Mencoba mengambil data dari daily_logs untuk schedule_id: $scheduleId',
);
// Clear existing data before fetching
_dailyLogs = [];
// Menggunakan tabel daily_logs sebagai sumber data
final res = await supabase
.from('daily_logs')
.select()
.eq('schedule_id', scheduleId)
.order('date', ascending: true);
// Check if still mounted after async operation
if (!mounted) return;
debugPrint('Berhasil mengambil ${res.length} daily logs untuk analisis');
// Filter out entries with null or invalid cost - more efficiently
_dailyLogs =
List<Map<String, dynamic>>.from(res)
.where(
(log) =>
log['cost'] != null &&
log['cost'] is num &&
log['cost'] >= 0,
)
.toList();
// If we don't have valid logs, try the fallback table
if (_dailyLogs.isEmpty && mounted) {
debugPrint(
'Tidak ada daily logs yang valid, mencoba mencari di daily_records',
);
try {
final recordsRes = await supabase
.from('daily_records')
.select()
.eq('schedule_id', scheduleId)
.order('date', ascending: true);
// Check if still mounted after this second async operation
if (!mounted) return;
debugPrint(
'Berhasil mengambil ${recordsRes.length} daily records untuk analisis',
);
// Filter out entries with null or invalid cost
_dailyLogs =
List<Map<String, dynamic>>.from(recordsRes)
.where(
(log) =>
log['cost'] != null &&
log['cost'] is num &&
log['cost'] >= 0,
)
.toList();
} catch (e) {
debugPrint('Tidak dapat mengambil data dari daily_records: $e');
// Ensure _dailyLogs is empty but initialized
_dailyLogs = [];
}
}
} catch (e) {
debugPrint('Error fetching daily logs: $e');
// Ensure _dailyLogs is empty but initialized
_dailyLogs = [];
}
}
void _processDailyLogs() {
if (_dailyLogs.isEmpty || widget.harvestData == null) {
debugPrint('Tidak ada daily logs atau harvest data untuk diproses');
return;
}
// PERBAIKAN KONSISTENSI: Gunakan biaya dari harvestData yang sama dengan manual input
// untuk memastikan konsistensi perhitungan antara summary dan chart
final manualCost = widget.harvestData!['cost'] ?? 0;
// Hitung total biaya dari daily logs untuk perbandingan
double dailyLogsCost = 0;
for (var log in _dailyLogs) {
double cost = (log['cost'] ?? 0).toDouble();
dailyLogsCost += cost;
}
// Gunakan biaya dari manual input untuk konsistensi
final totalCost = manualCost;
final income = widget.harvestData!['income'] ?? 0;
// Tetapi untuk debugging, kita cek apakah ada perbedaan perhitungan
final harvestQuantity = widget.harvestData!['quantity'] ?? 0;
final pricePerKg = widget.harvestData!['price_per_kg'] ?? 0;
final calculatedIncome = harvestQuantity * 100 * pricePerKg;
final profit = income - totalCost;
final profitMargin = income > 0 ? (profit / income) * 100 : 0;
debugPrint('=== DAILY LOGS CALCULATION (FIXED) ===');
debugPrint('Manual input cost (used): $totalCost');
debugPrint('Daily logs accumulated cost (reference): $dailyLogsCost');
debugPrint('Cost difference: ${dailyLogsCost - totalCost}');
debugPrint('Income dari harvestData: $income');
debugPrint('Calculated income (quantity × 100 × price): $calculatedIncome');
debugPrint('Income difference: ${income - calculatedIncome}');
debugPrint('Profit yang dihitung: $profit');
debugPrint('Profit margin yang dihitung: $profitMargin%');
// Kategori biaya default untuk analisis
// PERBAIKAN: Gunakan breakdown biaya dari harvestData untuk konsistensi
_costBreakdown = [
{
'name': 'Bibit',
'cost': widget.harvestData!['seed_cost'] ?? 0,
'color': Colors.green,
},
{
'name': 'Pupuk',
'cost': widget.harvestData!['fertilizer_cost'] ?? 0,
'color': Colors.blue,
},
{
'name': 'Pestisida',
'cost': widget.harvestData!['pesticide_cost'] ?? 0,
'color': Colors.red,
},
{
'name': 'Tenaga Kerja',
'cost': widget.harvestData!['labor_cost'] ?? 0,
'color': Colors.orange,
},
{
'name': 'Irigasi',
'cost': widget.harvestData!['irrigation_cost'] ?? 0,
'color': Colors.purple,
},
];
// Status panen berdasarkan rasio keuntungan yang baru
String status;
final productivity = widget.harvestData!['productivity'] ?? 0;
if (productivity >= 5.0 && profitMargin >= 30) {
status = 'Baik';
} else if (productivity >= 5.0 || profitMargin >= 30) {
status = 'Cukup';
} else {
status = 'Kurang';
}
// Membuat ringkasan keuangan dengan data yang konsisten dengan manual input
_financialSummary = {
'total_cost':
totalCost, // Gunakan biaya dari harvestData untuk konsistensi
'income': income, // Gunakan income dari harvestData
'profit': profit, // Profit yang dihitung dengan biaya konsisten
'profit_margin': profitMargin, // Profit margin yang benar
'rc_ratio': totalCost > 0 ? (income / totalCost) : 0,
'bc_ratio': totalCost > 0 ? (profit / totalCost) : 0,
'roi': totalCost > 0 ? (profit / totalCost) * 100 : 0,
'productivity': productivity,
'status': status,
'quantity': widget.harvestData!['quantity'] ?? 0,
'area': widget.harvestData!['area'] ?? 0,
'price_per_kg': widget.harvestData!['price_per_kg'] ?? 0,
};
debugPrint('=== FINANCIAL SUMMARY (DAILY LOGS - FIXED) ===');
debugPrint('Updated financial summary: $_financialSummary');
debugPrint(
'Profit should now match manual input calculation: ${_financialSummary['profit']}',
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (widget.isManualInput && widget.harvestData == null) {
return const Center(
child: Text('Silakan selesaikan analisis panen terlebih dahulu'),
);
}
// Wrap in SafeArea to avoid keyboard overlap issues
return SafeArea(
child: Column(
children: [
// Memory-optimized TabBar with less decoration
TabBar(
controller: _tabController,
isScrollable: true,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
labelPadding: const EdgeInsets.symmetric(horizontal: 16),
onTap: (index) {
// Dismiss keyboard when switching tabs
FocusScope.of(context).unfocus();
},
tabs: const [
Tab(
icon: Icon(Icons.analytics, size: 20),
text: 'Ringkasan',
height: 64,
),
Tab(
icon: Icon(Icons.pie_chart, size: 20),
text: 'Komposisi Biaya',
height: 64,
),
Tab(
icon: Icon(Icons.bar_chart, size: 20),
text: 'Perbandingan Keuangan',
height: 64,
),
Tab(
icon: Icon(Icons.show_chart, size: 20),
text: 'Tren Pengeluaran',
height: 64,
),
],
),
const SizedBox(height: 16),
// Use Expanded to avoid overflow issues with keyboard
Expanded(
child: TabBarView(
controller: _tabController,
// Avoid rebuilding tabs that are not visible
physics: const NeverScrollableScrollPhysics(),
children: [
_buildSummaryTab(),
_buildCostBreakdownTab(),
_buildFinancialComparisonTab(),
_buildDailyExpensesTrendTab(),
],
),
),
],
),
);
}
Widget _buildSummaryTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 4,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'Status Panen: ${_financialSummary['status'] ?? 'N/A'}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: _getStatusColor(_financialSummary['status']),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
SizedBox(
height: 150,
child: Stack(
alignment: Alignment.center,
children: [
PieChart(
PieChartData(
startDegreeOffset: 180,
sectionsSpace: 0,
centerSpaceRadius: 80,
sections: [
PieChartSectionData(
value:
(_financialSummary['profit_margin'] ?? 0.0)
.clamp(0.0, 100.0) /
100.0,
color: _getProfitRatioColor(
(_financialSummary['profit_margin'] ?? 0.0)
.toDouble(),
),
radius: 20,
showTitle: false,
),
PieChartSectionData(
value:
1.0 -
(_financialSummary['profit_margin'] ?? 0.0)
.clamp(0.0, 100.0) /
100.0,
color: Colors.grey.shade200,
radius: 20,
showTitle: false,
),
],
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Keuntungan',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
currency.format(
_financialSummary['profit'] ?? 0,
),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
const SizedBox(height: 45),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
_buildSummaryCard(
'Produktivitas',
'${(_financialSummary['productivity'] ?? 0).toStringAsFixed(2)} kilogram/ha',
Icons.agriculture,
Colors.green,
),
_buildSummaryCard(
'Keuntungan',
'${(_financialSummary['profit_margin'] ?? 0).toStringAsFixed(2)}%',
Icons.trending_up,
Colors.blue,
),
_buildSummaryCard(
'Total Biaya',
currency.format(_financialSummary['total_cost'] ?? 0),
Icons.money_off,
Colors.red,
),
_buildSummaryCard(
'Pendapatan',
currency.format(_financialSummary['income'] ?? 0),
Icons.attach_money,
Colors.green,
),
],
),
],
),
),
),
const SizedBox(height: 24),
_buildSummaryAnalysis(),
],
),
);
}
Widget _buildSummaryAnalysis() {
final profitMargin = (_financialSummary['profit_margin'] ?? 0.0).toDouble();
final productivity = (_financialSummary['productivity'] ?? 0.0).toDouble();
final cropName = _financialSummary['crop_name'] ?? 'Tanaman';
final rcRatio = (_financialSummary['rc_ratio'] ?? 0.0).toDouble();
final bcRatio = (_financialSummary['bc_ratio'] ?? 0.0).toDouble();
final roi = (_financialSummary['roi'] ?? 0.0).toDouble();
final weatherCondition = _financialSummary['weather_condition'] ?? 'Normal';
final irrigationType =
_financialSummary['irrigation_type'] ?? 'Irigasi Teknis';
final soilType = _financialSummary['soil_type'] ?? 'Lempung';
final fertilizerType = _financialSummary['fertilizer_type'] ?? 'NPK';
String statusText;
String recommendationText;
String conditionText = '';
// Analisis profitabilitas berdasarkan R/C Ratio dan profit margin
if (rcRatio >= 1.5 && profitMargin >= 30) {
statusText =
'Usaha tani $cropName ini sangat layak dengan R/C Ratio ${rcRatio.toStringAsFixed(2)} dan margin keuntungan ${profitMargin.toStringAsFixed(2)}%.';
recommendationText =
'Pertahankan praktik pertanian yang sudah diterapkan dan pertimbangkan untuk memperluas area tanam atau meningkatkan produktivitas.';
} else if (rcRatio >= 1.0 && profitMargin >= 15) {
statusText =
'Usaha tani $cropName ini layak dengan R/C Ratio ${rcRatio.toStringAsFixed(2)} dan margin keuntungan ${profitMargin.toStringAsFixed(2)}%.';
recommendationText =
'Ada ruang untuk peningkatan. Pertimbangkan untuk mengoptimalkan penggunaan input atau mencari pasar dengan harga jual yang lebih baik.';
} else if (rcRatio >= 1.0) {
statusText =
'Usaha tani $cropName ini cukup layak dengan R/C Ratio ${rcRatio.toStringAsFixed(2)} namun margin keuntungan rendah (${profitMargin.toStringAsFixed(2)}%).';
recommendationText =
'Perlu evaluasi menyeluruh terhadap struktur biaya dan proses produksi untuk meningkatkan profitabilitas di masa mendatang.';
} else {
statusText =
'Usaha tani $cropName ini tidak layak dengan R/C Ratio ${rcRatio.toStringAsFixed(2)} dan mengalami kerugian (margin ${profitMargin.toStringAsFixed(2)}%).';
recommendationText =
'Perlu tindakan segera untuk mengevaluasi faktor-faktor yang menyebabkan kerugian dan membuat perubahan signifikan pada siklus tanam berikutnya.';
}
// Analisis produktivitas berdasarkan jenis tanaman
String productivityText;
double targetProductivity = _getTargetProductivity(cropName);
if (productivity > targetProductivity * 1.2) {
productivityText =
'Produktivitas lahan sangat tinggi (${productivity.toStringAsFixed(0)} kg/ha), jauh di atas rata-rata nasional untuk tanaman $cropName (${targetProductivity.toStringAsFixed(0)} kg/ha).';
} else if (productivity > targetProductivity * 0.8) {
productivityText =
'Produktivitas lahan baik (${productivity.toStringAsFixed(0)} kg/ha), mendekati rata-rata nasional untuk tanaman $cropName (${targetProductivity.toStringAsFixed(0)} kg/ha).';
} else {
productivityText =
'Produktivitas lahan kurang optimal (${productivity.toStringAsFixed(0)} kg/ha), di bawah rata-rata nasional untuk tanaman $cropName (${targetProductivity.toStringAsFixed(0)} kg/ha).';
}
// Analisis kondisi tanam
if (weatherCondition != 'Normal') {
conditionText +=
'Kondisi cuaca $weatherCondition dapat mempengaruhi hasil panen. ';
}
if (irrigationType.contains('Tadah Hujan')) {
conditionText +=
'Sistem irigasi tadah hujan meningkatkan risiko kegagalan saat kekeringan. ';
}
if (soilType.contains('Pasir')) {
conditionText +=
'Tanah berpasir memiliki retensi air dan nutrisi rendah. ';
} else if (soilType.contains('Liat')) {
conditionText += 'Tanah liat memiliki drainase yang kurang baik. ';
}
return Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analisis Ringkasan Panen',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Divider(),
const SizedBox(height: 8),
Text(statusText),
const SizedBox(height: 16),
Text(productivityText),
if (conditionText.isNotEmpty) ...[
const SizedBox(height: 16),
Text(conditionText),
],
const SizedBox(height: 16),
const Text(
'Rekomendasi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(recommendationText),
],
),
),
);
}
Widget _buildSummaryCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(icon, color: color, size: 14),
const SizedBox(width: 4),
Expanded(
child: Text(
title,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
],
),
);
}
Widget _buildCostBreakdownTab() {
if (_costBreakdown.isEmpty) {
return const Center(child: Text('Tidak ada data biaya yang tersedia'));
}
// Hitung total untuk persentase
final totalCost = _costBreakdown.fold<double>(
0,
(sum, item) => sum + ((item['cost'] ?? 0).toDouble()),
);
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1.3,
child: PieChart(
PieChartData(
sections: _buildPieChartSections(totalCost),
centerSpaceRadius: 40,
sectionsSpace: 2,
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
// Implement touch callback if needed
},
),
),
),
),
const SizedBox(height: 24),
const Text(
'Rincian Biaya:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _costBreakdown.length,
itemBuilder: (context, index) {
final item = _costBreakdown[index];
final cost = (item['cost'] ?? 0).toDouble();
final percentage = totalCost > 0 ? cost / totalCost * 100 : 0.0;
return ListTile(
leading: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: item['color'] as Color,
shape: BoxShape.circle,
),
),
title: Text(item['name'] as String),
trailing: Text(
'${currency.format(cost)} (${percentage.toStringAsFixed(1)}%)',
style: const TextStyle(fontWeight: FontWeight.bold),
),
);
},
),
const SizedBox(height: 24),
_buildCostBreakdownAnalysis(totalCost),
],
),
);
}
Widget _buildCostBreakdownAnalysis(double totalCost) {
// Mencari kategori dengan biaya tertinggi
Map<String, dynamic>? highestCostCategory;
for (var item in _costBreakdown) {
if (highestCostCategory == null ||
(item['cost'] ?? 0) > (highestCostCategory['cost'] ?? 0)) {
highestCostCategory = item;
}
}
// Analisis struktur biaya
String costAnalysis;
String recommendation;
String cropName = _financialSummary['crop_name'] ?? 'Tanaman';
if (highestCostCategory != null) {
final highestCost = (highestCostCategory['cost'] ?? 0).toDouble();
final highestPercentage =
totalCost > 0 ? (highestCost / totalCost * 100) : 0;
if (highestPercentage > 40) {
costAnalysis =
'Biaya ${highestCostCategory['name']} mendominasi struktur biaya produksi $cropName (${highestPercentage.toStringAsFixed(1)}% dari total biaya). Hal ini menciptakan ketergantungan tinggi pada komponen biaya ini.';
recommendation =
'Pertimbangkan cara untuk mengurangi ketergantungan pada biaya ${highestCostCategory['name']}, misalnya dengan mencari alternatif yang lebih ekonomis atau mengoptimalkan penggunaannya. Bandingkan dengan praktik petani sukses lainnya untuk tanaman $cropName.';
} else if (highestPercentage > 25) {
costAnalysis =
'Biaya ${highestCostCategory['name']} merupakan komponen signifikan dalam struktur biaya $cropName (${highestPercentage.toStringAsFixed(1)}% dari total biaya). Struktur biaya cukup berimbang namun masih bisa dioptimalkan.';
recommendation =
'Evaluasi efisiensi penggunaan ${highestCostCategory['name']} untuk mengurangi biaya tanpa mengorbankan produktivitas. Pertimbangkan teknologi atau metode baru untuk mengoptimalkan penggunaan input ini.';
} else {
costAnalysis =
'Struktur biaya untuk tanaman $cropName cukup berimbang dengan komponen terbesar ${highestCostCategory['name']} hanya menyumbang ${highestPercentage.toStringAsFixed(1)}% dari total biaya.';
recommendation =
'Pertahankan pendekatan seimbang dalam manajemen biaya, namun tetap periksa apakah ada komponen biaya yang dapat dikurangi. Dokumentasikan praktik manajemen biaya yang efektif ini untuk siklus tanam berikutnya.';
}
// Tambahkan analisis berdasarkan jenis tanaman
if (cropName.toLowerCase().contains('padi')) {
if (highestCostCategory['name'] == 'Tenaga Kerja') {
recommendation +=
' Pertimbangkan mekanisasi untuk mengurangi biaya tenaga kerja yang tinggi pada budidaya padi.';
} else if (highestCostCategory['name'] == 'Pupuk') {
recommendation +=
' Pertimbangkan penggunaan pupuk organik atau teknik pemupukan berimbang untuk tanaman padi.';
}
} else if (cropName.toLowerCase().contains('jagung')) {
if (highestCostCategory['name'] == 'Bibit') {
recommendation +=
' Evaluasi penggunaan varietas jagung hibrida yang lebih produktif meskipun harga bibit lebih tinggi.';
}
}
} else {
costAnalysis =
'Tidak ada data biaya yang cukup untuk analisis struktur biaya tanaman $cropName.';
recommendation =
'Catat komponen biaya dengan lebih detail untuk analisis lebih akurat di masa mendatang.';
}
return Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analisis Komposisi Biaya',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Divider(),
const SizedBox(height: 8),
Text(costAnalysis),
const SizedBox(height: 16),
const Text(
'Rekomendasi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(recommendation),
],
),
),
);
}
List<PieChartSectionData> _buildPieChartSections(double total) {
return _costBreakdown.map((item) {
final cost = (item['cost'] ?? 0).toDouble();
final percentage = total > 0 ? cost / total * 100 : 0.0;
return PieChartSectionData(
value: cost,
color: item['color'] as Color,
title: '${percentage.toStringAsFixed(1)}%',
radius: 100,
titleStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
);
}).toList();
}
Widget _buildFinancialComparisonTab() {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1.5,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
String text = '';
switch (value.toInt()) {
case 0:
text = 'Biaya';
break;
case 1:
text = 'Pendapatan';
break;
case 2:
text = 'Keuntungan';
break;
}
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(text),
);
},
reservedSize: 30,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60,
getTitlesWidget: (value, meta) {
return Text(
currency.format(value).replaceAll(',00', ''),
style: const TextStyle(fontSize: 10),
);
},
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(show: true),
borderData: FlBorderData(show: true),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: (_financialSummary['total_cost'] ?? 0).toDouble(),
color: Colors.red,
width: 30,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(
toY: (_financialSummary['income'] ?? 0).toDouble(),
color: Colors.green,
width: 30,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
),
BarChartGroupData(
x: 2,
barRods: [
BarChartRodData(
toY: (_financialSummary['profit'] ?? 0).toDouble(),
color: Colors.blue,
width: 30,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
),
],
),
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ringkasan Keuangan',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildFinancialRow(
'Total Biaya:',
currency.format(_financialSummary['total_cost'] ?? 0),
),
_buildFinancialRow(
'Pendapatan Kotor:',
currency.format(_financialSummary['income'] ?? 0),
),
_buildFinancialRow(
'Keuntungan Bersih:',
currency.format(_financialSummary['profit'] ?? 0),
),
_buildFinancialRow(
'ROI:',
'${(_financialSummary['roi'] ?? 0).toStringAsFixed(2)}%',
),
],
),
),
),
const SizedBox(height: 24),
_buildFinancialComparisonAnalysis(),
],
),
);
}
Widget _buildFinancialRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildFinancialComparisonAnalysis() {
final totalCost = (_financialSummary['total_cost'] ?? 0.0).toDouble();
final income = (_financialSummary['income'] ?? 0.0).toDouble();
final profit = (_financialSummary['profit'] ?? 0.0).toDouble();
final profitMargin = (_financialSummary['profit_margin'] ?? 0.0).toDouble();
final rcRatio = (_financialSummary['rc_ratio'] ?? 0.0).toDouble();
final bcRatio = (_financialSummary['bc_ratio'] ?? 0.0).toDouble();
final roi = (_financialSummary['roi'] ?? 0.0).toDouble();
final cropName = _financialSummary['crop_name'] ?? 'Tanaman';
final bepPrice = _financialSummary['bep_price'] ?? 0.0;
final pricePerKg = _financialSummary['price_per_kg'] ?? 0.0;
final productivity = _financialSummary['productivity'] ?? 0.0;
final targetProductivity = _getTargetProductivity(cropName);
String profitabilityAnalysis;
String ratioAnalysis;
String marketAnalysis;
String recommendation;
// Analisis profitabilitas
if (profit <= 0) {
profitabilityAnalysis =
'Panen $cropName ini merugi sebesar ${currency.format(profit.abs())}. Total biaya produksi (${currency.format(totalCost)}) melebihi pendapatan (${currency.format(income)}).';
recommendation =
'Evaluasi seluruh proses produksi dan struktur biaya. Pertimbangkan untuk mencari pasar dengan harga jual lebih tinggi (saat ini ${currency.format(pricePerKg)}/kg) atau beralih ke varietas $cropName yang lebih produktif.';
} else if (profitMargin < 15) {
profitabilityAnalysis =
'Panen $cropName ini menghasilkan keuntungan minimal sebesar ${currency.format(profit)} dengan margin profit hanya ${profitMargin.toStringAsFixed(2)}%.';
recommendation =
'Periksa komponen biaya yang mungkin terlalu tinggi dan cari cara untuk meningkatkan produktivitas atau efisiensi tanpa menambah biaya.';
} else if (profitMargin < 30) {
profitabilityAnalysis =
'Panen $cropName ini cukup menguntungkan dengan keuntungan ${currency.format(profit)} dan margin profit ${profitMargin.toStringAsFixed(2)}%.';
recommendation =
'Pertahankan praktik yang baik dan cari peluang untuk meningkatkan skala produksi atau efisiensi lebih lanjut.';
} else {
profitabilityAnalysis =
'Panen $cropName ini sangat menguntungkan dengan keuntungan ${currency.format(profit)} dan margin profit mencapai ${profitMargin.toStringAsFixed(2)}%.';
recommendation =
'Pertahankan praktik yang sudah sangat baik dan pertimbangkan untuk meningkatkan skala produksi untuk keuntungan yang lebih besar.';
}
// Analisis R/C dan B/C Ratio (standar evaluasi pertanian Indonesia)
if (rcRatio < 1.0) {
ratioAnalysis =
'R/C Ratio sebesar ${rcRatio.toStringAsFixed(2)} menunjukkan usaha tani $cropName tidak layak secara ekonomi karena pendapatan lebih kecil dari biaya produksi.';
} else if (rcRatio >= 1.0 && rcRatio < 1.5) {
ratioAnalysis =
'R/C Ratio sebesar ${rcRatio.toStringAsFixed(2)} menunjukkan usaha tani $cropName cukup layak secara ekonomi, namun masih berisiko jika terjadi kenaikan biaya produksi.';
} else {
ratioAnalysis =
'R/C Ratio sebesar ${rcRatio.toStringAsFixed(2)} menunjukkan usaha tani $cropName sangat layak secara ekonomi karena pendapatan jauh lebih besar dari biaya produksi.';
}
ratioAnalysis +=
' B/C Ratio sebesar ${bcRatio.toStringAsFixed(2)} ${bcRatio < 0
? 'menunjukkan kerugian.'
: bcRatio < 1
? 'menunjukkan keuntungan yang kurang optimal.'
: 'menunjukkan perbandingan keuntungan terhadap biaya yang baik.'}';
ratioAnalysis +=
' ROI sebesar ${roi.toStringAsFixed(2)}% ${roi < 15
? 'tergolong rendah untuk usaha tani.'
: roi < 30
? 'tergolong cukup baik untuk usaha tani.'
: 'tergolong sangat baik untuk usaha tani.'}';
// Analisis pasar
if (pricePerKg > bepPrice * 1.5) {
marketAnalysis =
'Harga pasar sangat menguntungkan dengan harga jual ${currency.format(pricePerKg)}/kg yang jauh melebihi BEP Harga ${currency.format(bepPrice)}/kg.';
} else if (pricePerKg > bepPrice * 1.2) {
marketAnalysis =
'Harga pasar cukup menguntungkan dengan harga jual ${currency.format(pricePerKg)}/kg yang lebih tinggi dari BEP Harga ${currency.format(bepPrice)}/kg.';
} else if (pricePerKg > bepPrice) {
marketAnalysis =
'Harga pasar memberikan keuntungan minimal dengan harga jual ${currency.format(pricePerKg)}/kg sedikit di atas BEP Harga ${currency.format(bepPrice)}/kg.';
} else {
marketAnalysis =
'Harga pasar tidak menguntungkan dengan harga jual ${currency.format(pricePerKg)}/kg di bawah BEP Harga ${currency.format(bepPrice)}/kg.';
}
// Tambahan analisis produktivitas
if (productivity > targetProductivity * 1.2) {
recommendation +=
' Produktivitas sangat baik (${productivity.toStringAsFixed(0)} kg/ha), pertahankan praktik budidaya yang sudah diterapkan.';
} else if (productivity < targetProductivity * 0.8) {
recommendation +=
' Produktivitas masih di bawah rata-rata nasional, pertimbangkan untuk meningkatkan teknik budidaya dan pemeliharaan tanaman.';
}
return Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analisis Keuangan',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Divider(),
const SizedBox(height: 8),
Text(profitabilityAnalysis),
const SizedBox(height: 16),
Text(ratioAnalysis),
const SizedBox(height: 16),
Text(marketAnalysis),
const SizedBox(height: 16),
const Text(
'Rekomendasi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(recommendation),
],
),
),
);
}
Widget _buildDailyExpensesTrendTab() {
if (_dailyLogs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.bar_chart_outlined, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
widget.isManualInput
? 'Grafik tren belum tersedia'
: 'Tidak ada catatan harian yang tersedia',
textAlign: TextAlign.center,
),
],
),
);
}
// Menyiapkan data untuk line chart
List<FlSpot> spots = [];
double maxY = 0;
for (int i = 0; i < _dailyLogs.length; i++) {
final cost = (_dailyLogs[i]['cost'] ?? 0).toDouble();
spots.add(FlSpot(i.toDouble(), cost));
if (cost > maxY) maxY = cost;
}
// Menghitung statistik dasar
double totalSpent = 0;
double maxSpent = 0;
double avgSpent = 0;
DateTime? maxSpentDate;
for (var log in _dailyLogs) {
final cost = (log['cost'] ?? 0).toDouble();
totalSpent += cost;
if (cost > maxSpent) {
maxSpent = cost;
maxSpentDate = DateTime.parse(log['date']);
}
}
if (_dailyLogs.isNotEmpty) {
avgSpent = totalSpent / _dailyLogs.length;
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 300,
child: LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value.toInt() >= 0 &&
value.toInt() < _dailyLogs.length) {
final date = DateTime.parse(
_dailyLogs[value.toInt()]['date'],
);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
DateFormat('dd/MM').format(date),
style: const TextStyle(fontSize: 10),
),
);
}
return const SizedBox();
},
reservedSize: 30,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Text(
currency.format(value).replaceAll(',00', ''),
style: const TextStyle(fontSize: 10),
);
},
reservedSize: 60,
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: true),
minY: 0,
maxY: maxY * 1.2,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.blue,
barWidth: 3,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: Colors.blue.withOpacity(0.2),
),
),
],
),
),
),
const SizedBox(height: 16),
_buildExpenseSummary(totalSpent, maxSpent, avgSpent, maxSpentDate),
const SizedBox(height: 16),
const Text(
'Riwayat Pengeluaran Harian',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _dailyLogs.length,
itemBuilder: (context, index) {
final log = _dailyLogs[index];
final date = DateTime.parse(log['date']);
final cost = (log['cost'] ?? 0).toDouble();
return Card(
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.withOpacity(0.1),
child: const Icon(Icons.receipt_long, color: Colors.blue),
),
title: Text(
DateFormat('dd MMMM yyyy').format(date),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
log['note'] ?? 'Tidak ada catatan',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
currency.format(cost),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
onTap: () {
_showDailyLogDetail(log);
},
),
);
},
),
const SizedBox(height: 16),
_buildExpenseTrendAnalysis(),
],
),
);
}
Widget _buildExpenseSummary(
double total,
double max,
double avg,
DateTime? maxDate,
) {
return Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ringkasan Pengeluaran',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Divider(),
const SizedBox(height: 8),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
_buildExpenseStatCard(
'Total Pengeluaran',
currency.format(total),
Icons.account_balance_wallet,
Colors.blue,
),
_buildExpenseStatCard(
'Pengeluaran Rata-rata',
currency.format(avg),
Icons.trending_up,
Colors.green,
),
_buildExpenseStatCard(
'Pengeluaran Tertinggi',
currency.format(max),
Icons.arrow_upward,
Colors.red,
),
_buildExpenseStatCard(
'Tanggal Tertinggi',
maxDate != null ? DateFormat('dd/MM').format(maxDate) : 'N/A',
Icons.calendar_today,
Colors.orange,
),
],
),
],
),
),
);
}
Widget _buildExpenseStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(icon, color: color, size: 14),
const SizedBox(width: 4),
Expanded(
child: Text(
title,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
],
),
);
}
Widget _buildExpenseTrendAnalysis() {
if (_dailyLogs.isEmpty || _dailyLogs.length < 2) {
return const SizedBox.shrink();
}
// Menghitung tren pengeluaran (naik/turun)
List<double> costs =
_dailyLogs
.map((r) => (r['cost'] ?? 0).toDouble())
.toList()
.cast<double>();
double firstHalfAvg = 0;
double secondHalfAvg = 0;
int midPoint = costs.length ~/ 2;
for (int i = 0; i < midPoint; i++) {
firstHalfAvg += costs[i];
}
for (int i = midPoint; i < costs.length; i++) {
secondHalfAvg += costs[i];
}
firstHalfAvg = firstHalfAvg / midPoint;
secondHalfAvg = secondHalfAvg / (costs.length - midPoint);
String trendAnalysis;
String recommendation;
double trendPercentage =
firstHalfAvg > 0
? ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100
: 0;
if (trendPercentage > 20) {
trendAnalysis =
'Tren pengeluaran menunjukkan peningkatan signifikan sebesar ${trendPercentage.abs().toStringAsFixed(1)}% di paruh kedua periode tanam.';
recommendation =
'Investigasi penyebab peningkatan biaya signifikan di paruh kedua periode. Hal ini mungkin menunjukkan adanya masalah yang perlu diatasi seperti serangan hama atau kebutuhan perawatan tanaman yang meningkat.';
} else if (trendPercentage > 5) {
trendAnalysis =
'Tren pengeluaran menunjukkan peningkatan moderat sebesar ${trendPercentage.abs().toStringAsFixed(1)}% di paruh kedua periode tanam.';
recommendation =
'Pantau dengan cermat pengeluaran pada fase-fases tertentu dan evaluasi apakah peningkatan biaya ini berkontribusi pada peningkatan hasil panen.';
} else if (trendPercentage > -5) {
trendAnalysis =
'Tren pengeluaran relatif stabil sepanjang periode tanam dengan perubahan hanya ${trendPercentage.abs().toStringAsFixed(1)}%.';
recommendation =
'Pertahankan manajemen keuangan yang stabil dan terkendali seperti yang sudah dilakukan.';
} else if (trendPercentage > -20) {
trendAnalysis =
'Tren pengeluaran menunjukkan penurunan moderat sebesar ${trendPercentage.abs().toStringAsFixed(1)}% di paruh kedua periode tanam.';
recommendation =
'Evaluasi apakah penurunan biaya ini merupakan hasil dari efisiensi atau mungkin karena pengurangan perawatan yang dapat mempengaruhi hasil panen.';
} else {
trendAnalysis =
'Tren pengeluaran menunjukkan penurunan signifikan sebesar ${trendPercentage.abs().toStringAsFixed(1)}% di paruh kedua periode tanam.';
recommendation =
'Pastikan penurunan biaya yang signifikan ini tidak mempengaruhi kualitas dan kuantitas hasil panen. Verifikasi apakah ada faktor penting yang terlewatkan dalam proses budidaya.';
}
return Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analisis Tren Pengeluaran',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Divider(),
const SizedBox(height: 8),
Text(trendAnalysis),
const SizedBox(height: 16),
const Text(
'Rekomendasi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(recommendation),
],
),
),
);
}
void _showDailyLogDetail(Map<String, dynamic> log) {
final date = DateTime.parse(log['date']);
final formattedDate = DateFormat('dd MMMM yyyy').format(date);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Detail Pengeluaran: $formattedDate',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildDetailRow('Jumlah:', currency.format(log['cost'] ?? 0)),
if (log['note'] != null) _buildDetailRow('Catatan:', log['note']),
if (log['image_url'] != null) ...[
const SizedBox(height: 16),
const Text(
'Foto:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
log['image_url'],
height: 200,
fit: BoxFit.cover,
errorBuilder:
(_, __, ___) =>
const Icon(Icons.image_not_supported, size: 100),
),
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('Tutup'),
),
),
],
),
);
},
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(child: Text(value)),
],
),
);
}
Color _getStatusColor(String? status) {
switch (status) {
case 'Baik':
return Colors.green;
case 'Cukup':
return Colors.orange;
case 'Kurang':
return Colors.red;
default:
return Colors.grey;
}
}
Color _getProfitRatioColor(double ratio) {
if (ratio >= 30) {
return Colors.green;
} else if (ratio >= 15) {
return Colors.orange;
} else {
return Colors.red;
}
}
// Fungsi untuk mendapatkan target produktivitas berdasarkan jenis tanaman
double _getTargetProductivity(String cropName) {
String crop = cropName.toLowerCase();
if (crop.contains('padi')) {
return 5500; // 5.5 ton/ha - Standar nasional
} else if (crop.contains('jagung')) {
return 5200; // 5.2 ton/ha - Standar nasional
} else if (crop.contains('kedelai')) {
return 1500; // 1.5 ton/ha - Standar nasional
} else if (crop.contains('bawang')) {
return 9500; // 9.5 ton/ha - Standar nasional
} else if (crop.contains('cabai') || crop.contains('cabe')) {
return 8000; // 8 ton/ha - Standar nasional
} else if (crop.contains('tomat')) {
return 16000; // 16 ton/ha - Standar nasional
} else if (crop.contains('kentang')) {
return 17000; // 17 ton/ha - Standar nasional
} else if (crop.contains('kopi')) {
return 700; // 0.7 ton/ha - Standar nasional
} else if (crop.contains('kakao') || crop.contains('coklat')) {
return 800; // 0.8 ton/ha - Standar nasional
} else if (crop.contains('tebu')) {
return 70000; // 70 ton/ha - Standar nasional
} else if (crop.contains('kelapa sawit') || crop.contains('sawit')) {
return 20000; // 20 ton/ha - Standar nasional
} else {
return 4000; // Default 4 ton/ha
}
}
}