import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/sensor_provider.dart'; import '../providers/ac_provider.dart'; import '../providers/ac_config_provider.dart'; import '../providers/ac_setup_provider.dart'; import '../providers/auth_provider.dart'; import '../models/sensor_model.dart'; import '../models/ac_model.dart'; import 'monitoring_history_page.dart'; import 'profile_page.dart'; import 'login_page.dart'; import '../core/app_color.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); @override State createState() => _DashboardPageState(); } class _DashboardPageState extends State { int currentIndex = 0; @override void initState() { super.initState(); context.read().startListening(); context.read().startListening(); context.read().startListening(); context.read().startListening(); } @override Widget build(BuildContext context) { final sensorProvider = context.watch(); final sensor = sensorProvider.sensor; final acProvider = context.watch(); final ac = acProvider.ac; final acConfig = context.watch(); final setupProvider = context.watch(); final pages = [ _buildDashboard( sensor, sensorProvider.isLoading, sensorProvider.errorMessage, ac, acProvider, acConfig, setupProvider, ), const MonitoringHistoryPage(), const ProfilePage(), ]; return Scaffold( body: pages[currentIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, onTap: (value) => setState(() => currentIndex = value), selectedItemColor: Colors.blueAccent, type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( icon: Icon(Icons.dashboard), label: 'Dashboard', ), BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Histori'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'), ], ), ); } // ====================== DASHBOARD BUILD ====================== Widget _buildDashboard( SensorModel? sensor, bool isLoading, String? errorMessage, ACModel ac, ACProvider acProvider, ACConfigProvider acConfig, ACSetupProvider setupProvider, ) { Widget bodyContent; if (isLoading) { bodyContent = const Center( child: CircularProgressIndicator(color: Colors.blueAccent), ); } else if (sensor == null) { bodyContent = Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 80, color: Colors.grey), const SizedBox(height: 16), const Text( 'Data Tidak Tersedia', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( errorMessage ?? 'Database kosong atau koneksi ke IoT terputus.', textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () { context.read().startListening(); context.read().startListening(); }, icon: const Icon(Icons.refresh), label: const Text('Coba Lagi'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, ), ), ], ), ), ); } else { final isAirGood = sensor.overallStatus == 'Sangat Baik' || sensor.overallStatus == 'Baik'; bodyContent = ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ // Header Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: const BoxDecoration( color: Colors.blueAccent, shape: BoxShape.circle, ), child: const Icon(Icons.ac_unit, color: Colors.white, size: 24), ), const SizedBox(width: 12), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Smart AC', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'IoT Dashboard', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), const Spacer(), // Live Badge Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( border: Border.all( color: Colors.green.withValues(alpha: 0.5), ), borderRadius: BorderRadius.circular(16), ), child: const Row( children: [ Icon( Icons.fiber_manual_record, color: Colors.green, size: 10, ), SizedBox(width: 4), Text( 'Live', style: TextStyle( color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.logout, color: Colors.white70), onPressed: () async { await context.read().logout(); if (!mounted) return; Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const LoginPage()), (route) => false, ); }, ), ], ), const SizedBox(height: 16), // Air Quality Banner Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isAirGood ? Colors.green : Colors.red, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Icon( isAirGood ? Icons.check_circle_outline : Icons.warning_amber_outlined, color: Colors.white, ), const SizedBox(width: 12), Expanded( child: Text( isAirGood ? 'Kualitas udara baik — semua parameter normal' : 'Kualitas udara buruk — Buka ventilasi ruangan', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ], ), ), const SizedBox(height: 20), const Text( 'Sensor Data', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white70, ), ), const SizedBox(height: 10), // 2x2 Sensor Grid Row( children: [ Expanded( child: _buildSensorCard( title: 'Suhu', value: '${sensor.temperature.toStringAsFixed(1)} °C', status: sensor.temperatureStatus, icon: Icons.thermostat, iconColor: Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: _buildSensorCard( title: 'Kelembaban', value: '${sensor.humidity.toStringAsFixed(1)} %', status: sensor.humidityStatus, icon: Icons.water_drop, iconColor: Colors.green, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildSensorCard( title: 'PM2.5', value: '${sensor.pm25.toStringAsFixed(2)} µg/m³', status: sensor.pm25Status, icon: Icons.grain, iconColor: Colors.amber, ), ), const SizedBox(width: 12), Expanded( child: _buildSensorCard( title: 'Karbon Dioksida', value: '${sensor.co2.toStringAsFixed(0)} ppm', status: sensor.co2Status, icon: Icons.co2, iconColor: Colors.redAccent, ), ), ], ), const SizedBox(height: 24), // ============ AC CARD (inline setup + manual control) ============ _buildACCard(ac, acProvider, acConfig, setupProvider), const SizedBox(height: 24), ], ); } return Scaffold( backgroundColor: AppColor.background, body: SafeArea(child: bodyContent), ); } // ====================== AC CARD ====================== // This single card handles both setup flow and manual control inline. Widget _buildACCard( ACModel ac, ACProvider acProvider, ACConfigProvider acConfig, ACSetupProvider setupProvider, ) { // Determine if we're currently in setup flow final bool isInSetup = setupProvider.mode == 'config' || setupProvider.mode == 'sweep'; // If in active setup flow, show the setup UI if (isInSetup) { return _buildSetupFlowCard(setupProvider); } // If has config, show manual control if (acConfig.hasConfig) { return _buildManualControlCard(ac, acProvider, setupProvider); } // No config and not in setup — show "start config" prompt return _buildStartConfigCard(setupProvider); } // ====================== START CONFIG CARD ====================== Widget _buildStartConfigCard(ACSetupProvider setupProvider) { return Card( color: AppColor.cardBg, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCardHeader( icon: Icons.tune, title: 'Konfigurasi AC', subtitle: 'Pilih merk lalu uji model hingga cocok', ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: setupProvider.isSending ? null : () => setupProvider.startConfigMode(), icon: setupProvider.isSending ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.settings), label: Text( setupProvider.isSending ? 'Mengirim...' : 'Mulai Konfigurasi AC', ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), const SizedBox(height: 12), const Text( 'Langkah singkat:', style: TextStyle(color: Colors.grey), ), const SizedBox(height: 6), const Text( '1) Tekan "Mulai Konfigurasi".\n' '2) Pilih merek AC dari daftar.\n' '3) Untuk tiap model tekan "YA" bila AC merespon.', style: TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ), ); } // ====================== SETUP FLOW CARD ====================== Widget _buildSetupFlowCard(ACSetupProvider setupProvider) { return Card( color: AppColor.cardBg, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCardHeader( icon: Icons.tune, title: 'Konfigurasi AC', subtitle: setupProvider.message, trailing: TextButton.icon( onPressed: setupProvider.isSending ? null : () => setupProvider.cancelConfig(), icon: const Icon(Icons.close, size: 16, color: Colors.redAccent), label: const Text( 'Batal', style: TextStyle(color: Colors.redAccent, fontSize: 12), ), ), ), const SizedBox(height: 16), // Step: select_brand — show brand list if (setupProvider.step == 'select_brand') _buildBrandGrid(setupProvider), // Step: confirm — show sweep confirmation if (setupProvider.step == 'confirm') _buildSweepConfirm(setupProvider), ], ), ), ); } Widget _buildBrandGrid(ACSetupProvider setupProvider) { if (setupProvider.brands.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(20), child: Column( children: [ CircularProgressIndicator(color: Colors.blueAccent), SizedBox(height: 12), Text( 'Memuat daftar merek...', style: TextStyle(color: Colors.grey), ), ], ), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pilih Merek AC:', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 12), ...setupProvider.brands.map((brand) { final int index = brand['index'] ?? 0; final String desc = brand['description'] ?? brand['name'] ?? ''; return Padding( padding: const EdgeInsets.only(bottom: 8), child: SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: setupProvider.isSending ? null : () => setupProvider.selectBrand(index), icon: const Icon(Icons.air, size: 18), label: Text(desc), style: OutlinedButton.styleFrom( foregroundColor: Colors.blueAccent, side: BorderSide( color: Colors.blueAccent.withValues(alpha: 0.4), ), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ); }), ], ); } Widget _buildSweepConfirm(ACSetupProvider setupProvider) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Show current brand/model being tested Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blueAccent.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.blueAccent.withValues(alpha: 0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Menguji: ${setupProvider.brandDesc}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), if (setupProvider.modelName.isNotEmpty) Text( 'Model: ${setupProvider.modelName}', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ), const SizedBox(height: 16), const Text( 'Apakah AC merespon sinyal ini?', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14, ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: setupProvider.isSending ? null : () => setupProvider.confirm('y'), icon: const Icon(Icons.check, size: 18), label: const Text('YA'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: setupProvider.isSending ? null : () => setupProvider.confirm('n'), icon: const Icon(Icons.close, size: 18), label: const Text('TIDAK'), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton( onPressed: setupProvider.isSending ? null : () => setupProvider.confirm('skip'), style: OutlinedButton.styleFrom( foregroundColor: Colors.white70, side: const BorderSide(color: Colors.white30), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text('SKIP'), ), ), ], ), ], ); } // ====================== MANUAL CONTROL CARD ====================== Widget _buildManualControlCard( ACModel ac, ACProvider acProvider, ACSetupProvider setupProvider, ) { return Card( color: AppColor.cardBg, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section header with reset button _buildCardHeader( icon: Icons.tune, title: 'Kontrol Manual', subtitle: 'Kirim sinyal IR ke AC via ESP32', trailing: IconButton( icon: const Icon(Icons.settings, color: Colors.blueAccent, size: 20), tooltip: 'Konfigurasi Ulang', onPressed: () => _showResetConfirmation(setupProvider), ), ), const SizedBox(height: 20), // Power, Swing, & Control Mode (Switch) Button Row Row( children: [ Expanded( flex: 5, child: OutlinedButton.icon( onPressed: () => acProvider.setPower(!ac.power), icon: Icon( ac.power ? Icons.power_off : Icons.power_settings_new, color: ac.power ? Colors.red : Colors.green, size: 18, ), label: Text( ac.power ? 'Matikan' : 'Nyalakan', style: TextStyle( color: ac.power ? Colors.red : Colors.green, fontSize: 11, ), ), style: OutlinedButton.styleFrom( side: BorderSide( color: ac.power ? Colors.red.withValues(alpha: 0.5) : Colors.green.withValues(alpha: 0.5), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 10), ), ), ), const SizedBox(width: 6), Expanded( flex: 5, child: OutlinedButton.icon( onPressed: () => acProvider.toggleSwing(), icon: Icon( Icons.swap_vert, color: ac.swing ? Colors.blueAccent : Colors.white60, size: 18, ), label: Text( ac.swing ? 'Swing ON' : 'Swing OFF', style: TextStyle( color: ac.swing ? Colors.blueAccent : Colors.white60, fontSize: 11, ), ), style: OutlinedButton.styleFrom( side: BorderSide( color: ac.swing ? Colors.blueAccent.withValues(alpha: 0.5) : Colors.white30, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 10), ), ), ), const SizedBox(width: 6), Expanded( flex: 4, child: Container( height: 44, // Match the height of OutlinedButton with vertical padding 10 decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( ac.controlMode == 'auto' ? 'Auto' : 'Manual', style: TextStyle( color: ac.controlMode == 'auto' ? Colors.blueAccent : Colors.orangeAccent, fontSize: 9, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 1), SizedBox( height: 20, child: Transform.scale( scale: 0.65, child: Switch.adaptive( value: ac.controlMode == 'auto', activeThumbColor: Colors.blueAccent, activeTrackColor: Colors.blueAccent.withValues(alpha: 0.3), inactiveThumbColor: Colors.orangeAccent, inactiveTrackColor: Colors.orangeAccent.withValues(alpha: 0.3), onChanged: (bool value) { acProvider.setControlMode(value ? 'auto' : 'manual'); }, ), ), ), ], ), ), ), ], ), const SizedBox(height: 16), // Mode & Fan cyclic buttons + Temp in a row Row( children: [ // Mode cyclic button Expanded( child: _buildCyclicButton( label: 'Mode', value: ac.mode, icon: _modeIcon(ac.mode), color: Colors.blueAccent, onTap: () => acProvider.cycleMode(), ), ), const SizedBox(width: 8), // Fan cyclic button Expanded( child: _buildCyclicButton( label: 'Kipas', value: ac.fan, icon: Icons.air, color: Colors.tealAccent, onTap: () => acProvider.cycleFan(), ), ), const SizedBox(width: 8), // Temperature control Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Suhu', style: TextStyle( color: Colors.grey, fontSize: 9, ), ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ GestureDetector( onTap: () => acProvider.setTemp(ac.temp - 1), child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( border: Border.all( color: Colors.white30, ), shape: BoxShape.circle, ), child: const Icon( Icons.remove, size: 12, color: Colors.white, ), ), ), Text( '${ac.temp}°', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.white, ), ), GestureDetector( onTap: () => acProvider.setTemp(ac.temp + 1), child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( border: Border.all( color: Colors.white30, ), shape: BoxShape.circle, ), child: const Icon( Icons.add, size: 12, color: Colors.white, ), ), ), ], ), ], ), ), ), ], ), ], ), ), ); } // ====================== RESET CONFIRMATION ====================== void _showResetConfirmation(ACSetupProvider setupProvider) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppColor.cardBg, title: const Text( 'Konfigurasi Ulang AC', style: TextStyle(color: Colors.white), ), content: const Text( 'Konfigurasi saat ini akan dihapus dan Anda harus memilih merek AC lagi. Lanjutkan?', style: TextStyle(color: Colors.white70), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Batal', style: TextStyle(color: Colors.grey)), ), TextButton( onPressed: () async { Navigator.of(ctx).pop(); await setupProvider.clearConfig(); await setupProvider.startConfigMode(); }, child: const Text('Ya, Reset', style: TextStyle(color: Colors.redAccent)), ), ], ), ); } // ====================== CARD HEADER ====================== Widget _buildCardHeader({ required IconData icon, required String title, required String subtitle, Widget? trailing, }) { return Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(icon, color: Colors.blue, size: 20), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( subtitle, style: const TextStyle(fontSize: 11, color: Colors.grey), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ?trailing, ], ); } // ====================== CYCLIC BUTTON ====================== Widget _buildCyclicButton({ required String label, required String value, required IconData icon, required Color color, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withValues(alpha: 0.4)), ), child: Column( children: [ Icon(icon, color: color, size: 16), const SizedBox(height: 2), Text( label, style: const TextStyle(color: Colors.grey, fontSize: 9), ), const SizedBox(height: 2), Text( value, style: TextStyle( color: color, fontSize: 11, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 2), Icon(Icons.refresh, color: color.withValues(alpha: 0.5), size: 10), ], ), ), ); } IconData _modeIcon(String mode) { switch (mode.toUpperCase()) { case 'COOL': return Icons.ac_unit; case 'DRY': return Icons.water_drop_outlined; case 'FAN': return Icons.air; case 'HEAT': return Icons.local_fire_department; case 'AUTO': return Icons.auto_mode; default: return Icons.ac_unit; } } // ====================== SENSOR CARD ====================== Widget _buildSensorCard({ required String title, required String value, required String status, required IconData icon, required Color iconColor, }) { return Card( color: AppColor.cardBg, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(width: 8), Expanded( child: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, color: Colors.grey, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 16), Text( value, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 10), _buildStatusChip(status), ], ), ), ); } Widget _buildStatusChip(String status) { Color color; if (status == 'Sangat Baik' || status == 'Baik') { color = AppColor.success; } else if (status == 'Buruk') { color = AppColor.danger; } else { color = AppColor.warning; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withValues(alpha: 0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( status.isEmpty ? 'Normal' : status, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.bold, ), ), ], ), ); } }