import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:intl/intl.dart'; import 'dart:async'; class HistoryPage extends StatefulWidget { const HistoryPage({super.key}); @override State createState() => _HistoryPageState(); } class _HistoryPageState extends State { DateTime? _selectedDate; final FirebaseFirestore _firestore = FirebaseFirestore.instance; Timer? _refreshTimer; int _refreshInterval = 1; // Default 1 menit final DateFormat _dateFormat = DateFormat('HH:mm:ss'); Map> _groupedData = {}; Set _expandedGroups = {}; // Tambahkan state untuk filter sensor String _selectedSensor = 'Semua'; final List _sensorOptions = [ 'Semua', 'Suhu', 'Kelembaban', 'Cahaya', 'Hujan', 'Status Cuaca', ]; @override void initState() { super.initState(); _startRefreshTimer(); } @override void dispose() { _refreshTimer?.cancel(); super.dispose(); } void _startRefreshTimer() { _refreshTimer?.cancel(); // Cancel existing timer if any _refreshTimer = Timer.periodic(Duration(minutes: _refreshInterval), (timer) { setState(() { // Force rebuild to refresh data }); }); } // Fungsi untuk mengelompokkan data berdasarkan waktu Map> _groupDataByTime( List docs) { Map> grouped = {}; for (var doc in docs) { final data = doc.data() as Map; final timestamp = (data['timestamp'] as Timestamp).toDate(); // Kelompokkan berdasarkan waktu lengkap (dd/MM/yyyy HH:mm) String groupKey = DateFormat('dd/MM/yyyy HH:mm').format(timestamp); if (!grouped.containsKey(groupKey)) { grouped[groupKey] = []; } grouped[groupKey]!.add(doc); } return grouped; } // Fungsi untuk menampilkan detail data void _showDataDetail(QueryDocumentSnapshot doc) { print('TAP DETAIL'); final data = doc.data() as Map; debugPrint('Data detail: ' + data.toString()); final timestamp = (data['timestamp'] as Timestamp?)?.toDate(); final suhu = (data['temperature'] ?? '-') as dynamic; final kelembaban = (data['humidity'] ?? '-') as dynamic; final cahaya = (data['light'] ?? '-') as dynamic; final rainValue = (data['rain'] ?? '-') as dynamic; final rainStatus = data['rain_status'] as String? ?? '-'; final weatherStatus = data['weather_status'] as String? ?? (cahaya is num ? _calculateWeatherStatus(cahaya.toDouble()) : '-'); showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ const Icon(Icons.info_outline, color: Colors.blue), const SizedBox(width: 8), const Text('Detail Data Sensor'), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Tampilkan isi data Map untuk debug if (data.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Data kosong: \\${doc.id}', style: const TextStyle(color: Colors.red), ), ) else Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Data: \\${data.toString()}', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ), _buildDetailRow( 'Waktu', timestamp != null ? DateFormat('dd/MM/yyyy HH:mm:ss').format(timestamp) : '-'), const Divider(), _buildDetailRow( 'Suhu', suhu is num ? '${suhu.toDouble().toStringAsFixed(1)}°C' : '-'), _buildDetailRow('Status Suhu', suhu is num ? _getStatusSuhu(suhu.toDouble()) : '-'), const Divider(), _buildDetailRow( 'Kelembaban', kelembaban is num ? '${kelembaban.toDouble().toStringAsFixed(1)}%' : '-'), _buildDetailRow( 'Status Kelembaban', kelembaban is num ? _getStatusKelembaban(kelembaban.toDouble()) : '-'), const Divider(), _buildDetailRow( 'Intensitas Cahaya', cahaya is num ? '${cahaya.toDouble().toStringAsFixed(0)} lux' : '-'), _buildDetailRow('Status Cuaca', weatherStatus), const Divider(), _buildDetailRow('Curah Hujan', rainValue is num ? '${rainValue.toStringAsFixed(0)}' : '-'), _buildDetailRow( 'Status Hujan', rainValue is num ? _getStatusHujan(rainValue.toDouble()) : '-'), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Tutup'), ), ], ), ); print('SHOW DIALOG DIPANGGIL'); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, child: Text( label, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ), const Text(': ', style: TextStyle(fontWeight: FontWeight.bold)), Expanded( child: Text( value, style: const TextStyle(fontSize: 14), ), ), ], ), ); } Widget _buildSensorRow(String label, String value, String status) { return Row( children: [ Expanded( flex: 2, child: Text( label, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), ), Expanded( flex: 1, child: Text( value, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), if (status.isNotEmpty) Expanded( flex: 1, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getStatusColor(status).withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: Text( status, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: _getStatusColor(status), ), textAlign: TextAlign.center, ), ), ), ], ); } // void _showRefreshIntervalDialog() { // showDialog( // context: context, // builder: (context) => AlertDialog( // title: const Text('Atur Interval Refresh'), // content: Column( // mainAxisSize: MainAxisSize.min, // children: [ // const Text('Pilih interval refresh data (dalam menit):'), // const SizedBox(height: 16), // DropdownButton( // value: _refreshInterval, // isExpanded: true, // items: const [ // DropdownMenuItem(value: 1, child: Text('1 menit')), // DropdownMenuItem(value: 2, child: Text('2 menit')), // DropdownMenuItem(value: 5, child: Text('5 menit')), // DropdownMenuItem(value: 10, child: Text('10 menit')), // DropdownMenuItem(value: 15, child: Text('15 menit')), // DropdownMenuItem(value: 30, child: Text('30 menit')), // ], // onChanged: (value) { // if (value != null) { // setState(() { // _refreshInterval = value; // }); // _startRefreshTimer(); // Navigator.pop(context); // } // }, // ), // ], // ), // actions: [ // TextButton( // onPressed: () => Navigator.pop(context), // child: const Text('Batal'), // ), // ], // ), // ); // } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Row( children: [ Text( 'Riwayat Data', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ], ), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ if (_selectedDate != null) IconButton( icon: const Icon(Icons.clear, color: Colors.white), onPressed: () { setState(() { _selectedDate = null; }); }, tooltip: 'Hapus Filter Tanggal', ), IconButton( icon: const Icon(Icons.calendar_today, color: Colors.white), onPressed: () => _selectDate(context), tooltip: 'Pilih Tanggal', ), // IconButton( // icon: const Icon(Icons.timer), // onPressed: _showRefreshIntervalDialog, // tooltip: 'Atur Interval Refresh', // ), ], ), body: Column( children: [ // Expanded agar StreamBuilder tetap memenuhi sisa layar Expanded( child: StreamBuilder( stream: _firestore .collection('sensor_history') .orderBy('timestamp', descending: true) .where('timestamp', isGreaterThanOrEqualTo: _selectedDate != null ? Timestamp.fromDate(DateTime(_selectedDate!.year, _selectedDate!.month, _selectedDate!.day)) : Timestamp.fromDate( DateTime.now().subtract(const Duration(days: 1)))) .where('timestamp', isLessThan: _selectedDate != null ? Timestamp.fromDate(DateTime(_selectedDate!.year, _selectedDate!.month, _selectedDate!.day + 1)) : Timestamp.fromDate( DateTime.now().add(const Duration(days: 1)))) .limit(100) // Batasi jumlah data yang ditampilkan .snapshots(includeMetadataChanges: true), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, color: Colors.red, size: 60, ), const SizedBox(height: 16), Text( 'Error: ${snapshot.error}', style: const TextStyle(color: Colors.red), ), ], ), ); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { return const Center( child: Text( 'Tidak ada data riwayat', style: TextStyle(fontSize: 16), ), ); } // Filter data berdasarkan tanggal yang dipilih final filteredDocs = _selectedDate != null ? snapshot.data!.docs.where((doc) { final data = doc.data() as Map; final timestamp = data['timestamp'] as Timestamp; final docDate = timestamp.toDate(); return docDate.year == _selectedDate!.year && docDate.month == _selectedDate!.month && docDate.day == _selectedDate!.day; }).toList() : snapshot.data!.docs; if (filteredDocs.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.calendar_today, size: 60, color: Colors.grey, ), const SizedBox(height: 16), Text( 'Tidak ada data untuk tanggal ${_selectedDate != null ? DateFormat('EEEE, dd/MM/yyyy').format(_selectedDate!) : 'yang dipilih'}', style: const TextStyle(fontSize: 16), textAlign: TextAlign.center, ), ], ), ); } // Tampilkan waktu update terakhir final lastUpdate = (filteredDocs.first.data() as Map)['timestamp'] as Timestamp; final lastUpdateTime = lastUpdate.toDate(); return Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Text( 'Terakhir diperbarui: ${DateFormat('HH:mm:ss').format(lastUpdateTime)}', style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), if (_selectedDate != null) ...[ const SizedBox(height: 4), Text( 'Tanggal: ${DateFormat('EEEE, dd/MM/yyyy').format(_selectedDate!)}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), ], ], ), ), Expanded( child: ListView.builder( itemCount: filteredDocs.length, itemBuilder: (context, index) { final doc = filteredDocs[index]; final data = doc.data() as Map; final timestamp = (data['timestamp'] as Timestamp).toDate(); final suhu = (data['temperature'] ?? 0.0) as num; final kelembaban = (data['humidity'] ?? 0.0) as num; final cahaya = (data['light'] ?? 0.0) as num; final rainValue = (data['rain'] ?? 0.0) as num; final weatherStatus = _getStatusCuaca(cahaya.toDouble()); return Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 12), child: InkWell( borderRadius: BorderRadius.circular(24), splashColor: Colors.blue.withOpacity(0.1), highlightColor: Colors.blue.withOpacity(0.05), onTap: () {}, // hanya untuk efek ripple child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.blue.shade50, Colors.blue.shade100.withOpacity(0.7), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.blueGrey.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 8), ), ], ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 22, vertical: 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.access_time, color: Colors.blue, size: 22), const SizedBox(width: 12), Text( DateFormat('dd/MM/yyyy HH:mm:ss') .format(timestamp), style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 18, color: Colors.blue, letterSpacing: 0.5, ), ), ], ), const SizedBox(height: 18), Row( children: [ const Icon(Icons.thermostat, color: Colors.redAccent, size: 20), const SizedBox(width: 8), Expanded( child: _buildDetailRow('Suhu', '${suhu.toDouble().toStringAsFixed(1)}°C')), const SizedBox(width: 8), _statusChip( _getStatusSuhu(suhu.toDouble()), _getStatusColor(_getStatusSuhu( suhu.toDouble())), ), ], ), const SizedBox(height: 8), Row( children: [ const Icon(Icons.water_drop, color: Colors.blueAccent, size: 20), const SizedBox(width: 8), Expanded( child: _buildDetailRow( 'Kelembaban', '${kelembaban.toDouble().toStringAsFixed(1)}%')), const SizedBox(width: 8), _statusChip( _getStatusKelembaban( kelembaban.toDouble()), _getStatusColor( _getStatusKelembaban( kelembaban.toDouble()))), ], ), const SizedBox(height: 8), Row( children: [ const Icon(Icons.wb_sunny, color: Colors.orange, size: 20), const SizedBox(width: 8), Expanded( child: _buildDetailRow('Cahaya', '${cahaya.toDouble().toStringAsFixed(0)} lux')), const SizedBox(width: 8), _statusChip(weatherStatus, _getStatusColor(weatherStatus)), ], ), const SizedBox(height: 8), Row( children: [ const Icon(Icons.grain, color: Colors.indigo, size: 20), const SizedBox(width: 8), Expanded( child: _buildDetailRow( 'Curah Hujan', '${rainValue.toStringAsFixed(0)}')), const SizedBox(width: 8), _statusChip( _getStatusHujan( rainValue.toDouble()), _getStatusColor(_getStatusHujan( rainValue.toDouble()))), ], ), ], ), ), ), ), ); }, ), ), ], ); }, ), ), ], ), ); } String _getStatusHujan(double kadar) { // Status hujan berdasarkan nilai sensor return (kadar < 500) ? "Hujan" : "Tidak Hujan"; } String _getStatusCuaca(double cahaya) { // Status cahaya berdasarkan nilai LDR return (cahaya < 700) ? "Terang" : "Gelap"; } Color _getStatusColor(String status) { // Warna untuk setiap status sensor switch (status) { case 'Hujan': return Colors.blue.shade700; case 'Tidak Hujan': return Colors.orange; case 'Terang': return Colors.orange; case 'Gelap': return Colors.grey.shade800; case 'Dingin': return Colors.blue.shade700; case 'Sejuk': return Colors.blue.shade400; case 'Normal': return Colors.green; case 'Hangat': return Colors.orange.shade700; case 'Panas': return Colors.red; case 'Kering': return Colors.orange; case 'Basah': return Colors.blue; default: return Colors.grey; } } String _calculateWeatherStatus(double cahaya) { // Status cuaca berdasarkan nilai LDR return (cahaya < 700) ? "Terang" : "Gelap"; } Color _getWeatherStatusColor(String status) { switch (status) { case 'Terang': return Colors.orange; case 'Gelap': return Colors.grey.shade800; default: return Colors.grey; } } String _getStatusSuhu(double suhu) { // Status suhu berdasarkan nilai sensor if (suhu <= 20) return 'Dingin'; if (suhu <= 23) return 'Sejuk'; if (suhu <= 26) return 'Normal'; if (suhu <= 27) return 'Hangat'; return 'Panas'; } String _getStatusKelembaban(double kelembaban) { // Status kelembaban berdasarkan nilai sensor if (kelembaban < 40) return 'Kering'; if (kelembaban > 80) return 'Basah'; return 'Normal'; } Color _getStatusSuhuColor(String status) { // Warna untuk setiap status suhu switch (status) { case 'Dingin': return Colors.blue.shade700; case 'Sejuk': return Colors.blue.shade400; case 'Normal': return Colors.orange; case 'Hangat': return Colors.orange.shade700; case 'Panas': return Colors.red; default: return Colors.grey; } } Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedDate ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( primary: Theme.of(context).colorScheme.primary, onPrimary: Colors.white, surface: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, ); if (picked != null) { setState(() { _selectedDate = picked; }); } } Widget _statusChip(String label, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle( color: color, fontWeight: FontWeight.bold, fontSize: 12, ), ), ); } }