add some features
This commit is contained in:
parent
9a68fd1492
commit
990f76e2b2
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:monitoring_jamur/features/history/data/models/humidity_record.dart';
|
||||
|
||||
class HistoryRepository {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
|
||||
Future<List<HumidityRecord>> fetchHistory() async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('humidity')
|
||||
.select()
|
||||
.order('id', ascending: false);
|
||||
|
||||
return (response as List)
|
||||
.map((json) => HumidityRecord.fromJson(json))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error fetching history: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteHistoryRecord(int id) async {
|
||||
try {
|
||||
await _supabase.from('humidity').delete().eq('id', id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error deleting record: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveHumidityData({
|
||||
required double humidity,
|
||||
required String status,
|
||||
required String lightStatus,
|
||||
required String pumpStatus,
|
||||
}) async {
|
||||
try {
|
||||
await _supabase.from('humidity').insert({
|
||||
'kelembapan': humidity,
|
||||
'status': status,
|
||||
'status_lampu': lightStatus,
|
||||
'status_pompa': pumpStatus,
|
||||
'tanggal_upload': DateFormat('HH:mm:ss').format(DateTime.now()),
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error saving humidity data: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
class HumidityRecord {
|
||||
final int id;
|
||||
final double humidity;
|
||||
final String status;
|
||||
final DateTime uploadDate;
|
||||
final String lightStatus;
|
||||
final String pumpStatus;
|
||||
|
||||
HumidityRecord({
|
||||
required this.id,
|
||||
required this.humidity,
|
||||
required this.status,
|
||||
required this.uploadDate,
|
||||
required this.lightStatus,
|
||||
required this.pumpStatus,
|
||||
});
|
||||
|
||||
factory HumidityRecord.fromJson(Map<String, dynamic> json) {
|
||||
DateTime parsedDate;
|
||||
final String rawDate = json['tanggal_upload'] as String;
|
||||
|
||||
try {
|
||||
// Try parsing as full ISO8601
|
||||
parsedDate = DateTime.parse(rawDate).toLocal();
|
||||
} catch (_) {
|
||||
// Handle "HH:mm:ss" format from 'time' column
|
||||
final now = DateTime.now();
|
||||
final parts = rawDate.split(':');
|
||||
parsedDate = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
parts.length > 2 ? int.parse(parts[2].split('.')[0]) : 0,
|
||||
);
|
||||
}
|
||||
|
||||
return HumidityRecord(
|
||||
id: json['id'] as int,
|
||||
humidity: (json['kelembapan'] as num).toDouble(),
|
||||
status: json['status'] as String,
|
||||
uploadDate: parsedDate,
|
||||
lightStatus: json['status_lampu'] as String,
|
||||
pumpStatus: json['status_pompa'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String get formattedDate => DateFormat('dd MMM yyyy, HH:mm').format(uploadDate);
|
||||
}
|
||||
|
|
@ -1,9 +1,37 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:monitoring_jamur/core/theme/app_theme.dart';
|
||||
import 'package:monitoring_jamur/features/history/data/history_repository.dart';
|
||||
import 'package:monitoring_jamur/features/history/data/models/humidity_record.dart';
|
||||
|
||||
class HistoryPage extends StatelessWidget {
|
||||
class HistoryPage extends StatefulWidget {
|
||||
const HistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryPage> createState() => _HistoryPageState();
|
||||
}
|
||||
|
||||
class _HistoryPageState extends State<HistoryPage> {
|
||||
final HistoryRepository _repository = HistoryRepository();
|
||||
List<HumidityRecord> _history = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHistory();
|
||||
}
|
||||
|
||||
Future<void> _loadHistory() async {
|
||||
setState(() => _isLoading = true);
|
||||
final data = await _repository.fetchHistory();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_history = data;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -24,7 +52,34 @@ class HistoryPage extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: Container(
|
||||
child: RefreshIndicator(
|
||||
color: AppTheme.primaryGreen,
|
||||
onRefresh: _loadHistory,
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppTheme.primaryGreen))
|
||||
: _history.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
itemCount: _history.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) =>
|
||||
_buildHistoryCard(_history[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceWhite,
|
||||
|
|
@ -58,11 +113,130 @@ class HistoryPage extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(int id) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: const Text('Hapus Data?'),
|
||||
content:
|
||||
const Text('Apakah Anda yakin ingin menghapus data monitoring ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child:
|
||||
const Text('Batal', style: TextStyle(color: AppTheme.textLight)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Hapus',
|
||||
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final success = await _repository.deleteHistoryRecord(id);
|
||||
if (success && mounted) {
|
||||
_loadHistory();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Data berhasil dihapus'),
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(24),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHistoryCard(HumidityRecord record) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 12, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(5),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'RECORD DATA',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppTheme.backgroundBeige,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.delete_outline_rounded,
|
||||
color: Colors.red, size: 22),
|
||||
onPressed: () => _confirmDelete(record.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow('Kelembapan', '${record.humidity.toInt()}%'),
|
||||
_buildInfoRow('Status', record.status),
|
||||
_buildInfoRow('Status Pompa', record.pumpStatus,
|
||||
valueColor: record.pumpStatus.contains('MENYALA')
|
||||
? AppTheme.primaryGreen
|
||||
: Colors.red),
|
||||
_buildInfoRow('Status Lampu', record.lightStatus,
|
||||
valueColor: record.lightStatus.contains('MENYALA')
|
||||
? AppTheme.primaryGreen
|
||||
: Colors.red),
|
||||
const Divider(height: 24, color: AppTheme.backgroundBeige),
|
||||
_buildInfoRow('Waktu', record.formattedDate, isSmall: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value,
|
||||
{Color? valueColor, bool isSmall = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'$label :',
|
||||
style: TextStyle(
|
||||
fontSize: isSmall ? 13 : 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isSmall ? 13 : 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: valueColor ?? AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:monitoring_jamur/core/theme/app_theme.dart';
|
||||
import 'package:monitoring_jamur/features/home/presentation/pages/statistics_page.dart';
|
||||
import 'package:monitoring_jamur/core/services/mqtt_service.dart';
|
||||
import 'package:monitoring_jamur/features/history/data/history_repository.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class DashboardPage extends StatefulWidget {
|
||||
|
|
@ -22,6 +23,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
bool get _lightStatus => _isAutoMode ? (_humidity > 90) : _isLightManual;
|
||||
|
||||
final MqttService _mqttService = MqttService();
|
||||
final HistoryRepository _historyRepository = HistoryRepository();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -132,7 +134,10 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
const SizedBox(height: 24),
|
||||
// Device Controls
|
||||
_buildDeviceControls(),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
// Save to History Button
|
||||
_buildSaveToHistoryButton(),
|
||||
const SizedBox(height: 16),
|
||||
// Statistics Button
|
||||
_buildStatisticsButton(context),
|
||||
const SizedBox(height: 32),
|
||||
|
|
@ -222,8 +227,14 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
return _buildDeviceTile(
|
||||
title: 'Pompa Air',
|
||||
isOn: isOn,
|
||||
onChanged: isAuto ? null : (val) {
|
||||
_mqttService.publishControl('pump', val ? 'on' : 'off');
|
||||
onChanged: isAuto
|
||||
? null
|
||||
: (val) {
|
||||
_mqttService.publishControl(
|
||||
'pump', val ? 'on' : 'off');
|
||||
if (val) {
|
||||
_saveManualAction('Pompa Air', true);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -235,8 +246,14 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
return _buildDeviceTile(
|
||||
title: 'Lampu Pemanas',
|
||||
isOn: isOn,
|
||||
onChanged: isAuto ? null : (val) {
|
||||
_mqttService.publishControl('light', val ? 'on' : 'off');
|
||||
onChanged: isAuto
|
||||
? null
|
||||
: (val) {
|
||||
_mqttService.publishControl(
|
||||
'light', val ? 'on' : 'off');
|
||||
if (val) {
|
||||
_saveManualAction('Lampu Pemanas', true);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -349,6 +366,123 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
);
|
||||
}
|
||||
|
||||
String _getHumidityStatus(double humidity) {
|
||||
if (humidity < 70) return 'Sangat Kering';
|
||||
if (humidity < 80) return 'Kering - Lembab';
|
||||
if (humidity <= 90) return 'Lembab (Ideal)';
|
||||
return 'Sangat Lembab';
|
||||
}
|
||||
|
||||
Future<void> _saveToHistory() async {
|
||||
final status = _getHumidityStatus(_humidity);
|
||||
|
||||
// Show loading
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(color: AppTheme.primaryGreen),
|
||||
),
|
||||
);
|
||||
|
||||
final mode = _isAutoMode ? 'OTOMATIS' : 'MANUAL';
|
||||
final pumpText = _mqttService.isPumpOn.value ? 'MENYALA - $mode' : 'MATI';
|
||||
final lightText = _mqttService.isLightOn.value ? 'MENYALA - $mode' : 'MATI';
|
||||
|
||||
final success = await _historyRepository.saveHumidityData(
|
||||
humidity: _humidity,
|
||||
status: status,
|
||||
lightStatus: lightText,
|
||||
pumpStatus: pumpText,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Pop loading
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
success ? 'Data berhasil disimpan ke histori!' : 'Gagal menyimpan data'),
|
||||
backgroundColor: success ? AppTheme.primaryGreen : Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(24),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveManualAction(String device, bool isOn) async {
|
||||
final status = _getHumidityStatus(_humidity);
|
||||
|
||||
// Prepare current statuses
|
||||
String pumpStatusText = _mqttService.isPumpOn.value ? 'MENYALA - MANUAL' : 'MATI';
|
||||
String lightStatusText = _mqttService.isLightOn.value ? 'MENYALA - MANUAL' : 'MATI';
|
||||
|
||||
// Override with the new action state
|
||||
if (device.contains('Pompa')) {
|
||||
pumpStatusText = 'MENYALA - MANUAL';
|
||||
} else {
|
||||
lightStatusText = 'MENYALA - MANUAL';
|
||||
}
|
||||
|
||||
await _historyRepository.saveHumidityData(
|
||||
humidity: _humidity,
|
||||
status: status,
|
||||
lightStatus: lightStatusText,
|
||||
pumpStatus: pumpStatusText,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$device dinyalakan (Data tersimpan!)'),
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(24),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSaveToHistoryButton() {
|
||||
return GestureDetector(
|
||||
onTap: _saveToHistory,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryGreen.withAlpha(40),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.save_rounded, color: Colors.white, size: 24),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'Simpan ke Histori',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
|
|
|||
|
|
@ -280,6 +280,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies:
|
|||
google_fonts: ^6.2.1
|
||||
shared_preferences: ^2.5.5
|
||||
mqtt_client: ^10.11.11
|
||||
intl: ^0.20.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue