MIF_E31221269/lib/screens/history.dart

893 lines
35 KiB
Dart

import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:intl/intl.dart'; // Untuk format tanggal
import 'package:jago/services/db_helper.dart'; // Pastikan sesuai dengan nama proyek
import 'package:excel/excel.dart' as excel;
import 'dart:io';
import 'dart:async'; // Tambahkan import untuk StreamSubscription
class HistoryPage extends StatefulWidget {
@override
_HistoryPageState createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
List<Map<String, dynamic>> historyData = [];
List<Map<String, dynamic>> filteredHistory = [];
late DatabaseHelper dbHelper;
DateTime? selectedDate;
late DatabaseReference ageRef;
int ageInWeeks = 2; // Default age
int selectedAge = 0; // 0 untuk umur saat ini, 1-4 untuk umur spesifik
// Untuk filter tipe log
String selectedLogType = 'Semua';
List<String> logTypes = ['Semua', 'Relay', 'Kontrol', 'Status', 'Sensor', 'Sistem'];
// Tambahkan variabel untuk row pages
int _rowsPerPage = 10;
List<int> _rowsPerPageOptions = [5, 10, 20, 50, 100];
int _currentPage = 0;
// Daftar untuk menyimpan semua subscription
List<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
dbHelper = DatabaseHelper();
ageRef = FirebaseDatabase.instance.ref("chicken_age");
_loadHistoryFromDB();
_fetchAgeFromFirebase();
}
@override
void dispose() {
// Membersihkan semua subscription saat widget di-dispose
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
super.dispose();
}
Future<void> _loadHistoryFromDB() async {
if (!mounted) return; // Cek apakah widget masih mounted sebelum melakukan operasi
List<Map<String, dynamic>> dbData = await dbHelper.getHistory();
if (mounted) { // Cek lagi setelah operasi async
setState(() {
historyData = dbData; // Gunakan semua data tanpa filter
_applyFilter();
});
}
}
Future<void> _refreshHistory() async {
await _loadHistoryFromDB();
}
void _applyFilter() {
setState(() {
// Filter berdasarkan tanggal jika dipilih
List<Map<String, dynamic>> dateFiltered;
if (selectedDate == null) {
dateFiltered = List.from(historyData);
} else {
String selectedDateString = DateFormat('yyyy-MM-dd').format(selectedDate!);
dateFiltered = historyData.where((data) {
DateTime dateTime = DateTime.parse(data['timestamp']);
return DateFormat('yyyy-MM-dd').format(dateTime) == selectedDateString;
}).toList();
}
// Filter berdasarkan tipe log jika bukan 'Semua'
List<Map<String, dynamic>> typeFiltered;
if (selectedLogType == 'Semua') {
typeFiltered = dateFiltered;
} else {
typeFiltered = dateFiltered.where((data) {
String eventText = data['event'] ?? '';
// Ekstrak tipe dari teks event
if (selectedLogType == 'Relay') {
return eventText.contains('[RELAY]');
} else if (selectedLogType == 'Kontrol') {
return eventText.contains('[CONTROL]');
} else if (selectedLogType == 'Status') {
return eventText.contains('[STATUS]');
} else if (selectedLogType == 'Sensor') {
return eventText.contains('[SENSOR]');
} else if (selectedLogType == 'Sistem') {
return eventText.contains('[SYSTEM]');
}
return false;
}).toList();
}
// Filter berdasarkan umur ayam yang dipilih
List<Map<String, dynamic>> ageFiltered = typeFiltered.where((data) {
String eventText = data['event'] ?? '';
// Cek apakah event memiliki tag umur
if (eventText.contains("[Umur:")) {
// Ekstrak umur dari log
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int logAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return logAge == ageInWeeks;
} else {
return logAge == selectedAge;
}
}
return false;
} else {
// Untuk log lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
// Hapus data duplikat berdasarkan konten event
final Map<String, Map<String, dynamic>> uniqueEvents = {};
// Urutkan berdasarkan timestamp terbaru
ageFiltered.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Ambil entri unik berdasarkan konten event
for (var item in ageFiltered) {
String eventText = item['event'] ?? '';
// Hapus timestamp dari pertimbangan keunikan jika ada
String uniqueKey = eventText.replaceAll(RegExp(r'\d{2}:\d{2}:\d{2}'), '').trim();
// Simpan hanya entri pertama (yang paling baru) untuk setiap event unik
if (!uniqueEvents.containsKey(uniqueKey)) {
uniqueEvents[uniqueKey] = item;
}
}
// Konversi kembali ke list dan urutkan berdasarkan timestamp terbaru
filteredHistory = uniqueEvents.values.toList();
filteredHistory.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Reset halaman saat filter berubah
_currentPage = 0;
});
}
// Tambahkan method untuk paginasi
List<Map<String, dynamic>> _getPaginatedData() {
if (filteredHistory.isEmpty) return [];
int startIndex = _currentPage * _rowsPerPage;
int endIndex = startIndex + _rowsPerPage;
if (startIndex >= filteredHistory.length) {
_currentPage = 0;
startIndex = 0;
endIndex = _rowsPerPage;
}
if (endIndex > filteredHistory.length) {
endIndex = filteredHistory.length;
}
return filteredHistory.sublist(startIndex, endIndex);
}
Future<void> _pickDate(BuildContext context) async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: selectedDate ?? DateTime.now(),
firstDate: DateTime(2024),
lastDate: DateTime(2030),
);
if (pickedDate != null) {
setState(() {
selectedDate = pickedDate;
_applyFilter();
});
}
}
void _fetchAgeFromFirebase() {
final subscription = ageRef.onValue.listen((event) {
final data = event.snapshot.value;
if (data != null && mounted) {
int newAge = int.parse(data.toString());
if (newAge != ageInWeeks) {
print("🐔 Umur ayam berubah dari $ageInWeeks ke $newAge");
setState(() {
ageInWeeks = newAge;
});
// Muat ulang history saat umur berubah
_loadHistoryFromDB();
}
}
});
// Tambahkan subscription ke daftar
_subscriptions.add(subscription);
}
void _downloadHistoryAsExcel() async {
// Terapkan filter duplikat pada historyData
final Map<String, Map<String, dynamic>> uniqueEvents = {};
// Buat salinan historyData dan urutkan berdasarkan timestamp terbaru
List<Map<String, dynamic>> sortedData = List.from(historyData);
sortedData.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Filter berdasarkan umur ayam yang dipilih
List<Map<String, dynamic>> ageFilteredData = sortedData.where((data) {
String eventText = data['event'] ?? '';
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int logAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return logAge == ageInWeeks;
} else {
return logAge == selectedAge;
}
}
return false;
} else {
// Untuk log lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
// Ambil entri unik berdasarkan konten event
for (var item in ageFilteredData) {
String eventText = item['event'] ?? '';
// Hapus timestamp dari pertimbangan keunikan jika ada
String uniqueKey = eventText.replaceAll(RegExp(r'\d{2}:\d{2}:\d{2}'), '').trim();
// Simpan hanya entri pertama (yang paling baru) untuk setiap event unik
if (!uniqueEvents.containsKey(uniqueKey)) {
uniqueEvents[uniqueKey] = item;
}
}
// Konversi kembali ke list dan urutkan berdasarkan timestamp
List<Map<String, dynamic>> uniqueData = uniqueEvents.values.toList();
uniqueData.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
var excelFile = excel.Excel.createExcel();
var sheet = excelFile['Sheet1'];
sheet.appendRow(['Timestamp', 'Tipe', 'Event', 'Lampu', 'Kipas', 'Umur Ayam']);
for (var data in uniqueData) {
String lampuStatus = data['lampu'] == 1 ? 'Hidup' : 'Mati';
String kipasStatus = data['kipas'] == 1 ? 'Hidup' : 'Mati';
String eventText = data['event'] ?? '';
// Ekstrak tipe dari teks event
String type = 'Umum';
if (eventText.contains('[RELAY]')) {
type = 'Relay';
} else if (eventText.contains('[CONTROL]')) {
type = 'Kontrol';
} else if (eventText.contains('[STATUS]')) {
type = 'Status';
} else if (eventText.contains('[SENSOR]')) {
type = 'Sensor';
} else if (eventText.contains('[SYSTEM]')) {
type = 'Sistem';
}
// Hapus tag tipe dari teks event untuk tampilan Excel
eventText = eventText.replaceAll(RegExp(r'\[(RELAY|CONTROL|STATUS|SENSOR|SYSTEM)\]\s*'), '');
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default umur untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, '').trim();
}
sheet.appendRow([
data['timestamp'],
type,
eventText,
lampuStatus,
kipasStatus,
eventAge
]);
}
// Tentukan nama file berdasarkan mode filter umur
int exportAge = selectedAge == 0 ? ageInWeeks : selectedAge;
final directory = Directory('/storage/emulated/0/Documents/Data History');
if (!(await directory.exists())) {
await directory.create(recursive: true);
}
String filePath = '${directory.path}/History_${DateFormat('yyyyMMdd').format(DateTime.now())}_Umur${exportAge}minggu.xlsx';
File(filePath).writeAsBytesSync(excelFile.encode()!);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Data history berhasil diunduh sebagai Excel di $filePath')));
}
@override
Widget build(BuildContext context) {
// Dapatkan data yang sudah dipaginasi
final paginatedData = _getPaginatedData();
final int totalPages = (filteredHistory.length / _rowsPerPage).ceil();
// Tentukan judul AppBar berdasarkan mode filter umur
String appBarTitle = selectedAge == 0
? 'Riwayat Aktivitas (Umur: $ageInWeeks minggu)'
: 'Riwayat Aktivitas (Umur: $selectedAge minggu)';
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
appBarTitle,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
)
),
),
body: Column(
children: [
// Header section with curved background
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
children: [
// Tanggal Filter
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: Color(0xFFA82429),
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
selectedDate == null
? 'Semua Tanggal'
: DateFormat('dd/MM/yyyy').format(selectedDate!),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
TextButton(
onPressed: () => _pickDate(context),
child: Text(
'Pilih',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold,
),
),
style: TextButton.styleFrom(
minimumSize: Size(0, 0),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
),
],
),
),
),
],
),
SizedBox(height: 12),
// Filter umur ayam dan tipe log
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedAge,
isExpanded: true,
icon: Icon(Icons.filter_list, color: Color(0xFFA82429)),
items: [
DropdownMenuItem<int>(
value: 0,
child: Text('Umur Saat Ini (${ageInWeeks} minggu)'),
),
DropdownMenuItem<int>(
value: 1,
child: Text('Umur 1 minggu'),
),
DropdownMenuItem<int>(
value: 2,
child: Text('Umur 2 minggu'),
),
DropdownMenuItem<int>(
value: 3,
child: Text('Umur 3 minggu'),
),
DropdownMenuItem<int>(
value: 4,
child: Text('Umur 4 minggu'),
),
],
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
selectedAge = newValue;
_applyFilter();
});
}
},
),
),
),
),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.file_download, color: Color(0xFFA82429)),
onPressed: _downloadHistoryAsExcel,
tooltip: 'Unduh Data Excel',
),
),
],
),
SizedBox(height: 12),
// Filter tipe log
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedLogType,
isExpanded: true,
icon: Icon(Icons.category, color: Color(0xFFA82429)),
items: logTypes.map((String type) {
IconData typeIcon;
switch (type) {
case 'Relay':
typeIcon = Icons.power;
break;
case 'Kontrol':
typeIcon = Icons.settings;
break;
case 'Status':
typeIcon = Icons.info;
break;
case 'Sensor':
typeIcon = Icons.sensors;
break;
case 'Sistem':
typeIcon = Icons.computer;
break;
default:
typeIcon = Icons.list;
}
return DropdownMenuItem<String>(
value: type,
child: Row(
children: [
Icon(typeIcon, size: 18, color: Color(0xFFA82429)),
SizedBox(width: 8),
Text(type),
],
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
selectedLogType = newValue;
_applyFilter();
});
}
},
),
),
),
],
),
),
// Paginasi controls
Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Baris per halaman: ',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
items: _rowsPerPageOptions.map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_rowsPerPage = newValue;
_currentPage = 0; // Reset ke halaman pertama
});
}
},
),
),
),
],
),
if (totalPages > 0)
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios, size: 16),
color: _currentPage > 0 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage > 0
? () {
setState(() {
_currentPage--;
});
}
: null,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_currentPage + 1} / $totalPages',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: Icon(Icons.arrow_forward_ios, size: 16),
color: _currentPage < totalPages - 1 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage < totalPages - 1
? () {
setState(() {
_currentPage++;
});
}
: null,
),
],
),
],
),
),
// List of history items
Expanded(
child: RefreshIndicator(
onRefresh: _refreshHistory,
color: Color(0xFFA82429),
child: paginatedData.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history_toggle_off,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
selectedAge == 0
? 'Tidak ada riwayat untuk umur ayam $ageInWeeks minggu'
: 'Tidak ada riwayat untuk umur ayam $selectedAge minggu',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: paginatedData.length,
itemBuilder: (context, index) {
final data = paginatedData[index];
final DateTime dateTime = DateTime.parse(data['timestamp']);
String eventText = data['event'] ?? 'Data tidak tersedia';
// Ekstrak tipe dari teks event
Color cardColor;
IconData iconData;
if (eventText.contains('[RELAY]')) {
eventText = eventText.replaceAll('[RELAY] ', '');
cardColor = Colors.blue;
iconData = Icons.power;
} else if (eventText.contains('[CONTROL]')) {
eventText = eventText.replaceAll('[CONTROL] ', '');
cardColor = Colors.green;
iconData = Icons.settings;
} else if (eventText.contains('[STATUS]')) {
eventText = eventText.replaceAll('[STATUS] ', '');
cardColor = Colors.orange;
iconData = Icons.info;
} else if (eventText.contains('[SENSOR]')) {
eventText = eventText.replaceAll('[SENSOR] ', '');
cardColor = Colors.purple;
iconData = Icons.sensors;
} else if (eventText.contains('[SYSTEM]')) {
eventText = eventText.replaceAll('[SYSTEM] ', '');
cardColor = Colors.red;
iconData = Icons.computer;
} else {
cardColor = Colors.blueGrey;
iconData = Icons.history;
}
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default umur untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, '').trim();
}
return Card(
elevation: 3,
margin: EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
// Colored header
Container(
color: cardColor,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
iconData,
color: Colors.white,
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateTime),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Umur: $eventAge minggu',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
// Status perangkat
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
eventText,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildDeviceStatus(
'Lampu',
data['lampu'] == 1,
Icons.lightbulb,
Colors.amber
),
_buildDeviceStatus(
'Kipas',
data['kipas'] == 1,
Icons.propane,
Colors.blue
),
],
),
],
),
),
],
),
),
);
},
),
),
),
],
),
);
}
Widget _buildDeviceStatus(String title, bool isActive, IconData icon, Color activeColor) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isActive ? activeColor.withOpacity(0.2) : Colors.grey.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(
icon,
size: 16,
color: isActive ? activeColor : Colors.grey,
),
SizedBox(width: 4),
Text(
'$title: ${isActive ? "Aktif" : "Nonaktif"}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isActive ? activeColor : Colors.grey,
),
),
],
),
);
}
}