MIF_E31222656/lib/screens/panen/analisis_chart_screen.dart

1553 lines
54 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']}');
// Memproses data biaya
_costBreakdown = [
{'name': 'Bibit', 'cost': data['seed_cost'] ?? 0, 'color': Colors.green},
{
'name': 'Pupuk',
'cost': data['fertilizer_cost'] ?? 0,
'color': Colors.blue,
},
{
'name': 'Pestisida',
'cost': data['pesticide_cost'] ?? 0,
'color': Colors.red,
},
{
'name': 'Tenaga Kerja',
'cost': data['labor_cost'] ?? 0,
'color': Colors.orange,
},
{
'name': 'Irigasi',
'cost': data['irrigation_cost'] ?? 0,
'color': Colors.purple,
},
];
// Membuat ringkasan keuangan dengan metrik standar pertanian Indonesia
_financialSummary = {
'total_cost': data['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
'roi': data['roi'] ?? 0, // Return on Investment (%)
'productivity': data['productivity'] ?? 0, // Produktivitas (kilogram/ha)
'status': data['status'] ?? 'N/A',
'quantity': data['quantity'] ?? 0, // Total panen (kilogram)
'area': data['area'] ?? 0, // Luas lahan (ha)
'price_per_kg': data['price_per_kg'] ?? 0, // Harga jual per kg
};
debugPrint('=== FINANCIAL SUMMARY (MANUAL) ===');
debugPrint('Financial summary: $_financialSummary');
}
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();
String statusText;
String recommendationText;
if (profitMargin >= 30) {
statusText =
'Anda mencapai keuntungan yang sangat baik pada panen ini dengan rasio keuntungan ${profitMargin.toStringAsFixed(2)}%.';
recommendationText =
'Pertahankan praktik pertanian yang sudah diterapkan dan pertimbangkan untuk memperluas area tanam atau meningkatkan produktivitas.';
} else if (profitMargin >= 15) {
statusText =
'Anda mencapai keuntungan yang cukup pada panen ini dengan rasio 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 (profitMargin > 0) {
statusText =
'Anda mencapai keuntungan yang minimal pada panen ini dengan rasio keuntungan ${profitMargin.toStringAsFixed(2)}%.';
recommendationText =
'Perlu evaluasi menyeluruh terhadap struktur biaya dan proses produksi untuk meningkatkan profitabilitas di masa mendatang.';
} else {
statusText =
'Anda mengalami kerugian pada panen ini dengan rasio keuntungan ${profitMargin.toStringAsFixed(2)}%.';
recommendationText =
'Perlu tindakan segera untuk mengevaluasi faktor-faktor yang menyebabkan kerugian dan membuat perubahan signifikan pada siklus tanam berikutnya.';
}
String productivityText;
if (productivity > 8000) {
productivityText =
'Produktivitas lahan sangat tinggi (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang sangat baik.';
} else if (productivity > 5000) {
productivityText =
'Produktivitas lahan baik (${productivity.toStringAsFixed(2)} kilogram/ha), menunjukkan praktik budidaya yang efektif.';
} else {
productivityText =
'Produktivitas lahan kurang optimal (${productivity.toStringAsFixed(2)} kilogram/ha), ada ruang untuk peningkatan praktik budidaya.';
}
return 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),
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;
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 (${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.';
} else if (highestPercentage > 25) {
costAnalysis =
'Biaya ${highestCostCategory['name']} merupakan komponen signifikan dalam struktur biaya (${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.';
} else {
costAnalysis =
'Struktur biaya 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.';
}
} else {
costAnalysis =
'Tidak ada data biaya yang cukup untuk analisis struktur biaya.';
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();
String profitabilityAnalysis;
String ratioAnalysis;
String marketAnalysis;
String recommendation;
// Analisis profitabilitas
if (profit <= 0) {
profitabilityAnalysis =
'Panen 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 atau beralih ke komoditas yang lebih menguntungkan.';
} else if (profitMargin < 15) {
profitabilityAnalysis =
'Panen 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 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 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 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 cukup layak secara ekonomi, namun masih berisiko jika terjadi kenaikan biaya produksi.';
} else {
ratioAnalysis =
'R/C Ratio sebesar ${rcRatio.toStringAsFixed(2)} menunjukkan usaha tani 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.'}';
// Analisis pasar
if (income > totalCost * 1.5) {
marketAnalysis =
'Harga pasar sangat menguntungkan dengan pendapatan ${currency.format(income)} yang jauh melebihi biaya produksi ${currency.format(totalCost)}.';
} else if (income > totalCost * 1.2) {
marketAnalysis =
'Harga pasar cukup menguntungkan dengan pendapatan ${currency.format(income)} yang lebih tinggi dari biaya produksi ${currency.format(totalCost)}.';
} else if (income > totalCost) {
marketAnalysis =
'Harga pasar memberikan keuntungan minimal dengan pendapatan ${currency.format(income)} sedikit di atas biaya produksi ${currency.format(totalCost)}.';
} else {
marketAnalysis =
'Harga pasar tidak menguntungkan dengan pendapatan ${currency.format(income)} di bawah biaya produksi ${currency.format(totalCost)}.';
}
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;
}
}
}