import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mqtt_client/mqtt_client.dart'; import '../services/mqtt_service.dart'; import '../services/supabase_service.dart'; class MonitoringScreen extends StatefulWidget { final VoidCallback? onProfileTap; const MonitoringScreen({super.key, this.onProfileTap}); @override State createState() => _MonitoringScreenState(); } class _MonitoringScreenState extends State { bool _isInitialLoading = true; double _prevSuhu = 0; double _prevKelembapan = 0; double _prevIntensitas = 0; @override void initState() { super.initState(); _initData(); } Future _initData() async { setState(() => _isInitialLoading = true); await _loadLimits(); if (mounted) setState(() => _isInitialLoading = false); } Future _loadLimits() async { try { final data = await SupabaseService().getBatasSensor(); if (data != null && mounted) { Provider.of(context, listen: false).setLimits( double.parse(data['suhu_max'].toString()), double.parse(data['rh_max'].toString()), double.parse(data['suhu_min'].toString()), double.parse(data['rh_min'].toString()), ); } } catch (e) { debugPrint('Error loading limits: $e'); } } @override Widget build(BuildContext context) { final mqtt = Provider.of(context); final curSuhu = double.tryParse(mqtt.suhu) ?? 0; final curHum = double.tryParse(mqtt.kelembapan) ?? 0; final curLux = double.tryParse(mqtt.intensitas) ?? 0; WidgetsBinding.instance.addPostFrameCallback((_) { _prevSuhu = curSuhu; _prevKelembapan = curHum; _prevIntensitas = curLux; }); return Scaffold( backgroundColor: Colors.black, body: SafeArea( child: _isInitialLoading ? const Center(child: CircularProgressIndicator(color: Colors.green)) : RefreshIndicator( onRefresh: _initData, color: Colors.green, child: LayoutBuilder( builder: (context, constraints) { // Tinggi tersedia = tinggi layar dikurangi header const headerHeight = 175.0; // header + timestamp + mode badge + padding const gaps = 15.0 * 2; // 2 gap antar 3 baris const padding = 20.0 * 2; // padding atas bawah final available = constraints.maxHeight - headerHeight - gaps - padding; final cardH = (available / 3).clamp(100.0, 180.0); return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header ── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Monitoring', style: TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)), const Text('Sistem Pengeringan Kopi', style: TextStyle(color: Colors.grey, fontSize: 16)), const SizedBox(height: 5), Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected ? Colors.green : Colors.red, ), ), const SizedBox(width: 8), Text( mqtt.client?.connectionStatus?.state == MqttConnectionState.connected ? 'Connected' : 'Disconnected', style: TextStyle( color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected ? Colors.green : Colors.red, fontSize: 12, ), ), ], ), ], ), GestureDetector( onTap: widget.onProfileTap, child: const CircleAvatar( backgroundColor: Color(0xFF1A1A1A), radius: 25, child: Icon(Icons.person, color: Colors.green), ), ), ], ), const SizedBox(height: 6), // ── Timestamp ── Text('Update: ${mqtt.timestamp}', style: const TextStyle(color: Colors.grey, fontSize: 11)), const SizedBox(height: 10), // ── Mode Badge ── Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: mqtt.mode == 'manual' ? Colors.orange.withOpacity(0.2) : Colors.green.withOpacity(0.2), borderRadius: BorderRadius.circular(20), border: Border.all( color: mqtt.mode == 'manual' ? Colors.orange : Colors.green, ), ), child: Text( 'Mode: ${mqtt.mode.toUpperCase()}', style: TextStyle( color: mqtt.mode == 'manual' ? Colors.orange : Colors.green, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 15), // ── Baris 1: Suhu & Kelembaban ── SizedBox( height: cardH, child: Row( children: [ Expanded( child: _buildSensorCard( title: 'Suhu', prev: _prevSuhu, cur: curSuhu, suffix: '°C', dec: 1, icon: Icons.thermostat, color: curSuhu > mqtt.maxSuhu ? Colors.red : Colors.redAccent, isAlert: curSuhu > mqtt.maxSuhu, isLow: curSuhu < mqtt.minSuhu, onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Suhu (°C)', 'color': Colors.redAccent, 'value': '${mqtt.suhu}°C'}), ), ), const SizedBox(width: 15), Expanded( child: _buildSensorCard( title: 'Kelembaban', prev: _prevKelembapan, cur: curHum, suffix: '%', dec: 1, icon: Icons.water_drop, color: curHum > mqtt.maxRh ? Colors.red : Colors.cyan, isAlert: curHum > mqtt.maxRh, isLow: curHum < mqtt.minRh, onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Kelembaban (%)', 'color': Colors.cyan, 'value': '${mqtt.kelembapan}%'}), ), ), ], ), ), const SizedBox(height: 15), // ── Baris 2: Cahaya full width ── SizedBox( height: cardH, width: double.infinity, child: _buildSensorCard( title: 'Cahaya', prev: _prevIntensitas, cur: curLux, suffix: ' Lux', dec: 1, icon: Icons.wb_sunny, color: Colors.orange, onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Cahaya (Lux)', 'color': Colors.orange, 'value': '${mqtt.intensitas} Lux'}), ), ), const SizedBox(height: 15), // ── Baris 3: Kipas 1 & Kipas 2 ── SizedBox( height: cardH, child: Row( children: [ Expanded( child: _buildControlCard( 'Kipas 1\n(Intake)', mqtt.isKipas1On, (val) => mqtt.perintahKipas("1", val), mqtt, ), ), const SizedBox(width: 15), Expanded( child: _buildControlCard( 'Kipas 2\n(Exhaust)', mqtt.isKipas2On, (val) => mqtt.perintahKipas("2", val), mqtt, ), ), ], ), ), // ── Alert Banner ── if (curSuhu > mqtt.maxSuhu || curHum > mqtt.maxRh) Container( margin: const EdgeInsets.only(top: 15), padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.red.withOpacity(0.2), borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.red), ), child: const Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.red), SizedBox(width: 10), Expanded( child: Text( 'Peringatan: Parameter pengeringan melewati batas aman!', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12), ), ), ], ), ), if (curSuhu < mqtt.minSuhu || curHum < mqtt.minRh) Container( margin: const EdgeInsets.only(top: 15), padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.2), borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.blue), ), child: const Row( children: [ Icon(Icons.info_outline, color: Colors.blue), SizedBox(width: 10), Expanded( child: Text( 'Info: Parameter pengeringan di bawah batas minimum!', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12), ), ), ], ), ), ], ), ), ), ); }, ), ), ), ); } Widget _buildSensorCard({ required String title, required double prev, required double cur, required String suffix, required int dec, required IconData icon, required Color color, bool isAlert = false, bool isLow = false, VoidCallback? onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: double.infinity, height: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isAlert ? Colors.red.withOpacity(0.1) : isLow ? Colors.blue.withOpacity(0.1) : const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20), border: isAlert ? Border.all(color: Colors.red, width: 2) : isLow ? Border.all(color: Colors.blue, width: 2) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color, size: 32), const SizedBox(height: 6), Text(title, style: const TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 4), TweenAnimationBuilder( tween: Tween(begin: prev, end: cur), duration: const Duration(milliseconds: 800), curve: Curves.easeOutCubic, builder: (context, value, _) => FittedBox( fit: BoxFit.scaleDown, child: Text( '${value.toStringAsFixed(dec)}$suffix', style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); } Widget _buildControlCard( String title, bool isOn, Function(bool) onChanged, MqttService mqtt, ) { return Container( width: double.infinity, height: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.settings_input_component, color: isOn ? Colors.green : Colors.grey, size: 16), const SizedBox(width: 6), Expanded( child: Text(title, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Mode: ${mqtt.mode.toUpperCase()}', style: const TextStyle( color: Colors.green, fontSize: 9, fontWeight: FontWeight.bold)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isOn ? 'ON' : 'OFF', style: TextStyle( color: isOn ? Colors.green : Colors.grey, fontSize: 12, fontWeight: FontWeight.bold), ), SizedBox( height: 28, child: Switch( value: isOn, onChanged: mqtt.mode == 'manual' ? onChanged : null, activeColor: Colors.green, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), ], ), ], ), ); } }