TKK_E32230814/lib/screens/threshold_form_page.dart

688 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/threshold_model.dart';
import '../services/threshold_service.dart';
import '../theme/app_color.dart';
/// Form untuk tambah/edit threshold sensor
class ThresholdFormPage extends StatefulWidget {
final ThresholdModel? threshold; // null = create new, not null = edit
const ThresholdFormPage({super.key, this.threshold});
@override
State<ThresholdFormPage> createState() => _ThresholdFormPageState();
}
class _ThresholdFormPageState extends State<ThresholdFormPage> {
final _formKey = GlobalKey<FormState>();
late String _thresholdId;
late double _batasBawah; // 0-100
late double _batasAtas; // 0-100
late int _durasi; // dalam detik
late bool _smartMode;
late List<bool> _potSelection; // [pot1, pot2, pot3, pot4, pot5]
late bool _pompaAir;
late bool _pompaPupuk;
late bool _pompaPengaduk;
late bool _aktif;
bool _isLoading = false;
bool _isEditMode = false;
@override
void initState() {
super.initState();
_initializeValues();
}
void _initializeValues() {
_isEditMode = widget.threshold != null;
if (_isEditMode) {
final threshold = widget.threshold!;
_thresholdId = threshold.id;
_batasBawah = threshold.batasBawah.toDouble();
_batasAtas = threshold.batasAtas.toDouble();
_durasi = threshold.durasi;
_smartMode = threshold.smartMode;
// Initialize pot selection
_potSelection = List.generate(5, (index) {
return threshold.potAktif.contains(index + 1);
});
_pompaAir = threshold.pompaAir;
_pompaPupuk = threshold.pompaPupuk;
_pompaPengaduk = threshold.pompaPengaduk;
_aktif = threshold.aktif;
} else {
// Default values for new threshold
_thresholdId = ''; // Will be set when saving
_batasBawah = 30.0;
_batasAtas = 70.0;
_durasi = 600; // 10 minutes
_smartMode = true;
_potSelection = [false, false, false, false, false];
_pompaAir = true;
_pompaPupuk = false;
_pompaPengaduk = false;
_aktif = true;
}
}
int get _durasiMenit => (_durasi / 60).round();
set _durasiMenit(int value) {
_durasi = value * 60;
}
List<int> get _selectedPots {
final List<int> selected = [];
for (int i = 0; i < _potSelection.length; i++) {
if (_potSelection[i]) {
selected.add(i + 1);
}
}
return selected;
}
bool _validate() {
if (!_formKey.currentState!.validate()) {
return false;
}
if (_selectedPots.isEmpty) {
_showError('Pilih minimal 1 pot');
return false;
}
if ((_pompaAir || _pompaPupuk) && _selectedPots.isEmpty) {
_showError('Tidak dapat menyimpan threshold: Pompa aktif tanpa kran/solenoid (POT) yang terbuka.');
return false;
}
if (_batasBawah >= _batasAtas) {
_showError('Batas bawah harus lebih kecil dari batas atas');
return false;
}
if (!_smartMode && _durasi <= 0) {
_showError('Durasi harus lebih dari 0');
return false;
}
return true;
}
Future<void> _save() async {
if (!_validate()) return;
setState(() => _isLoading = true);
try {
// Get or create ID
if (!_isEditMode) {
_thresholdId = await ThresholdService.getNextThresholdId();
}
// Create threshold model
final threshold = ThresholdModel(
id: _thresholdId,
aktif: _aktif,
batasBawah: _batasBawah.toInt(),
batasAtas: _batasAtas.toInt(),
durasi: _durasi,
smartMode: _smartMode,
potAktif: _selectedPots,
pompaAir: _pompaAir,
pompaPupuk: _pompaPupuk,
pompaPengaduk: _pompaPengaduk,
);
// Save to Firebase
final success = await ThresholdService.saveThreshold(threshold);
setState(() => _isLoading = false);
if (success) {
if (mounted) {
_showSuccess(
_isEditMode
? 'Threshold berhasil diupdate'
: 'Threshold berhasil ditambahkan',
);
Navigator.pop(context, true);
}
} else {
_showError('Gagal menyimpan threshold');
}
} catch (e) {
setState(() => _isLoading = false);
_showError('Error: $e');
}
}
void _showSuccess(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(
backgroundColor: AppColor.primary,
foregroundColor: Colors.white,
title: Text(_isEditMode ? 'Edit Threshold' : 'Tambah Threshold'),
centerTitle: true,
),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildRangeSection(),
const SizedBox(height: 24),
_buildModeSection(),
const SizedBox(height: 24),
_buildPotSelection(),
const SizedBox(height: 24),
_buildPumpSection(),
const SizedBox(height: 24),
_buildStatusSection(),
const SizedBox(height: 32),
_buildSaveButton(),
],
),
),
);
}
Widget _buildRangeSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.water_drop, color: Colors.blue[700]),
const SizedBox(width: 8),
Text(
'Range Kelembaban',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
const SizedBox(height: 16),
// Batas Bawah
Text(
'Batas Bawah: ${_batasBawah.toInt()}%',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Slider(
value: _batasBawah,
min: 0,
max: 100,
divisions: 100,
label: '${_batasBawah.toInt()}%',
onChanged: (value) {
setState(() {
_batasBawah = value;
if (_batasBawah >= _batasAtas) {
_batasAtas = (_batasBawah + 1).clamp(0, 100);
}
});
},
activeColor: AppColor.primary,
),
const SizedBox(height: 8),
// Batas Atas
Text(
'Batas Atas: ${_batasAtas.toInt()}%',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Slider(
value: _batasAtas,
min: 0,
max: 100,
divisions: 100,
label: '${_batasAtas.toInt()}%',
onChanged: (value) {
setState(() {
_batasAtas = value;
if (_batasAtas <= _batasBawah) {
_batasBawah = (_batasAtas - 1).clamp(0, 100);
}
});
},
activeColor: AppColor.primary,
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 18, color: Colors.blue[700]),
const SizedBox(width: 8),
Expanded(
child: Text(
'Siram jika kelembaban < ${_batasBawah.toInt()}%',
style: TextStyle(fontSize: 12, color: Colors.blue[900]),
),
),
],
),
),
],
),
),
);
}
Widget _buildModeSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.psychology, color: Colors.orange[700]),
const SizedBox(width: 8),
Text(
'Mode Penyiraman',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
const SizedBox(height: 16),
// Smart Mode Toggle
// SwitchListTile(
// value: _smartMode,
// onChanged: (value) {
// setState(() {
// _smartMode = value;
// });
// },
// title: Text(
// _smartMode ? 'Smart Mode' : 'Fixed Duration',
// style: const TextStyle(fontWeight: FontWeight.w600),
// ),
// subtitle: Text(
// _smartMode
// ? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
// : 'Siram dengan durasi tetap',
// style: const TextStyle(fontSize: 12),
// ),
// activeColor: AppColor.primary,
// contentPadding: EdgeInsets.zero,
// ),
ListTile(
title: const Text(
'Smart Mode',
style: TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
'Siram sampai kelembaban ${_batasAtas.toInt()}%',
style: const TextStyle(fontSize: 12),
),
// contentPadding: EdgeInsets.zero,
// trailing: Switch(
// value: _smartMode,
// onChanged: null, // Disabled
// activeColor: AppColor.primary,
// ),
),
// Duration input (hanya jika Fixed Duration)
if (!_smartMode) ...[
const SizedBox(height: 16),
TextFormField(
initialValue: _durasiMenit.toString(),
decoration: InputDecoration(
labelText: 'Durasi (menit)',
hintText: 'Masukkan durasi',
prefixIcon: const Icon(Icons.timer),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppColor.primary, width: 2),
),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Durasi tidak boleh kosong';
}
final durasi = int.tryParse(value);
if (durasi == null || durasi <= 0) {
return 'Durasi harus lebih dari 0';
}
return null;
},
onChanged: (value) {
final menit = int.tryParse(value);
if (menit != null) {
setState(() {
_durasiMenit = menit;
});
}
},
),
],
],
),
),
);
}
Widget _buildPotSelection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.local_florist, color: Colors.green[700]),
const SizedBox(width: 8),
Text(
'Pilih Pot',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
const SizedBox(height: 16),
// Pot selection buttons
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(5, (index) {
final potNumber = index + 1;
final isSelected = _potSelection[index];
return FilterChip(
label: Text('Pot $potNumber'),
selected: isSelected,
onSelected: (selected) {
if (!selected) {
final selectedCount = _potSelection.where((p) => p).length;
if (selectedCount == 1 && (_pompaAir || _pompaPupuk)) {
_showAlertDialog(
context,
title: 'Solenoid Terakhir',
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
}
setState(() {
_potSelection[index] = selected;
});
},
selectedColor: AppColor.primary.withOpacity(0.2),
checkmarkColor: AppColor.primary,
backgroundColor: Colors.grey[100],
side: BorderSide(
color: isSelected ? AppColor.primary : Colors.grey[300]!,
width: isSelected ? 2 : 1,
),
);
}),
),
const SizedBox(height: 12),
// Quick actions
Row(
children: [
TextButton.icon(
onPressed: () {
setState(() {
_potSelection = [true, true, true, true, true];
});
},
icon: const Icon(Icons.select_all, size: 18),
label: const Text('Pilih Semua'),
),
const SizedBox(width: 8),
TextButton.icon(
onPressed: () {
if (_pompaAir || _pompaPupuk) {
_showAlertDialog(
context,
title: 'Hapus Semua Pot',
message: 'Tidak dapat menonaktifkan seluruh kran/solenoid saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
setState(() {
_potSelection = [false, false, false, false, false];
});
},
icon: const Icon(Icons.clear, size: 18),
label: const Text('Hapus Semua'),
),
],
),
],
),
),
);
}
Widget _buildPumpSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.water, color: Colors.blue[700]),
const SizedBox(width: 8),
Text(
'Pompa',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
const SizedBox(height: 8),
SwitchListTile(
value: _pompaAir,
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() {
_pompaAir = value;
});
},
title: const Text('Pompa Air'),
subtitle: const Text('Aktifkan pompa air saat penyiraman'),
activeColor: AppColor.primary,
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
value: _pompaPupuk,
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() {
_pompaPupuk = value;
});
},
title: const Text('Pompa Larutan Nutrisi'),
subtitle: const Text(
'Aktifkan pompa larutan nutrisi saat penyiraman',
),
activeColor: AppColor.primary,
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
value: false,
onChanged: null, // <- tanpa ()
title: const Text('Pompa Pengaduk'),
subtitle: const Text(
'Aktifkan pompa pengaduk untuk mengaduk larutan',
),
activeColor: AppColor.primary,
contentPadding: EdgeInsets.zero,
),
],
),
),
);
}
Widget _buildStatusSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: SwitchListTile(
value: _aktif,
onChanged: (value) {
setState(() {
_aktif = value;
});
},
title: const Text(
'Status Threshold',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(_aktif ? 'Aktif' : 'Nonaktif'),
activeColor: AppColor.primary,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
);
}
Widget _buildSaveButton() {
return ElevatedButton(
onPressed: _isLoading ? null : _save,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2,
),
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
_isEditMode ? 'Update Threshold' : 'Simpan Threshold',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}