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 { List> historyData = []; List> 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 logTypes = ['Semua', 'Relay', 'Kontrol', 'Status', 'Sensor', 'Sistem']; // Tambahkan variabel untuk row pages int _rowsPerPage = 10; List _rowsPerPageOptions = [5, 10, 20, 50, 100]; int _currentPage = 0; // Daftar untuk menyimpan semua subscription List _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 _loadHistoryFromDB() async { if (!mounted) return; // Cek apakah widget masih mounted sebelum melakukan operasi List> dbData = await dbHelper.getHistory(); if (mounted) { // Cek lagi setelah operasi async setState(() { historyData = dbData; // Gunakan semua data tanpa filter _applyFilter(); }); } } Future _refreshHistory() async { await _loadHistoryFromDB(); } void _applyFilter() { setState(() { // Filter berdasarkan tanggal jika dipilih List> 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> 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> 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> 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> _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 _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> uniqueEvents = {}; // Buat salinan historyData dan urutkan berdasarkan timestamp terbaru List> sortedData = List.from(historyData); sortedData.sort((a, b) => DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp'])) ); // Filter berdasarkan umur ayam yang dipilih List> 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> 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( value: selectedAge, isExpanded: true, icon: Icon(Icons.filter_list, color: Color(0xFFA82429)), items: [ DropdownMenuItem( value: 0, child: Text('Umur Saat Ini (${ageInWeeks} minggu)'), ), DropdownMenuItem( value: 1, child: Text('Umur 1 minggu'), ), DropdownMenuItem( value: 2, child: Text('Umur 2 minggu'), ), DropdownMenuItem( value: 3, child: Text('Umur 3 minggu'), ), DropdownMenuItem( 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( 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( 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( value: _rowsPerPage, isDense: true, items: _rowsPerPageOptions.map((int value) { return DropdownMenuItem( 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, ), ), ], ), ); } }