4733 lines
183 KiB
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'] ?? 'm²'}',
|
|
);
|
|
} else if (_selectedFieldData!.containsKey('area') &&
|
|
_selectedFieldData!['area'] != null) {
|
|
fieldArea =
|
|
double.tryParse(_selectedFieldData!['area'].toString()) ?? 0.0;
|
|
debugPrint('Found area in selected field: $fieldArea m²');
|
|
} 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'] ?? 'm²'}',
|
|
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'] ?? 'm²'}'
|
|
: '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)} m²',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color:
|
|
_availableAreaForSelectedPetakDisplay != null &&
|
|
_availableAreaForSelectedPetakDisplay! <= 0
|
|
? Colors.red[700]
|
|
: Colors.green[700],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|