1553 lines
54 KiB
Dart
1553 lines
54 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']}');
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
}
|