TKK_E32230206/lib/features/iot/widgets/sensor_limit_card.dart

390 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import '../../../core/app_theme.dart';
class SensorLimitCard extends StatefulWidget {
final double suhuMinLimit;
final double suhuMaxLimit;
final double kelembapanLimit;
final Future<void> Function(double suhuMin, double suhuMax, double kelembapan) onSave;
const SensorLimitCard({
super.key,
required this.suhuMinLimit,
required this.suhuMaxLimit,
required this.kelembapanLimit,
required this.onSave,
});
// Nilai default untuk tanaman jamur
static const double defaultSuhuMin = 22.0;
static const double defaultSuhuMax = 28.0;
static const double defaultKelembapan = 90.0;
@override
State<SensorLimitCard> createState() => _SensorLimitCardState();
}
class _SensorLimitCardState extends State<SensorLimitCard> {
late double _draftSuhuMin;
late double _draftSuhuMax;
late double _draftKelembapan;
bool _isSaving = false;
@override
void initState() {
super.initState();
_draftSuhuMin = widget.suhuMinLimit;
_draftSuhuMax = widget.suhuMaxLimit;
_draftKelembapan = widget.kelembapanLimit;
}
// Sinkronisasi draft jika nilai dari provider berubah dari luar
@override
void didUpdateWidget(SensorLimitCard oldWidget) {
super.didUpdateWidget(oldWidget);
final serverChanged = oldWidget.suhuMinLimit != widget.suhuMinLimit ||
oldWidget.suhuMaxLimit != widget.suhuMaxLimit ||
oldWidget.kelembapanLimit != widget.kelembapanLimit;
final draftUnchanged = _draftSuhuMin == oldWidget.suhuMinLimit &&
_draftSuhuMax == oldWidget.suhuMaxLimit &&
_draftKelembapan == oldWidget.kelembapanLimit;
// Update draft hanya jika user belum mengedit
if (serverChanged && draftUnchanged) {
_draftSuhuMin = widget.suhuMinLimit;
_draftSuhuMax = widget.suhuMaxLimit;
_draftKelembapan = widget.kelembapanLimit;
}
}
bool get _hasChanges =>
_draftSuhuMin != widget.suhuMinLimit ||
_draftSuhuMax != widget.suhuMaxLimit ||
_draftKelembapan != widget.kelembapanLimit;
bool get _isDefault =>
_draftSuhuMin == SensorLimitCard.defaultSuhuMin &&
_draftSuhuMax == SensorLimitCard.defaultSuhuMax &&
_draftKelembapan == SensorLimitCard.defaultKelembapan;
void _applyDefault() {
setState(() {
_draftSuhuMin = SensorLimitCard.defaultSuhuMin;
_draftSuhuMax = SensorLimitCard.defaultSuhuMax;
_draftKelembapan = SensorLimitCard.defaultKelembapan;
});
}
void _cancel() {
setState(() {
_draftSuhuMin = widget.suhuMinLimit;
_draftSuhuMax = widget.suhuMaxLimit;
_draftKelembapan = widget.kelembapanLimit;
});
}
Future<void> _save() async {
setState(() => _isSaving = true);
await widget.onSave(_draftSuhuMin, _draftSuhuMax, _draftKelembapan);
if (mounted) setState(() => _isSaving = false);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.accent,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.accent.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header ─────────────────────────────────────────────
Row(
children: [
const Icon(Icons.tune, color: Colors.white, size: 18),
const SizedBox(width: 6),
const Expanded(
child: Text(
'Batasan Sensor Otomatis',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
// Tombol Default — tampil jika nilai draft bukan default
if (!_isDefault)
GestureDetector(
onTap: _applyDefault,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Default',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 16),
// ── Slider Rentang Suhu (Min - Maks) ───────────────────
_RangeSliderRow(
icon: Icons.thermostat,
label: 'Batas Rentang Suhu',
startValue: _draftSuhuMin,
endValue: _draftSuhuMax,
displayValue: '${_draftSuhuMin.round()}°C - ${_draftSuhuMax.round()}°C',
min: 15,
max: 50,
divisions: 35,
onChanged: (values) {
setState(() {
_draftSuhuMin = values.start;
_draftSuhuMax = values.end;
});
},
),
const SizedBox(height: 8),
// ── Slider Kelembapan ──────────────────────────────────
_SliderRow(
icon: Icons.water_drop_outlined,
label: 'Kelembapan Min',
value: _draftKelembapan,
displayValue: '${_draftKelembapan.round()}%',
min: 50,
max: 100,
divisions: 50,
onChanged: (v) => setState(() => _draftKelembapan = v),
),
// ── Tombol Aksi (hanya tampil jika ada perubahan) ──────
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: _hasChanges
? Padding(
padding: const EdgeInsets.only(top: 14),
child: Row(
children: [
// Tombol Batal
Expanded(
child: OutlinedButton(
onPressed: _isSaving ? null : _cancel,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white54),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Batal',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(width: 10),
// Tombol Simpan
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _isSaving ? null : _save,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.accent,
padding: const EdgeInsets.symmetric(vertical: 10),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isSaving
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check, size: 16),
SizedBox(width: 6),
Text(
'Simpan',
style: TextStyle(
fontSize: 13, fontWeight: FontWeight.bold),
),
],
),
),
),
],
),
)
: const SizedBox.shrink(),
),
],
),
);
}
}
// ── Range Slider Row ──────────────────────────────────────────────
class _RangeSliderRow extends StatelessWidget {
final IconData icon;
final String label;
final double startValue;
final double endValue;
final String displayValue;
final double min;
final double max;
final int divisions;
final ValueChanged<RangeValues> onChanged;
const _RangeSliderRow({
required this.icon,
required this.label,
required this.startValue,
required this.endValue,
required this.displayValue,
required this.min,
required this.max,
required this.divisions,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Colors.white70, size: 14),
const SizedBox(width: 4),
Expanded(
child: Text(
label,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
),
Text(
displayValue,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white24,
thumbColor: Colors.white,
overlayColor: Colors.white24,
trackHeight: 3,
),
child: RangeSlider(
values: RangeValues(startValue, endValue),
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
],
);
}
}
// ── Slider Row ────────────────────────────────────────────────────
class _SliderRow extends StatelessWidget {
final IconData icon;
final String label;
final double value;
final String displayValue;
final double min;
final double max;
final int divisions;
final ValueChanged<double> onChanged;
const _SliderRow({
required this.icon,
required this.label,
required this.value,
required this.displayValue,
required this.min,
required this.max,
required this.divisions,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Colors.white70, size: 14),
const SizedBox(width: 4),
Expanded(
child: Text(
label,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
),
Text(
displayValue,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white24,
thumbColor: Colors.white,
overlayColor: Colors.white24,
trackHeight: 3,
),
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
],
);
}
}