MIF_E31222656/lib/screens/calendar/add_schedule_dialog.dart

4733 lines
183 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:tugas_akhir_supabase/screens/calendar/field_management_screen.dart';
class AddScheduleDialog extends StatefulWidget {
final Function(Map<String, dynamic>)? onScheduleAdded;
final List<Map<String, dynamic>> existingSchedules;
final DateTime? initialStartDate;
final Map<String, dynamic>? scheduleToEdit;
const AddScheduleDialog({
super.key,
this.onScheduleAdded,
required this.existingSchedules,
this.initialStartDate,
this.scheduleToEdit,
});
@override
State<AddScheduleDialog> createState() => _AddScheduleDialogState();
}
class _AddScheduleDialogState extends State<AddScheduleDialog> {
// Form step tracking
int _currentStep = 0;
final _formKey = GlobalKey<FormState>();
final _scrollController = ScrollController();
// Track validation state for each field
bool _seedCostValid = false;
bool _fertilizerCostValid = false;
bool _pesticideCostValid = false;
bool _irrigationCostValid = false;
bool _laborCostValid = false;
bool _expectedYieldValid = false;
// Validation state untuk field tambahan
bool _landPreparationCostValid = false;
bool _toolsEquipmentCostValid = false;
bool _transportationCostValid = false;
bool _postHarvestCostValid = false;
bool _otherCostValid = false;
// Existing controllers
final _cropNameController = TextEditingController();
final _notesController = TextEditingController();
final _seedCostController = TextEditingController();
final _fertilizerCostController = TextEditingController();
final _pesticideCostController = TextEditingController();
final _irrigationCostController = TextEditingController();
final _expectedYieldController = TextEditingController();
// New controllers for additional fields
final _varietyController = TextEditingController();
final _soilTypeController = TextEditingController();
final _waterSourceController = TextEditingController();
final _plantingMethodController = TextEditingController();
final _plantingDistanceController = TextEditingController();
final _previousCropController = TextEditingController();
final _laborCostController = TextEditingController();
final _weatherNotesController = TextEditingController();
// Controller tambahan untuk analisis hasil panen
final _landPreparationCostController = TextEditingController();
final _toolsEquipmentCostController = TextEditingController();
final _transportationCostController = TextEditingController();
final _postHarvestCostController = TextEditingController();
final _otherCostController = TextEditingController();
// Tambahan untuk jarak tanam dua kolom
final _plantingDistanceRowController = TextEditingController();
final _plantingDistanceColController = TextEditingController();
final _plantingDistanceRowFocus = FocusNode();
final _plantingDistanceColFocus = FocusNode();
String? _plantingDistanceError;
// Tambahan untuk multi-select pupuk
final Set<String> _selectedFertilizers = {};
final Map<String, TextEditingController> _fertilizerDosageControllers = {};
DateTime _startDate = DateTime.now();
DateTime _endDate = DateTime.now().add(const Duration(days: 90));
String? _selectedFieldId;
int? _selectedPlot;
Map<String, dynamic>? _selectedFieldData;
double?
_usedAreaForSelectedPetak; // Tambahan: area yang sudah digunakan di petak terpilih
double?
_availableAreaForSelectedPetak; // Tambahan: sisa area yang tersedia di petak terpilih
double?
_availableAreaForSelectedPetakDisplay; // Untuk display sisa lahan setelah dikurangi input user
final _usedAreaController =
TextEditingController(); // Tambahan: controller untuk input lahan yang digunakan
final _usedAreaFocus = FocusNode();
// Default selections for new dropdown fields
String _selectedSoilType = 'Lempung';
String _selectedWaterSource = 'Irigasi';
String _selectedPlantingMethod = 'Konvensional';
String _selectedPlantingSeason = 'Musim Kemarau';
// Dropdown untuk kondisi analisis panen
String _selectedWeatherCondition = 'Normal';
String _selectedIrrigationType = 'Irigasi Teknis';
String _selectedFertilizerType = 'NPK';
List<Map<String, dynamic>> _fields = [];
bool _isLoading = false;
bool _isLoadingFields = true;
bool _isSaved = false;
bool _isEditMode = false;
final List<String> _cropOptions = [
'Padi',
'Jagung',
'Kedelai',
'Cabai',
'Tomat',
'Bawang',
'Kopi',
'Tembakau',
'Lainnya',
];
// Options for new dropdown fields
final List<String> _soilTypeOptions = [
'Lempung',
'Pasir',
'Liat',
'Lempung Berpasir',
'Liat Berpasir',
'Lainnya',
];
final List<String> _waterSourceOptions = [
'Irigasi',
'Hujan',
'Pompa',
'Sumur',
'Mata Air',
'Lainnya',
];
final List<String> _plantingMethodOptions = [
'Konvensional',
'Jajar Legowo',
'SRI',
'Tabela',
'Tapin',
'Lainnya',
];
final List<String> _plantingSeasonOptions = [
'Musim Hujan',
'Musim Kemarau',
'Peralihan Hujan ke Kemarau',
'Peralihan Kemarau ke Hujan',
];
// Options tambahan untuk analisis hasil panen
final List<String> _weatherConditionOptions = [
'Normal',
'Kekeringan',
'Banjir',
'Curah Hujan Tinggi',
];
final List<String> _irrigationTypeOptions = [
'Irigasi Teknis',
'Irigasi Setengah Teknis',
'Irigasi Sederhana',
'Tadah Hujan',
];
final List<String> _fertilizerTypeOptions = [
'NPK',
'Urea',
'TSP/SP-36',
'KCL',
'Organik',
'Campuran',
];
// Variety options based on crop type
final Map<String, List<String>> _varietiesByType = {
'Padi': [
'Ciherang',
'IR64',
'Situ Bagendit',
'Mekongga',
'Inpari 32',
'Mentik Wangi',
'Lainnya',
],
'Jagung': ['BISI-18', 'Pioneer P21', 'NK212', 'Pertiwi-3', 'Lainnya'],
'Kedelai': ['Anjasmoro', 'Grobogan', 'Dena 1', 'Lainnya'],
'Cabai': ['Lado F1', 'TM 999', 'Hot Beauty', 'Lainnya'],
'Tomat': ['Servo F1', 'Permata', 'Tymoti F1', 'Lainnya'],
'Bawang': ['Bima Brebes', 'Thailand', 'Tajuk', 'Lainnya'],
'Kopi': ['Arabika', 'Robusta', 'Lainnya'],
'Tembakau': ['Maesan 1', 'Maesan 2', 'Kasturi', 'Kemloko', 'Lainnya'],
};
// FocusNode untuk mengelola fokus keyboard
final _cropNameFocus = FocusNode();
final _notesFocus = FocusNode();
final _seedCostFocus = FocusNode();
final _fertilizerCostFocus = FocusNode();
final _pesticideCostFocus = FocusNode();
final _irrigationCostFocus = FocusNode();
final _expectedYieldFocus = FocusNode();
final _laborCostFocus = FocusNode();
// FocusNode tambahan untuk field baru
final _landPreparationCostFocus = FocusNode();
final _toolsEquipmentCostFocus = FocusNode();
final _transportationCostFocus = FocusNode();
final _postHarvestCostFocus = FocusNode();
final _otherCostFocus = FocusNode();
String? _varietyError;
// Tambahkan error state untuk semua field wajib
String? _cropNameError;
String? _startDateError;
String? _endDateError;
String? _fieldError;
String? _plotError;
String? _usedAreaError;
String? _soilTypeError;
String? _waterSourceError;
String? _previousCropError;
String? _plantingMethodError;
String? _weatherNotesError;
String? _weatherConditionError;
String? _irrigationTypeError;
String? _fertilizerTypeError;
String? _seedCostError;
String? _fertilizerCostError;
String? _pesticideCostError;
String? _irrigationCostError;
String? _laborCostError;
String? _landPreparationCostError;
String? _toolsEquipmentCostError;
String? _transportationCostError;
String? _postHarvestCostError;
String? _otherCostError;
String? _expectedYieldError;
// Tambahkan variabel untuk pupuk kustom
final _customFertilizerNameController = TextEditingController();
final _customFertilizerDosageController = TextEditingController();
bool _showCustomFertilizerInput = false;
@override
void initState() {
super.initState();
// Persiapan data dan controller
_setupDateRange();
_initControllers();
_setupFocusNodes();
if (widget.scheduleToEdit != null) {
_isEditMode = true;
Future.microtask(() => _initializeWithExistingData());
}
// Inisialisasi _selectedFertilizers dengan nilai default
if (_selectedFertilizerType.isNotEmpty) {
_selectedFertilizers.add(_selectedFertilizerType);
}
// Load fields dengan delay kecil untuk memastikan widget sudah terpasang
Future.microtask(() {
_loadFields();
});
}
@override
void dispose() {
// Dispose semua controller
_cropNameController.dispose();
_notesController.dispose();
_seedCostController.dispose();
_fertilizerCostController.dispose();
_pesticideCostController.dispose();
_irrigationCostController.dispose();
_expectedYieldController.dispose();
_laborCostController.dispose();
// Dispose controller baru
_varietyController.dispose();
_soilTypeController.dispose();
_waterSourceController.dispose();
_plantingMethodController.dispose();
_plantingDistanceController.dispose();
_previousCropController.dispose();
_weatherNotesController.dispose();
// Dispose controller tambahan untuk analisis hasil panen
_landPreparationCostController.dispose();
_toolsEquipmentCostController.dispose();
_transportationCostController.dispose();
_postHarvestCostController.dispose();
_otherCostController.dispose();
// Dispose controller pupuk
for (final controller in _fertilizerDosageControllers.values) {
controller.dispose();
}
_customFertilizerNameController.dispose();
_customFertilizerDosageController.dispose();
// Dispose focus nodes
_cropNameFocus.dispose();
_notesFocus.dispose();
_seedCostFocus.dispose();
_fertilizerCostFocus.dispose();
_pesticideCostFocus.dispose();
_irrigationCostFocus.dispose();
_expectedYieldFocus.dispose();
_laborCostFocus.dispose();
_landPreparationCostFocus.dispose();
_toolsEquipmentCostFocus.dispose();
_transportationCostFocus.dispose();
_postHarvestCostFocus.dispose();
_otherCostFocus.dispose();
_scrollController.dispose();
_usedAreaController.dispose(); // Tambahan
_plantingDistanceRowController.dispose();
_plantingDistanceColController.dispose();
_plantingDistanceRowFocus.dispose();
_plantingDistanceColFocus.dispose();
_usedAreaFocus.dispose();
super.dispose();
}
// Metode bantuan untuk scroll ke field aktif
void _scrollToField(FocusNode focusNode) {
if (!focusNode.hasFocus || !_scrollController.hasClients) return;
// Delay sedikit untuk memastikan keyboard sudah muncul
Future.delayed(const Duration(milliseconds: 300), () {
if (!mounted || !_scrollController.hasClients) return;
// Scroll to bottom untuk memastikan field yang aktif terlihat
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
// Fungsi untuk setup focus nodes
void _setupFocusNodes() {
// Tambahkan listener ke setiap focus node
_cropNameFocus.addListener(() => _scrollToField(_cropNameFocus));
_notesFocus.addListener(() => _scrollToField(_notesFocus));
_seedCostFocus.addListener(() => _scrollToField(_seedCostFocus));
_fertilizerCostFocus.addListener(
() => _scrollToField(_fertilizerCostFocus),
);
_pesticideCostFocus.addListener(() => _scrollToField(_pesticideCostFocus));
_irrigationCostFocus.addListener(
() => _scrollToField(_irrigationCostFocus),
);
_expectedYieldFocus.addListener(() => _scrollToField(_expectedYieldFocus));
_laborCostFocus.addListener(() => _scrollToField(_laborCostFocus));
_landPreparationCostFocus.addListener(
() => _scrollToField(_landPreparationCostFocus),
);
_toolsEquipmentCostFocus.addListener(
() => _scrollToField(_toolsEquipmentCostFocus),
);
_transportationCostFocus.addListener(
() => _scrollToField(_transportationCostFocus),
);
_postHarvestCostFocus.addListener(
() => _scrollToField(_postHarvestCostFocus),
);
_otherCostFocus.addListener(() => _scrollToField(_otherCostFocus));
}
void _scrollToTop() {
// Hapus focus untuk menyembunyikan keyboard
FocusScope.of(context).unfocus();
}
String _formatCostForDisplay(dynamic cost) {
if (cost == null) return '';
String costStr = cost.toString();
// Remove .0 for whole numbers
if (costStr.endsWith('.0')) {
return costStr.substring(0, costStr.length - 2);
}
return costStr;
}
Future<void> _initializeWithExistingData() async {
try {
final schedule = widget.scheduleToEdit!;
_cropNameController.text = schedule['crop_name'] ?? '';
_notesController.text = schedule['notes'] ?? '';
// Use the formatter to hide zero values
_seedCostController.text = _formatCostForDisplay(schedule['seed_cost']);
_fertilizerCostController.text = _formatCostForDisplay(
schedule['fertilizer_cost'],
);
_pesticideCostController.text = _formatCostForDisplay(
schedule['pesticide_cost'],
);
_irrigationCostController.text = _formatCostForDisplay(
schedule['irrigation_cost'],
);
_expectedYieldController.text = _formatCostForDisplay(
schedule['expected_yield'],
);
_laborCostController.text = _formatCostForDisplay(schedule['labor_cost']);
// Initialize new fields if they exist
_varietyController.text = schedule['variety_name'] ?? '';
_soilTypeController.text = schedule['soil_type'] ?? '';
_waterSourceController.text = schedule['water_source'] ?? '';
_plantingMethodController.text = schedule['planting_method'] ?? '';
_plantingDistanceController.text = schedule['planting_distance'] ?? '';
_previousCropController.text = schedule['previous_crop'] ?? '';
_weatherNotesController.text = schedule['weather_notes'] ?? '';
// Inisialisasi controller tambahan untuk analisis hasil panen
_landPreparationCostController.text = _formatCostForDisplay(
schedule['land_preparation_cost'],
);
_toolsEquipmentCostController.text = _formatCostForDisplay(
schedule['tools_equipment_cost'],
);
_transportationCostController.text = _formatCostForDisplay(
schedule['transportation_cost'],
);
_postHarvestCostController.text = _formatCostForDisplay(
schedule['post_harvest_cost'],
);
_otherCostController.text = _formatCostForDisplay(schedule['other_cost']);
// Set selected values for dropdowns if they exist
if (schedule['soil_type'] != null &&
_soilTypeOptions.contains(schedule['soil_type'])) {
_selectedSoilType = schedule['soil_type'];
}
if (schedule['water_source'] != null &&
_waterSourceOptions.contains(schedule['water_source'])) {
_selectedWaterSource = schedule['water_source'];
}
if (schedule['planting_method'] != null &&
_plantingMethodOptions.contains(schedule['planting_method'])) {
_selectedPlantingMethod = schedule['planting_method'];
}
if (schedule['planting_season'] != null &&
_plantingSeasonOptions.contains(schedule['planting_season'])) {
_selectedPlantingSeason = schedule['planting_season'];
}
// Set selected values untuk dropdown tambahan
if (schedule['weather_condition'] != null &&
_weatherConditionOptions.contains(schedule['weather_condition'])) {
_selectedWeatherCondition = schedule['weather_condition'];
}
if (schedule['irrigation_type'] != null &&
_irrigationTypeOptions.contains(schedule['irrigation_type'])) {
_selectedIrrigationType = schedule['irrigation_type'];
}
// Inisialisasi _selectedFertilizers berdasarkan fertilizer_type
if (schedule['fertilizer_type'] != null) {
if (schedule['fertilizer_type'] is String) {
_selectedFertilizers.add(schedule['fertilizer_type']);
// Tambahkan controller untuk takaran pupuk yang sudah ada
if (!_fertilizerDosageControllers.containsKey(
schedule['fertilizer_type'],
)) {
_fertilizerDosageControllers[schedule['fertilizer_type']] =
TextEditingController(text: '300'); // Default value
}
} else if (schedule['fertilizer_type'] is List) {
for (final type in schedule['fertilizer_type']) {
_selectedFertilizers.add(type);
// Tambahkan controller untuk setiap jenis pupuk
if (!_fertilizerDosageControllers.containsKey(type)) {
_fertilizerDosageControllers[type] = TextEditingController(
text: '300',
); // Default value
}
}
}
}
// Jika ada data takaran pupuk dalam format JSON, inisialisasi controller
if (schedule['fertilizer_dosages'] != null) {
try {
final Map<String, dynamic> dosages =
schedule['fertilizer_dosages'] is String
? jsonDecode(schedule['fertilizer_dosages'])
: schedule['fertilizer_dosages'];
dosages.forEach((type, dosage) {
if (_selectedFertilizers.contains(type)) {
_fertilizerDosageControllers[type]?.text = dosage.toString();
}
});
} catch (e) {
debugPrint('Error parsing fertilizer_dosages: $e');
}
}
if (schedule['fertilizer_type'] != null &&
_fertilizerTypeOptions.contains(schedule['fertilizer_type'])) {
_selectedFertilizerType = schedule['fertilizer_type'];
}
if (schedule['start_date'] != null) {
_startDate = DateTime.parse(schedule['start_date']);
}
if (schedule['end_date'] != null) {
_endDate = DateTime.parse(schedule['end_date']);
}
_selectedFieldId = schedule['field_id'];
_selectedPlot = schedule['plot'];
// Tambahan: isi controller area yang digunakan saat edit
if (schedule['area_size'] != null) {
final areaUsed = schedule['area_size'];
if (areaUsed is num) {
_usedAreaController.text =
areaUsed % 1 == 0
? areaUsed.toInt().toString()
: areaUsed.toString();
} else {
_usedAreaController.text = areaUsed.toString();
}
}
// Tambahan: update sisa lahan setelah controller diisi
await _updateAvailableAreaForSelectedPetak();
} catch (e) {
debugPrint('Error initializing data: $e');
}
}
Future<void> _loadFields() async {
if (!mounted) return;
setState(() => _isLoadingFields = true);
try {
// Pastikan hanya mengambil lahan milik user saat ini
final userId = Supabase.instance.client.auth.currentUser?.id;
if (userId == null) {
debugPrint('Error: User ID is null in _loadFields');
setState(() => _isLoadingFields = false);
return;
}
debugPrint('==== LOADING FIELDS DETAIL ====');
debugPrint('Loading fields for user: $userId');
debugPrint('Current timestamp: ${DateTime.now().toString()}');
// Tambahkan logs untuk memastikan query benar
final supabase = Supabase.instance.client;
debugPrint('Supabase instance active: ${supabase != null}');
debugPrint('Auth user valid: ${supabase.auth.currentUser != null}');
// Query ke tabel fields untuk mendapatkan data lahan user
final response = await supabase
.from('fields')
.select(
'id, name, user_id, plot_count, region, location, area_size, area_unit, ownership_type, owner_name, region_specific_data',
)
.eq('user_id', userId)
.order('created_at', ascending: false)
.timeout(const Duration(seconds: 15));
debugPrint('Fields response received. Type: ${response.runtimeType}');
debugPrint('Response length: ${response.length}');
if (response.isNotEmpty) {
debugPrint('First field data: ${response.first}');
} else {
debugPrint('No fields found for this user: $userId');
// Cek apakah user id di tabel fields sama dengan user yang sedang login
final anyFields = await supabase
.from('fields')
.select('id, name, user_id')
.limit(5)
.timeout(const Duration(seconds: 8));
if (anyFields.isNotEmpty) {
debugPrint(
'Some fields exist in the database. Checking user_id match:',
);
for (var field in anyFields) {
debugPrint(
'Field "${field['name'] ?? "Unknown"}" belongs to user: ${field['user_id']}',
);
debugPrint(
'Does it match current user? ${field['user_id'] == userId}',
);
}
}
}
if (!mounted) return;
setState(() {
// Transform respons dengan format yang benar
_fields = List<Map<String, dynamic>>.from(
response.map((field) {
// Buat salinan field untuk dimodifikasi
var transformedField = Map<String, dynamic>.from(field);
// Pastikan 'area' tersedia untuk widget yang membutuhkannya
if (field.containsKey('area_size') && !field.containsKey('area')) {
transformedField['area'] = field['area_size'];
}
debugPrint('Transformed field: $transformedField');
return transformedField;
}),
);
if (_fields.isNotEmpty) {
debugPrint('Found ${_fields.length} fields for user');
if (_isEditMode && _selectedFieldId != null) {
_selectedFieldData = _fields.firstWhere(
(field) => field['id'] == _selectedFieldId,
orElse: () => _fields.first,
);
} else {
_selectedFieldId = _fields.first['id'];
_selectedFieldData = _fields.first;
}
} else {
debugPrint('No fields available after transformation');
}
_isLoadingFields = false;
});
} catch (e) {
debugPrint('==== ERROR LOADING FIELDS ====');
debugPrint('Error type: ${e.runtimeType}');
debugPrint('Error loading fields: $e');
debugPrint('Stack trace: ${StackTrace.current}');
if (!mounted) return;
setState(() => _isLoadingFields = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.toString().contains('timeout')
? 'Koneksi timeout. Periksa koneksi internet Anda.'
: 'Gagal memuat data lahan: ${e.toString()}',
),
backgroundColor: Colors.red,
),
);
}
}
List<int> _getTakenPlots(
Map<String, dynamic> fieldData,
DateTime startDate,
DateTime endDate,
) {
if (fieldData['id'] == null) return [];
final takenPlotsSet = <int>{};
for (var schedule in widget.existingSchedules) {
if (schedule['field_id'] != fieldData['id']) continue;
if (_isEditMode && schedule['id'] == widget.scheduleToEdit!['id'])
continue;
final scheduleStartDate = DateTime.tryParse(schedule['start_date'] ?? '');
final scheduleEndDate = DateTime.tryParse(schedule['end_date'] ?? '');
if (scheduleStartDate == null || scheduleEndDate == null) continue;
final overlap =
(startDate.isBefore(scheduleEndDate) ||
startDate.isAtSameMomentAs(scheduleEndDate)) &&
(endDate.isAfter(scheduleStartDate) ||
endDate.isAtSameMomentAs(scheduleStartDate));
if (overlap && schedule['plot'] is int) {
takenPlotsSet.add(schedule['plot'] as int);
}
}
return takenPlotsSet.toList();
}
Future<void> _pickDate(bool isStartDate) async {
final picked = await showDatePicker(
context: context,
initialDate: isStartDate ? _startDate : _endDate,
firstDate:
isStartDate
? DateTime.now().subtract(const Duration(days: 365))
: _startDate,
lastDate: DateTime.now().add(const Duration(days: 730)),
);
if (picked != null && mounted) {
setState(() {
if (isStartDate) {
_startDate = picked;
if (_endDate.isBefore(_startDate)) {
_endDate = _startDate.add(const Duration(days: 1));
}
} else {
_endDate = picked;
}
_selectedPlot = null;
});
}
}
double _safeParseDouble(String? text) {
if (text == null || text.trim().isEmpty) return 0.0;
try {
final cleanText = text.trim().replaceAll(',', '.');
return double.tryParse(cleanText) ?? 0.0;
} catch (e) {
return 0.0;
}
}
Future<void> _submit() async {
debugPrint('==== _submit method called ====');
// Check if already loading or saved
if (_isLoading) {
debugPrint('_submit aborted: Already loading');
return;
}
if (_isSaved) {
debugPrint('_submit aborted: Already saved');
return;
}
// === VALIDASI MANUAL FIELD WAJIB ===
if (_cropNameController.text.isEmpty) {
FocusScope.of(context).requestFocus(_cropNameFocus);
_scrollToField(_cropNameFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Nama tanaman harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_selectedFieldId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Lahan harus dipilih!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_usedAreaController.text.isEmpty) {
FocusScope.of(context).requestFocus(_usedAreaFocus);
_scrollToField(_usedAreaFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Luas lahan yang digunakan harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_seedCostController.text.isEmpty) {
FocusScope.of(context).requestFocus(_seedCostFocus);
_scrollToField(_seedCostFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Biaya bibit harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_fertilizerCostController.text.isEmpty) {
FocusScope.of(context).requestFocus(_fertilizerCostFocus);
_scrollToField(_fertilizerCostFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Biaya pupuk harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_pesticideCostController.text.isEmpty) {
FocusScope.of(context).requestFocus(_pesticideCostFocus);
_scrollToField(_pesticideCostFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Biaya pestisida harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_irrigationCostController.text.isEmpty) {
FocusScope.of(context).requestFocus(_irrigationCostFocus);
_scrollToField(_irrigationCostFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Biaya irigasi harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_expectedYieldController.text.isEmpty) {
FocusScope.of(context).requestFocus(_expectedYieldFocus);
_scrollToField(_expectedYieldFocus);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Estimasi hasil panen harus diisi!'),
backgroundColor: Colors.red,
),
);
return;
}
// Validasi pupuk
if (_selectedFertilizers.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Minimal satu jenis pupuk harus dipilih!'),
backgroundColor: Colors.red,
),
);
return;
}
// ... tambahkan validasi untuk field wajib lain jika perlu ...
// Validasi form (untuk validasi tambahan dari Form widget)
final isValid = _formKey.currentState?.validate() ?? false;
debugPrint('Form validation result: $isValid');
if (!isValid) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mohon lengkapi data yang diperlukan.'),
backgroundColor: Colors.red,
),
);
return;
}
// Begin loading
debugPrint('Setting loading state to true');
if (mounted) setState(() => _isLoading = true);
try {
debugPrint('Getting current user ID');
final userId = Supabase.instance.client.auth.currentUser?.id;
if (userId == null) {
throw Exception('User tidak ditemukan.');
}
debugPrint('Current user ID: $userId');
// Get area from selected field if available
double? fieldArea;
if (_selectedFieldData != null) {
// Coba ambil dari area_size dulu, jika tidak ada baru ambil dari area
if (_selectedFieldData!.containsKey('area_size') &&
_selectedFieldData!['area_size'] != null) {
fieldArea =
double.tryParse(_selectedFieldData!['area_size'].toString()) ??
0.0;
debugPrint(
'Found area_size in selected field: $fieldArea ${_selectedFieldData!['area_unit'] ?? ''}',
);
} else if (_selectedFieldData!.containsKey('area') &&
_selectedFieldData!['area'] != null) {
fieldArea =
double.tryParse(_selectedFieldData!['area'].toString()) ?? 0.0;
debugPrint('Found area in selected field: $fieldArea');
} else {
debugPrint('No area found in selected field: $_selectedFieldData');
}
} else {
debugPrint('Warning: _selectedFieldData is null');
}
// Simpan informasi wilayah dan unit budidaya spesifik region
Map<String, dynamic> regionSpecificDetails = {};
if (_selectedFieldData != null &&
_selectedFieldData!.containsKey('region') &&
_selectedFieldData!.containsKey('region_specific_data') &&
_selectedFieldData!['region_specific_data'] != null) {
final region = _selectedFieldData!['region'];
final regionData = _selectedFieldData!['region_specific_data'];
regionSpecificDetails['region'] = region;
// Salin data spesifik region untuk disimpan di jadwal tanam
switch (region) {
case 'Jawa':
if (regionData['sistem_petak'] != null) {
regionSpecificDetails['sistem_petak'] =
regionData['sistem_petak'];
}
if (regionData['jenis_irigasi'] != null) {
regionSpecificDetails['jenis_irigasi'] =
regionData['jenis_irigasi'];
}
break;
case 'Sumatera':
if (regionData['sistem_blok'] != null) {
regionSpecificDetails['sistem_blok'] = regionData['sistem_blok'];
}
if (regionData['jenis_tanah'] != null) {
regionSpecificDetails['jenis_tanah'] = regionData['jenis_tanah'];
}
break;
case 'Kalimantan':
if (regionData['sistem_ladang'] != null) {
regionSpecificDetails['sistem_ladang'] =
regionData['sistem_ladang'];
}
if (regionData['jarak_sungai'] != null) {
regionSpecificDetails['jarak_sungai'] =
regionData['jarak_sungai'];
}
break;
case 'Sulawesi':
if (regionData['sistem_kebun'] != null) {
regionSpecificDetails['sistem_kebun'] =
regionData['sistem_kebun'];
}
if (regionData['kontur_lahan'] != null) {
regionSpecificDetails['kontur_lahan'] =
regionData['kontur_lahan'];
}
break;
case 'Bali & Nusa Tenggara':
if (regionData['sistem_subak'] != null) {
regionSpecificDetails['sistem_subak'] =
regionData['sistem_subak'];
}
if (regionData['sumber_air'] != null) {
regionSpecificDetails['sumber_air'] = regionData['sumber_air'];
}
break;
case 'Maluku & Papua':
if (regionData['sistem_kebun'] != null) {
regionSpecificDetails['sistem_kebun'] =
regionData['sistem_kebun'];
}
if (regionData['tipe_hutan'] != null) {
regionSpecificDetails['tipe_hutan'] = regionData['tipe_hutan'];
}
break;
}
debugPrint('Region specific details: $regionSpecificDetails');
} else {
debugPrint('No region_specific_data available');
}
// Persiapkan data pupuk yang dipilih dan takarannya
final Map<String, dynamic> fertilizerDosages = {};
for (final type in _selectedFertilizers) {
final controller = _fertilizerDosageControllers[type];
if (controller != null) {
// Konversi dari per hektar ke per meter persegi
final dosagePerHa = double.tryParse(controller.text) ?? 0;
final dosagePerM2 = dosagePerHa / 10000;
fertilizerDosages[type] = dosagePerM2;
}
}
// Pilih pupuk utama untuk disimpan di kolom fertilizer_type (untuk kompatibilitas)
final mainFertilizerType =
_selectedFertilizers.isNotEmpty
? _selectedFertilizers.first
: _selectedFertilizerType;
// Simpan data pupuk dalam region_specific_details karena tidak ada kolom fertilizer_dosages
regionSpecificDetails['fertilizer_dosages'] = fertilizerDosages;
regionSpecificDetails['selected_fertilizers'] = List<String>.from(
_selectedFertilizers,
);
// Prepare data to save
debugPrint('Preparing data to save');
final data = {
'user_id': userId,
'crop_name': _cropNameController.text.trim(),
'start_date': _startDate.toIso8601String(),
'end_date': _endDate.toIso8601String(),
'field_id': _selectedFieldId,
'plot': _selectedPlot,
'area_size': _safeParseDouble(_usedAreaController.text),
'area_unit': _selectedFieldData?['area_unit'],
'notes':
_notesController.text.trim().isEmpty
? null
: _notesController.text.trim(),
'status': 'active',
'seed_cost': _safeParseDouble(_seedCostController.text),
'fertilizer_cost': _safeParseDouble(_fertilizerCostController.text),
'pesticide_cost': _safeParseDouble(_pesticideCostController.text),
'irrigation_cost': _safeParseDouble(_irrigationCostController.text),
'expected_yield': _safeParseDouble(_expectedYieldController.text),
// Data tambahan untuk variabel baru
'variety_name':
_varietyController.text.trim().isEmpty
? null
: _varietyController.text.trim(),
'soil_type': _selectedSoilType,
'water_source': _selectedWaterSource,
'planting_method': _selectedPlantingMethod,
'planting_season': _selectedPlantingSeason,
'planting_distance':
_plantingDistanceController.text.trim().isEmpty
? null
: _plantingDistanceController.text.trim(),
'previous_crop':
_previousCropController.text.trim().isEmpty
? null
: _previousCropController.text.trim(),
'labor_cost': _safeParseDouble(_laborCostController.text),
'weather_notes':
_weatherNotesController.text.trim().isEmpty
? null
: _weatherNotesController.text.trim(),
// Data tambahan untuk analisis hasil panen
'land_preparation_cost': _safeParseDouble(
_landPreparationCostController.text,
),
'tools_equipment_cost': _safeParseDouble(
_toolsEquipmentCostController.text,
),
'transportation_cost': _safeParseDouble(
_transportationCostController.text,
),
'post_harvest_cost': _safeParseDouble(_postHarvestCostController.text),
'other_cost': _safeParseDouble(_otherCostController.text),
'weather_condition': _selectedWeatherCondition,
'irrigation_type': _selectedIrrigationType,
'fertilizer_type': mainFertilizerType,
// Simpan data region-specific dalam kolom JSON terpisah
'region_specific_details': jsonEncode(regionSpecificDetails),
};
debugPrint('Data to insert/update: $data');
String scheduleId;
if (!_isEditMode) {
// Generate new ID for new records
final newId = const Uuid().v4();
data['id'] = newId;
data['created_at'] = DateTime.now().toIso8601String();
scheduleId = newId;
debugPrint('Generated new ID for schedule: $scheduleId');
} else {
// Use existing ID for edit mode
scheduleId = widget.scheduleToEdit!['id'];
debugPrint('Using existing ID for schedule update: $scheduleId');
}
// Save to database
final client = Supabase.instance.client;
debugPrint('Saving data to Supabase...');
if (_isEditMode) {
debugPrint('Updating existing record...');
await client.from('crop_schedules').update(data).eq('id', scheduleId);
debugPrint('Record updated successfully');
} else {
debugPrint('Inserting new record...');
await client.from('crop_schedules').insert(data);
debugPrint('Record inserted successfully');
}
// Check if still mounted before updating UI
if (!mounted) {
debugPrint('Widget no longer mounted, aborting UI updates');
return;
}
// Update UI state
debugPrint('Setting saved state to true');
setState(() => _isSaved = true);
setState(() => _isLoading = false);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Jadwal berhasil ${_isEditMode ? 'diperbarui' : 'disimpan'}.',
),
backgroundColor: Colors.green,
),
);
// Fetch and return the new data
debugPrint('Fetching the saved record...');
try {
final newScheduleData =
await client
.from('crop_schedules')
.select()
.eq('id', scheduleId)
.single();
debugPrint('Record fetched successfully: ${newScheduleData != null}');
if (widget.onScheduleAdded != null) {
debugPrint('Calling onScheduleAdded callback');
widget.onScheduleAdded!(newScheduleData);
}
} catch (fetchError) {
debugPrint('Error fetching saved record: $fetchError');
// Continue with closing the dialog even if fetch fails
}
// Close dialog with a short delay
debugPrint('Closing dialog...');
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e, stackTrace) {
// Handle errors
debugPrint('==== ERROR IN _submit ====');
debugPrint('Error type: ${e.runtimeType}');
debugPrint('Error message: $e');
debugPrint('Stack trace:');
debugPrint(stackTrace.toString());
// Reset loading state
if (mounted) setState(() => _isLoading = false);
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menyimpan jadwal: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
action: SnackBarAction(
label: 'Coba Lagi',
onPressed: _submit,
textColor: Colors.white,
),
),
);
}
}
@override
Widget build(BuildContext context) {
List<int> availablePlots = [];
if (_selectedFieldData != null &&
_selectedFieldData!['plot_count'] != null) {
final int plotCount = _selectedFieldData!['plot_count'] as int;
final takenPlots = _getTakenPlots(
_selectedFieldData!,
_startDate,
_endDate,
);
availablePlots =
List.generate(
plotCount,
(i) => i + 1,
).where((p) => !takenPlots.contains(p)).toList();
if (_selectedPlot != null && !availablePlots.contains(_selectedPlot)) {
_selectedPlot = null;
}
}
// Gunakan MediaQuery untuk mendapatkan ukuran keyboard
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
return Material(
color: Colors.transparent,
child: Container(
height: MediaQuery.of(context).size.height * 0.9,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isEditMode ? 'Edit Jadwal Tanam' : 'Tambah Jadwal Tanam',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Konten form yang dapat di-scroll
Expanded(
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.only(bottom: keyboardHeight + 20),
child: Form(
key: _formKey,
child: Stepper(
physics: const ClampingScrollPhysics(),
currentStep: _currentStep,
controlsBuilder: (context, details) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
children: [
if (_currentStep > 0)
Expanded(
child: OutlinedButton(
onPressed: () {
if (_currentStep > 0) {
setState(() => _currentStep--);
}
},
child: const Text('Sebelumnya'),
),
),
if (_currentStep > 0) const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed:
_isLoading
? null
: () {
// Validasi per step
bool valid = true;
String? errorMsg;
// Step 0: Informasi dasar
if (_currentStep == 0) {
if (_cropNameController
.text
.isEmpty) {
valid = false;
errorMsg =
'Nama tanaman harus diisi!';
setState(
() =>
_cropNameError =
'Nama tanaman wajib diisi',
);
FocusScope.of(
context,
).requestFocus(_cropNameFocus);
} else {
setState(
() => _cropNameError = null,
);
}
if (_varietyController
.text
.isEmpty) {
valid = false;
errorMsg =
'Varietas tanaman harus dipilih!';
setState(
() =>
_varietyError =
'Varietas wajib diisi',
);
} else {
setState(
() => _varietyError = null,
);
}
setState(
() => _startDateError = null,
);
setState(
() => _endDateError = null,
);
}
// Step 1: Detail lahan
else if (_currentStep == 1) {
if (_selectedFieldId == null) {
valid = false;
errorMsg = 'Lahan harus dipilih!';
setState(
() =>
_fieldError =
'Lahan wajib dipilih',
);
} else {
setState(
() => _fieldError = null,
);
}
if (_selectedPlot == null) {
valid = false;
errorMsg =
'Plot/Petak/Unit harus dipilih!';
setState(
() =>
_plotError =
'Unit wajib dipilih',
);
} else {
setState(() => _plotError = null);
}
if (_usedAreaController
.text
.isEmpty) {
valid = false;
errorMsg =
'Luas lahan yang digunakan harus diisi!';
setState(
() =>
_usedAreaError =
'Luas lahan wajib diisi',
);
FocusScope.of(
context,
).requestFocus(_usedAreaFocus);
} else {
setState(
() => _usedAreaError = null,
);
}
if (_selectedFieldData != null &&
_selectedFieldData!['area_size'] ==
null) {
valid = false;
errorMsg =
'Luas lahan utama harus diisi!';
}
if (_selectedSoilType.isEmpty) {
valid = false;
errorMsg =
'Tipe tanah harus dipilih!';
setState(
() =>
_soilTypeError =
'Tipe tanah wajib dipilih',
);
} else {
setState(
() => _soilTypeError = null,
);
}
if (_selectedWaterSource.isEmpty) {
valid = false;
errorMsg =
'Sumber air harus dipilih!';
setState(
() =>
_waterSourceError =
'Sumber air wajib dipilih',
);
} else {
setState(
() => _waterSourceError = null,
);
}
if (_previousCropController
.text
.isEmpty) {
valid = false;
errorMsg =
'Tanaman sebelumnya harus diisi!';
setState(
() =>
_previousCropError =
'Tanaman sebelumnya wajib diisi',
);
} else {
setState(
() => _previousCropError = null,
);
}
}
// Step 2: Metode budidaya
else if (_currentStep == 2) {
if (_selectedPlantingMethod
.isEmpty) {
valid = false;
errorMsg =
'Metode tanam harus dipilih!';
setState(
() =>
_plantingMethodError =
'Metode tanam wajib dipilih',
);
} else {
setState(
() =>
_plantingMethodError = null,
);
}
if (_plantingDistanceRowController
.text
.isEmpty ||
_plantingDistanceColController
.text
.isEmpty) {
valid = false;
errorMsg =
'Jarak tanam harus diisi!';
setState(
() =>
_plantingDistanceError =
'Jarak tanam wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_plantingDistanceRowController
.text
.isEmpty
? _plantingDistanceRowFocus
: _plantingDistanceColFocus,
);
} else {
setState(
() =>
_plantingDistanceError =
null,
);
}
if (_weatherNotesController
.text
.isEmpty) {
valid = false;
errorMsg =
'Catatan iklim/cuaca harus diisi!';
setState(
() =>
_weatherNotesError =
'Catatan wajib diisi',
);
} else {
setState(
() => _weatherNotesError = null,
);
}
}
// Step 3: Kondisi tanam
else if (_currentStep == 3) {
if (_selectedWeatherCondition
.isEmpty) {
valid = false;
errorMsg =
'Kondisi cuaca harus dipilih!';
setState(
() =>
_weatherConditionError =
'Kondisi cuaca wajib dipilih',
);
} else {
setState(
() =>
_weatherConditionError =
null,
);
}
if (_selectedIrrigationType
.isEmpty) {
valid = false;
errorMsg =
'Jenis irigasi harus dipilih!';
setState(
() =>
_irrigationTypeError =
'Jenis irigasi wajib dipilih',
);
} else {
setState(
() =>
_irrigationTypeError = null,
);
}
if (_selectedFertilizerType
.isEmpty) {
valid = false;
errorMsg =
'Jenis pupuk harus dipilih!';
setState(
() =>
_fertilizerTypeError =
'Jenis pupuk wajib dipilih',
);
} else {
setState(
() =>
_fertilizerTypeError = null,
);
}
}
// Step 4: Estimasi biaya
else if (_currentStep == 4) {
if (_seedCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya bibit harus diisi!';
setState(
() =>
_seedCostError =
'Biaya bibit wajib diisi',
);
FocusScope.of(
context,
).requestFocus(_seedCostFocus);
} else {
setState(
() => _seedCostError = null,
);
}
if (_fertilizerCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya pupuk harus diisi!';
setState(
() =>
_fertilizerCostError =
'Biaya pupuk wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_fertilizerCostFocus,
);
} else {
setState(
() =>
_fertilizerCostError = null,
);
}
if (_pesticideCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya pestisida harus diisi!';
setState(
() =>
_pesticideCostError =
'Biaya pestisida wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_pesticideCostFocus,
);
} else {
setState(
() =>
_pesticideCostError = null,
);
}
if (_irrigationCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya irigasi harus diisi!';
setState(
() =>
_irrigationCostError =
'Biaya irigasi wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_irrigationCostFocus,
);
} else {
setState(
() =>
_irrigationCostError = null,
);
}
if (_laborCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya tenaga kerja harus diisi!';
setState(
() =>
_laborCostError =
'Biaya tenaga kerja wajib diisi',
);
FocusScope.of(
context,
).requestFocus(_laborCostFocus);
} else {
setState(
() => _laborCostError = null,
);
}
}
// Step 5: Biaya tambahan
else if (_currentStep == 5) {
if (_landPreparationCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya persiapan lahan harus diisi!';
setState(
() =>
_landPreparationCostError =
'Biaya persiapan lahan wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_landPreparationCostFocus,
);
} else {
setState(
() =>
_landPreparationCostError =
null,
);
}
if (_toolsEquipmentCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Sewa alat & peralatan harus diisi!';
setState(
() =>
_toolsEquipmentCostError =
'Sewa alat & peralatan wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_toolsEquipmentCostFocus,
);
} else {
setState(
() =>
_toolsEquipmentCostError =
null,
);
}
if (_transportationCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya transportasi harus diisi!';
setState(
() =>
_transportationCostError =
'Biaya transportasi wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_transportationCostFocus,
);
} else {
setState(
() =>
_transportationCostError =
null,
);
}
if (_postHarvestCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya pasca panen harus diisi!';
setState(
() =>
_postHarvestCostError =
'Biaya pasca panen wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_postHarvestCostFocus,
);
} else {
setState(
() =>
_postHarvestCostError =
null,
);
}
if (_otherCostController
.text
.isEmpty) {
valid = false;
errorMsg =
'Biaya lain-lain harus diisi!';
setState(
() =>
_otherCostError =
'Biaya lain-lain wajib diisi',
);
FocusScope.of(
context,
).requestFocus(_otherCostFocus);
} else {
setState(
() => _otherCostError = null,
);
}
}
// Step 6: Estimasi hasil
else if (_currentStep == 6) {
if (_expectedYieldController
.text
.isEmpty) {
valid = false;
errorMsg =
'Estimasi hasil panen harus diisi!';
setState(
() =>
_expectedYieldError =
'Estimasi hasil panen wajib diisi',
);
FocusScope.of(
context,
).requestFocus(
_expectedYieldFocus,
);
} else {
setState(
() =>
_expectedYieldError = null,
);
}
}
if (!valid) {
if (errorMsg != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(errorMsg),
backgroundColor: Colors.red,
),
);
}
return;
}
if (_currentStep < 6) {
setState(() => _currentStep++);
Future.delayed(
const Duration(milliseconds: 50),
() {
if (_scrollController
.hasClients) {
_scrollController.animateTo(
0,
duration: const Duration(
milliseconds: 200,
),
curve: Curves.easeOut,
);
}
},
);
} else {
Future.microtask(() => _submit());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
),
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
_currentStep < 6
? 'Lanjut'
: 'Simpan',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
},
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 100,
),
onStepContinue:
null, // Tidak perlu karena sudah ditangani di controlsBuilder
onStepCancel:
null, // Tidak perlu karena sudah ditangani di controlsBuilder
steps: [
Step(
title: Text('Informasi Dasar'),
content: _buildBasicInfoStep(),
isActive: _currentStep >= 0,
),
Step(
title: Text('Detail Lahan'),
content: _buildLandDetailsStep(availablePlots),
isActive: _currentStep >= 1,
),
Step(
title: Text('Metode Budidaya'),
content: _buildCultivationMethodStep(),
isActive: _currentStep >= 2,
),
Step(
title: Text('Kondisi Tanam'),
content: _buildPlantingConditionsStep(),
isActive: _currentStep >= 3,
),
Step(
title: Text('Estimasi Biaya'),
content: _buildCostEstimationStep(),
isActive: _currentStep >= 4,
),
Step(
title: Text('Biaya Tambahan'),
content: _buildAdditionalCostsStep(),
isActive: _currentStep >= 5,
),
Step(
title: Text('Estimasi Hasil'),
content: _buildYieldEstimationStep(),
isActive: _currentStep >= 6,
),
],
),
),
),
),
],
),
),
);
}
// Step 1: Informasi dasar (tanggal dan tanaman)
Widget _buildBasicInfoStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Periode Tanam'),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildDateField(
'Tanggal Mulai',
_startDate,
() => _pickDate(true),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildDateField(
'Tanggal Selesai',
_endDate,
() => _pickDate(false),
),
),
],
),
const SizedBox(height: 20),
_buildSectionTitle('Informasi Tanaman'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value:
_cropOptions.contains(_cropNameController.text)
? _cropNameController.text
: 'Lainnya',
items: _cropOptions,
onChanged: (String? newValue) {
setState(() {
if (newValue != 'Lainnya') {
_cropNameController.text = newValue!;
// Reset varietas saat jenis tanaman berubah
_varietyController.text = '';
} else {
_cropNameController.clear();
}
});
},
labelText: 'Jenis Tanaman',
icon: Icons.eco,
),
if (!_cropOptions.contains(_cropNameController.text) ||
_cropNameController.text.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: _buildTextField(
controller: _cropNameController,
labelText: 'Nama Tanaman (Lainnya)',
icon: Icons.eco,
validator:
(v) =>
v == null || v.isEmpty
? 'Nama tanaman harus diisi'
: null,
),
),
const SizedBox(height: 16),
_buildVarietyDropdown(),
if (_varietyError != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
_varietyError!,
style: TextStyle(color: Colors.red.shade700, fontSize: 12),
),
),
const SizedBox(height: 16),
_buildDropdownField<String>(
value: _selectedPlantingSeason,
items: _plantingSeasonOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedPlantingSeason = newValue);
}
},
labelText: 'Musim Tanam',
icon: Icons.wb_sunny,
),
],
);
}
// Step 2: Detail lahan dan plot
Widget _buildLandDetailsStep(List<int> availablePlots) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Detail Lahan & Unit Budidaya'),
const SizedBox(height: 12),
_isLoadingFields
? const Center(child: CircularProgressIndicator())
: _fields.isEmpty
? _buildEmptyFieldsWarning()
: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(Icons.check_circle, color: Colors.green.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'Ditemukan ${_fields.length} lahan yang terdaftar',
style: TextStyle(
fontSize: 14,
color: Colors.green.shade700,
),
),
),
],
),
),
const SizedBox(height: 16),
_buildDropdownField<String>(
value: _selectedFieldId,
items: _fields,
onChanged: (String? value) {
setState(() {
_selectedFieldId = value;
_selectedFieldData = _fields.firstWhere(
(f) => f['id'] == value,
orElse: () => {},
);
_selectedPlot = null;
_usedAreaController.clear();
_availableAreaForSelectedPetak = null;
_usedAreaForSelectedPetak = null;
});
},
labelText: 'Pilih Lahan',
icon: Icons.landscape,
),
if (_selectedFieldData != null) ...[
// Display region information
if (_selectedFieldData!.containsKey('region') &&
_selectedFieldData!['region'] != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(
Icons.map,
color: Colors.blue.shade700,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Wilayah: ${_selectedFieldData!['region']}',
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade700,
),
),
),
],
),
),
],
// Display area information if available
if ((_selectedFieldData!.containsKey('area_size') &&
_selectedFieldData!['area_size'] != null) ||
(_selectedFieldData!.containsKey('area') &&
_selectedFieldData!['area'] != null)) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(
Icons.landscape,
color: Colors.green.shade700,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Luas Lahan: ${_selectedFieldData!['area_size'] ?? _selectedFieldData!['area']} ${_selectedFieldData!['area_unit'] ?? ''}',
style: TextStyle(
fontSize: 14,
color: Colors.green.shade700,
),
),
),
],
),
),
],
// Sistem budidaya berdasarkan wilayah
if (_selectedFieldData!.containsKey('region_specific_data') &&
_selectedFieldData!['region_specific_data'] != null) ...[
const SizedBox(height: 8),
_buildRegionSpecificUnitSelector(
_selectedFieldData!['region'],
_selectedFieldData!['region_specific_data'],
availablePlots,
),
]
// Jika tidak ada region_specific_data tapi ada plot_count
else if (_selectedFieldData!.containsKey('plot_count') &&
_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged: (int? value) async {
setState(() {
_selectedPlot =
value is int
? value
: int.tryParse(value.toString());
_usedAreaController.clear();
});
print(
'Petak dipilih: $_selectedPlot, type: \\${_selectedPlot.runtimeType}',
);
await _updateAvailableAreaForSelectedPetak();
},
labelText: 'Pilih Nomor Petak',
icon: Icons.format_list_numbered,
hint:
availablePlots.isEmpty
? 'Tidak ada petak tersedia'
: null,
),
// Debug print sebelum kondisi
if (_selectedPlot != null) ...[
// print('DEBUG: _selectedPlot = \\$_selectedPlot, type = \\${_selectedPlot.runtimeType}');
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk petak ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '')
.replaceAll(',', '.')
.replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] !=
null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! +
originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input =
double.tryParse(
(value)
.replaceAll(',', '.')
.replaceAll(' ', ''),
) ??
0.0;
setState(() {
// Hitung sisa lahan yang tersedia
final fieldArea =
double.tryParse(
_selectedFieldData!['area_size']
?.toString() ??
'',
) ??
0.0;
final used = _usedAreaForSelectedPetak ?? 0.0;
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] !=
null)
? double.tryParse(
widget
.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// Hitung sisa lahan dengan benar
_availableAreaForSelectedPetakDisplay =
(fieldArea -
used -
input +
originalAreaSize)
.clamp(0.0, fieldArea);
debugPrint(
'INPUT CHANGED: value=$value, input=$input',
);
debugPrint(
'CALCULATION: fieldArea=$fieldArea, used=$used, originalSize=$originalAreaSize',
);
debugPrint(
'RESULT: availableDisplay=${_availableAreaForSelectedPetakDisplay}',
);
});
},
focusNode: _usedAreaFocus,
),
_buildRemainingAreaInfo(),
],
),
],
],
],
],
),
const SizedBox(height: 20),
_buildSectionTitle('Informasi Tanah'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value: _selectedSoilType,
items: _soilTypeOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedSoilType = newValue);
}
},
labelText: 'Tipe Tanah',
icon: Icons.terrain,
),
const SizedBox(height: 16),
_buildDropdownField<String>(
value: _selectedWaterSource,
items: _waterSourceOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedWaterSource = newValue);
}
},
labelText: 'Sumber Air',
icon: Icons.water_drop,
),
const SizedBox(height: 16),
_buildTextField(
controller: _previousCropController,
labelText: 'Tanaman Sebelumnya',
icon: Icons.history,
helperText: 'Tanaman yang ditanam sebelumnya di lahan ini',
),
],
);
}
// Widget untuk menampilkan unit budidaya berdasarkan region
Widget _buildRegionSpecificUnitSelector(
String? region,
Map<String, dynamic>? regionData,
List<int> availablePlots,
) {
if (region == null || regionData == null) {
return const SizedBox.shrink();
}
// Membuat container untuk info sistem lahan
Widget buildSystemInfo(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.shade200),
),
child: Row(
children: [
Icon(icon, color: Colors.amber.shade700, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
'$label: $value',
style: TextStyle(fontSize: 14, color: Colors.amber.shade700),
),
),
],
),
);
}
// Widget untuk pemilihan unit budidaya spesifik region
Widget unitSelector;
switch (region) {
case 'Jawa':
// Di Jawa menggunakan sistem petak sawah
final sistemPetak = regionData['sistem_petak'];
final jenisIrigasi = regionData['jenis_irigasi'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemPetak != null)
buildSystemInfo('Sistem Petak', sistemPetak, Icons.grid_on),
if (jenisIrigasi != null) ...[
const SizedBox(height: 8),
buildSystemInfo('Jenis Irigasi', jenisIrigasi, Icons.water),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Nomor Petak',
icon: Icons.format_list_numbered,
hint:
availablePlots.isEmpty ? 'Tidak ada petak tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk petak ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
case 'Sumatera':
// Di Sumatera menggunakan sistem blok
final sistemBlok = regionData['sistem_blok'];
final jenisTanah = regionData['jenis_tanah'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemBlok != null)
buildSystemInfo('Sistem Blok', sistemBlok, Icons.dashboard),
if (jenisTanah != null) ...[
const SizedBox(height: 8),
buildSystemInfo('Jenis Tanah', jenisTanah, Icons.layers),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Nomor Blok',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada blok tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk blok ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
case 'Kalimantan':
// Di Kalimantan menggunakan sistem ladang
final sistemLadang = regionData['sistem_ladang'];
final jarakSungai = regionData['jarak_sungai'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemLadang != null)
buildSystemInfo('Sistem Ladang', sistemLadang, Icons.agriculture),
if (jarakSungai != null) ...[
const SizedBox(height: 8),
buildSystemInfo(
'Jarak dari Sungai',
'$jarakSungai meter',
Icons.waves,
),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Area Ladang',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada area tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk area ladang ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
case 'Sulawesi':
// Di Sulawesi menggunakan sistem kebun
final sistemKebun = regionData['sistem_kebun'];
final konturLahan = regionData['kontur_lahan'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemKebun != null)
buildSystemInfo('Sistem Kebun', sistemKebun, Icons.eco),
if (konturLahan != null) ...[
const SizedBox(height: 8),
buildSystemInfo('Kontur Lahan', konturLahan, Icons.terrain),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Area Kebun',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada area tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk area kebun ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
case 'Bali & Nusa Tenggara':
// Di Bali menggunakan sistem subak
final sistemSubak = regionData['sistem_subak'];
final sumberAir = regionData['sumber_air'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemSubak != null)
buildSystemInfo('Sistem Subak', sistemSubak, Icons.water_damage),
if (sumberAir != null) ...[
const SizedBox(height: 8),
buildSystemInfo('Sumber Air', sumberAir, Icons.water),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Unit Subak',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada unit tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk unit subak ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
case 'Maluku & Papua':
// Di Maluku & Papua menggunakan sistem kebun dan hutan
final sistemKebun = regionData['sistem_kebun'];
final tipeHutan = regionData['tipe_hutan'];
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (sistemKebun != null)
buildSystemInfo('Sistem Kebun', sistemKebun, Icons.forest),
if (tipeHutan != null) ...[
const SizedBox(height: 8),
buildSystemInfo('Tipe Hutan', tipeHutan, Icons.park),
],
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Area Kebun',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada area tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk area kebun ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
break;
default:
// Default untuk region lainnya, gunakan sistem plot standar
unitSelector = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_selectedFieldData!['plot_count'] != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) => setState(() => _selectedPlot = value),
labelText: 'Pilih Nomor Plot',
icon: Icons.format_list_numbered,
hint: availablePlots.isEmpty ? 'Tidak ada plot tersedia' : null,
),
if (_selectedPlot != null) ...[
const SizedBox(height: 12),
_buildTextField(
controller: _usedAreaController,
labelText: 'Lahan yang Digunakan (m²)',
icon: Icons.straighten,
keyboardType: TextInputType.number,
isRequired: true,
helperText:
'Wajib diisi. Masukkan luas lahan yang digunakan untuk plot ini.',
validator: (value) {
final input = double.tryParse(
(value ?? '').replaceAll(',', '.').replaceAll(' ', ''),
);
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
debugPrint(
'VALIDATOR: value=$value, input=$input, sisa=${_availableAreaForSelectedPetak}, original=$originalAreaSize',
);
if (value == null || value.isEmpty) {
debugPrint('VALIDATOR: kosong');
return 'Lahan yang digunakan harus diisi';
}
if (input == null || input <= 0) {
debugPrint('VALIDATOR: bukan angka');
return 'Masukkan angka yang valid';
}
if (_availableAreaForSelectedPetak != null) {
// In edit mode, we need to allow the original area value
final allowedArea =
_availableAreaForSelectedPetak! + originalAreaSize;
if (input > allowedArea) {
debugPrint(
'VALIDATOR: melebihi sisa (input=$input > allowed=$allowedArea)',
);
return 'Lahan melebihi sisa lahan (${allowedArea.toStringAsFixed(2)} m²)';
}
}
debugPrint('VALIDATOR: OK');
return null;
},
onChanged: (value) {
final input = double.tryParse(
(value).replaceAll(',', '.').replaceAll(' ', ''),
);
setState(() {
if (input != null && input > 0) {
// Get original area size if in edit mode
final originalAreaSize =
(_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null)
? double.tryParse(
widget.scheduleToEdit!['area_size']
.toString(),
) ??
0.0
: 0.0;
// In edit mode, we don't subtract the input from available area for display
// because the original area is already excluded from the calculation
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
});
},
),
_buildRemainingAreaInfo(),
],
],
],
);
}
return unitSelector;
}
// Step 3: Metode budidaya
Widget _buildCultivationMethodStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Metode Budidaya'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value: _selectedPlantingMethod,
items: _plantingMethodOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedPlantingMethod = newValue);
}
},
labelText: 'Metode Tanam',
icon: Icons.agriculture,
),
const SizedBox(height: 16),
// Ubah menjadi dua kolom input angka
Row(
children: [
Expanded(
child: TextFormField(
controller: _plantingDistanceRowController,
focusNode: _plantingDistanceRowFocus,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Jarak Baris (cm)',
errorText: _plantingDistanceError,
border: OutlineInputBorder(),
),
onChanged: (v) => setState(() => _plantingDistanceError = null),
),
),
const SizedBox(width: 8),
const Text('x', style: TextStyle(fontSize: 18)),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _plantingDistanceColController,
focusNode: _plantingDistanceColFocus,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Jarak Tanam (cm)',
errorText: _plantingDistanceError,
border: OutlineInputBorder(),
),
onChanged: (v) => setState(() => _plantingDistanceError = null),
),
),
],
),
if (_plantingDistanceError != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
_plantingDistanceError!,
style: TextStyle(color: Colors.red.shade700, fontSize: 12),
),
),
const SizedBox(height: 20),
_buildSectionTitle('Catatan Tambahan'),
const SizedBox(height: 12),
_buildTextField(
controller: _weatherNotesController,
labelText: 'Catatan Iklim & Cuaca',
icon: Icons.wb_cloudy,
maxLines: 2,
helperText: 'Misal: Prediksi curah hujan, risiko kekeringan, dll',
),
const SizedBox(height: 16),
_buildTextField(
controller: _notesController,
labelText: 'Catatan Umum',
icon: Icons.note,
maxLines: 3,
focusNode: _notesFocus,
),
],
);
}
// Step 4: Kondisi tanam
Widget _buildPlantingConditionsStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Kondisi Iklim & Cuaca'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value: _selectedWeatherCondition,
items: _weatherConditionOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedWeatherCondition = newValue);
}
},
labelText: 'Kondisi Cuaca',
icon: Icons.wb_sunny,
),
const SizedBox(height: 16),
_buildTextField(
controller: _weatherNotesController,
labelText: 'Catatan Iklim & Cuaca',
icon: Icons.wb_cloudy,
maxLines: 2,
helperText: 'Misal: Prediksi curah hujan, risiko kekeringan, dll',
),
const SizedBox(height: 20),
_buildSectionTitle('Irigasi & Pemupukan'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value: _selectedIrrigationType,
items: _irrigationTypeOptions,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() => _selectedIrrigationType = newValue);
}
},
labelText: 'Jenis Irigasi',
icon: Icons.water_drop,
),
const SizedBox(height: 16),
// Ganti dropdown dengan multi-select checkboxes untuk pupuk
_buildSectionTitle('Jenis Pupuk'),
const SizedBox(height: 8),
// Penjelasan untuk user
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue.shade700, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'Pilih jenis pupuk yang digunakan dan masukkan takaran per hektar',
style: TextStyle(fontSize: 12, color: Colors.blue.shade700),
),
),
],
),
),
const SizedBox(height: 12),
// NPK
_buildFertilizerCheckboxWithDosage('NPK', 'Takaran NPK (gram/ha)', 300),
// Urea
_buildFertilizerCheckboxWithDosage(
'Urea',
'Takaran Urea (gram/ha)',
250,
),
// TSP/SP-36
_buildFertilizerCheckboxWithDosage(
'TSP/SP-36',
'Takaran TSP/SP-36 (gram/ha)',
200,
),
// KCL
_buildFertilizerCheckboxWithDosage('KCL', 'Takaran KCL (gram/ha)', 150),
// Organik
_buildFertilizerCheckboxWithDosage(
'Organik',
'Takaran Pupuk Organik (kg/ha)',
2000,
),
// Campuran
_buildFertilizerCheckboxWithDosage(
'Campuran',
'Takaran Pupuk Campuran (gram/ha)',
300,
),
// Tambahkan opsi pupuk kustom
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () {
setState(() {
_showCustomFertilizerInput = true;
});
},
icon: Icon(Icons.add, color: Colors.green.shade700),
label: Text(
'Tambah Pupuk Lainnya',
style: TextStyle(color: Colors.green.shade700),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.green.shade700),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// Form input pupuk kustom
if (_showCustomFertilizerInput) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tambah Pupuk Kustom',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade800,
),
),
const SizedBox(height: 8),
Row(
children: [
// Input nama pupuk kustom
Expanded(
flex: 2,
child: TextFormField(
controller: _customFertilizerNameController,
decoration: InputDecoration(
labelText: 'Nama Pupuk',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
),
),
),
const SizedBox(width: 8),
// Input takaran pupuk kustom
Expanded(
flex: 2,
child: TextFormField(
controller: _customFertilizerDosageController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Takaran (gram/ha)',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
setState(() {
_showCustomFertilizerInput = false;
_customFertilizerNameController.clear();
_customFertilizerDosageController.clear();
});
},
child: Text('Batal'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final name =
_customFertilizerNameController.text.trim();
final dosage =
_customFertilizerDosageController.text.trim();
if (name.isNotEmpty && dosage.isNotEmpty) {
setState(() {
// Tambahkan ke daftar pupuk yang dipilih
_selectedFertilizers.add(name);
// Tambahkan controller untuk takaran
_fertilizerDosageControllers[name] =
TextEditingController(text: dosage);
// Reset form
_showCustomFertilizerInput = false;
_customFertilizerNameController.clear();
_customFertilizerDosageController.clear();
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
),
child: Text('Tambahkan'),
),
],
),
],
),
),
],
// Tampilkan pupuk kustom yang sudah ditambahkan
..._selectedFertilizers
.where((name) => !_fertilizerTypeOptions.contains(name))
.map(
(customName) => Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(Icons.check_circle, color: Colors.green.shade700),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: Text(
customName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade800,
),
),
),
Expanded(
flex: 2,
child: Text(
'Takaran: ${_fertilizerDosageControllers[customName]?.text ?? "0"} gram/ha',
style: TextStyle(color: Colors.green.shade800),
),
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red.shade700),
onPressed: () {
setState(() {
_selectedFertilizers.remove(customName);
_fertilizerDosageControllers[customName]?.dispose();
_fertilizerDosageControllers.remove(customName);
});
},
),
],
),
),
),
),
],
);
}
// Widget helper untuk checkbox pupuk dengan input takaran
Widget _buildFertilizerCheckboxWithDosage(
String fertilizerName,
String dosagelabel,
int defaultDosage,
) {
// Cek apakah pupuk ini dipilih
final isSelected = _selectedFertilizers.contains(fertilizerName);
// Ambil controller untuk takaran pupuk ini
final controller =
_fertilizerDosageControllers[fertilizerName] ??
TextEditingController(text: defaultDosage.toString());
// Simpan controller jika belum ada
if (!_fertilizerDosageControllers.containsKey(fertilizerName)) {
_fertilizerDosageControllers[fertilizerName] = controller;
}
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Checkbox(
value: isSelected,
activeColor: Colors.green.shade700,
onChanged: (bool? value) {
setState(() {
if (value == true) {
_selectedFertilizers.add(fertilizerName);
} else {
_selectedFertilizers.remove(fertilizerName);
}
});
},
),
SizedBox(width: 8),
// Label pupuk
Expanded(
flex: 2,
child: Text(
fertilizerName,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.green.shade800 : Colors.black87,
),
),
),
// Input takaran pupuk
Expanded(
flex: 3,
child: TextFormField(
controller: controller,
enabled: isSelected,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: dosagelabel,
labelStyle: TextStyle(fontSize: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
),
style: TextStyle(fontSize: 14),
),
),
],
),
);
}
// Step 5: Estimasi biaya
Widget _buildCostEstimationStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Estimasi Biaya *'),
const SizedBox(height: 4),
Text(
'Semua biaya harus diisi untuk analisis',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildCostTextField(
_seedCostController,
'Biaya Bibit (Rp)',
Icons.grass,
focusNode: _seedCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya bibit harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_fertilizerCostController,
'Biaya Pupuk Total (Rp)',
Icons.local_florist,
focusNode: _fertilizerCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya pupuk harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_pesticideCostController,
'Biaya Pestisida (Rp)',
Icons.bug_report,
focusNode: _pesticideCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya pestisida harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_irrigationCostController,
'Biaya Irigasi (Rp)',
Icons.water_drop,
focusNode: _irrigationCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya irigasi harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_laborCostController,
'Biaya Tenaga Kerja (Rp)',
Icons.people,
focusNode: _laborCostFocus,
isRequired: false,
helperText: 'Total biaya untuk tenaga kerja',
),
],
);
}
// Step 6: Biaya tambahan
Widget _buildAdditionalCostsStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Biaya Tambahan'),
const SizedBox(height: 4),
Text(
'Semua biaya ini akan digunakan untuk analisis hasil panen',
style: TextStyle(
fontSize: 12,
color: Colors.blue.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildCostTextField(
_landPreparationCostController,
'Biaya Persiapan Lahan (Rp)',
Icons.landscape,
focusNode: _landPreparationCostFocus,
helperText: 'Biaya pengolahan, pembajakan, dll',
),
const SizedBox(height: 16),
_buildCostTextField(
_toolsEquipmentCostController,
'Sewa Alat & Peralatan (Rp)',
Icons.build,
focusNode: _toolsEquipmentCostFocus,
helperText: 'Biaya untuk peralatan pertanian',
),
const SizedBox(height: 16),
_buildCostTextField(
_transportationCostController,
'Biaya Transportasi (Rp)',
Icons.local_shipping,
focusNode: _transportationCostFocus,
helperText: 'Biaya transportasi untuk pengangkutan, dll',
),
const SizedBox(height: 16),
_buildCostTextField(
_postHarvestCostController,
'Biaya Pasca Panen (Rp)',
Icons.inventory_2,
focusNode: _postHarvestCostFocus,
helperText: 'Biaya pengolahan setelah panen',
),
const SizedBox(height: 16),
_buildCostTextField(
_otherCostController,
'Biaya Lain-lain (Rp)',
Icons.more_horiz,
focusNode: _otherCostFocus,
helperText: 'Biaya tambahan lainnya',
),
],
);
}
// Step 7: Estimasi hasil
Widget _buildYieldEstimationStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Estimasi Hasil Panen *'),
const SizedBox(height: 4),
Text(
'Data ini wajib diisi untuk analisis hasil panen',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildCostTextField(
_expectedYieldController,
'Hasil Panen (Kg)',
Icons.agriculture,
focusNode: _expectedYieldFocus,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Estimasi hasil panen harus diisi';
}
return null;
},
isRequired: true,
helperText: 'Wajib diisi untuk analisis hasil panen',
),
const SizedBox(height: 24),
_buildSectionTitle('Ringkasan Jadwal'),
const SizedBox(height: 12),
_buildScheduleSummary(),
],
);
}
Widget _buildScheduleSummary() {
final tanaman =
_cropNameController.text.isNotEmpty
? _cropNameController.text
: 'Belum dipilih';
final varietas =
_varietyController.text.isNotEmpty
? _varietyController.text
: 'Tidak ditentukan';
final field =
_selectedFieldData != null
? _selectedFieldData!['name'] ?? 'Tidak dipilih'
: 'Tidak dipilih';
final plot =
_selectedPlot != null ? 'Plot $_selectedPlot' : 'Tidak dipilih';
// Get area information if available
final area =
(_selectedFieldData != null &&
((_selectedFieldData!.containsKey('area_size') &&
_selectedFieldData!['area_size'] != null) ||
(_selectedFieldData!.containsKey('area') &&
_selectedFieldData!['area'] != null)))
? '${_selectedFieldData!['area_size'] ?? _selectedFieldData!['area']} ${_selectedFieldData!['area_unit'] ?? ''}'
: 'Tidak diketahui';
final periodeTanam =
'${DateFormat('dd MMM yyyy').format(_startDate)} - ${DateFormat('dd MMM yyyy').format(_endDate)}';
final totalBiaya =
_safeParseDouble(_seedCostController.text) +
_safeParseDouble(_fertilizerCostController.text) +
_safeParseDouble(_pesticideCostController.text) +
_safeParseDouble(_irrigationCostController.text) +
_safeParseDouble(_laborCostController.text) +
_safeParseDouble(_landPreparationCostController.text) +
_safeParseDouble(_toolsEquipmentCostController.text) +
_safeParseDouble(_transportationCostController.text) +
_safeParseDouble(_postHarvestCostController.text) +
_safeParseDouble(_otherCostController.text);
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Tanaman section
_buildSummaryItem('Tanaman', tanaman, isBold: true),
_buildSummaryItem('Varietas', varietas),
const Divider(color: Colors.green, height: 24),
// Lahan section
_buildSummaryItem('Lahan', field, isBold: true),
if (_selectedFieldData != null &&
_selectedFieldData!['region'] != null)
_buildSummaryItem('Wilayah', _selectedFieldData!['region']),
_buildSummaryItem('Luas Lahan', area),
// Unit budidaya section berdasarkan region
if (_selectedFieldData != null &&
_selectedFieldData!['region'] != null) ...[
const SizedBox(height: 8),
_buildRegionalUnitSummary(
_selectedFieldData!['region'],
_selectedFieldData!['region_specific_data'],
),
] else if (_selectedPlot != null)
_buildSummaryItem('Plot', plot),
const Divider(color: Colors.green, height: 24),
// Sistem budidaya section
_buildSummaryItem('Periode', periodeTanam),
_buildSummaryItem('Metode Tanam', _selectedPlantingMethod),
_buildSummaryItem('Sumber Air', _selectedWaterSource),
_buildSummaryItem('Jenis Irigasi', _selectedIrrigationType),
_buildSummaryItem('Jenis Pupuk', _selectedFertilizerType),
_buildSummaryItem('Kondisi Cuaca', _selectedWeatherCondition),
_buildSummaryItem('Musim Tanam', _selectedPlantingSeason),
const Divider(color: Colors.green, height: 24),
// Ekonomi section
_buildSummaryItem(
'Total Biaya',
formatter.format(totalBiaya),
isBold: true,
),
_buildSummaryItem(
'Estimasi Hasil',
'${_expectedYieldController.text} kg',
isBold: true,
),
],
),
);
}
// Helper untuk menampilkan informasi unit budidaya berdasarkan region
Widget _buildRegionalUnitSummary(
String? region,
Map<String, dynamic>? regionData,
) {
if (region == null || regionData == null) {
return const SizedBox.shrink();
}
final List<Widget> summaryItems = [];
switch (region) {
case 'Jawa':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem('Unit', 'Petak $_selectedPlot', isBold: true),
);
}
if (regionData['sistem_petak'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Petak', regionData['sistem_petak']),
);
}
if (regionData['jenis_irigasi'] != null) {
summaryItems.add(
_buildSummaryItem('Jenis Irigasi', regionData['jenis_irigasi']),
);
}
break;
case 'Sumatera':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem('Unit', 'Blok $_selectedPlot', isBold: true),
);
}
if (regionData['sistem_blok'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Blok', regionData['sistem_blok']),
);
}
if (regionData['jenis_tanah'] != null) {
summaryItems.add(
_buildSummaryItem('Jenis Tanah', regionData['jenis_tanah']),
);
}
break;
case 'Kalimantan':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem(
'Unit',
'Area Ladang $_selectedPlot',
isBold: true,
),
);
}
if (regionData['sistem_ladang'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Ladang', regionData['sistem_ladang']),
);
}
if (regionData['jarak_sungai'] != null) {
summaryItems.add(
_buildSummaryItem(
'Jarak dari Sungai',
'${regionData['jarak_sungai']} meter',
),
);
}
break;
case 'Sulawesi':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem(
'Unit',
'Area Kebun $_selectedPlot',
isBold: true,
),
);
}
if (regionData['sistem_kebun'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Kebun', regionData['sistem_kebun']),
);
}
if (regionData['kontur_lahan'] != null) {
summaryItems.add(
_buildSummaryItem('Kontur Lahan', regionData['kontur_lahan']),
);
}
break;
case 'Bali & Nusa Tenggara':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem(
'Unit',
'Unit Subak $_selectedPlot',
isBold: true,
),
);
}
if (regionData['sistem_subak'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Subak', regionData['sistem_subak']),
);
}
if (regionData['sumber_air'] != null) {
summaryItems.add(
_buildSummaryItem('Sumber Air', regionData['sumber_air']),
);
}
break;
case 'Maluku & Papua':
if (_selectedPlot != null) {
summaryItems.add(
_buildSummaryItem(
'Unit',
'Area Kebun $_selectedPlot',
isBold: true,
),
);
}
if (regionData['sistem_kebun'] != null) {
summaryItems.add(
_buildSummaryItem('Sistem Kebun', regionData['sistem_kebun']),
);
}
if (regionData['tipe_hutan'] != null) {
summaryItems.add(
_buildSummaryItem('Tipe Hutan', regionData['tipe_hutan']),
);
}
break;
default:
if (_selectedPlot != null) {
summaryItems.add(_buildSummaryItem('Plot', 'Plot $_selectedPlot'));
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: summaryItems,
);
}
Widget _buildSummaryItem(String label, String value, {bool isBold = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: isBold ? Colors.green.shade800 : Colors.black,
),
),
),
],
),
);
}
// Helper widget untuk dropdown varietas tanaman
Widget _buildVarietyDropdown() {
final cropType = _cropNameController.text;
final List<String> varieties = _varietiesByType[cropType] ?? ['Lainnya'];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdownField<String>(
value:
varieties.contains(_varietyController.text)
? _varietyController.text
: null,
items: varieties,
onChanged: (String? value) {
if (value != null) {
setState(() {
_varietyController.text = value;
_varietyError = null;
});
}
},
labelText: 'Varietas Tanaman',
icon: Icons.grass,
hint: 'Pilih varietas',
// errorText: _varietyError, // HAPUS
),
if (_varietyError != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
_varietyError!,
style: TextStyle(color: Colors.red.shade700, fontSize: 12),
),
),
],
);
}
Widget _buildCostTextField(
TextEditingController controller,
String label,
IconData icon, {
FocusNode? focusNode,
String? Function(String?)? validator,
bool isRequired = false,
String? helperText,
}) {
// Determine which validation state to update
void Function(bool) updateValidState;
bool isValid = false;
if (controller == _seedCostController) {
updateValidState = (value) => setState(() => _seedCostValid = value);
isValid = _seedCostValid;
} else if (controller == _fertilizerCostController) {
updateValidState =
(value) => setState(() => _fertilizerCostValid = value);
isValid = _fertilizerCostValid;
} else if (controller == _pesticideCostController) {
updateValidState = (value) => setState(() => _pesticideCostValid = value);
isValid = _pesticideCostValid;
} else if (controller == _irrigationCostController) {
updateValidState =
(value) => setState(() => _irrigationCostValid = value);
isValid = _irrigationCostValid;
} else if (controller == _expectedYieldController) {
updateValidState = (value) => setState(() => _expectedYieldValid = value);
isValid = _expectedYieldValid;
} else {
updateValidState = (_) {};
}
return _buildTextField(
controller: controller,
labelText: label,
icon: icon,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
focusNode: focusNode,
validator: validator,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
isRequired: isRequired,
helperText: helperText,
onChanged: (value) {
final isFieldValid = value.isNotEmpty;
updateValidState(isFieldValid);
// Trigger validation to update error messages
_formKey.currentState?.validate();
},
isValid: isValid,
);
}
Widget _buildEmptyFieldsWarning() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 32),
const SizedBox(height: 8),
const Text(
'Belum ada lahan terdaftar.',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'Silakan tambahkan lahan terlebih dahulu.',
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Tambahkan pesan debug untuk membantu troubleshooting
Text(
'ID pengguna: ${Supabase.instance.client.auth.currentUser?.id ?? 'Tidak diketahui'}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () async {
// Refresh data lahan
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Memuat ulang data lahan...'),
backgroundColor: Colors.blue,
),
);
await _loadFields();
},
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: () {
// Tutup dialog jadwal tanam
Navigator.pop(context);
// Arahkan ke halaman manajemen lahan
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const FieldManagementScreen(),
),
);
},
icon: const Icon(Icons.add_location_alt),
label: const Text('Tambah Lahan'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839), // Warna primary app
foregroundColor: Colors.white,
),
),
],
),
],
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String labelText,
IconData? icon,
String? Function(String?)? validator,
TextInputType keyboardType = TextInputType.text,
int? maxLines,
FocusNode? focusNode,
bool isRequired = false,
String? helperText,
TextInputAction? textInputAction,
void Function(String)? onFieldSubmitted,
void Function(String)? onChanged,
bool isValid = false,
}) {
return TextFormField(
controller: controller,
focusNode: focusNode,
autofocus: false,
decoration: InputDecoration(
labelText: isRequired ? '$labelText *' : labelText,
prefixIcon: icon != null ? Icon(icon) : null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
helperText: helperText,
helperStyle: TextStyle(
color: isRequired ? Colors.red.shade700 : Colors.grey.shade600,
fontSize: 12,
),
// Show green border when valid
enabledBorder:
isValid
? OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Colors.green.shade500,
width: 1.5,
),
)
: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
// Change icon color to green when valid
prefixIconColor: isValid ? Colors.green : null,
),
keyboardType: keyboardType,
maxLines: maxLines,
onChanged: onChanged,
validator:
validator ??
(value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$labelText harus diisi';
}
if (keyboardType ==
const TextInputType.numberWithOptions(decimal: true) &&
value != null &&
value.isNotEmpty) {
try {
final cleanText = value.trim().replaceAll(',', '.');
double.parse(cleanText);
} catch (e) {
return 'Format angka tidak valid';
}
}
return null;
},
textInputAction: textInputAction ?? TextInputAction.next,
onFieldSubmitted:
onFieldSubmitted ?? (_) => FocusScope.of(context).nextFocus(),
);
}
Widget _buildDropdownField<T>({
required T? value,
required List<dynamic> items,
required void Function(T?) onChanged,
required String labelText,
required IconData icon,
String? hint,
}) {
List<DropdownMenuItem<T>> dropdownItems = [];
if (items is List<String>) {
dropdownItems =
items
.map(
(item) => DropdownMenuItem<T>(
value: item as T,
child: Text(item, style: const TextStyle(fontSize: 14)),
),
)
.toList();
} else if (items is List<Map<String, dynamic>>) {
dropdownItems =
items
.map(
(field) => DropdownMenuItem<T>(
value: field['id'] as T,
child: Text(
field['name'] ?? 'Tanpa Nama',
style: const TextStyle(fontSize: 14),
),
),
)
.toList();
} else if (items is List<int>) {
dropdownItems =
items
.map(
(plot) => DropdownMenuItem<T>(
value: plot as T,
child: Text(
'Petak $plot',
style: const TextStyle(fontSize: 14),
),
),
)
.toList();
}
final T? selectedValue =
(value != null &&
items.any(
(item) => (item is Map ? item['id'] == value : item == value),
))
? value
: null;
return DropdownButtonFormField<T>(
value: selectedValue,
items: dropdownItems,
onChanged: onChanged,
decoration: _inputDecoration(
labelText,
icon,
isValid: selectedValue != null,
),
isExpanded: true,
hint: Text(
hint ?? 'Pilih $labelText',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
icon: const Icon(Icons.arrow_drop_down),
);
}
Widget _buildDateField(String label, DateTime date, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: InputDecorator(
decoration: _inputDecoration(
label,
Icons.calendar_today,
isValid: true,
),
child: Text(
DateFormat('dd MMM yyyy').format(date),
style: const TextStyle(fontSize: 14),
),
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
);
}
InputDecoration _inputDecoration(
String labelText,
IconData icon, {
String? hintText,
bool isValid = false,
}) {
final Color primaryColor = const Color(0xFF056839);
final Color validColor = Colors.green.shade500;
return InputDecoration(
labelText: labelText,
hintText: hintText,
prefixIcon: Icon(icon, color: isValid ? validColor : primaryColor),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isValid ? validColor : Colors.grey.shade300,
width: isValid ? 1.5 : 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isValid ? validColor : primaryColor,
width: 2,
),
),
filled: true,
fillColor: Colors.white,
labelStyle: TextStyle(fontSize: 14, color: isValid ? validColor : null),
hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),
);
}
// Helper method untuk menentukan musim tanam berdasarkan tanggal
String _getPlantingSeason() {
final month = _startDate.month;
if (month >= 10 || month <= 3) {
return 'Musim Hujan';
} else if (month >= 4 && month <= 5) {
return 'Peralihan Hujan ke Kemarau';
} else if (month >= 6 && month <= 8) {
return 'Musim Kemarau';
} else {
return 'Peralihan Kemarau ke Hujan';
}
}
// Metode untuk inisialisasi tanggal awal dan akhir
void _setupDateRange() {
if (widget.initialStartDate != null) {
_startDate = widget.initialStartDate!;
_endDate = _startDate.add(const Duration(days: 90));
}
// Set planting season based on current date
_selectedPlantingSeason = _getPlantingSeason();
}
// Metode untuk inisialisasi nilai awal controller
void _initControllers() {
if (!_isEditMode) {
// Set default crop name, but leave costs empty
_cropNameController.text = 'Padi';
}
}
// Metode untuk mengambil data field
void _setupFieldData() {
if (_isEditMode) {
// Check if fields already have values when editing
if (mounted) {
setState(() {
_seedCostValid = _seedCostController.text.isNotEmpty;
_fertilizerCostValid = _fertilizerCostController.text.isNotEmpty;
_pesticideCostValid = _pesticideCostController.text.isNotEmpty;
_irrigationCostValid = _irrigationCostController.text.isNotEmpty;
_expectedYieldValid = _expectedYieldController.text.isNotEmpty;
_laborCostValid = _laborCostController.text.isNotEmpty;
_landPreparationCostValid =
_landPreparationCostController.text.isNotEmpty;
_toolsEquipmentCostValid =
_toolsEquipmentCostController.text.isNotEmpty;
_transportationCostValid =
_transportationCostController.text.isNotEmpty;
_postHarvestCostValid = _postHarvestCostController.text.isNotEmpty;
_otherCostValid = _otherCostController.text.isNotEmpty;
});
}
}
}
Future<void> _updateAvailableAreaForSelectedPetak() async {
debugPrint('==== UPDATING AVAILABLE AREA ====');
debugPrint(
'selectedFieldId: $_selectedFieldId, selectedPlot: $_selectedPlot',
);
if (_selectedFieldId == null ||
_selectedPlot == null ||
_selectedFieldData == null) {
setState(() {
_availableAreaForSelectedPetak = null;
_usedAreaForSelectedPetak = null;
_availableAreaForSelectedPetakDisplay = null;
});
debugPrint('Early return: Missing field data');
return;
}
final fieldArea =
double.tryParse(_selectedFieldData!['area_size']?.toString() ?? '') ??
0.0;
debugPrint('Field Area from data: $fieldArea');
if (fieldArea == 0) {
setState(() {
_availableAreaForSelectedPetak = null;
_usedAreaForSelectedPetak = null;
_availableAreaForSelectedPetakDisplay = null;
});
debugPrint('Early return: Field area is 0');
return;
}
final client = Supabase.instance.client;
final excludeId =
(_isEditMode && widget.scheduleToEdit != null)
? widget.scheduleToEdit!['id']
: null;
final startDate = _startDate.toIso8601String();
final endDate = _endDate.toIso8601String();
debugPrint('Query params: field_id=$_selectedFieldId, plot=$_selectedPlot');
debugPrint('Date range: $startDate to $endDate');
debugPrint('Exclude ID: $excludeId');
try {
// First, get ALL schedules for this field and plot to check what's there
final allSchedules = await client
.from('crop_schedules')
.select('id, area_size, start_date, end_date')
.eq('field_id', _selectedFieldId!)
.eq('plot', _selectedPlot!);
debugPrint('ALL schedules for this field/plot:');
for (final schedule in allSchedules) {
debugPrint(
'ID: ${schedule['id']}, Area: ${schedule['area_size']}, Dates: ${schedule['start_date']} - ${schedule['end_date']}',
);
}
// Now get overlapping schedules
var query = client
.from('crop_schedules')
.select('id, area_size, start_date, end_date')
.eq('field_id', _selectedFieldId!)
.eq('plot', _selectedPlot!);
if (excludeId != null) {
query = query.neq('id', excludeId);
debugPrint('Excluding schedule with ID: $excludeId');
}
// Overlap periode: (start_date <= endDate AND end_date >= startDate)
query = query.or('and(start_date.lte.$endDate,end_date.gte.$startDate)');
final response = await query;
debugPrint('OVERLAPPING schedules found: ${response.length}');
for (final schedule in response) {
debugPrint(
'ID: ${schedule['id']}, Area: ${schedule['area_size']}, Dates: ${schedule['start_date']} - ${schedule['end_date']}',
);
}
double used = 0.0;
for (final row in response) {
final areaSize =
double.tryParse(row['area_size']?.toString() ?? '') ?? 0.0;
used += areaSize;
debugPrint('Adding area: $areaSize, running total: $used');
}
// Get the original area size from the schedule being edited
double originalAreaSize = 0.0;
if (_isEditMode &&
widget.scheduleToEdit != null &&
widget.scheduleToEdit!['area_size'] != null) {
originalAreaSize =
double.tryParse(widget.scheduleToEdit!['area_size'].toString()) ??
0.0;
debugPrint('Original area size (being edited): $originalAreaSize');
}
setState(() {
_usedAreaForSelectedPetak = used;
_availableAreaForSelectedPetak = (fieldArea - used).clamp(
0.0,
fieldArea,
);
// Set display value for remaining area
if (_isEditMode && _usedAreaController.text.isNotEmpty) {
final currentInput = double.tryParse(_usedAreaController.text) ?? 0.0;
_availableAreaForSelectedPetakDisplay = (fieldArea -
used -
currentInput +
originalAreaSize)
.clamp(0.0, fieldArea);
debugPrint('Edit mode with input: $currentInput');
} else {
_availableAreaForSelectedPetakDisplay =
_availableAreaForSelectedPetak;
}
debugPrint('Final calculation:');
debugPrint('Total field area: $fieldArea');
debugPrint('Used area (by other schedules): $used');
debugPrint('Available area: $_availableAreaForSelectedPetak');
debugPrint(
'Display available area: $_availableAreaForSelectedPetakDisplay',
);
});
} catch (e) {
debugPrint('ERROR querying crop_schedules: $e');
}
}
// Tambahkan widget tampilan sisa lahan di semua lokasi yang memiliki input area yang digunakan
Widget _buildRemainingAreaInfo() {
if (_availableAreaForSelectedPetak == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
'Sisa lahan petak: ${(_availableAreaForSelectedPetakDisplay ?? _availableAreaForSelectedPetak)!.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 12,
color:
_availableAreaForSelectedPetakDisplay != null &&
_availableAreaForSelectedPetakDisplay! <= 0
? Colors.red[700]
: Colors.green[700],
),
),
);
}
}