1772 lines
72 KiB
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)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|