1765 lines
64 KiB
Dart
1765 lines
64 KiB
Dart
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']} m²');
|
||
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
|
||
}
|
||
}
|
||
}
|