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)? onScheduleAdded; final List> existingSchedules; final DateTime? initialStartDate; final Map? scheduleToEdit; const AddScheduleDialog({ super.key, this.onScheduleAdded, required this.existingSchedules, this.initialStartDate, this.scheduleToEdit, }); @override State createState() => _AddScheduleDialogState(); } class _AddScheduleDialogState extends State { // Form step tracking int _currentStep = 0; final _formKey = GlobalKey(); 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 _selectedFertilizers = {}; final Map _fertilizerDosageControllers = {}; DateTime _startDate = DateTime.now(); DateTime _endDate = DateTime.now().add(const Duration(days: 90)); String? _selectedFieldId; int? _selectedPlot; Map? _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> _fields = []; bool _isLoading = false; bool _isLoadingFields = true; bool _isSaved = false; bool _isEditMode = false; final List _cropOptions = [ 'Padi', 'Jagung', 'Kedelai', 'Cabai', 'Tomat', 'Bawang', 'Kopi', 'Tembakau', 'Lainnya', ]; // Options for new dropdown fields final List _soilTypeOptions = [ 'Lempung', 'Pasir', 'Liat', 'Lempung Berpasir', 'Liat Berpasir', 'Lainnya', ]; final List _waterSourceOptions = [ 'Irigasi', 'Hujan', 'Pompa', 'Sumur', 'Mata Air', 'Lainnya', ]; final List _plantingMethodOptions = [ 'Konvensional', 'Jajar Legowo', 'SRI', 'Tabela', 'Tapin', 'Lainnya', ]; final List _plantingSeasonOptions = [ 'Musim Hujan', 'Musim Kemarau', 'Peralihan Hujan ke Kemarau', 'Peralihan Kemarau ke Hujan', ]; // Options tambahan untuk analisis hasil panen final List _weatherConditionOptions = [ 'Normal', 'Kekeringan', 'Banjir', 'Curah Hujan Tinggi', ]; final List _irrigationTypeOptions = [ 'Irigasi Teknis', 'Irigasi Setengah Teknis', 'Irigasi Sederhana', 'Tadah Hujan', ]; final List _fertilizerTypeOptions = [ 'NPK', 'Urea', 'TSP/SP-36', 'KCL', 'Organik', 'Campuran', ]; // Variety options based on crop type final Map> _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 _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 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 _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>.from( response.map((field) { // Buat salinan field untuk dimodifikasi var transformedField = Map.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 _getTakenPlots( Map fieldData, DateTime startDate, DateTime endDate, ) { if (fieldData['id'] == null) return []; final takenPlotsSet = {}; 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 _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 _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 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 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.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 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( 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( 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 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( 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( 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( value: _selectedSoilType, items: _soilTypeOptions, onChanged: (String? newValue) { if (newValue != null) { setState(() => _selectedSoilType = newValue); } }, labelText: 'Tipe Tanah', icon: Icons.terrain, ), const SizedBox(height: 16), _buildDropdownField( 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? regionData, List 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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? regionData, ) { if (region == null || regionData == null) { return const SizedBox.shrink(); } final List 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 varieties = _varietiesByType[cropType] ?? ['Lainnya']; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDropdownField( 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({ required T? value, required List items, required void Function(T?) onChanged, required String labelText, required IconData icon, String? hint, }) { List> dropdownItems = []; if (items is List) { dropdownItems = items .map( (item) => DropdownMenuItem( value: item as T, child: Text(item, style: const TextStyle(fontSize: 14)), ), ) .toList(); } else if (items is List>) { dropdownItems = items .map( (field) => DropdownMenuItem( value: field['id'] as T, child: Text( field['name'] ?? 'Tanpa Nama', style: const TextStyle(fontSize: 14), ), ), ) .toList(); } else if (items is List) { dropdownItems = items .map( (plot) => DropdownMenuItem( 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( 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 _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], ), ), ); } }