TKK_E32231651/lib/history.dart

1772 lines
72 KiB
Dart

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<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
String selectedChart = 'Suhu';
String selectedRange = 'Semua';
bool _isLoading = false;
// Untuk selection mode
bool _isSelectionMode = false;
Set<int> _selectedIds = {};
// Paginasi
int _currentPage = 0;
final int _itemsPerPage = 15;
final List<String> 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<void> _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<Map<String, dynamic>> _getFilteredData() {
if (historyGlobal.isEmpty) return [];
if (selectedRange == 'Semua') {
List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> _getDisplayData() {
final data = _getFilteredData();
return data.reversed.toList();
}
// Data untuk grafik dengan sampling
List<Map<String, dynamic>> _getChartData() {
final data = _getFilteredData();
if (data.length <= 50) return data;
// Sampling data untuk grafik
final sampled = <Map<String, dynamic>>[];
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<Map<String, dynamic>> _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<void> _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<bool>(
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<Map<String, dynamic>> 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<void> _deleteAllHistory() async {
final confirm = await showDialog<bool>(
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<String>(
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<String> 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<FlSpot> 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<FlSpot> spots, double minValue, double maxValue,
List<Map<String, dynamic>> 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<String>(
segments: rangeOptions.map((range) {
return ButtonSegment(
value: range,
label: Text(isSmallScreen
? range.substring(0, 3)
: range));
}).toList(),
selected: {selectedRange},
onSelectionChanged:
(Set<String> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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)),
],
),
);
}
}