diff --git a/lib/features/history/data/history_repository.dart b/lib/features/history/data/history_repository.dart new file mode 100644 index 0000000..e75e5b6 --- /dev/null +++ b/lib/features/history/data/history_repository.dart @@ -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> 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 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 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; + } + } +} diff --git a/lib/features/history/data/models/humidity_record.dart b/lib/features/history/data/models/humidity_record.dart new file mode 100644 index 0000000..08a2b1a --- /dev/null +++ b/lib/features/history/data/models/humidity_record.dart @@ -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 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); +} diff --git a/lib/features/history/presentation/pages/history_page.dart b/lib/features/history/presentation/pages/history_page.dart index a641bc0..8d1ea21 100644 --- a/lib/features/history/presentation/pages/history_page.dart +++ b/lib/features/history/presentation/pages/history_page.dart @@ -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 createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + final HistoryRepository _repository = HistoryRepository(); + List _history = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadHistory(); + } + + Future _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,40 +52,23 @@ class HistoryPage extends StatelessWidget { ), const SizedBox(height: 24), Expanded( - child: 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, - ), - ), - ], - ), - ), + 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]), + ), ), ), ], @@ -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 _confirmDelete(int id) async { + final confirmed = await showDialog( + 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, + ), + ), + ], + ), + ); + } } diff --git a/lib/features/home/presentation/pages/dashboard_page.dart b/lib/features/home/presentation/pages/dashboard_page.dart index beeac5f..2f8d64b 100644 --- a/lib/features/home/presentation/pages/dashboard_page.dart +++ b/lib/features/home/presentation/pages/dashboard_page.dart @@ -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 { 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 { 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,9 +227,15 @@ class _DashboardPageState extends State { 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,9 +246,15 @@ class _DashboardPageState extends State { 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 { ); } + 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 _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 _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, diff --git a/pubspec.lock b/pubspec.lock index d2bd969..fe6e429 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 88a13c9..58384e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: