import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/app_theme.dart'; import '../../auth/auth_notifier.dart'; import '../../auth/auth_state.dart'; import '../providers/monitoring_provider.dart'; import '../providers/control_notifier.dart'; import '../widgets/chart_range_selector.dart'; import '../widgets/chart_type_selector.dart'; import '../widgets/greeting_section.dart'; import '../widgets/device_status_card.dart'; import '../widgets/monitoring_card.dart'; import '../widgets/monitoring_chart.dart'; class MonitoringPage extends ConsumerWidget { const MonitoringPage({super.key}); String _getUserName(WidgetRef ref) { final authState = ref.watch(authProvider); if (authState is AuthSuccess) return authState.user.name; return 'Pengguna'; } String _getDeviceCode(WidgetRef ref) { final authState = ref.watch(authProvider); if (authState is AuthSuccess) return authState.user.deviceId ?? '-'; return '-'; } void _showClaimDeviceDialog(BuildContext context, WidgetRef ref) { final controller = TextEditingController(); final formKey = GlobalKey(); bool dialogLoading = false; String? dialogError; showDialog( context: context, barrierDismissible: true, builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Text( 'Hubungkan Alat', style: TextStyle(fontWeight: FontWeight.bold), ), content: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Masukkan Kode Perangkat (Claim Code) yang tertera pada alat Anda.', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary), ), const SizedBox(height: 16), TextFormField( controller: controller, decoration: InputDecoration( labelText: 'Claim Code', prefixIcon: const Icon(Icons.qr_code_scanner_outlined, color: AppTheme.primary), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: AppTheme.primary, width: 2), ), ), validator: (v) => (v == null || v.trim().isEmpty) ? 'Claim Code tidak boleh kosong' : null, ), if (dialogError != null) ...[ const SizedBox(height: 12), Text( dialogError!, style: const TextStyle(color: AppTheme.statusDanger, fontSize: 13), ), ], ], ), ), actions: [ TextButton( onPressed: dialogLoading ? null : () => Navigator.pop(context), child: const Text('Batal'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: dialogLoading ? null : () async { if (!formKey.currentState!.validate()) return; setState(() { dialogLoading = true; dialogError = null; }); try { await ref.read(authProvider.notifier).claimDevice(controller.text); if (context.mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Perangkat berhasil dihubungkan!'), backgroundColor: AppTheme.statusNormal, ), ); } } catch (e) { setState(() { dialogLoading = false; dialogError = e.toString().replaceFirst('Exception: ', ''); }); } }, child: dialogLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ) : const Text('Hubungkan', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ), ], ); }, ); }, ); } @override Widget build(BuildContext context, WidgetRef ref) { final monitoring = ref.watch(monitoringProvider); final control = ref.watch(controlProvider); final userName = _getUserName(ref); final authState = ref.watch(authProvider); final hasDevice = authState is AuthSuccess && authState.user.deviceId != null; return Scaffold( backgroundColor: AppTheme.bgPage, body: SafeArea( child: RefreshIndicator( onRefresh: () => hasDevice ? ref.read(monitoringProvider.notifier).fetchData(showLoading: true) : Future.value(), child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GreetingSection( userName: userName, subtitle: hasDevice ? 'Monitoring Real-Time Kumbung' : 'Selamat datang kembali!', ), const SizedBox(height: 20), if (!hasDevice) ...[ const SizedBox(height: 40), _EmptyDeviceState( onAddPressed: () => _showClaimDeviceDialog(context, ref), ), ] else ...[ DeviceStatusCard( isOnline: monitoring.isDeviceOnline, deviceCode: _getDeviceCode(ref), lastSeen: monitoring.latest?.timestamp, ), const SizedBox(height: 20), // ── Error banner ────────────────────────────── if (monitoring.errorMessage != null) _ErrorBanner(message: monitoring.errorMessage!), // ── Loading skeleton ────────────────────────── if (monitoring.isLoading) const _LoadingSkeleton() else if (monitoring.latest == null) ...[ // ── Belum ada data sensor ───────────────────── _NoDataBanner(), ] else ...[ // ── Sensor cards Row 1 ──────────────────────── Row( children: [ Expanded( child: MonitoringCard( value: monitoring.temperature.toStringAsFixed(1), unit: '°C', subtitle: 'Suhu Real Time', status: monitoring.tempStatus(control.suhuMinLimit, control.suhuMaxLimit), gradient: AppTheme.suhuGradient, timestamp: monitoring.latest?.timestamp, ), ), const SizedBox(width: 16), Expanded( child: MonitoringCard( value: monitoring.humidity.toStringAsFixed(1), unit: '%', subtitle: 'Kelembapan Real Time', status: monitoring.humidStatus(control.kelembapanLimit), gradient: AppTheme.humidGradient, timestamp: monitoring.latest?.timestamp, ), ), ], ), const SizedBox(height: 16), // ── Info Waktu Penyiraman Terbaik & Rentang Aman ── Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.info_outline, color: AppTheme.primary, size: 20), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Info Rekomendasi Budidaya', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 4), Text( 'Batas Suhu Aman: ${control.suhuMinLimit.round()}°C s.d ${control.suhuMaxLimit.round()}°C.\n' 'Waktu penyiraman terbaik untuk menjaga kelembapan kumbung adalah Pagi hari (06:00-08:00) dan Sore hari (16:00-18:00).', style: const TextStyle( fontSize: 11, color: AppTheme.textSecondary, height: 1.4, ), ), ], ), ), ], ), ), const SizedBox(height: 30), // ── Chart filter ────────────────────────────── const ChartRangeSelector(), const SizedBox(height: 12), const ChartTypeSelector(), const SizedBox(height: 20), // ── Chart ───────────────────────────────────── const MonitoringChart(), ], ], const SizedBox(height: 20), ], ), ), ), ), ); } } // ── Empty Device State ────────────────────────────────────────── class _EmptyDeviceState extends StatelessWidget { final VoidCallback onAddPressed; const _EmptyDeviceState({required this.onAddPressed}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 36), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: onAddPressed, child: Container( width: 84, height: 84, decoration: BoxDecoration( color: AppTheme.accentLight, shape: BoxShape.circle, border: Border.all( color: AppTheme.accent.withOpacity(0.2), width: 2, ), ), child: const Icon( Icons.add, size: 44, color: AppTheme.accent, ), ), ), const SizedBox(height: 24), const Text( 'Belum Ada Alat Terhubung', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 12), const Text( 'Tekan tombol + di atas untuk menambahkan perangkat baru Anda menggunakan Claim Code agar dapat memulai monitoring real-time kumbung jamur Anda.', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, height: 1.5, ), ), ], ), ); } } // ── Error banner ─────────────────────────────────────────────── class _ErrorBanner extends StatelessWidget { final String message; const _ErrorBanner({required this.message}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: AppTheme.statusDanger.withOpacity(0.1), border: Border.all(color: AppTheme.statusDanger.withOpacity(0.3)), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ const Icon(Icons.wifi_off, color: AppTheme.statusDanger, size: 18), const SizedBox(width: 10), Expanded( child: Text( 'Gagal mengambil data: $message', style: const TextStyle( color: AppTheme.statusDanger, fontSize: 13, ), ), ), ], ), ); } } // ── Loading skeleton ─────────────────────────────────────────── class _LoadingSkeleton extends StatelessWidget { const _LoadingSkeleton(); @override Widget build(BuildContext context) { return Row( children: [ Expanded(child: _skeletonBox(height: 120)), const SizedBox(width: 16), Expanded(child: _skeletonBox(height: 120)), ], ); } Widget _skeletonBox({required double height}) { return Container( height: height, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(20), ), ); } } // ── No Data banner ───────────────────────────────────────────── class _NoDataBanner extends StatelessWidget { @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), decoration: BoxDecoration( color: Colors.grey.shade100, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(16), ), child: const Column( children: [ Icon(Icons.sensors_off, color: Colors.grey, size: 40), SizedBox(height: 12), Text( 'Belum Ada Data Sensor', style: TextStyle( color: Colors.grey, fontSize: 16, fontWeight: FontWeight.bold, ), ), SizedBox(height: 6), Text( 'Pastikan perangkat ESP32 menyala dan\nterhubung ke internet.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey, fontSize: 13), ), ], ), ); } }