import 'package:flutter/material.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../services/mqtt_service.dart'; import '../models/sensor_data.dart'; import '../services/notification_manager.dart'; import 'notification_screen.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override DashboardScreenState createState() => DashboardScreenState(); } class DashboardScreenState extends State { final MqttService _mqttService = MqttService(); final NotificationManager _notificationManager = NotificationManager(); // Data dari sensor double suhu = 35.2; double berat = 420.0; double target = 280.0; // Data historis untuk grafik List suhuHistory = []; List beratHistory = []; double chartTimeCounter = 0; // Status relay bool heater = false; bool fanIn = false; bool lamp = false; bool exhaust = false; // Status sistem String statusSistem = 'DISCONNECTED'; bool isConnected = false; String lastStatusMessage = 'Tekan tombol untuk memulai pengeringan'; // Waktu mulai dan durasi DateTime? waktuMulai; Duration durasi = Duration.zero; // Current navigation index int _currentIndex = 0; // Status pengeringan bool modePengeringan = false; bool pengeringanSelesai = false; bool beratTersimpan = false; // Track previous status untuk notifikasi String _previousStatus = ''; bool _previousBeratTersimpan = false; @override void initState() { super.initState(); waktuMulai = DateTime.now(); // Inisialisasi waktu mulai _initializeChartData(); _initializeNotifications(); _connectMQTT(); _startDurationTimer(); } void _initializeNotifications() async { await _notificationManager.initialize(); } void _initializeChartData() { // Initialize dengan data dummy untuk demo for (int i = 0; i < 10; i++) { suhuHistory.add(FlSpot(i.toDouble(), 35.0 + (i % 3) * 0.5)); beratHistory.add(FlSpot(i.toDouble(), 450.0 - i * 3.0)); } chartTimeCounter = 10; } void _startDurationTimer() { if (!mounted) return; Future.delayed(const Duration(seconds: 1), () { if (!mounted) return; // Update durasi jika ada waktu mulai if (waktuMulai != null) { if (mounted) { setState(() { durasi = DateTime.now().difference(waktuMulai!); }); } } // Lanjutkan timer _startDurationTimer(); }); } // Connect ke MQTT broker Future _connectMQTT() async { if (!mounted) return; setState(() { statusSistem = 'CONNECTING'; }); try { // Tambahkan timeout untuk mencegah stuck final connected = await _mqttService.connectWithRetry() .timeout( const Duration(seconds: 10), onTimeout: () { print('⏱️ MQTT connection timeout'); return false; }, ); if (!mounted) return; if (connected) { setState(() { isConnected = true; // Jangan set status BERJALAN otomatis, tunggu data dari ESP32 if (statusSistem == 'DISCONNECTED' || statusSistem == 'CONNECTING') { statusSistem = 'READY'; lastStatusMessage = 'Tekan tombol untuk memulai pengeringan'; } }); // Listen untuk data dari ESP32 _mqttService.dataStream.listen((data) { if (mounted) { setState(() { try { final sensorData = SensorData.fromJson(data); suhu = sensorData.suhu; berat = sensorData.berat; target = sensorData.target; heater = sensorData.relay1; fanIn = sensorData.relay2; lamp = sensorData.relay3; exhaust = sensorData.relay4; // Update status sistem berdasarkan data dari ESP32 String newStatus = sensorData.status; if (newStatus == 'START') { statusSistem = 'READY'; modePengeringan = true; pengeringanSelesai = false; beratTersimpan = false; lastStatusMessage = 'Menunggu deteksi ikan...'; if (waktuMulai == null) { waktuMulai = DateTime.now(); } // Notifikasi pengeringan dimulai if (_previousStatus != 'START') { _notificationManager.notifyPengeringanDimulai(); } } else if (newStatus == 'BERJALAN') { statusSistem = 'BERJALAN'; modePengeringan = true; pengeringanSelesai = false; if (!beratTersimpan && target > 0) { beratTersimpan = true; lastStatusMessage = 'Proses pengeringan sedang berlangsung'; // Notifikasi ikan terdeteksi (hanya sekali) if (!_previousBeratTersimpan) { _notificationManager.notifyIkanTerdeteksi(berat); } } else if (!beratTersimpan) { lastStatusMessage = 'Menunggu deteksi ikan...'; } else { lastStatusMessage = 'Proses pengeringan sedang berlangsung'; } } else if (newStatus == 'SELESAI') { statusSistem = 'SELESAI'; pengeringanSelesai = true; lastStatusMessage = 'Pengeringan selesai, tekan tombol untuk reset'; // Notifikasi pengeringan selesai if (_previousStatus != 'SELESAI') { _notificationManager.notifyPengeringanSelesai(berat); } } else { statusSistem = newStatus; } // Update previous status _previousStatus = newStatus; _previousBeratTersimpan = beratTersimpan; // Update chart data if (suhuHistory.length > 20) { suhuHistory.removeAt(0); beratHistory.removeAt(0); // Reindex for (int i = 0; i < suhuHistory.length; i++) { suhuHistory[i] = FlSpot(i.toDouble(), suhuHistory[i].y); beratHistory[i] = FlSpot(i.toDouble(), beratHistory[i].y); } chartTimeCounter = suhuHistory.length.toDouble(); } suhuHistory.add(FlSpot(chartTimeCounter, suhu)); beratHistory.add(FlSpot(chartTimeCounter, berat)); chartTimeCounter++; print('📊 Data updated: $sensorData'); } catch (e) { print('❌ Error parsing sensor data: $e'); } }); } }); // Listen untuk status messages _mqttService.statusStream.listen((status) { if (mounted) { setState(() { lastStatusMessage = status; }); } }); // Listen untuk connection status _mqttService.connectionStream.listen((state) { if (mounted) { setState(() { isConnected = state == MqttConnectionState.connected; }); } }); } else { if (mounted) { setState(() { isConnected = false; statusSistem = 'DISCONNECTED'; lastStatusMessage = 'Gagal terhubung ke server'; }); } } } catch (e) { print('❌ Error in _connectMQTT: $e'); if (mounted) { setState(() { isConnected = false; statusSistem = 'DISCONNECTED'; lastStatusMessage = 'Error: ${e.toString()}'; }); } } } @override void dispose() { super.dispose(); } String _formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, '0'); String hours = twoDigits(duration.inHours); String minutes = twoDigits(duration.inMinutes.remainder(60)); String seconds = twoDigits(duration.inSeconds.remainder(60)); return "$hours:$minutes:$seconds"; } Widget _buildStatusCard() { // Tentukan warna dan icon berdasarkan status Color statusColor; IconData statusIcon; if (statusSistem == 'SELESAI') { statusColor = Colors.green; statusIcon = Icons.check_circle; } else if (statusSistem == 'BERJALAN') { statusColor = Colors.blue; statusIcon = Icons.play_circle; } else if (statusSistem == 'READY') { statusColor = Colors.orange; statusIcon = Icons.hourglass_empty; } else { statusColor = Colors.grey; statusIcon = Icons.info; } return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( children: [ Row( children: [ // Icon status Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( statusIcon, color: statusColor, size: 24, ), ), const SizedBox(width: 10), // Status text Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Status Pengeringan', style: TextStyle( fontSize: 11, color: Colors.grey, ), ), const SizedBox(height: 2), Text( statusSistem, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: statusColor, ), ), const SizedBox(height: 2), Row( children: [ Container( width: 5, height: 5, decoration: BoxDecoration( color: statusColor, shape: BoxShape.circle, ), ), const SizedBox(width: 5), Expanded( child: Text( lastStatusMessage, style: const TextStyle( fontSize: 9, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), ], ), const SizedBox(height: 10), // Waktu dan durasi dalam row terpisah Row( children: [ Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Waktu Mulai', style: TextStyle( fontSize: 9, color: Colors.grey, ), ), const SizedBox(height: 3), Text( waktuMulai != null ? DateFormat('HH:mm:ss').format(waktuMulai!) : '--:--:--', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, ), ), ], ), ), ), const SizedBox(width: 8), Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Durasi', style: TextStyle( fontSize: 9, color: Colors.grey, ), ), const SizedBox(height: 3), Row( children: [ Icon(Icons.timer, size: 13, color: statusColor), const SizedBox(width: 3), Expanded( child: Text( _formatDuration(durasi), style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: statusColor, ), maxLines: 1, ), ), ], ), ], ), ), ), ], ), ], ), ); } Widget _buildMetricCard({ required IconData icon, required Color iconColor, required String label, required String value, required String unit, required String status, required List chartData, required Color chartColor, }) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, color: iconColor, size: 16), ), const SizedBox(width: 6), Expanded( child: Text( label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( child: Text( value, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 3), Padding( padding: const EdgeInsets.only(bottom: 3), child: Text( unit, style: const TextStyle( fontSize: 11, color: Colors.grey, ), ), ), ], ), const SizedBox(height: 2), Text( status, style: const TextStyle( fontSize: 9, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } Widget _buildProgressCard() { // Hitung progress: dari berat awal ke target // Progress = (berat awal - berat sekarang) / (berat awal - target) * 100 // Asumsi: berat awal = berat sekarang + (berat sekarang - target) jika berat > target double progress = 0; if (target > 0 && berat > target) { // Berat masih di atas target, hitung progress double beratAwal = berat + (berat - target); // estimasi berat awal progress = ((beratAwal - berat) / (beratAwal - target) * 100).clamp(0, 100); } else if (target > 0 && berat <= target) { // Sudah mencapai atau melewati target progress = 100; } return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Progres', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 10), Center( child: SizedBox( width: 75, height: 75, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 75, height: 75, child: CircularProgressIndicator( value: progress / 100, strokeWidth: 6, backgroundColor: Colors.grey.shade200, valueColor: const AlwaysStoppedAnimation(Colors.green), ), ), Text( '${progress.toInt()}%', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green, ), ), ], ), ), ), const SizedBox(height: 8), const Center( child: Text( 'Menuju Target', style: TextStyle( fontSize: 9, color: Colors.grey, ), ), ), ], ), ); } Widget _buildProgressBar() { double sisaBerat = berat - target; if (sisaBerat < 0) sisaBerat = 0; // Hitung progress: semakin mendekati target, semakin tinggi progress double progressValue = 0; if (target > 0 && berat > target) { // Asumsi berat awal adalah 2x target atau berat saat ini + sisa double beratAwal = berat + sisaBerat; progressValue = ((beratAwal - berat) / (beratAwal - target)).clamp(0.0, 1.0); } else if (target > 0 && berat <= target) { progressValue = 1.0; // Sudah mencapai target } return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: const Icon(Icons.water_drop, color: Colors.blue, size: 16), ), const SizedBox(width: 6), const Text( 'Progress Pengeringan', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ], ), const SizedBox(height: 6), Text( 'Sisa ${sisaBerat.toInt()} gram menuju target', style: const TextStyle( fontSize: 10, color: Colors.grey, ), ), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: progressValue, minHeight: 10, backgroundColor: Colors.grey.shade200, valueColor: const AlwaysStoppedAnimation(Colors.green), ), ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '${berat.toInt()} g', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.black, ), ), const SizedBox(width: 3), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(3), ), child: const Icon(Icons.flag, size: 8, color: Colors.grey), ), ], ), const Text( 'Berat Saat Ini', style: TextStyle( fontSize: 8, color: Colors.grey, ), ), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${target.toInt()} g', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.black, ), ), const Text( 'Target Berat', style: TextStyle( fontSize: 8, color: Colors.grey, ), ), ], ), ), ], ), ], ), ); } Widget _buildDeviceCard({ required IconData icon, required Color iconColor, required String label, required String description, required bool status, }) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: iconColor, size: 24), ), PopupMenuButton( icon: const Icon(Icons.more_vert, size: 18), itemBuilder: (context) => [ const PopupMenuItem( value: 'info', child: Text('Info'), ), ], ), ], ), const SizedBox(height: 10), Text( label, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( description, style: const TextStyle( fontSize: 10, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: status ? iconColor.withOpacity(0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( status ? 'ON' : 'OFF', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: status ? iconColor : Colors.grey, ), ), ), const SizedBox(height: 8), // Toggle switch Transform.scale( scale: 0.75, child: Switch( value: status, onChanged: null, // Disabled karena mode monitoring activeColor: iconColor, ), ), ], ), ); } Widget _buildCombinedHeaterFanCard() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Flexible( child: Text( 'PTC Heater & Fan In', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), PopupMenuButton( icon: const Icon(Icons.more_vert, size: 16), itemBuilder: (context) => [ const PopupMenuItem( value: 'info', child: Text('Info'), ), ], ), ], ), const SizedBox(height: 10), // PTC Heater Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.deepOrange.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: const Icon(Icons.whatshot, color: Colors.deepOrange, size: 18), ), const SizedBox(width: 8), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'PTC Heater', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( 'Pemanas', style: TextStyle( fontSize: 8, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: heater ? Colors.deepOrange.withOpacity(0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(5), ), child: Text( heater ? 'ON' : 'OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: heater ? Colors.deepOrange : Colors.grey, ), ), ), ], ), const SizedBox(height: 10), // Divider Divider(color: Colors.grey.shade200, height: 1), const SizedBox(height: 10), // Fan In Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: const Icon(Icons.air, color: Colors.blue, size: 18), ), const SizedBox(width: 8), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Fan In', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( 'Kipas Masuk', style: TextStyle( fontSize: 8, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: fanIn ? Colors.blue.withOpacity(0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(5), ), child: Text( fanIn ? 'ON' : 'OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: fanIn ? Colors.blue : Colors.grey, ), ), ), ], ), ], ), ); } Widget _buildExhaustCard() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Flexible( child: Text( 'Exhaust Fan', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), PopupMenuButton( icon: const Icon(Icons.more_vert, size: 16), itemBuilder: (context) => [ const PopupMenuItem( value: 'info', child: Text('Info'), ), ], ), ], ), const SizedBox(height: 10), // Exhaust Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.teal.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: const Icon(Icons.wind_power, color: Colors.teal, size: 18), ), const SizedBox(width: 8), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Exhaust', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( 'Kipas Keluar', style: TextStyle( fontSize: 8, color: Colors.grey, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: exhaust ? Colors.teal.withOpacity(0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(5), ), child: Text( exhaust ? 'ON' : 'OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: exhaust ? Colors.teal : Colors.grey, ), ), ), ], ), ], ), ); } Widget _buildRealtimeChart() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.purple.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.show_chart, color: Colors.purple, size: 20), ), const SizedBox(width: 8), const Text( 'Grafik Real-time', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ], ), const SizedBox(height: 20), // Suhu Chart Row( children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), const SizedBox(width: 8), const Text( 'Grafik Suhu (°C)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), const SizedBox(height: 12), SizedBox( height: 120, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 5, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.shade200, strokeWidth: 1, ); }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 35, interval: 5, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), style: const TextStyle(fontSize: 10, color: Colors.grey), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 25, interval: 2, getTitlesWidget: (value, meta) { int minutes = (value.toInt() * 6) % 60; int hours = 10 + ((value.toInt() * 6) ~/ 60); return Padding( padding: const EdgeInsets.only(top: 8), child: Text( '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 9, color: Colors.grey), ), ); }, ), ), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), minY: 20, maxY: 45, lineBarsData: [ LineChartBarData( spots: suhuHistory, isCurved: true, color: Colors.red, barWidth: 2.5, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { if (index == suhuHistory.length - 1) { return FlDotCirclePainter( radius: 5, color: Colors.white, strokeWidth: 2.5, strokeColor: Colors.red, ); } return FlDotCirclePainter( radius: 0, color: Colors.transparent, ); }, ), belowBarData: BarAreaData( show: true, color: Colors.red.withOpacity(0.1), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( '${spot.y.toStringAsFixed(1)}°C', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ); }).toList(); }, ), ), ), ), ), const SizedBox(height: 24), // Berat Chart Row( children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), const SizedBox(width: 8), const Text( 'Grafik Berat (gram)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), const SizedBox(height: 12), SizedBox( height: 120, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 50, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.shade200, strokeWidth: 1, ); }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 35, interval: 100, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), style: const TextStyle(fontSize: 10, color: Colors.grey), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 25, interval: 2, getTitlesWidget: (value, meta) { int minutes = (value.toInt() * 6) % 60; int hours = 10 + ((value.toInt() * 6) ~/ 60); return Padding( padding: const EdgeInsets.only(top: 8), child: Text( '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 9, color: Colors.grey), ), ); }, ), ), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), minY: 0, maxY: 600, lineBarsData: [ LineChartBarData( spots: beratHistory, isCurved: true, color: Colors.blue, barWidth: 2.5, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { if (index == beratHistory.length - 1) { return FlDotCirclePainter( radius: 5, color: Colors.white, strokeWidth: 2.5, strokeColor: Colors.blue, ); } return FlDotCirclePainter( radius: 0, color: Colors.transparent, ); }, ), belowBarData: BarAreaData( show: true, color: Colors.blue.withOpacity(0.1), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( '${spot.y.toStringAsFixed(0)}g', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ); }).toList(); }, ), ), ), ), ), ], ), ); } @override Widget build(BuildContext context) { // List of screens final List screens = [ _buildDashboardContent(), const NotificationScreen(), ]; return Scaffold( backgroundColor: Colors.grey.shade50, appBar: _currentIndex == 0 ? AppBar( backgroundColor: Colors.white, elevation: 0, title: const Text( 'Dashboard', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, ), ), actions: [ Padding( padding: const EdgeInsets.only(right: 16), child: Row( children: [ Icon( Icons.wifi, color: isConnected ? Colors.green : Colors.grey, size: 18, ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isConnected ? Colors.green : Colors.grey, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( isConnected ? 'ONLINE' : 'OFFLINE', style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), Padding( padding: const EdgeInsets.only(right: 12), child: Center( child: Text( DateFormat('HH:mm:ss').format(DateTime.now()), style: const TextStyle( fontSize: 12, color: Colors.grey, fontWeight: FontWeight.w500, ), ), ), ), ], ) : null, body: screens[_currentIndex], bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, backgroundColor: Colors.white, selectedItemColor: Colors.blue, unselectedItemColor: Colors.grey, currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; }); }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Dashboard', ), BottomNavigationBarItem( icon: Icon(Icons.notifications), label: 'Notifikasi', ), ], ), ); } Widget _buildDashboardContent() { return RefreshIndicator( onRefresh: () async { try { await _connectMQTT(); } catch (e) { print('❌ Error during refresh: $e'); // Tetap selesaikan refresh meskipun error } }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Status Card _buildStatusCard(), const SizedBox(height: 12), // Metrics Row Row( children: [ Expanded( child: _buildMetricCard( icon: Icons.thermostat, iconColor: Colors.red, label: 'Suhu', value: suhu.toStringAsFixed(1), unit: '°C', status: 'Normal', chartData: suhuHistory, chartColor: Colors.red, ), ), const SizedBox(width: 10), Expanded( child: _buildMetricCard( icon: Icons.scale, iconColor: Colors.blue, label: 'Berat Ikan', value: berat.toStringAsFixed(0), unit: 'gram', status: 'Berat Saat Ini', chartData: beratHistory, chartColor: Colors.blue, ), ), ], ), const SizedBox(height: 10), // Target and Progress Row Row( children: [ Expanded( child: _buildMetricCard( icon: Icons.flag, iconColor: Colors.purple, label: 'Target Berat', value: target.toStringAsFixed(0), unit: 'gram', status: 'Berat Target', chartData: const [], chartColor: Colors.purple, ), ), const SizedBox(width: 10), Expanded( child: _buildProgressCard(), ), ], ), const SizedBox(height: 12), // Progress Bar _buildProgressBar(), const SizedBox(height: 12), // Device Cards Grid Row( children: [ Expanded( child: _buildCombinedHeaterFanCard(), ), const SizedBox(width: 10), Expanded( child: _buildExhaustCard(), ), ], ), const SizedBox(height: 12), ], ), ), ); } }