// CONTOH IMPLEMENTASI DASHBOARD DENGAN API SERVICE // File ini adalah contoh, copy kode yang diperlukan ke dashboard_screen.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../services/api_service.dart'; import '../services/notification_manager.dart'; import '../models/sensor_data.dart'; import '../widgets/temperature_chart.dart'; import '../widgets/weight_chart.dart'; import 'chart_screen.dart'; import 'notification_screen.dart'; class DashboardScreenWithApi extends StatefulWidget { const DashboardScreenWithApi({super.key}); @override State createState() => _DashboardScreenWithApiState(); } class _DashboardScreenWithApiState extends State { final ApiService _apiService = ApiService(); final NotificationManager _notificationManager = NotificationManager(); // Data dari sensor double suhu = 0.0; double berat = 0.0; double target = 0.0; // Data historis untuk grafik List suhuHistory = []; List beratHistory = []; double chartTimeCounter = 0; // Status relay bool heater = false; bool fanIn = false; bool exhaust = false; // Status sistem String statusSistem = 'DISCONNECTED'; bool isApiConnected = false; String lastStatusMessage = 'Menghubungkan...'; // Track previous status untuk notifikasi String _previousStatus = ''; // Timer untuk auto-update Timer? _pollingTimer; // Current navigation index int _currentIndex = 0; @override void initState() { super.initState(); _initializeChartData(); // Load initial chart data _initializeNotifications(); _initializeConnections(); _startPolling(); } // Initialize notifications Future _initializeNotifications() async { await _notificationManager.initialize(); } // Initialize chart data Future _initializeChartData() async { // Init dengan dummy data dulu _initializeDummyChartData(); try { // Load 20 data terakhir dari API untuk grafik final historyData = await _apiService.getDataHistory(limit: 20); if (historyData.isNotEmpty && mounted) { setState(() { suhuHistory.clear(); beratHistory.clear(); for (int i = 0; i < historyData.length; i++) { final data = historyData[i]; suhuHistory.add(FlSpot(i.toDouble(), data.suhu)); beratHistory.add(FlSpot(i.toDouble(), data.berat)); } chartTimeCounter = historyData.length.toDouble(); }); } } catch (e) { print('❌ Error loading chart data: $e'); // Tetap pakai dummy data jika error } } void _initializeDummyChartData() { suhuHistory.clear(); beratHistory.clear(); double beratAwal = 450.0; double suhuBase = 28.0; for (int i = 0; i < 15; i++) { double suhuVal = suhuBase + (i * 0.3) + (i % 2 == 0 ? 0.2 : -0.1); suhuHistory.add(FlSpot(i.toDouble(), suhuVal)); double beratVal = beratAwal - (i * 2.5); beratHistory.add(FlSpot(i.toDouble(), beratVal)); } chartTimeCounter = 15; } // Auto-update data setiap 5 detik void _startPolling() { _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) { if (mounted && isApiConnected) { _loadLatestData(); } }); } Future _initializeConnections() async { // Test API connection final apiConnected = await _apiService.testConnection(); setState(() { isApiConnected = apiConnected; // Set status langsung ke READY jika API connected if (apiConnected) { statusSistem = 'READY'; lastStatusMessage = 'Terhubung ke server'; } else { statusSistem = 'DISCONNECTED'; lastStatusMessage = 'Gagal terhubung ke server'; } }); if (apiConnected) { print('✅ API connected'); // Load initial data from API await _loadLatestData(); } // MQTT dihapus - hanya pakai API polling } Future _loadLatestData() async { try { final data = await _apiService.getLatestData(); if (data != null && mounted) { setState(() { suhu = data.suhu; berat = data.berat; target = data.target; heater = data.relay1; fanIn = data.relay2; exhaust = data.relay4; // 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++; // Update status dan trigger notifikasi String newStatus = data.status; // Notifikasi hanya untuk pengeringan selesai if (newStatus != _previousStatus) { // HANYA notifikasi pengeringan selesai if (newStatus == 'SELESAI' && _previousStatus != 'SELESAI') { print('🔔 Dashboard: Status changed to SELESAI'); print(' Previous: $_previousStatus → Current: $newStatus'); print(' Berat: $berat'); print(' Triggering notification...'); _notificationManager.notifyPengeringanSelesai(berat); } _previousStatus = newStatus; } statusSistem = newStatus; }); } else if (mounted) { // Data null, tampilkan error _showErrorSnackBar('Gagal memuat data dari server'); } } catch (e) { print('❌ Error loading data: $e'); if (mounted) { _showErrorSnackBar('Error: ${e.toString()}'); } } } // ===================================================== // UI HELPERS // ===================================================== void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.green, duration: const Duration(seconds: 2), ), ); } void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, duration: const Duration(seconds: 3), ), ); } @override void dispose() { _pollingTimer?.cancel(); // Jangan disconnect MQTT karena singleton super.dispose(); } @override Widget build(BuildContext context) { // List of screens final List screens = [ _buildDashboardContent(), const NotificationScreen(), ]; return Scaffold( appBar: _currentIndex == 0 ? AppBar( title: const Text('Dashboard Pengering Ikan'), actions: [ // API status only Padding( padding: const EdgeInsets.only(right: 16), child: Icon( isApiConnected ? Icons.cloud_done : Icons.cloud_off, color: isApiConnected ? Colors.green : Colors.red, size: 24, ), ), ], ) : null, body: screens[_currentIndex], bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, backgroundColor: Colors.white, selectedItemColor: Colors.blue, unselectedItemColor: Colors.grey, currentIndex: _currentIndex, onTap: (index) { if (index == 2) { // Navigate to chart screen Navigator.push( context, MaterialPageRoute(builder: (context) => const ChartScreen()), ); } else { setState(() { _currentIndex = index; }); } }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Dashboard', ), BottomNavigationBarItem( icon: Icon(Icons.notifications), label: 'Notifikasi', ), BottomNavigationBarItem( icon: Icon(Icons.bar_chart), label: 'Grafik', ), ], ), ); } Widget _buildDashboardContent() { return RefreshIndicator( onRefresh: _loadLatestData, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Status Card _buildStatusCard(), const SizedBox(height: 16), // Metrics Row( children: [ Expanded( child: _buildMetricCard( icon: Icons.thermostat, iconColor: Colors.orange, label: 'Suhu', value: suhu.toStringAsFixed(1), unit: '°C', ), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard( icon: Icons.scale, iconColor: Colors.blue, label: 'Berat', value: berat.toStringAsFixed(0), unit: 'g', ), ), ], ), const SizedBox(height: 12), // Target _buildMetricCard( icon: Icons.flag, iconColor: Colors.green, label: 'Target Berat', value: target.toStringAsFixed(0), unit: 'g', ), const SizedBox(height: 16), // ======= GRAFIK CHARTS ======= // Temperature Chart Widget TemperatureChart( dataPoints: suhuHistory, title: 'Suhu Real-time', lineColor: Colors.orange, gradientStartColor: Colors.orange, gradientEndColor: Colors.red, ), const SizedBox(height: 12), // Weight Chart Widget WeightChart( dataPoints: beratHistory, targetWeight: target, title: 'Berat Real-time', lineColor: Colors.blue, gradientStartColor: Colors.blue, gradientEndColor: Colors.cyan, ), const SizedBox(height: 16), // Devices const Text( 'Perangkat', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), GridView.count( crossAxisCount: 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 0.9, children: [ _buildDeviceCard( icon: Icons.whatshot, iconColor: Colors.deepOrange, label: 'Heater', status: heater, ), _buildDeviceCard( icon: Icons.air, iconColor: Colors.blue, label: 'Fan', status: fanIn, ), _buildDeviceCard( icon: Icons.wind_power, iconColor: Colors.teal, label: 'Exhaust', status: exhaust, ), ], ), ], ), ), ); } Widget _buildStatusCard() { Color statusColor; IconData statusIcon; String statusText = statusSistem; switch (statusSistem) { case 'SELESAI': statusColor = Colors.green; statusIcon = Icons.check_circle; break; case 'BERJALAN': statusColor = Colors.blue; statusIcon = Icons.play_circle; break; case 'READY': statusColor = Colors.orange; statusIcon = Icons.hourglass_empty; break; case 'SCANNING': statusColor = Colors.purple; statusIcon = Icons.search; break; case 'CONNECTED': statusColor = Colors.green; statusIcon = Icons.check_circle_outline; break; case 'DISCONNECTED': statusColor = Colors.red; statusIcon = Icons.error_outline; statusText = 'TERPUTUS'; break; case 'CONNECTING': statusColor = Colors.orange; statusIcon = Icons.sync; break; default: statusColor = Colors.grey; statusIcon = Icons.info; } return Container( padding: const EdgeInsets.all(16), 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: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( statusIcon, color: statusColor, size: 32, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Status Sistem', style: TextStyle( fontSize: 12, color: Colors.grey, ), ), const SizedBox(height: 4), Text( statusText, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: statusColor, ), ), const SizedBox(height: 4), Text( statusSistem == 'DISCONNECTED' ? 'ESP32 tidak terhubung. Periksa koneksi perangkat.' : lastStatusMessage, style: const TextStyle( fontSize: 12, color: Colors.grey, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } Widget _buildMetricCard({ required IconData icon, required Color iconColor, required String label, required String value, required String unit, }) { return Container( padding: const EdgeInsets.all(16), 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: [ Icon(icon, color: iconColor, size: 20), const SizedBox(width: 8), Text( label, style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( value, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( unit, style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ), ], ), ], ), ); } Widget _buildDeviceCard({ required IconData icon, required Color iconColor, required String label, required bool status, }) { 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( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: status ? iconColor.withOpacity(0.1) : Colors.grey.shade100, shape: BoxShape.circle, ), child: Icon( icon, color: status ? iconColor : Colors.grey, size: 24, ), ), const SizedBox(height: 4), Text( label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 3), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: status ? iconColor.withOpacity(0.15) : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( status ? 'ON' : 'OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: status ? iconColor : Colors.grey, ), ), ), ], ), ); } }