add some features

This commit is contained in:
oxel 2026-04-24 16:05:03 +07:00
parent 9a68fd1492
commit 990f76e2b2
6 changed files with 465 additions and 42 deletions

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -1,9 +1,37 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:monitoring_jamur/core/theme/app_theme.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}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -24,40 +52,23 @@ class HistoryPage extends StatelessWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Expanded( Expanded(
child: Container( child: RefreshIndicator(
width: double.infinity, color: AppTheme.primaryGreen,
decoration: BoxDecoration( onRefresh: _loadHistory,
color: AppTheme.surfaceWhite, child: _isLoading
borderRadius: BorderRadius.circular(32), ? const Center(
boxShadow: [ child: CircularProgressIndicator(
BoxShadow( color: AppTheme.primaryGreen))
color: Colors.black.withAlpha(5), : _history.isEmpty
blurRadius: 20, ? _buildEmptyState()
offset: const Offset(0, 10), : ListView.separated(
), padding: const EdgeInsets.only(bottom: 24),
], itemCount: _history.length,
), separatorBuilder: (context, index) =>
child: const Center( const SizedBox(height: 16),
child: Column( itemBuilder: (context, index) =>
mainAxisAlignment: MainAxisAlignment.center, _buildHistoryCard(_history[index]),
children: [ ),
Icon(
Icons.history_rounded,
size: 64,
color: AppTheme.backgroundBeige,
),
SizedBox(height: 16),
Text(
'tidak ada data',
style: TextStyle(
fontSize: 16,
color: AppTheme.textLight,
fontWeight: FontWeight.w500,
),
),
],
),
),
), ),
), ),
], ],
@ -66,4 +77,167 @@ class HistoryPage extends StatelessWidget {
), ),
); );
} }
Widget _buildEmptyState() {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppTheme.surfaceWhite,
borderRadius: BorderRadius.circular(32),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history_rounded,
size: 64,
color: AppTheme.backgroundBeige,
),
SizedBox(height: 16),
Text(
'tidak ada data',
style: TextStyle(
fontSize: 16,
color: AppTheme.textLight,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
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,
),
),
],
),
);
}
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:monitoring_jamur/core/theme/app_theme.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/features/home/presentation/pages/statistics_page.dart';
import 'package:monitoring_jamur/core/services/mqtt_service.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; import 'dart:math' as math;
class DashboardPage extends StatefulWidget { class DashboardPage extends StatefulWidget {
@ -22,6 +23,7 @@ class _DashboardPageState extends State<DashboardPage> {
bool get _lightStatus => _isAutoMode ? (_humidity > 90) : _isLightManual; bool get _lightStatus => _isAutoMode ? (_humidity > 90) : _isLightManual;
final MqttService _mqttService = MqttService(); final MqttService _mqttService = MqttService();
final HistoryRepository _historyRepository = HistoryRepository();
@override @override
void initState() { void initState() {
@ -132,7 +134,10 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Device Controls // Device Controls
_buildDeviceControls(), _buildDeviceControls(),
const SizedBox(height: 32), const SizedBox(height: 24),
// Save to History Button
_buildSaveToHistoryButton(),
const SizedBox(height: 16),
// Statistics Button // Statistics Button
_buildStatisticsButton(context), _buildStatisticsButton(context),
const SizedBox(height: 32), const SizedBox(height: 32),
@ -222,9 +227,15 @@ class _DashboardPageState extends State<DashboardPage> {
return _buildDeviceTile( return _buildDeviceTile(
title: 'Pompa Air', title: 'Pompa Air',
isOn: isOn, isOn: isOn,
onChanged: isAuto ? null : (val) { onChanged: isAuto
_mqttService.publishControl('pump', val ? 'on' : 'off'); ? null
}, : (val) {
_mqttService.publishControl(
'pump', val ? 'on' : 'off');
if (val) {
_saveManualAction('Pompa Air', true);
}
},
); );
}, },
), ),
@ -235,9 +246,15 @@ class _DashboardPageState extends State<DashboardPage> {
return _buildDeviceTile( return _buildDeviceTile(
title: 'Lampu Pemanas', title: 'Lampu Pemanas',
isOn: isOn, isOn: isOn,
onChanged: isAuto ? null : (val) { onChanged: isAuto
_mqttService.publishControl('light', val ? 'on' : 'off'); ? 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() { Widget _buildStatusCard() {
return Container( return Container(
width: double.infinity, width: double.infinity,

View File

@ -280,6 +280,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" 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: json_annotation:
dependency: transitive dependency: transitive
description: description:

View File

@ -38,6 +38,7 @@ dependencies:
google_fonts: ^6.2.1 google_fonts: ^6.2.1
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5
mqtt_client: ^10.11.11 mqtt_client: ^10.11.11
intl: ^0.20.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: