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

302 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import '../../../core/app_theme.dart';
class SensorLimitCard extends StatefulWidget {
final double suhuLimit;
final double kelembapanLimit;
final Future<void> Function(double suhu, double kelembapan) onSave;
const SensorLimitCard({
super.key,
required this.suhuLimit,
required this.kelembapanLimit,
required this.onSave,
});
// Nilai default untuk tanaman jamur
static const double defaultSuhu = 28.0;
static const double defaultKelembapan = 90.0;
@override
State<SensorLimitCard> createState() => _SensorLimitCardState();
}
class _SensorLimitCardState extends State<SensorLimitCard> {
late double _draftSuhu;
late double _draftKelembapan;
bool _isSaving = false;
@override
void initState() {
super.initState();
_draftSuhu = widget.suhuLimit;
_draftKelembapan = widget.kelembapanLimit;
}
// Sinkronisasi draft jika nilai dari provider berubah dari luar
// (misal setelah reload awal / restore dari server)
@override
void didUpdateWidget(SensorLimitCard oldWidget) {
super.didUpdateWidget(oldWidget);
final serverChanged = oldWidget.suhuLimit != widget.suhuLimit ||
oldWidget.kelembapanLimit != widget.kelembapanLimit;
final draftUnchanged = _draftSuhu == oldWidget.suhuLimit &&
_draftKelembapan == oldWidget.kelembapanLimit;
// Update draft hanya jika user belum mengedit
if (serverChanged && draftUnchanged) {
_draftSuhu = widget.suhuLimit;
_draftKelembapan = widget.kelembapanLimit;
}
}
bool get _hasChanges =>
_draftSuhu != widget.suhuLimit ||
_draftKelembapan != widget.kelembapanLimit;
bool get _isDefault =>
_draftSuhu == SensorLimitCard.defaultSuhu &&
_draftKelembapan == SensorLimitCard.defaultKelembapan;
void _applyDefault() {
setState(() {
_draftSuhu = SensorLimitCard.defaultSuhu;
_draftKelembapan = SensorLimitCard.defaultKelembapan;
});
}
void _cancel() {
setState(() {
_draftSuhu = widget.suhuLimit;
_draftKelembapan = widget.kelembapanLimit;
});
}
Future<void> _save() async {
setState(() => _isSaving = true);
await widget.onSave(_draftSuhu, _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 Suhu ────────────────────────────────────────
_SliderRow(
icon: Icons.thermostat,
label: 'Suhu Maks',
value: _draftSuhu,
displayValue: '${_draftSuhu.round()}°C',
min: 15,
max: 50,
divisions: 35,
onChanged: (v) => setState(() => _draftSuhu = v),
),
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(),
),
],
),
);
}
}
// ── 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,
),
),
],
);
}
}