import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'global_data.dart'; class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @override State createState() => _HistoryScreenState(); } class _HistoryScreenState extends State { String selectedChart = 'Suhu'; String selectedRange = 'Semua'; bool _isLoading = false; // Untuk selection mode bool _isSelectionMode = false; Set _selectedIds = {}; // Paginasi int _currentPage = 0; final int _itemsPerPage = 15; final List rangeOptions = [ 'Semua', 'Hari Ini', 'Minggu Ini', 'Bulan Ini' ]; String _getRangeParam() { switch (selectedRange) { case 'Hari Ini': return 'hari_ini'; case 'Minggu Ini': return 'minggu_ini'; case 'Bulan Ini': return 'bulan_ini'; default: return 'semua'; } } Future _loadDataFromServer({bool forceRefresh = false}) async { setState(() { _isLoading = true; }); String rangeParam = _getRangeParam(); await HistoryManager.loadHistory( range: rangeParam, forceRefresh: forceRefresh); if (mounted) { setState(() { _isLoading = false; _currentPage = 0; }); } } List> _getFilteredData() { if (historyGlobal.isEmpty) return []; if (selectedRange == 'Semua') { List> sorted = List.from(historyGlobal); sorted.sort((a, b) { final waktuA = a['waktu'] as DateTime; final waktuB = b['waktu'] as DateTime; return waktuA.compareTo(waktuB); }); return sorted; } final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); List> filtered = historyGlobal.where((data) { final waktu = data['waktu'] as DateTime; switch (selectedRange) { case 'Hari Ini': return waktu.isAfter(today.subtract(const Duration(days: 1))); case 'Minggu Ini': return waktu.isAfter(today.subtract(const Duration(days: 7))); case 'Bulan Ini': return waktu.isAfter(DateTime(now.year, now.month - 1, now.day)); default: return true; } }).toList(); filtered.sort((a, b) { final waktuA = a['waktu'] as DateTime; final waktuB = b['waktu'] as DateTime; return waktuA.compareTo(waktuB); }); return filtered; } List> _getDisplayData() { final data = _getFilteredData(); return data.reversed.toList(); } // Data untuk grafik dengan sampling List> _getChartData() { final data = _getFilteredData(); if (data.length <= 50) return data; // Sampling data untuk grafik final sampled = >[]; final step = data.length / 50; for (int i = 0; i < 50; i++) { final index = (i * step).floor(); if (index < data.length) { sampled.add(data[index]); } } // Tambahkan data terakhir if (sampled.isNotEmpty && sampled.last != data.last) { sampled.add(data.last); } return sampled; } // Data paginasi untuk list List> _getPaginatedData() { final displayData = _getDisplayData(); final start = _currentPage * _itemsPerPage; final end = (start + _itemsPerPage) > displayData.length ? displayData.length : start + _itemsPerPage; return displayData.sublist(start, end); } int _getTotalPages() { final total = _getDisplayData().length; return (total / _itemsPerPage).ceil(); } Future _deleteSelectedHistory() async { if (_selectedIds.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Pilih minimal satu data untuk dihapus'), backgroundColor: Colors.orange, ), ); return; } final confirm = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Row( children: [ Icon(Icons.delete_forever, color: Colors.red), SizedBox(width: 8), Text('Hapus Data Terpilih'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Apakah anda yakin ingin menghapus ${_selectedIds.length} data yang dipilih?', textAlign: TextAlign.center, ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(10), ), child: const Row( children: [ Icon(Icons.warning, size: 16, color: Colors.orange), SizedBox(width: 8), Expanded( child: Text( 'Data yang dihapus tidak dapat dikembalikan!', style: TextStyle(fontSize: 11, color: Colors.orange), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Batal'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('Hapus'), ), ], ), ); if (confirm != true) return; setState(() { _isLoading = true; }); final displayData = _getDisplayData(); final List> toDelete = []; for (var id in _selectedIds) { if (id < displayData.length) { toDelete.add(displayData[id]); } } historyGlobal.removeWhere((item) { return toDelete.any((deleteItem) => deleteItem['waktu'].toString() == item['waktu'].toString()); }); await HistoryManager.saveHistoryLocal(); await _loadDataFromServer(forceRefresh: true); setState(() { _isLoading = false; _isSelectionMode = false; _selectedIds.clear(); _currentPage = 0; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${toDelete.length} data berhasil dihapus'), backgroundColor: Colors.green, ), ); } Future _deleteAllHistory() async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Row( children: [ Icon(Icons.delete_forever, color: Colors.red), SizedBox(width: 8), Text('Hapus Semua History'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Apakah anda yakin ingin menghapus semua history?'), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(10), ), child: Text( '${historyGlobal.length} data akan dihapus', style: const TextStyle(fontWeight: FontWeight.bold), ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: const Row( children: [ Icon(Icons.warning, size: 16, color: Colors.orange), SizedBox(width: 8), Expanded( child: Text( 'Data yang dihapus tidak dapat dikembalikan!', style: TextStyle(fontSize: 11, color: Colors.orange), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Batal'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('Hapus'), ), ], ), ); if (confirm != true) return; setState(() { _isLoading = true; }); await HistoryManager.clearHistory(); await _loadDataFromServer(forceRefresh: true); setState(() { _isLoading = false; _isSelectionMode = false; _selectedIds.clear(); _currentPage = 0; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Semua history berhasil dihapus'), backgroundColor: Colors.red, ), ); } void _toggleSelectionMode() { setState(() { _isSelectionMode = !_isSelectionMode; if (!_isSelectionMode) { _selectedIds.clear(); } }); } void _toggleSelection(int index) { setState(() { if (_selectedIds.contains(index)) { _selectedIds.remove(index); } else { _selectedIds.add(index); } }); } void _selectAll() { final displayData = _getDisplayData(); setState(() { _selectedIds = Set.from(List.generate(displayData.length, (i) => i)); }); } void _deselectAll() { setState(() { _selectedIds.clear(); }); } double rata(String key) { final filteredData = _getFilteredData(); if (filteredData.isEmpty) return 0; double total = 0; for (var item in filteredData) { total += item[key] as double; } return total / filteredData.length; } double getMinValue(String key) { final filteredData = _getFilteredData(); if (filteredData.isEmpty) return 0; double min = double.infinity; for (var item in filteredData) { double value = item[key] as double; if (value < min) min = value; } return min < 0 ? min : 0; } double getMaxValue(String key) { final filteredData = _getFilteredData(); if (filteredData.isEmpty) return 100; double max = 0; for (var item in filteredData) { double value = item[key] as double; if (value > max) max = value; } return max + (max * 0.1); } String getUnit() { switch (selectedChart) { case 'Suhu': return '°C'; case 'Kelembapan': return '%'; case 'Berat': return 'g'; default: return ''; } } Color getChartColor() { switch (selectedChart) { case 'Suhu': return Colors.red; case 'Kelembapan': return Colors.blue; case 'Berat': return Colors.green; default: return Colors.red; } } IconData getChartIcon() { switch (selectedChart) { case 'Suhu': return Icons.thermostat; case 'Kelembapan': return Icons.water_drop; case 'Berat': return Icons.fitness_center; default: return Icons.show_chart; } } void showIntervalDialog() { TextEditingController valueController = TextEditingController(text: HistoryManager.intervalValue.toString()); String tempInterval = HistoryManager.intervalUnit; showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setStateDialog) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20)), title: const Row( children: [ Icon(Icons.timer, color: Colors.red), SizedBox(width: 8), Text('Setting Interval'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Atur interval penyimpanan data sensor'), const SizedBox(height: 20), Row( children: [ Expanded( child: SegmentedButton( segments: const [ ButtonSegment( value: 'detik', label: Text('Detik'), icon: Icon(Icons.timer, size: 16)), ButtonSegment( value: 'menit', label: Text('Menit'), icon: Icon(Icons.access_time, size: 16)), ButtonSegment( value: 'jam', label: Text('Jam'), icon: Icon(Icons.schedule, size: 16)), ], selected: {tempInterval}, onSelectionChanged: (Set newSelection) { setStateDialog(() { tempInterval = newSelection.first; }); }, ), ), ], ), const SizedBox(height: 20), Row( children: [ const Text('Setiap '), Expanded( child: TextField( controller: valueController, keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(10)), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), ), ), ), Text(' $tempInterval'), ], ), const SizedBox(height: 10), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(10), ), child: Column( children: [ const Icon(Icons.info, color: Colors.blue), const SizedBox(height: 4), Text( 'Data akan disimpan setiap ${valueController.text} $tempInterval', style: const TextStyle(fontSize: 12), textAlign: TextAlign.center, ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Batal')), ElevatedButton( onPressed: () async { int newValue = int.tryParse(valueController.text) ?? 5; await HistoryManager.updateInterval(newValue, tempInterval); if (mounted) { setState(() {}); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Interval diubah menjadi setiap $newValue $tempInterval'), backgroundColor: Colors.green, ), ); } }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('Simpan'), ), ], ); }, ); }, ); } int _getBottomTitleInterval(int dataLength) { if (dataLength <= 8) return 1; if (dataLength <= 15) return 2; if (dataLength <= 25) return 3; if (dataLength <= 40) return 5; if (dataLength <= 60) return 8; if (dataLength <= 100) return 12; if (dataLength <= 150) return 20; return 30; } Widget _buildLineChart() { final chartData = _getChartData(); final filteredData = _getFilteredData(); if (filteredData.isEmpty) { return Container( height: 250, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.show_chart, size: 48, color: Colors.grey.shade400), const SizedBox(height: 8), Text( 'Tidak ada data untuk rentang waktu ini', style: TextStyle(color: Colors.grey.shade500), ), const SizedBox(height: 4), Text( 'Pilih rentang waktu lain atau tunggu data terkumpul', style: TextStyle(fontSize: 11, color: Colors.grey.shade400), ), ], ), ); } String key = ''; switch (selectedChart) { case 'Suhu': key = 'suhu'; break; case 'Kelembapan': key = 'kelembapan'; break; case 'Berat': key = 'berat'; break; } final List spots = []; final double minValue = getMinValue(key); final double maxValue = getMaxValue(key); for (int i = 0; i < chartData.length; i++) { final value = chartData[i][key] as double; spots.add(FlSpot(i.toDouble(), value)); } final isManyData = chartData.length > 30; final chartWidth = isManyData ? chartData.length * 25.0 : double.infinity; return SizedBox( height: 280, child: isManyData ? SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: chartWidth, height: 280, child: _buildChart(spots, minValue, maxValue, chartData), ), ) : _buildChart(spots, minValue, maxValue, chartData), ); } Widget _buildChart(List spots, double minValue, double maxValue, List> chartData) { final interval = _getBottomTitleInterval(chartData.length); return LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: true, horizontalInterval: (maxValue - minValue) / 5, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.shade300, strokeWidth: 1, dashArray: [5, 5], ); }, getDrawingVerticalLine: (value) { return FlLine( color: Colors.grey.shade300, strokeWidth: 1, dashArray: [5, 5], ); }, ), titlesData: FlTitlesData( show: true, rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 35, interval: interval.toDouble(), getTitlesWidget: (value, meta) { if (value.toInt() >= chartData.length) return const SizedBox(); final data = chartData[value.toInt()]; final waktu = data['waktu'] as DateTime; String timeStr; if (chartData.length > 60) { // Banyak data: tampilkan tanggal timeStr = '${waktu.day}/${waktu.month}'; } else if (chartData.length > 30) { // Sedang: tanggal + jam timeStr = '${waktu.day}/${waktu.month}\n${waktu.hour}h'; } else { // Sedikit: jam:menit timeStr = '${waktu.hour.toString().padLeft(2, '0')}:${waktu.minute.toString().padLeft(2, '0')}'; } return Padding( padding: const EdgeInsets.only(top: 8), child: Text( timeStr, style: const TextStyle(fontSize: 9), textAlign: TextAlign.center, ), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 45, interval: (maxValue - minValue) / 5, getTitlesWidget: (value, meta) { return Text( value.toStringAsFixed(1), style: const TextStyle(fontSize: 10), ); }, ), ), ), borderData: FlBorderData( show: true, border: Border.all(color: Colors.grey.shade300, width: 1), ), minX: 0, maxX: (chartData.length - 1).toDouble(), minY: minValue < 0 ? minValue : 0, maxY: maxValue, lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, color: getChartColor(), barWidth: 2.5, isStrokeCapRound: true, dotData: FlDotData( show: chartData.length <= 30, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: chartData.length > 20 ? 2 : 4, color: Colors.white, strokeWidth: 2, strokeColor: getChartColor(), ); }, ), belowBarData: BarAreaData( show: true, color: getChartColor().withOpacity(0.1), ), ), ], extraLinesData: ExtraLinesData( horizontalLines: [ HorizontalLine( y: rata(selectedChart == 'Suhu' ? 'suhu' : selectedChart == 'Kelembapan' ? 'kelembapan' : 'berat'), color: Colors.grey.shade400, strokeWidth: 1, dashArray: [5, 5], label: HorizontalLineLabel( show: true, labelResolver: (line) => 'Rata-rata', style: const TextStyle( fontSize: 8, color: Colors.grey, ), ), ), ], ), ), ); } @override void initState() { super.initState(); _loadDataFromServer(forceRefresh: true); } @override Widget build(BuildContext context) { final displayData = _getDisplayData(); final filteredData = _getFilteredData(); final paginatedData = _getPaginatedData(); final bool hasData = filteredData.isNotEmpty; final int selectedCount = _selectedIds.length; final totalPages = _getTotalPages(); final size = MediaQuery.of(context).size; final isSmallScreen = size.width < 400; return Scaffold( backgroundColor: const Color(0xfff8f9fa), appBar: AppBar( title: Text( _isSelectionMode ? 'Pilih Data ($selectedCount)' : 'Riwayat Sensor', style: const TextStyle(fontWeight: FontWeight.bold), ), backgroundColor: _isSelectionMode ? Colors.blue : Colors.red, foregroundColor: Colors.white, elevation: 0, actions: [ if (_isSelectionMode) ...[ IconButton( icon: const Icon(Icons.select_all), onPressed: _selectAll, tooltip: 'Pilih Semua', ), IconButton( icon: const Icon(Icons.deselect), onPressed: _deselectAll, tooltip: 'Batal Pilih', ), IconButton( icon: const Icon(Icons.delete), onPressed: _selectedIds.isEmpty ? null : _deleteSelectedHistory, tooltip: 'Hapus Terpilih', ), ], IconButton( icon: Icon(_isSelectionMode ? Icons.close : Icons.checklist), onPressed: _toggleSelectionMode, tooltip: _isSelectionMode ? 'Batal' : 'Pilih Data', ), if (!_isSelectionMode) ...[ IconButton( icon: const Icon(Icons.timer), onPressed: showIntervalDialog, tooltip: 'Setting Interval', ), IconButton( icon: const Icon(Icons.delete_forever), onPressed: historyGlobal.isEmpty ? null : _deleteAllHistory, tooltip: 'Hapus Semua History', ), IconButton( icon: const Icon(Icons.refresh), onPressed: () async { await _loadDataFromServer(forceRefresh: true); setState(() {}); }, tooltip: 'Refresh Data', ), ], ], ), body: RefreshIndicator( onRefresh: () async { await _loadDataFromServer(forceRefresh: true); setState(() {}); }, child: _isLoading ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.red), SizedBox(height: 16), Text('Memuat data...'), ], ), ) : SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(isSmallScreen ? 10 : 16), child: Column( children: [ // Info Interval Card Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.blue.shade200), ), child: Row( children: [ const Icon(Icons.timer, color: Colors.blue, size: 18), const SizedBox(width: 6), Expanded( child: Text( 'Interval: ${HistoryManager.intervalValue} ${HistoryManager.intervalUnit}', style: TextStyle( color: Colors.blue.shade700, fontSize: isSmallScreen ? 11 : 13, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), TextButton.icon( onPressed: showIntervalDialog, icon: const Icon(Icons.edit, size: 14), label: const Text('Ubah'), style: TextButton.styleFrom( foregroundColor: Colors.blue, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), ), const SizedBox(height: 16), // Statistik Card - Responsive Container( padding: EdgeInsets.all(isSmallScreen ? 12 : 16), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.red.shade700, Colors.red.shade500]), borderRadius: BorderRadius.circular(20), ), child: isSmallScreen ? Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem( Icons.thermostat, 'Suhu', hasData ? '${rata("suhu").toStringAsFixed(1)}°C' : '0°C'), _buildStatItem( Icons.water_drop, 'Kelembapan', hasData ? '${rata("kelembapan").toStringAsFixed(1)}%' : '0%'), ], ), const SizedBox(height: 8), _buildStatItem( Icons.fitness_center, 'Berat', hasData ? '${rata("berat").toStringAsFixed(1)}g' : '0g'), ], ) : Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem( Icons.thermostat, 'Suhu', hasData ? '${rata("suhu").toStringAsFixed(1)}°C' : '0°C'), _buildStatItem( Icons.water_drop, 'Kelembapan', hasData ? '${rata("kelembapan").toStringAsFixed(1)}%' : '0%'), _buildStatItem( Icons.fitness_center, 'Berat', hasData ? '${rata("berat").toStringAsFixed(1)}g' : '0g'), ], ), ), const SizedBox(height: 16), // Filter Range - Responsive Container( padding: EdgeInsets.all(isSmallScreen ? 8 : 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( blurRadius: 10, color: Colors.black.withOpacity(0.05)), ], ), child: Row( children: [ const Icon(Icons.filter_list, color: Colors.red, size: 18), const SizedBox(width: 6), if (!isSmallScreen) const Text('Rentang Waktu:'), const SizedBox(width: 6), Expanded( child: SegmentedButton( segments: rangeOptions.map((range) { return ButtonSegment( value: range, label: Text(isSmallScreen ? range.substring(0, 3) : range)); }).toList(), selected: {selectedRange}, onSelectionChanged: (Set newSelection) async { setState(() { selectedRange = newSelection.first; _isLoading = true; }); await _loadDataFromServer(forceRefresh: true); setState(() { _isLoading = false; if (_isSelectionMode) { _selectedIds.clear(); } _currentPage = 0; }); }, ), ), ], ), ), const SizedBox(height: 16), // Chart Type Selector - Responsive Container( padding: EdgeInsets.all(isSmallScreen ? 4 : 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( blurRadius: 10, color: Colors.black.withOpacity(0.05)), ], ), child: Row( children: [ _buildChartSelector('Suhu', Icons.thermostat, Colors.red, isSmallScreen), _buildChartSelector('Kelembapan', Icons.water_drop, Colors.blue, isSmallScreen), _buildChartSelector('Berat', Icons.fitness_center, Colors.green, isSmallScreen), ], ), ), const SizedBox(height: 16), // Chart Card Container( padding: EdgeInsets.all(isSmallScreen ? 12 : 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( blurRadius: 10, color: Colors.black.withOpacity(0.05)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(getChartIcon(), color: getChartColor(), size: 20), const SizedBox(width: 6), Text( 'Grafik $selectedChart', style: TextStyle( fontSize: isSmallScreen ? 14 : 16, fontWeight: FontWeight.bold), ), const Spacer(), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: getChartColor().withOpacity(0.1), borderRadius: BorderRadius.circular(15), ), child: Text( '${filteredData.length} data', style: TextStyle( fontSize: 10, color: getChartColor(), ), ), ), ], ), const SizedBox(height: 12), _buildLineChart(), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 16, height: 2, color: getChartColor(), ), const SizedBox(width: 6), Text( '$selectedChart (${getUnit()})', style: TextStyle( fontSize: 10, color: Colors.grey.shade600, ), ), if (filteredData.length > 50) ...[ const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(10), ), child: Text( 'Sampled', style: TextStyle( fontSize: 8, color: Colors.orange.shade700, ), ), ), ], ], ), ], ), ), const SizedBox(height: 16), // Ringkasan Data - Responsive Container( padding: EdgeInsets.all(isSmallScreen ? 12 : 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( blurRadius: 10, color: Colors.black.withOpacity(0.05)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Ringkasan Data', style: TextStyle( fontSize: isSmallScreen ? 14 : 16, fontWeight: FontWeight.bold)), const SizedBox(height: 10), isSmallScreen ? Column( children: [ Row( children: [ Expanded( child: _buildSummaryItem( Icons.thermostat, 'Suhu', hasData ? '${_getMaxValueFromList(filteredData, 'suhu').toStringAsFixed(1)}°C' : '0°C', '${_getMinValueFromList(filteredData, 'suhu').toStringAsFixed(1)}°C', Colors.red.shade50, Colors.red, ), ), const SizedBox(width: 8), Expanded( child: _buildSummaryItem( Icons.water_drop, 'Kelembapan', hasData ? '${_getMaxValueFromList(filteredData, 'kelembapan').toStringAsFixed(1)}%' : '0%', '${_getMinValueFromList(filteredData, 'kelembapan').toStringAsFixed(1)}%', Colors.blue.shade50, Colors.blue, ), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: _buildSummaryItem( Icons.fitness_center, 'Berat', hasData ? '${_getMaxValueFromList(filteredData, 'berat').toStringAsFixed(0)}g' : '0g', '${_getMinValueFromList(filteredData, 'berat').toStringAsFixed(0)}g', Colors.green.shade50, Colors.green, ), ), const SizedBox(width: 8), Expanded( child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ const Icon(Icons.data_usage, color: Colors.orange, size: 20), const SizedBox(height: 4), Text('Total Data', style: TextStyle( color: Colors .grey.shade600, fontSize: 10)), Text( '${filteredData.length}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18), ), Text( 'dari ${historyGlobal.length}', style: TextStyle( color: Colors .grey.shade500, fontSize: 9)), ], ), ), ), ], ), ], ) : Row( children: [ Expanded( child: _buildSummaryItem( Icons.thermostat, 'Suhu', hasData ? '${_getMaxValueFromList(filteredData, 'suhu').toStringAsFixed(1)}°C' : '0°C', '${_getMinValueFromList(filteredData, 'suhu').toStringAsFixed(1)}°C', Colors.red.shade50, Colors.red, ), ), const SizedBox(width: 12), Expanded( child: _buildSummaryItem( Icons.water_drop, 'Kelembapan', hasData ? '${_getMaxValueFromList(filteredData, 'kelembapan').toStringAsFixed(1)}%' : '0%', '${_getMinValueFromList(filteredData, 'kelembapan').toStringAsFixed(1)}%', Colors.blue.shade50, Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: _buildSummaryItem( Icons.fitness_center, 'Berat', hasData ? '${_getMaxValueFromList(filteredData, 'berat').toStringAsFixed(0)}g' : '0g', '${_getMinValueFromList(filteredData, 'berat').toStringAsFixed(0)}g', Colors.green.shade50, Colors.green, ), ), const SizedBox(width: 12), Expanded( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ const Icon(Icons.data_usage, color: Colors.orange), const SizedBox(height: 4), Text('Total Data', style: TextStyle( color: Colors.grey.shade600, fontSize: 11)), Text( '${filteredData.length}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20), ), Text('dari ${historyGlobal.length}', style: TextStyle( color: Colors.grey.shade500, fontSize: 10)), ], ), ), ), ], ), ], ), ), const SizedBox(height: 16), // Data List Header Row( children: [ const Icon(Icons.list_alt, color: Colors.red, size: 18), const SizedBox(width: 6), Text('Detail Data', style: TextStyle( fontSize: isSmallScreen ? 14 : 16, fontWeight: FontWeight.bold)), const Spacer(), Text( '${displayData.length} data', style: TextStyle( fontSize: 11, color: Colors.grey.shade500), ), ], ), const SizedBox(height: 8), // Data List if (displayData.isNotEmpty) Column( children: [ ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: paginatedData.length, itemBuilder: (context, index) { final data = paginatedData[index]; final int originalIndex = _currentPage * _itemsPerPage + index; final DateTime waktu = data['waktu'] as DateTime; final int nomorUrut = displayData.length - originalIndex; final bool isSelected = _selectedIds.contains(originalIndex); return GestureDetector( onTap: () { if (_isSelectionMode) { _toggleSelection(originalIndex); } }, onLongPress: () { if (!_isSelectionMode) { _toggleSelectionMode(); _toggleSelection(originalIndex); } }, child: Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( color: isSelected ? Colors.blue.shade50 : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? Colors.blue : Colors.grey.shade200, width: isSelected ? 2 : 1, ), boxShadow: [ BoxShadow( blurRadius: 4, color: Colors.black.withOpacity(0.04)), ], ), child: ListTile( dense: true, contentPadding: EdgeInsets.symmetric( horizontal: isSmallScreen ? 10 : 16, vertical: 2), leading: _isSelectionMode ? Container( width: 20, height: 20, decoration: BoxDecoration( shape: BoxShape.circle, color: isSelected ? Colors.blue : Colors.grey.shade300, ), child: isSelected ? const Icon( Icons.check, color: Colors.white, size: 14, ) : null, ) : CircleAvatar( radius: isSmallScreen ? 12 : 14, backgroundColor: Colors.red.shade50, child: Text('$nomorUrut', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: isSmallScreen ? 9 : 11)), ), title: isSmallScreen ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${(data["suhu"] as double).toStringAsFixed(1)}°C | ${(data["kelembapan"] as double).toStringAsFixed(1)}% | ${(data["berat"] as double).toStringAsFixed(0)}g', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12), overflow: TextOverflow.ellipsis, ), Text( '${waktu.day}/${waktu.month} ${waktu.hour}:${waktu.minute}', style: TextStyle( fontSize: 10, color: Colors.grey.shade500), ), ], ) : Row( children: [ const Icon(Icons.thermostat, size: 14, color: Colors.red), const SizedBox(width: 4), Text( '${(data["suhu"] as double).toStringAsFixed(1)}°C', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 13)), const SizedBox(width: 12), const Icon(Icons.water_drop, size: 14, color: Colors.blue), const SizedBox(width: 4), Text( '${(data["kelembapan"] as double).toStringAsFixed(1)}%', style: const TextStyle( fontSize: 13)), const SizedBox(width: 12), const Icon(Icons.fitness_center, size: 14, color: Colors.green), const SizedBox(width: 4), Text( '${(data["berat"] as double).toStringAsFixed(0)}g', style: const TextStyle( fontSize: 13)), ], ), subtitle: isSmallScreen ? null : Text( '${waktu.day}/${waktu.month}/${waktu.year} ${waktu.hour.toString().padLeft(2, '0')}:${waktu.minute.toString().padLeft(2, '0')}:${waktu.second.toString().padLeft(2, '0')}', style: TextStyle( fontSize: 10, color: Colors.grey.shade500), ), trailing: _isSelectionMode ? null : const Icon(Icons.chevron_right, size: 16, color: Colors.grey), onTap: _isSelectionMode ? () => _toggleSelection(originalIndex) : () { showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 20)), title: const Row( children: [ Icon(Icons.sensors, color: Colors.red), SizedBox(width: 8), Text('Detail Data'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ _buildDetailItem( Icons.thermostat, 'Suhu', '${(data["suhu"] as double).toStringAsFixed(1)} °C', Colors.red), const Divider(), _buildDetailItem( Icons.water_drop, 'Kelembapan', '${(data["kelembapan"] as double).toStringAsFixed(1)} %', Colors.blue), const Divider(), _buildDetailItem( Icons.fitness_center, 'Berat', '${(data["berat"] as double).toStringAsFixed(1)} g', Colors.green), const Divider(), _buildDetailItem( Icons.access_time, 'Waktu', '${waktu.day}/${waktu.month}/${waktu.year} ${waktu.hour}:${waktu.minute}:${waktu.second}', Colors.grey), ], ), actions: [ TextButton( onPressed: () => Navigator.pop( context), child: const Text('Tutup')), ], ), ); }, ), ), ); }, ), // Pagination if (totalPages > 1) Container( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 20), onPressed: _currentPage > 0 ? () => setState(() => _currentPage--) : null, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 8), Text( '${_currentPage + 1}/$totalPages', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.chevron_right, size: 20), onPressed: _currentPage + 1 < totalPages ? () => setState(() => _currentPage++) : null, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 16), Text( '${paginatedData.length} data', style: TextStyle( fontSize: 10, color: Colors.grey.shade500), ), ], ), ), ], ) else Container( padding: const EdgeInsets.all(30), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.grey.shade200), ), child: Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade100, shape: BoxShape.circle, ), child: const Icon(Icons.inbox, size: 40, color: Colors.grey), ), const SizedBox(height: 12), const Text( 'Belum Ada Data', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Colors.grey), ), const SizedBox(height: 6), Text( 'Data akan muncul setelah monitoring', style: TextStyle( fontSize: 11, color: Colors.grey.shade500), ), ], ), ), const SizedBox(height: 20), ], ), ), ), ); } double _getMaxValueFromList(List> data, String key) { double max = 0; for (var item in data) { double value = item[key] as double; if (value > max) max = value; } return max; } double _getMinValueFromList(List> data, String key) { double min = double.infinity; for (var item in data) { double value = item[key] as double; if (value < min) min = value; } return min; } Widget _buildStatItem(IconData icon, String label, String value) { return Column( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 20, color: Colors.white), ), const SizedBox(height: 4), Text(label, style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 10)), Text(value, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)), ], ); } Widget _buildSummaryItem(IconData icon, String label, String max, String min, Color bgColor, Color color) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon(icon, color: color, size: 18), const SizedBox(height: 2), Text(label, style: TextStyle(color: Colors.grey.shade600, fontSize: 10)), Text('Max: $max', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)), Text('Min: $min', style: TextStyle(fontSize: 10, color: Colors.grey.shade600)), ], ), ); } Widget _buildChartSelector( String title, IconData icon, Color color, bool isSmallScreen) { bool isSelected = selectedChart == title; return Expanded( child: GestureDetector( onTap: () => setState(() => selectedChart = title), child: Container( padding: EdgeInsets.symmetric(vertical: isSmallScreen ? 8 : 12), decoration: BoxDecoration( color: isSelected ? color : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: isSmallScreen ? 14 : 18, color: isSelected ? Colors.white : color), const SizedBox(width: 4), Text(title, style: TextStyle( color: isSelected ? Colors.white : color, fontSize: isSmallScreen ? 10 : 13, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, )), ], ), ), ), ); } Widget _buildDetailItem( IconData icon, String label, String value, Color color) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 18), ), const SizedBox(width: 12), Expanded( child: Text(label, style: TextStyle(color: Colors.grey.shade600))), Text(value, style: TextStyle(fontWeight: FontWeight.bold, color: color)), ], ), ); } }