TKK_E32221098/lib/utils/rewidgets/global/editingplant/manualparam.dart

519 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:seedina/provider/rtdb_handler.dart';
import 'package:seedina/utils/style/gcolor.dart'; //
class EditInfo extends StatefulWidget {
const EditInfo({super.key});
@override
State<EditInfo> createState() => _EditInfoState();
}
class _EditInfoState extends State<EditInfo> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _latinController;
late TextEditingController _waktuSiramController;
late TextEditingController _jedaSiramController;
late TextEditingController _suhuAirMinController;
late TextEditingController _suhuAirMaxController;
late TextEditingController _nutrisiMinController;
late TextEditingController _nutrisiMaxController;
late TextEditingController _suhuLingMinController;
late TextEditingController _suhuLingMaxController;
late TextEditingController _humiLingMinController;
late TextEditingController _humiLingMaxController;
bool _isSaving = false;
// bool _isLoadingForm = true; // Tidak lagi diperlukan jika Consumer menangani update
String _currentPlantSelectionInProvider = "";
@override
void initState() {
super.initState();
_titleController = TextEditingController();
_latinController = TextEditingController();
_waktuSiramController = TextEditingController();
_jedaSiramController = TextEditingController();
_suhuAirMinController = TextEditingController();
_suhuAirMaxController = TextEditingController();
_nutrisiMinController = TextEditingController();
_nutrisiMaxController = TextEditingController();
_suhuLingMinController = TextEditingController();
_suhuLingMaxController = TextEditingController();
_humiLingMinController = TextEditingController();
_humiLingMaxController = TextEditingController();
// Ambil data awal saat initState
// dan simpan tipe tanaman dari provider untuk deteksi perubahan nanti
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<HandlingProvider>(context, listen: false);
_currentPlantSelectionInProvider = provider.draftSelectedPlantForEditing;
_loadParametersFromProviderDraft(provider.draftParametersForEditingReadonly);
});
}
// Tidak lagi menggunakan didChangeDependencies untuk reload form secara otomatis
// karena bisa me-reset input pengguna. Kita akan mengandalkan Consumer.
void _loadParametersFromProviderDraft(Map<String, dynamic> params) {
if (!mounted) return;
// setState(() { _isLoadingForm = true; }); // Tidak lagi diperlukan
// Logika untuk memastikan title dan latin sesuai untuk Kustom saat load
// Provider sekarang sudah memastikan _draftParametersForEditing['title'] dan ['latin']
// sudah benar untuk mode Kustom ("Tanaman Lain", "Parameter Kustom").
_titleController.text = params['title']?.toString() ?? '';
_latinController.text = params['latin']?.toString() ?? '';
_waktuSiramController.text = params['waktu_siram']?.toString() ?? '';
_jedaSiramController.text = params['jeda_siram']?.toString() ?? '';
_suhuAirMinController.text = params['min_suhuair']?.toString() ?? '';
_suhuAirMaxController.text = params['max_suhuair']?.toString() ?? '';
_nutrisiMinController.text = params['min_tdsair']?.toString() ?? '';
_nutrisiMaxController.text = params['max_tdsair']?.toString() ?? '';
_suhuLingMinController.text = params['min_suhuling']?.toString() ?? '';
_suhuLingMaxController.text = params['max_suhuling']?.toString() ?? '';
_humiLingMinController.text = params['min_humiling']?.toString() ?? '';
_humiLingMaxController.text = params['max_humiling']?.toString() ?? '';
// if (mounted) {
// setState(() { _isLoadingForm = false; });
// }
// Cukup panggil setState di build jika menggunakan Consumer untuk trigger rebuild
}
Future<void> _submitCustomParameters() async {
if (!_formKey.currentState!.validate()) {
if(mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Harap perbaiki semua error pada form.'), backgroundColor: Colors.orangeAccent),
);
}
return;
}
if(!mounted) return;
setState(() => _isSaving = true);
final provider = Provider.of<HandlingProvider>(context, listen: false);
// Ambil default title Kustom jika input title kosong
String customTitle = _titleController.text.trim();
if (customTitle.isEmpty) {
customTitle = provider.parameters['Kustom']!['title'].toString();
}
Map<String, dynamic> parametersToSaveFromForm = {
'title': customTitle,
'latin': _latinController.text.trim(), // Latin bisa kosong
'waktu_siram': int.tryParse(_waktuSiramController.text) ?? provider.parameters['Kustom']!['waktu_siram'],
'jeda_siram': int.tryParse(_jedaSiramController.text) ?? provider.parameters['Kustom']!['jeda_siram'],
'min_suhuair': double.tryParse(_suhuAirMinController.text) ?? provider.parameters['Kustom']!['min_suhuair'],
'max_suhuair': double.tryParse(_suhuAirMaxController.text) ?? provider.parameters['Kustom']!['max_suhuair'],
'min_tdsair': int.tryParse(_nutrisiMinController.text) ?? provider.parameters['Kustom']!['min_tdsair'],
'max_tdsair': int.tryParse(_nutrisiMaxController.text) ?? provider.parameters['Kustom']!['max_tdsair'],
'min_suhuling': double.tryParse(_suhuLingMinController.text) ?? provider.parameters['Kustom']!['min_suhuling'],
'max_suhuling': double.tryParse(_suhuLingMaxController.text) ?? provider.parameters['Kustom']!['max_suhuling'],
'min_humiling': int.tryParse(_humiLingMinController.text) ?? provider.parameters['Kustom']!['min_humiling'],
'max_humiling': int.tryParse(_humiLingMaxController.text) ?? provider.parameters['Kustom']!['max_humiling'],
// Thumbnail akan diurus oleh provider.applyCustomDraftToActive
};
// ignore: unused_local_variable
bool success = await provider.applyCustomDraftToActive(context, parametersToSaveFromForm);
// Pesan sukses/gagal sudah dihandle provider
if (mounted) {
setState(() => _isSaving = false);
// Jika sukses, dan kita ingin form merefleksikan data yang baru disimpan (terutama jika title berubah)
// Kita bisa panggil _loadParametersFromProviderDraft lagi dengan data terbaru dari provider.
// Provider akan memanggil notifyListeners setelah applyCustomDraftToActive,
// jadi Consumer di bawah akan otomatis memanggil _load...
}
}
@override
void dispose() {
_titleController.dispose();
_latinController.dispose();
_waktuSiramController.dispose();
_jedaSiramController.dispose();
_suhuAirMinController.dispose();
_suhuAirMaxController.dispose();
_nutrisiMinController.dispose();
_nutrisiMaxController.dispose();
_suhuLingMinController.dispose();
_suhuLingMaxController.dispose();
_humiLingMinController.dispose();
_humiLingMaxController.dispose();
super.dispose();
}
String? _validateRequired(String? value, String fieldName) {
if (value == null || value.trim().isEmpty) {
// Khusus untuk nama tanaman kustom, tidak wajib diisi (akan default ke "Tanaman Lain")
// Validasi nama preset ada di TextFormField langsung.
// if (fieldName.toLowerCase().contains("nama tanaman")) return null;
// Untuk field lain tetap wajib:
// return '$fieldName wajib diisi';
}
// Untuk nama tanaman, cek apakah sama dengan nama preset (kecuali "Kustom")
if (fieldName.toLowerCase().contains("nama tanaman") && value != null && value.trim().isNotEmpty) {
final p = Provider.of<HandlingProvider>(context, listen: false);
// Cek apakah nama yang dimasukkan adalah salah satu key preset (case insensitive)
// dan BUKAN merupakan nama default untuk Kustom itu sendiri ("Tanaman Lain")
// atau nama latin default Kustom.
String lowerValue = value.trim().toLowerCase();
bool isPresetName = p.parameters.keys.any((key) {
return key != "Kustom" && key.toLowerCase() == lowerValue;
});
if (isPresetName) {
return "'$value' adalah nama preset, tidak bisa digunakan untuk Kustom.";
}
}
// Untuk field selain nama tanaman, jika wajib:
if (!fieldName.toLowerCase().contains("nama tanaman") && (value == null || value.trim().isEmpty)) {
return '$fieldName wajib diisi';
}
return null;
}
String? _validateInt(String? value, String fieldName, {int? minValAbs, int? maxValAbs, bool allowZero = true}) {
String? requiredError = _validateRequired(value, fieldName);
if (requiredError != null) return requiredError;
final val = int.tryParse(value!);
if (val == null) return '$fieldName harus berupa angka bulat';
if (!allowZero && val == 0) return '$fieldName tidak boleh nol';
if (val < 0 && !allowZero && (minValAbs == null || minValAbs >=0) ) return '$fieldName tidak boleh negatif';
if (minValAbs != null && val < minValAbs) return '$fieldName minimal $minValAbs';
if (maxValAbs != null && val > maxValAbs) return '$fieldName maksimal $maxValAbs';
return null;
}
String? _validateDouble(String? value, String fieldName, {double? minValAbs, double? maxValAbs}) {
String? requiredError = _validateRequired(value, fieldName);
if (requiredError != null) return requiredError;
final val = double.tryParse(value!);
if (val == null) return '$fieldName harus berupa angka desimal';
if (fieldName.toLowerCase().contains("kelembapan") && (val < 0.0 || val > 100.0)) return 'Kelembapan harus antara 0.0 - 100.0';
if (minValAbs != null && val < minValAbs) return '$fieldName minimal $minValAbs';
if (maxValAbs != null && val > maxValAbs) return '$fieldName maksimal $maxValAbs';
return null;
}
Widget _buildSingleTextFormField({
required String label,
required TextEditingController controller,
required String unit,
bool isDecimal = false,
num? absoluteMin,
num? absoluteMax,
bool allowZeroForInt = true,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
controller: controller,
keyboardType: isDecimal
? const TextInputType.numberWithOptions(decimal: true, signed: true)
: TextInputType.number,
inputFormatters: isDecimal
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: label,
suffixText: unit,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
isDense: true,
helperText: isDecimal ? "Gunakan '.' untuk desimal" : "Angka bulat",
helperStyle: TextStyle(fontSize: 11, color: Colors.grey.shade600)
),
validator: (value) {
return isDecimal
? _validateDouble(value, label, minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
: _validateInt(value, label, minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt(), allowZero: allowZeroForInt);
},
),
);
}
Widget _buildMinMaxTextFormFieldRow({
required String label,
required TextEditingController minController,
required TextEditingController maxController,
required String unit,
bool isDecimal = false,
num? absoluteMin,
num? absoluteMax,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextFormField(
controller: minController,
keyboardType: isDecimal
? const TextInputType.numberWithOptions(decimal: true, signed: true)
: TextInputType.number,
inputFormatters: isDecimal
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: "Min",
hintText: "Nilai Min",
suffixText: unit,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
validator: (value) {
String? commonError = isDecimal
? _validateDouble(value, "Min $label", minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
: _validateInt(value, "Min $label", minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt());
if (commonError != null) return commonError;
final maxStr = maxController.text;
if (maxStr.isNotEmpty && value != null && value.isNotEmpty) {
final currentMin = isDecimal ? double.tryParse(value) : int.tryParse(value);
final currentMax = isDecimal ? double.tryParse(maxStr) : int.tryParse(maxStr);
if (currentMin != null && currentMax != null && currentMin > currentMax) {
return 'Min harus <= Maks';
}
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: maxController,
keyboardType: isDecimal
? const TextInputType.numberWithOptions(decimal: true, signed: true)
: TextInputType.number,
inputFormatters: isDecimal
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: "Maks",
hintText: "Nilai Maks",
suffixText: unit,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
validator: (value) {
String? commonError = isDecimal
? _validateDouble(value, "Maks $label", minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
: _validateInt(value, "Maks $label", minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt());
if (commonError != null) return commonError;
final minStr = minController.text;
if (minStr.isNotEmpty && value != null && value.isNotEmpty) {
final currentMin = isDecimal ? double.tryParse(minStr) : int.tryParse(minStr);
final currentMax = isDecimal ? double.tryParse(value) : int.tryParse(value);
if (currentMin != null && currentMax != null && currentMin > currentMax) {
return 'Maks harus >= Min';
}
}
return null;
},
),
),
],
),
if (isDecimal) Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text("Gunakan '.' untuk desimal", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
) else Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text("Angka bulat", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 4.0),
child: Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: GColors.myBiru)), //
);
}
@override
Widget build(BuildContext context) {
// Gunakan Consumer untuk mendapatkan data draft terbaru dan re-load form jika perlu
return Consumer<HandlingProvider>(
builder: (context, provider, child) {
// Cek apakah tipe tanaman di provider berubah sejak widget ini dibangun
// Jika berubah (misal dari preset ke Kustom atau sebaliknya karena interaksi di luar widget ini),
// maka kita perlu me-reload controller dengan data draft baru.
if (_currentPlantSelectionInProvider != provider.draftSelectedPlantForEditing) {
_currentPlantSelectionInProvider = provider.draftSelectedPlantForEditing;
// Panggil _loadParametersFromProviderDraft di post frame callback agar tidak error
// saat build sedang berlangsung.
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadParametersFromProviderDraft(provider.draftParametersForEditingReadonly);
});
}
final plantInfoForImage = provider.draftPlantInfoForEditingPage;
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
boxShadow: [
BoxShadow(
color: GColors.shadowColor.withOpacity(0.15), //
blurRadius: 6,
spreadRadius: 2,
offset: const Offset(0, 3))
]),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: provider.isLoading && provider.draftParametersForEditingReadonly.isEmpty
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: AssetImage(plantInfoForImage['thumbnail'] ?? 'assets/myicon/unknown.png'), //
fit: BoxFit.cover),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: "Nama Tanaman Kustom",
border: UnderlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.auto, // Biar label naik
hintText: "Contoh: Cabe Rawit Saya"
),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
validator: (value) {
return _validateRequired(value, "Nama Tanaman Kustom");
}
),
TextFormField(
controller: _latinController,
decoration: const InputDecoration(
labelText: "Nama Latin/Deskripsi (Opsional)",
border: UnderlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.auto,
hintText: "Contoh: Capsicum frutescens"
),
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
),
],
),
)
],
),
const Divider(height: 32, thickness: 1),
_buildSectionTitle("Pengaturan Penyiraman"),
_buildSingleTextFormField(
label: "Waktu Siram",
controller: _waktuSiramController,
unit: "Menit",
isDecimal: false,
absoluteMin: 1,
allowZeroForInt: false,
),
_buildSingleTextFormField(
label: "Jeda Siram",
controller: _jedaSiramController,
unit: "Menit",
isDecimal: false,
absoluteMin: 1,
allowZeroForInt: false,
),
_buildSectionTitle("Parameter Air"),
_buildMinMaxTextFormFieldRow(
label: "Nutrisi (TDS)",
minController: _nutrisiMinController,
maxController: _nutrisiMaxController,
unit: "ppm",
isDecimal: false,
absoluteMin: 0),
_buildMinMaxTextFormFieldRow(
label: "Suhu Air",
minController: _suhuAirMinController,
maxController: _suhuAirMaxController,
unit: "°C",
isDecimal: true),
_buildSectionTitle("Parameter Lingkungan"),
_buildMinMaxTextFormFieldRow(
label: "Suhu Lingkungan",
minController: _suhuLingMinController,
maxController: _suhuLingMaxController,
unit: "°C",
isDecimal: true),
_buildMinMaxTextFormFieldRow(
label: "Kelembapan Udara",
minController: _humiLingMinController,
maxController: _humiLingMaxController,
unit: "%",
isDecimal: false,
absoluteMin: 0,
absoluteMax: 100
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isSaving ? null : _submitCustomParameters,
style: ElevatedButton.styleFrom(
backgroundColor: GColors.myBiru, //
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
child: _isSaving
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.save_alt_outlined),
SizedBox(width: 8),
Text('Simpan Parameter Kustom', style: TextStyle(fontSize: 16))
],
),
),
const SizedBox(height: 16),
],
),
),
),
),
);
}
);
}
}