TKK_E32220671/lib/history_page.dart

741 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
DateTime? _selectedDate;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Timer? _refreshTimer;
int _refreshInterval = 1; // Default 1 menit
final DateFormat _dateFormat = DateFormat('HH:mm:ss');
Map<String, List<QueryDocumentSnapshot>> _groupedData = {};
Set<String> _expandedGroups = {};
// Tambahkan state untuk filter sensor
String _selectedSensor = 'Semua';
final List<String> _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<String, List<QueryDocumentSnapshot>> _groupDataByTime(
List<QueryDocumentSnapshot> docs) {
Map<String, List<QueryDocumentSnapshot>> grouped = {};
for (var doc in docs) {
final data = doc.data() as Map<String, dynamic>;
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<String, dynamic>;
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<int>(
// 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<QuerySnapshot>(
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<String, dynamic>;
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<String, dynamic>)['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<String, dynamic>;
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<void> _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,
),
),
);
}
}