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 'package:flutter/services.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 { final _formKey = GlobalKey(); // Track validation state for each field bool _seedCostValid = false; bool _fertilizerCostValid = false; bool _pesticideCostValid = false; bool _irrigationCostValid = false; bool _expectedYieldValid = false; final _cropNameController = TextEditingController(); final _notesController = TextEditingController(); final _seedCostController = TextEditingController(); final _fertilizerCostController = TextEditingController(); final _pesticideCostController = TextEditingController(); final _irrigationCostController = TextEditingController(); final _expectedYieldController = TextEditingController(); // Tambahkan ScrollController untuk auto-scroll final _scrollController = ScrollController(); DateTime _startDate = DateTime.now(); DateTime _endDate = DateTime.now().add(const Duration(days: 90)); String? _selectedFieldId; int? _selectedPlot; Map? _selectedFieldData; List> _fields = []; bool _isLoading = false; bool _isLoadingFields = true; bool _isSaved = false; bool _isEditMode = false; final List _cropOptions = [ 'Padi', 'Jagung', 'Kedelai', 'Cabai', 'Tomat', 'Bawang', '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(); @override void initState() { super.initState(); _isEditMode = widget.scheduleToEdit != null; if (_isEditMode) { _initializeWithExistingData(); // Check if fields already have values when editing Future.delayed(Duration.zero, () { setState(() { _seedCostValid = _seedCostController.text.isNotEmpty; _fertilizerCostValid = _fertilizerCostController.text.isNotEmpty; _pesticideCostValid = _pesticideCostController.text.isNotEmpty; _irrigationCostValid = _irrigationCostController.text.isNotEmpty; _expectedYieldValid = _expectedYieldController.text.isNotEmpty; }); }); } else { if (widget.initialStartDate != null) { _startDate = widget.initialStartDate!; _endDate = _startDate.add(const Duration(days: 90)); } // Set default crop name, but leave costs empty _cropNameController.text = 'Padi'; } _loadFields(); // Konfigurasi focus nodes untuk auto-scroll saat keyboard muncul _setupFocusNodes(); } void _setupFocusNodes() { // Fungsi untuk scroll ke field yang sedang difokuskan void scrollToFocusedField(FocusNode focusNode) { // Hapus kode yang mungkin mengganggu keyboard } // Tambahkan listener ke setiap focus node tanpa implementasi yang mengganggu keyboard _seedCostFocus.addListener(() {}); _fertilizerCostFocus.addListener(() {}); _pesticideCostFocus.addListener(() {}); _irrigationCostFocus.addListener(() {}); _expectedYieldFocus.addListener(() {}); _notesFocus.addListener(() {}); } @override void dispose() { // Dispose semua controller _cropNameController.dispose(); _notesController.dispose(); _seedCostController.dispose(); _fertilizerCostController.dispose(); _pesticideCostController.dispose(); _irrigationCostController.dispose(); _expectedYieldController.dispose(); // Dispose semua focus node _cropNameFocus.dispose(); _notesFocus.dispose(); _seedCostFocus.dispose(); _fertilizerCostFocus.dispose(); _pesticideCostFocus.dispose(); _irrigationCostFocus.dispose(); _expectedYieldFocus.dispose(); super.dispose(); } 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; } void _initializeWithExistingData() { 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'], ); 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']; } catch (e) { debugPrint('Error initializing data: $e'); } } Future _loadFields() async { if (!mounted) return; setState(() => _isLoadingFields = true); try { final response = await Supabase.instance.client .from('fields') .select('id, name, plot_count') .order('name', ascending: true) .timeout(const Duration(seconds: 8)); if (!mounted) return; setState(() { _fields = List>.from(response); if (_isEditMode && _selectedFieldId != null) { _selectedFieldData = _fields.firstWhere( (field) => field['id'] == _selectedFieldId, orElse: () => _fields.isNotEmpty ? _fields.first : {}, ); } else if (_fields.isNotEmpty) { _selectedFieldId ??= _fields.first['id']; _selectedFieldData = _fields.first; } _isLoadingFields = false; }); } catch (e) { if (!mounted) return; setState(() => _isLoadingFields = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( e.toString().contains('timeout') ? 'Koneksi timeout.' : 'Gagal memuat data lahan.', ), 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 { if (_isLoading || _isSaved) return; if (!_formKey.currentState!.validate()) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Mohon lengkapi data yang diperlukan.'), backgroundColor: Colors.red, ), ); return; } if (mounted) setState(() => _isLoading = true); try { final userId = Supabase.instance.client.auth.currentUser?.id; if (userId == null) throw Exception('User tidak ditemukan.'); final data = { 'user_id': userId, 'crop_name': _cropNameController.text.trim(), 'start_date': _startDate.toIso8601String(), 'end_date': _endDate.toIso8601String(), 'field_id': _selectedFieldId, 'plot': _selectedPlot, '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), }; if (!_isEditMode) { data['id'] = const Uuid().v4(); data['created_at'] = DateTime.now().toIso8601String(); } final scheduleId = _isEditMode ? widget.scheduleToEdit!['id'] : data['id']; if (_isEditMode) { await Supabase.instance.client .from('crop_schedules') .update(data) .eq('id', scheduleId); } else { await Supabase.instance.client.from('crop_schedules').insert(data); } if (!mounted) return; setState(() => _isSaved = true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Jadwal berhasil ${_isEditMode ? 'diperbarui' : 'disimpan'}.', ), backgroundColor: Colors.green, ), ); final newScheduleData = await Supabase.instance.client .from('crop_schedules') .select() .eq('id', scheduleId) .single(); widget.onScheduleAdded?.call(newScheduleData); await Future.delayed(const Duration(milliseconds: 300)); if (mounted) Navigator.of(context).pop(true); } catch (e) { if (mounted) setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menyimpan jadwal: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } @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( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header yang tetap di atas Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _isEditMode ? 'Edit Jadwal Tanam' : 'Tambah Jadwal Tanam', style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // Konten form yang dapat di-scroll Flexible( child: SingleChildScrollView( controller: _scrollController, padding: EdgeInsets.only( bottom: keyboardHeight > 0 ? keyboardHeight + 80 : 20, left: 20, right: 20, ), child: Form( key: _formKey, child: 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!; } else { _cropNameController.clear(); } }); }, labelText: 'Nama 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: 20), _buildSectionTitle('Detail Lahan & Plot'), const SizedBox(height: 12), _isLoadingFields ? const Center(child: CircularProgressIndicator()) : _fields.isEmpty ? _buildEmptyFieldsWarning() : Column( children: [ _buildDropdownField( value: _selectedFieldId, items: _fields, onChanged: (String? value) { setState(() { _selectedFieldId = value; _selectedFieldData = _fields.firstWhere( (f) => f['id'] == value, orElse: () => {}, ); _selectedPlot = null; }); }, labelText: 'Pilih Lahan', icon: Icons.landscape, ), if (_selectedFieldData != 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, ), ], ], ), const SizedBox(height: 20), _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 (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: 20), _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: 20), _buildSectionTitle('Catatan Tambahan (Opsional)'), const SizedBox(height: 12), _buildTextField( controller: _notesController, labelText: 'Catatan', icon: Icons.note, maxLines: 3, focusNode: _notesFocus, ), // Tambahkan padding bawah untuk memastikan konten terlihat saat keyboard muncul const SizedBox(height: 30), ], ), ), ), ), // Tombol simpan yang tetap di bawah Padding( padding: const EdgeInsets.all(20), child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: _isSaved || _isLoading ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF056839), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.save, size: 18), const SizedBox(width: 8), Text( 'Simpan Jadwal', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ], ), ), ); } 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: const Column( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 32), SizedBox(height: 8), Text( 'Belum ada lahan terdaftar.', style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 4), Text( 'Silakan tambahkan lahan terlebih dahulu.', textAlign: TextAlign.center, ), ], ), ); } 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( 'Plot $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), ); } }