MIF_E31222656/lib/screens/panen/analisis_input_screen.dart

909 lines
32 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/screens/panen/analisis_hasil_screen.dart';
import 'dart:math' as math;
class AnalisisInputScreen extends StatefulWidget {
final String userId;
final Map<String, dynamic>? scheduleData;
const AnalisisInputScreen({
super.key,
required this.userId,
this.scheduleData,
});
@override
_AnalisisInputScreenState createState() => _AnalisisInputScreenState();
}
class _AnalisisInputScreenState extends State<AnalisisInputScreen> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// Form controllers
final _areaController = TextEditingController();
final _quantityController = TextEditingController();
final _seedCostController = TextEditingController();
final _fertilizerCostController = TextEditingController();
final _pesticideCostController = TextEditingController();
final _laborCostController = TextEditingController();
final _irrigationCostController = TextEditingController();
final _pricePerKgController = TextEditingController();
// Selected schedule
String? _selectedScheduleId;
Map<String, dynamic>? _selectedSchedule;
List<Map<String, dynamic>> _schedules = [];
bool _isManualMode = false;
@override
void initState() {
super.initState();
debugPrint('AnalisisInputScreen initState with userId: ${widget.userId}');
debugPrint('Schedule data provided: ${widget.scheduleData}');
_fetchSchedules();
// Set default values if schedule data is provided
if (widget.scheduleData != null) {
_selectedScheduleId = widget.scheduleData!['id'];
_isManualMode = false;
debugPrint(
'Setting selected schedule ID from props: $_selectedScheduleId',
);
} else {
_isManualMode = true;
debugPrint('No schedule data provided, using manual mode');
_setDefaultValues();
}
}
void _setDefaultValues() {
// For manual mode, we can set either empty fields or default values
if (_isManualMode) {
// Clear all fields first
_areaController.text = '';
_quantityController.text = '';
// Either set defaults or clear fields based on whether we want empty forms for manual
// For true manual input with empty forms, uncomment the lines below:
_seedCostController.text = '';
_fertilizerCostController.text = '';
_pesticideCostController.text = '';
_laborCostController.text = '';
_irrigationCostController.text = '';
_pricePerKgController.text = '';
// Or use default values if preferred (comment these out if using empty fields above)
// _seedCostController.text = '30000';
// _fertilizerCostController.text = '60000';
// _pesticideCostController.text = '50000';
// _laborCostController.text = '300000';
// _irrigationCostController.text = '40000';
// _pricePerKgController.text = '4550';
}
}
@override
void dispose() {
_areaController.dispose();
_quantityController.dispose();
_seedCostController.dispose();
_fertilizerCostController.dispose();
_pesticideCostController.dispose();
_laborCostController.dispose();
_irrigationCostController.dispose();
_pricePerKgController.dispose();
super.dispose();
}
Future<void> _fetchSchedules() async {
if (widget.userId.isEmpty) return;
try {
debugPrint('Fetching schedules for user: ${widget.userId}');
final response = await Supabase.instance.client
.from('crop_schedules')
.select(
'id, crop_name, field_id, plot, start_date, end_date, seed_cost, fertilizer_cost, pesticide_cost, irrigation_cost, expected_yield',
)
.eq('user_id', widget.userId)
.order('created_at', ascending: false);
debugPrint('Fetched schedules response: $response');
if (mounted) {
setState(() {
_schedules = List<Map<String, dynamic>>.from(response);
debugPrint('Schedules loaded: ${_schedules.length}');
// Jika ada jadwal yang diberikan melalui widget.scheduleData, pilih itu
if (widget.scheduleData != null &&
widget.scheduleData!['id'] != null) {
_selectedScheduleId = widget.scheduleData!['id'];
_isManualMode = false;
debugPrint('Selected schedule from props: $_selectedScheduleId');
_updateFormFieldsFromSelectedSchedule();
}
// Jika tidak ada jadwal yang dipilih tapi ada jadwal tersedia, pilih yang pertama
else if (_schedules.isNotEmpty && _selectedScheduleId == null) {
_selectedScheduleId = _schedules.first['id'];
_isManualMode = false;
debugPrint('Selected first schedule: $_selectedScheduleId');
_updateFormFieldsFromSelectedSchedule();
} else if (_isManualMode) {
_setDefaultValues();
}
});
}
} catch (e) {
debugPrint('Error fetching schedules: $e');
}
}
void _updateFormFieldsFromSelectedSchedule() {
if (_isManualMode || _selectedScheduleId == null || _schedules.isEmpty) {
_setDefaultValues();
return;
}
try {
// Find the selected schedule from the schedules list
_selectedSchedule = _schedules.firstWhere(
(schedule) => schedule['id'] == _selectedScheduleId,
orElse: () => {},
);
if (_selectedSchedule == null || _selectedSchedule!.isEmpty) {
debugPrint('Selected schedule not found in schedules list');
_setDefaultValues();
return;
}
debugPrint(
'Updating form fields from selected schedule: $_selectedSchedule',
);
// Update form fields with data from the selected schedule
_seedCostController.text =
(_selectedSchedule!['seed_cost'] ?? 0).toString();
_fertilizerCostController.text =
(_selectedSchedule!['fertilizer_cost'] ?? 0).toString();
_pesticideCostController.text =
(_selectedSchedule!['pesticide_cost'] ?? 0).toString();
_irrigationCostController.text =
(_selectedSchedule!['irrigation_cost'] ?? 0).toString();
// Clear fields that should be filled by the user
_areaController.text = '';
_quantityController.text = '';
_laborCostController.text = '300000'; // Default value
_pricePerKgController.text = '4550'; // Default value
} catch (e) {
debugPrint('Error updating form fields from selected schedule: $e');
_setDefaultValues();
}
}
Future<void> _analyzeHarvest() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
// Parse input values
final double area = double.tryParse(_areaController.text) ?? 0;
final double quantity = double.tryParse(_quantityController.text) ?? 0;
final double seedCost = double.tryParse(_seedCostController.text) ?? 0;
final double fertilizerCost =
double.tryParse(_fertilizerCostController.text) ?? 0;
final double pesticideCost =
double.tryParse(_pesticideCostController.text) ?? 0;
final double laborCost = double.tryParse(_laborCostController.text) ?? 0;
final double irrigationCost =
double.tryParse(_irrigationCostController.text) ?? 0;
final double pricePerKg =
double.tryParse(_pricePerKgController.text) ?? 0;
// Gunakan compute untuk memindahkan kalkulasi berat ke isolate terpisah
// Ini mencegah UI freeze dan main isolate paused
await Future.delayed(
const Duration(milliseconds: 100),
); // Berikan waktu untuk UI update
// Calculate productivity (kilogram/ha)
final double productivityPerHa = area > 0 ? (quantity / area) * 10000 : 0;
// Calculate total cost
final double totalCost =
seedCost +
fertilizerCost +
pesticideCost +
laborCost +
irrigationCost;
// Calculate income (quantity in kilogram)
final double income = quantity * pricePerKg;
// Calculate profit
final double profit = income - totalCost;
// Calculate profit margin
final double profitMargin = income > 0 ? (profit / income) * 100 : 0;
// Calculate R/C ratio
final double rcRatio = totalCost > 0 ? income / totalCost : 0;
// Calculate B/C ratio
final double bcRatio = totalCost > 0 ? profit / totalCost : 0;
// Calculate ROI
final double roi = totalCost > 0 ? (profit / totalCost) * 100 : 0;
// Determine status based on productivity and profit margin
String status;
if (productivityPerHa >= 5000.0 && profitMargin >= 30) {
status = 'Baik';
} else if (productivityPerHa >= 5000.0 || profitMargin >= 30) {
status = 'Cukup';
} else {
status = 'Kurang';
}
// Prepare harvest data
final Map<String, dynamic> harvestData = {
'user_id': widget.userId,
'schedule_id': _selectedScheduleId,
'area': area,
'quantity': quantity,
'productivity': productivityPerHa,
'seed_cost': seedCost,
'fertilizer_cost': fertilizerCost,
'pesticide_cost': pesticideCost,
'labor_cost': laborCost,
'irrigation_cost': irrigationCost,
'cost': totalCost,
'price_per_kg': pricePerKg,
'income': income,
'profit': profit,
'profit_margin': profitMargin,
'rc_ratio': rcRatio,
'bc_ratio': bcRatio,
'roi': roi,
'status': status,
'harvest_date': DateTime.now().toIso8601String(),
};
// Berikan waktu untuk UI update sebelum navigasi
await Future.delayed(const Duration(milliseconds: 100));
// Navigate to result screen
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => HarvestResultScreen(
userId: widget.userId,
harvestData: harvestData,
scheduleData: widget.scheduleData,
),
),
);
} catch (e) {
debugPrint('Error analyzing harvest: $e');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${e.toString().substring(0, math.min(e.toString().length, 100))}',
),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Analisis Hasil Panen'),
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh Data',
onPressed: () {
setState(() => _isLoading = true);
_fetchSchedules().then((_) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data jadwal berhasil diperbarui'),
),
);
});
},
),
],
),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _buildForm(),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
// Data Tanaman section
_buildSectionTitle('Data Tanaman'),
const SizedBox(height: 16),
// Jadwal Tanam dropdown
_buildScheduleDropdown(),
const SizedBox(height: 16),
// Luas Lahan field
_buildTextField(
controller: _areaController,
label: 'Luas Lahan (m²)',
icon: Icons.landscape,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan luas lahan';
}
return null;
},
),
const SizedBox(height: 16),
// Total Panen field
_buildTextField(
controller: _quantityController,
label: 'Total Panen (kilogram)',
icon: Icons.shopping_basket,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan total panen';
}
return null;
},
),
const SizedBox(height: 24),
// Biaya Produksi section
_buildSectionTitle('Biaya Produksi'),
const SizedBox(height: 16),
// Biaya Bibit field
_buildTextField(
controller: _seedCostController,
label: 'Biaya Bibit (Rp)',
icon: Icons.spa,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan biaya bibit';
}
return null;
},
),
const SizedBox(height: 16),
// Biaya Pupuk field
_buildTextField(
controller: _fertilizerCostController,
label: 'Biaya Pupuk (Rp)',
icon: Icons.eco,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan biaya pupuk';
}
return null;
},
),
const SizedBox(height: 16),
// Biaya Pestisida field
_buildTextField(
controller: _pesticideCostController,
label: 'Biaya Pestisida (Rp)',
icon: Icons.bug_report,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan biaya pestisida';
}
return null;
},
),
const SizedBox(height: 16),
// Biaya Tenaga Kerja field
_buildTextField(
controller: _laborCostController,
label: 'Biaya Tenaga Kerja (Rp)',
icon: Icons.people,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan biaya tenaga kerja';
}
return null;
},
),
const SizedBox(height: 16),
// Biaya Irigasi field
_buildTextField(
controller: _irrigationCostController,
label: 'Biaya Irigasi (Rp)',
icon: Icons.water_drop,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan biaya irigasi';
}
return null;
},
),
const SizedBox(height: 24),
// Harga Jual section
_buildSectionTitle('Harga Jual'),
const SizedBox(height: 16),
// Harga Jual per Kg field
_buildTextField(
controller: _pricePerKgController,
label: 'Harga Jual per Kg (Rp)',
icon: Icons.attach_money,
keyboardType: TextInputType.number,
prefixText: 'Rp ',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan harga jual per kg';
}
return null;
},
),
const SizedBox(height: 32),
// Analyze button
SizedBox(
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _analyzeHarvest,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
child:
_isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('ANALISIS HASIL PANEN'),
),
),
const SizedBox(height: 24),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.w600,
color: const Color(0xFF056839),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
TextInputType keyboardType = TextInputType.text,
String? Function(String?)? validator,
String? prefixText,
}) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, color: const Color(0xFF056839)),
prefixText: prefixText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF056839), width: 2),
),
),
inputFormatters:
keyboardType == TextInputType.number
? [FilteringTextInputFormatter.digitsOnly]
: null,
);
}
Widget _buildScheduleDropdown() {
return InkWell(
onTap: () => _showScheduleSelector(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.calendar_today, color: const Color(0xFF056839)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pilih Jadwal Tanam',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 4),
Text(
_getSelectedScheduleText(),
style: const TextStyle(fontSize: 16),
),
],
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
String _getSelectedScheduleText() {
if (_isManualMode) {
return 'Manual';
}
if (_selectedScheduleId != null) {
try {
final selectedSchedule = _schedules.firstWhere(
(s) => s['id'] == _selectedScheduleId,
orElse: () => {'crop_name': 'Jadwal tidak ditemukan'},
);
return selectedSchedule['crop_name'] ?? 'Jadwal tidak ditemukan';
} catch (e) {
debugPrint('Error finding selected schedule: $e');
return 'Jadwal tidak ditemukan';
}
}
return 'Pilih Jadwal Tanam';
}
void _showScheduleSelector() {
try {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Pilih Jadwal Tanam',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 8),
Text(
'Jadwal yang tersedia: ${_schedules.length}',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 16),
Expanded(
child:
_schedules.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Belum ada jadwal tanam',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Gunakan mode manual untuk saat ini',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
)
: ListView.builder(
itemCount:
_schedules.length + 1, // +1 for Manual option
itemBuilder: (context, index) {
if (index == 0) {
// Manual option
return Card(
elevation: _isManualMode ? 2 : 0,
color:
_isManualMode
? const Color(0xFFE8F5E9)
: null,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(
0xFF056839,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.add,
color: Color(0xFF056839),
),
),
title: const Text('Input Manual'),
subtitle: const Text(
'Masukkan data secara manual',
),
trailing:
_isManualMode
? const Icon(
Icons.check_circle,
color: Color(0xFF056839),
)
: null,
onTap: () {
try {
setState(() {
_selectedScheduleId = null;
_selectedSchedule = null;
_isManualMode = true;
});
Navigator.pop(context);
// Use setState again to ensure UI updates properly
setState(() {
_setDefaultValues();
});
ScaffoldMessenger.of(
context,
).showSnackBar(
const SnackBar(
content: Text(
'Mode manual dipilih. Semua field dikosongkan.',
),
duration: Duration(seconds: 2),
),
);
debugPrint('Selected manual mode');
} catch (e) {
debugPrint(
'Error selecting manual mode: $e',
);
}
},
),
);
} else {
// Schedule options
final schedule = _schedules[index - 1];
final isSelected =
!_isManualMode &&
_selectedScheduleId == schedule['id'];
// Format dates if available
String dateInfo = '';
if (schedule['start_date'] != null &&
schedule['end_date'] != null) {
try {
final startDate = DateTime.parse(
schedule['start_date'],
);
final endDate = DateTime.parse(
schedule['end_date'],
);
dateInfo =
'${startDate.day}/${startDate.month}/${startDate.year} - ${endDate.day}/${endDate.month}/${endDate.year}';
} catch (e) {
dateInfo = 'Tanggal tidak valid';
debugPrint('Error parsing dates: $e');
}
}
return Card(
elevation: isSelected ? 2 : 0,
color:
isSelected
? const Color(0xFFE8F5E9)
: null,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(
0xFF056839,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.eco,
color: Color(0xFF056839),
),
),
title: Text(
schedule['crop_name'] ?? 'Tanaman',
),
subtitle: Text(
dateInfo.isNotEmpty
? 'Plot: ${schedule['plot'] ?? '-'}$dateInfo'
: 'Plot: ${schedule['plot'] ?? '-'}',
style: const TextStyle(fontSize: 12),
),
trailing:
isSelected
? const Icon(
Icons.check_circle,
color: Color(0xFF056839),
)
: null,
onTap: () {
try {
setState(() {
_selectedScheduleId = schedule['id'];
_selectedSchedule = schedule;
_isManualMode = false;
});
Navigator.pop(context);
// Use setState again to ensure UI updates properly
setState(() {
_updateFormFieldsFromSelectedSchedule();
});
debugPrint(
'Selected schedule: ${schedule['id']} - ${schedule['crop_name']}',
);
} catch (e) {
debugPrint(
'Error selecting schedule: $e',
);
}
},
),
);
}
},
),
),
],
),
);
},
);
} catch (e) {
debugPrint('Error showing schedule selector: $e');
// Fallback to simple dialog if bottom sheet fails
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Pilih Jadwal Tanam'),
content: const Text(
'Terjadi kesalahan saat menampilkan jadwal. Silakan coba lagi nanti.',
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_isManualMode = true;
_selectedScheduleId = null;
_selectedSchedule = null;
_setDefaultValues();
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Mode manual dipilih. Semua field dikosongkan.',
),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Gunakan Mode Manual'),
),
],
),
);
}
}
}