From 0162607d62cb398e49df6a79a7b626a8614ff04a Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Tue, 12 May 2026 16:24:51 +0700 Subject: [PATCH] woke --- .../blocs/atmospheric_conditions_bloc.dart | 113 ----- .../blocs/atmospheric_conditions_event.dart | 10 - .../blocs/atmospheric_conditions_state.dart | 53 --- .../views/atmospheric_screen.dart | 393 ------------------ .../blocs/atmospheric_conditions_bloc.dart | 50 ++- .../blocs/atmospheric_conditions_state.dart | 6 +- .../views/atmospheric_screen.dart | 261 +++++++++++- .../evaporasi/blocs/evaporasi_bloc.dart | 12 +- .../lib/src/firebase_monitoring_repo.dart | 1 + .../src/models/atmospheric_conditions.dart | 63 ++- .../lib/src/models/evaporasi.dart | 24 +- 11 files changed, 378 insertions(+), 608 deletions(-) delete mode 100644 atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart delete mode 100644 atmospheric_conditions/blocs/atmospheric_conditions_event.dart delete mode 100644 atmospheric_conditions/blocs/atmospheric_conditions_state.dart delete mode 100644 atmospheric_conditions/views/atmospheric_screen.dart diff --git a/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart b/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart deleted file mode 100644 index 6ba087a..0000000 --- a/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:monitoring_repository/monitoring_repository.dart'; - -part 'atmospheric_conditions_event.dart'; -part 'atmospheric_conditions_state.dart'; - -class AtmosphericConditionsBloc extends Bloc { - final MonitoringRepository _repository; - StreamSubscription? _subscription; - static const int _maxHistoryItems = 1500; - - AtmosphericConditionsBloc({required MonitoringRepository repository}) - : _repository = repository, - super(const AtmosphericConditionsState()) { - on(_onStarted); - on<_AtmosphericConditionsUpdated>(_onUpdated); - } - - /// 🚀 START - Future _onStarted( - WatchAtmosphericConditionsStarted event, - Emitter emit, - ) async { - emit(state.copyWith(isLoading: true)); - - await _subscription?.cancel(); - - // Preload today's history from Firebase history table. - final now = DateTime.now(); - final historyFromFirebase = await _repository.getSensorHistory( - '/sensor/history', - (json) => AtmosphericConditions.fromJson(json), - ); - - final todayHistory = historyFromFirebase.where((item) => _isSameDay(item.timestamp, now)).toList()..sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - final trimmedHistory = todayHistory.length > _maxHistoryItems ? todayHistory.sublist(todayHistory.length - _maxHistoryItems) : todayHistory; - - final latestFromHistory = trimmedHistory.isNotEmpty ? trimmedHistory.last : null; - - emit(state.copyWith( - temperature: latestFromHistory?.temperature ?? state.temperature, - humidity: latestFromHistory?.humidity ?? state.humidity, - pressure: latestFromHistory?.pressure ?? state.pressure, - altitude: latestFromHistory?.altitude ?? state.altitude, - timeMs: latestFromHistory?.timeMs ?? state.timeMs, - history: trimmedHistory, - isLoading: false, - )); - - _subscription = _repository - .getSensorStream( - '/sensor/latest', - (json) => AtmosphericConditions.fromJson(json), - ) - .listen((data) { - add(_AtmosphericConditionsUpdated(data)); - }); - } - - /// ⚡ REALTIME UPDATE - void _onUpdated( - _AtmosphericConditionsUpdated event, - Emitter emit, - ) { - final now = DateTime.now(); - final updatedHistory = state.history.where((item) => _isSameDay(item.timestamp, now)).toList(); - - final shouldAppend = updatedHistory.isEmpty || updatedHistory.last.timeMs != event.data.timeMs || updatedHistory.last.pressure != event.data.pressure || updatedHistory.last.timestamp != event.data.timestamp; - - if (shouldAppend) { - updatedHistory.add(event.data); - - if (updatedHistory.length > _maxHistoryItems) { - updatedHistory.removeAt(0); - } - } - - emit(state.copyWith( - temperature: event.data.temperature, - humidity: event.data.humidity, - pressure: event.data.pressure, - altitude: event.data.altitude, - timeMs: event.data.timeMs, - history: updatedHistory, - isLoading: false, - )); - } - - bool _isSameDay(DateTime a, DateTime b) { - return a.year == b.year && a.month == b.month && a.day == b.day; - } - - @override - Future close() async { - await _subscription?.cancel(); - return super.close(); - } -} - -/// INTERNAL EVENT -class _AtmosphericConditionsUpdated extends AtmosphericConditionsEvent { - final AtmosphericConditions data; - - const _AtmosphericConditionsUpdated(this.data); - - @override - List get props => [ - data - ]; -} diff --git a/atmospheric_conditions/blocs/atmospheric_conditions_event.dart b/atmospheric_conditions/blocs/atmospheric_conditions_event.dart deleted file mode 100644 index e446497..0000000 --- a/atmospheric_conditions/blocs/atmospheric_conditions_event.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of 'atmospheric_conditions_bloc.dart'; - -abstract class AtmosphericConditionsEvent extends Equatable { - const AtmosphericConditionsEvent(); - - @override - List get props => []; -} - -class WatchAtmosphericConditionsStarted extends AtmosphericConditionsEvent {} diff --git a/atmospheric_conditions/blocs/atmospheric_conditions_state.dart b/atmospheric_conditions/blocs/atmospheric_conditions_state.dart deleted file mode 100644 index 0860623..0000000 --- a/atmospheric_conditions/blocs/atmospheric_conditions_state.dart +++ /dev/null @@ -1,53 +0,0 @@ -part of 'atmospheric_conditions_bloc.dart'; - -class AtmosphericConditionsState extends Equatable { - final double temperature; - final double humidity; - final double pressure; - final double altitude; - final int timeMs; - final List history; - - final bool isLoading; - - const AtmosphericConditionsState({ - this.temperature = 0.0, - this.humidity = 0.0, - this.pressure = 0.0, - this.altitude = 0.0, - this.timeMs = 0, - this.history = const [], - this.isLoading = true, - }); - - AtmosphericConditionsState copyWith({ - double? temperature, - double? humidity, - double? pressure, - double? altitude, - int? timeMs, - List? history, - bool? isLoading, - }) { - return AtmosphericConditionsState( - temperature: temperature ?? this.temperature, - humidity: humidity ?? this.humidity, - pressure: pressure ?? this.pressure, - altitude: altitude ?? this.altitude, - timeMs: timeMs ?? this.timeMs, - history: history ?? this.history, - isLoading: isLoading ?? this.isLoading, - ); - } - - @override - List get props => [ - temperature, - humidity, - pressure, - altitude, - timeMs, - history, - isLoading, - ]; -} diff --git a/atmospheric_conditions/views/atmospheric_screen.dart b/atmospheric_conditions/views/atmospheric_screen.dart deleted file mode 100644 index cfb7b9a..0000000 --- a/atmospheric_conditions/views/atmospheric_screen.dart +++ /dev/null @@ -1,393 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:monitoring_repository/monitoring_repository.dart'; -import '../blocs/atmospheric_conditions_bloc.dart'; -import '../../shared/utils/excel/excel_export_service.dart'; -import '../../shared/widgets/export_excel_button.dart'; - -String formatUptimeShort(int timeMs) { - final duration = Duration(milliseconds: timeMs); - final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); - return "$minutes:$seconds"; -} - -String formatUptimeLong(int timeMs) { - final duration = Duration(milliseconds: timeMs); - final hours = duration.inHours.toString().padLeft(2, '0'); - final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); - return "$hours:$minutes:$seconds"; -} - -String formatClockTime(DateTime timestamp) { - final h = timestamp.hour.toString().padLeft(2, '0'); - final m = timestamp.minute.toString().padLeft(2, '0'); - final s = timestamp.second.toString().padLeft(2, '0'); - return "$h:$m:$s"; -} - -class AtmosphericScreen extends StatelessWidget { - const AtmosphericScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey.shade100, - appBar: AppBar( - title: const Text( - "Kondisi Atmosfer", - style: TextStyle(fontWeight: FontWeight.bold), - ), - centerTitle: true, - backgroundColor: Colors.transparent, - elevation: 0, - foregroundColor: Colors.black, - ), - body: BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - _mainPressure(state), - const SizedBox(height: 24), - _historyChart(state), - const SizedBox(height: 24), - _HistoryTableCard(history: state.history), - const SizedBox(height: 24), - ExportExcelButton( - onExport: () { - final historyData = state.history - .map((e) => { - 'timeMs': e.timeMs, - 'pressure': e.pressure, - 'timestamp': e.timestamp, - }) - .toList(); - - return ExcelExportService.atmospheric( - pressure: state.pressure, - timeMs: state.timeMs, - timestamp: state.history.isNotEmpty ? state.history.last.timestamp : DateTime.now(), - historyData: historyData, - ); - }, - label: 'Export Excel Hari Ini', - ), - ], - ), - ); - }, - ), - ); - } - - /// ========================= - /// 🔵 PRESSURE (HERO CARD) - /// ========================= - Widget _mainPressure(AtmosphericConditionsState state) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 40), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - const Color.fromARGB(255, 38, 255, 222), - const Color.fromARGB(255, 53, 132, 229) - ], - ), - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: const Color.fromARGB(255, 0, 191, 255).withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ) - ], - ), - child: Column( - children: [ - const Icon(Icons.speed, color: Colors.white, size: 50), - const SizedBox(height: 10), - Text( - state.pressure.toStringAsFixed(1), - style: const TextStyle( - fontSize: 70, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const Text( - "hPa", - style: TextStyle(color: Colors.white70, fontSize: 18), - ), - ], - ), - ); - } - - Widget _historyChart(AtmosphericConditionsState state) { - final history = state.history; - - if (history.isEmpty) { - return _emptyCard("Grafik histori tekanan hari ini belum ada data"); - } - - final points = []; - double minY = history.first.pressure; - double maxY = history.first.pressure; - - for (int i = 0; i < history.length; i++) { - final pressure = history[i].pressure; - points.add(FlSpot(i.toDouble(), pressure)); - - if (pressure < minY) { - minY = pressure; - } - if (pressure > maxY) { - maxY = pressure; - } - } - - final range = (maxY - minY).abs(); - final padding = range < 1 ? 0.5 : range * 0.15; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Grafik Histori Tekanan Hari Ini", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 12), - SizedBox( - height: 220, - child: LineChart( - LineChartData( - minX: 0, - maxX: (history.length - 1).toDouble(), - minY: minY - padding, - maxY: maxY + padding, - gridData: FlGridData(show: true), - borderData: FlBorderData(show: false), - titlesData: FlTitlesData( - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 44, - interval: range < 1 ? 0.5 : null, - getTitlesWidget: (value, meta) => Text( - value.toStringAsFixed(1), - style: const TextStyle(fontSize: 10), - ), - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: history.length > 6 ? (history.length / 5).ceilToDouble() : 1, - getTitlesWidget: (value, meta) { - final index = value.toInt(); - if (index < 0 || index >= history.length) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - formatClockTime(history[index].timestamp), - style: const TextStyle(fontSize: 10), - ), - ); - }, - ), - ), - ), - lineBarsData: [ - LineChartBarData( - spots: points, - isCurved: true, - color: Colors.blue, - barWidth: 3, - dotData: FlDotData(show: history.length <= 10), - belowBarData: BarAreaData( - show: true, - color: Colors.blue.withOpacity(0.15), - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - - Widget _emptyCard(String text) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - text, - style: const TextStyle(color: Colors.grey), - ), - ); - } -} - -class _HistoryTableCard extends StatefulWidget { - final List history; - - const _HistoryTableCard({required this.history}); - - @override - State<_HistoryTableCard> createState() => _HistoryTableCardState(); -} - -class _HistoryTableCardState extends State<_HistoryTableCard> { - static const int _rowsPerPage = 10; - int _currentPage = 0; - - @override - void didUpdateWidget(covariant _HistoryTableCard oldWidget) { - super.didUpdateWidget(oldWidget); - - final totalPages = _totalPages; - if (totalPages > 0 && _currentPage >= totalPages) { - _currentPage = totalPages - 1; - } - } - - int get _totalPages { - if (widget.history.isEmpty) { - return 0; - } - return (widget.history.length / _rowsPerPage).ceil(); - } - - List get _pageItems { - final reversedHistory = widget.history.reversed.toList(); - final start = _currentPage * _rowsPerPage; - final end = (start + _rowsPerPage).clamp(0, reversedHistory.length); - return reversedHistory.sublist(start, end); - } - - void _goToPreviousPage() { - if (_currentPage > 0) { - setState(() => _currentPage--); - } - } - - void _goToNextPage() { - if (_currentPage < _totalPages - 1) { - setState(() => _currentPage++); - } - } - - @override - Widget build(BuildContext context) { - if (widget.history.isEmpty) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - child: const Text( - "Tabel histori tekanan hari ini belum ada data", - style: TextStyle(color: Colors.grey), - ), - ); - } - - final pageItems = _pageItems; - final hasMultiplePages = _totalPages > 1; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Tabel Histori Tekanan Hari Ini", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - "Halaman ${_currentPage + 1} dari $_totalPages · ${widget.history.length} data", - style: const TextStyle(color: Colors.grey, fontSize: 12), - ), - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columns: const [ - DataColumn(label: Text("No")), - DataColumn(label: Text("Waktu")), - DataColumn(label: Text("Tekanan")), - ], - rows: List.generate(pageItems.length, (index) { - final item = pageItems[index]; - final absoluteIndex = (_currentPage * _rowsPerPage) + index + 1; - return DataRow( - cells: [ - DataCell(Text(absoluteIndex.toString())), - DataCell(Text(formatClockTime(item.timestamp))), - DataCell(Text("${item.pressure.toStringAsFixed(1)} hPa")), - ], - ); - }), - ), - ), - if (hasMultiplePages) ...[ - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton.icon( - onPressed: _currentPage == 0 ? null : _goToPreviousPage, - icon: const Icon(Icons.chevron_left), - label: const Text('Sebelumnya'), - ), - TextButton.icon( - onPressed: _currentPage >= _totalPages - 1 ? null : _goToNextPage, - icon: const Icon(Icons.chevron_right), - label: const Text('Berikutnya'), - ), - ], - ), - ], - ], - ), - ); - } -} diff --git a/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart b/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart index a4d6cb5..2ca754e 100644 --- a/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart +++ b/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart @@ -10,6 +10,7 @@ class AtmosphericConditionsBloc extends Bloc { final MonitoringRepository _repository; StreamSubscription? _subscription; + static const int _maxHistoryItems = 1500; AtmosphericConditionsBloc({required MonitoringRepository repository}) : _repository = repository, @@ -24,9 +25,35 @@ class AtmosphericConditionsBloc Emitter emit, ) async { emit(state.copyWith(isLoading: true)); - await _subscription?.cancel(); + final historyFromFirebase = await _repository.getSensorHistory( + 'sensor/history', + (json) => AtmosphericConditions.fromJson(json), + ); + + final today = DateTime.now(); + final todayHistory = historyFromFirebase + .where((item) => _isSameDay(item.timestamp, today)) + .toList() + ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + final trimmedHistory = todayHistory.length > _maxHistoryItems + ? todayHistory.sublist(todayHistory.length - _maxHistoryItems) + : todayHistory; + + final latestFromHistory = + trimmedHistory.isNotEmpty ? trimmedHistory.last : null; + + emit(state.copyWith( + temperature: latestFromHistory?.temperature ?? state.temperature, + humidity: latestFromHistory?.humidity ?? state.humidity, + pressure: latestFromHistory?.pressure ?? state.pressure, + altitude: latestFromHistory?.altitude ?? state.altitude, + history: trimmedHistory, + isLoading: false, + )); + _subscription = _repository .getSensorStream( 'sensor/latest', @@ -42,15 +69,36 @@ class AtmosphericConditionsBloc _AtmosphericConditionsUpdated event, Emitter emit, ) { + final now = DateTime.now(); + final updatedHistory = + state.history.where((item) => _isSameDay(item.timestamp, now)).toList(); + + final isDuplicate = updatedHistory.isNotEmpty && + updatedHistory.last.timestamp == event.data.timestamp && + updatedHistory.last.pressure == event.data.pressure && + updatedHistory.last.humidity == event.data.humidity; + + if (!isDuplicate) { + updatedHistory.add(event.data); + if (updatedHistory.length > _maxHistoryItems) { + updatedHistory.removeAt(0); + } + } + emit(state.copyWith( temperature: event.data.temperature, humidity: event.data.humidity, pressure: event.data.pressure, altitude: event.data.altitude, + history: updatedHistory, isLoading: false, )); } + bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + @override Future close() async { await _subscription?.cancel(); diff --git a/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_state.dart b/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_state.dart index 01b12ba..4dd05e0 100644 --- a/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_state.dart +++ b/lib/screens/monitoring/atmospheric_conditions/blocs/atmospheric_conditions_state.dart @@ -5,7 +5,7 @@ class AtmosphericConditionsState extends Equatable { final double humidity; final double pressure; final double altitude; - + final List history; final bool isLoading; const AtmosphericConditionsState({ @@ -13,6 +13,7 @@ class AtmosphericConditionsState extends Equatable { this.humidity = 0.0, this.pressure = 0.0, this.altitude = 0.0, + this.history = const [], this.isLoading = true, }); @@ -21,6 +22,7 @@ class AtmosphericConditionsState extends Equatable { double? humidity, double? pressure, double? altitude, + List? history, bool? isLoading, }) { return AtmosphericConditionsState( @@ -28,6 +30,7 @@ class AtmosphericConditionsState extends Equatable { humidity: humidity ?? this.humidity, pressure: pressure ?? this.pressure, altitude: altitude ?? this.altitude, + history: history ?? this.history, isLoading: isLoading ?? this.isLoading, ); } @@ -38,6 +41,7 @@ class AtmosphericConditionsState extends Equatable { humidity, pressure, altitude, + history, isLoading, ]; } diff --git a/lib/screens/monitoring/atmospheric_conditions/views/atmospheric_screen.dart b/lib/screens/monitoring/atmospheric_conditions/views/atmospheric_screen.dart index da77570..3f01571 100644 --- a/lib/screens/monitoring/atmospheric_conditions/views/atmospheric_screen.dart +++ b/lib/screens/monitoring/atmospheric_conditions/views/atmospheric_screen.dart @@ -1,9 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; import '../blocs/atmospheric_conditions_bloc.dart'; import '../../shared/utils/pdf/pdf_export_service.dart'; import '../../shared/widgets/export_pdf_button.dart'; +String formatClockTime(DateTime timestamp) { + final h = timestamp.hour.toString().padLeft(2, '0'); + final m = timestamp.minute.toString().padLeft(2, '0'); + final s = timestamp.second.toString().padLeft(2, '0'); + return '$h:$m:$s'; +} + class AtmosphericScreen extends StatelessWidget { const AtmosphericScreen({super.key}); @@ -13,7 +22,7 @@ class AtmosphericScreen extends StatelessWidget { backgroundColor: Colors.grey.shade100, appBar: AppBar( title: const Text( - "Kondisi Atmosfer", + 'Kondisi Atmosfer', style: TextStyle(fontWeight: FontWeight.bold), ), centerTitle: true, @@ -32,20 +41,31 @@ class AtmosphericScreen extends StatelessWidget { child: Column( children: [ _mainTemperature(state), - const SizedBox(height: 25), + const SizedBox(height: 24), _gridInfo(state), - const SizedBox(height: 25), - - // ← Tombol Export PDF + const SizedBox(height: 24), + _historyChart(state), + const SizedBox(height: 24), + _HistoryTableCard(history: state.history), + const SizedBox(height: 24), ExportPdfButton( onExport: () => PdfExportService.atmospheric( temperature: state.temperature, humidity: state.humidity, pressure: state.pressure, altitude: state.altitude, - timestamp: DateTime.now(), - // atmospheric state belum punya history, - // kalau nanti ditambah tinggal isi historyData di sini + timestamp: state.history.isNotEmpty + ? state.history.last.timestamp + : DateTime.now(), + historyData: state.history + .map((entry) => { + 'timestamp': entry.timestamp, + 'temperature': entry.temperature, + 'humidity': entry.humidity, + 'pressure': entry.pressure, + 'altitude': entry.altitude, + }) + .toList(), ), ), ], @@ -64,18 +84,18 @@ class AtmosphericScreen extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40), decoration: BoxDecoration( - gradient: LinearGradient( + gradient: const LinearGradient( colors: [ - const Color.fromARGB(255, 38, 255, 222), - const Color.fromARGB(255, 53, 132, 229) + Color.fromARGB(255, 38, 255, 222), + Color.fromARGB(255, 53, 132, 229), ], ), borderRadius: BorderRadius.circular(25), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: const Color.fromARGB(255, 0, 191, 255).withOpacity(0.3), + color: Color.fromRGBO(0, 191, 255, 0.3), blurRadius: 20, - offset: const Offset(0, 10), + offset: Offset(0, 10), ) ], ), @@ -92,7 +112,7 @@ class AtmosphericScreen extends StatelessWidget { ), ), const Text( - "°C", + '°C', style: TextStyle(color: Colors.white70, fontSize: 18), ), ], @@ -113,20 +133,20 @@ class AtmosphericScreen extends StatelessWidget { childAspectRatio: 1.6, children: [ _infoCard( - "Kelembapan", - "${state.humidity.toStringAsFixed(1)} %", + 'Kelembapan', + '${state.humidity.toStringAsFixed(1)} %', Icons.water_drop, Colors.blue, ), _infoCard( - "Tekanan", - "${state.pressure.toStringAsFixed(1)} hPa", + 'Tekanan', + '${state.pressure.toStringAsFixed(1)} hPa', Icons.speed, Colors.green, ), _infoCard( - "Ketinggian", - "${state.altitude.toStringAsFixed(1)} m", + 'Ketinggian', + '${state.altitude.toStringAsFixed(1)} m', Icons.terrain, Colors.brown, ), @@ -158,7 +178,204 @@ class AtmosphericScreen extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold), ), ], - ) + ), + ], + ), + ); + } + + Widget _historyChart(AtmosphericConditionsState state) { + final history = state.history; + + if (history.isEmpty) { + return _emptyCard('Grafik histori tekanan hari ini belum ada data'); + } + + final points = []; + double minY = history.first.pressure; + double maxY = history.first.pressure; + + for (var i = 0; i < history.length; i++) { + final pressure = history[i].pressure; + points.add(FlSpot(i.toDouble(), pressure)); + minY = pressure < minY ? pressure : minY; + maxY = pressure > maxY ? pressure : maxY; + } + + final range = (maxY - minY).abs(); + final padding = range < 1 ? 0.5 : range * 0.15; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Grafik Histori Tekanan Hari Ini', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + SizedBox( + height: 220, + child: LineChart( + LineChartData( + minX: 0, + maxX: history.length > 1 ? (history.length - 1).toDouble() : 1, + minY: minY - padding, + maxY: maxY + padding, + gridData: const FlGridData(show: true), + borderData: FlBorderData(show: false), + titlesData: FlTitlesData( + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 44, + interval: range < 1 ? 0.5 : null, + getTitlesWidget: (value, meta) => Text( + value.toStringAsFixed(1), + style: const TextStyle(fontSize: 10), + ), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: history.length > 6 + ? (history.length / 5).ceilToDouble() + : 1, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index < 0 || index >= history.length) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + formatClockTime(history[index].timestamp), + style: const TextStyle(fontSize: 10), + ), + ); + }, + ), + ), + ), + lineBarsData: [ + LineChartBarData( + spots: points, + isCurved: true, + color: Colors.blue, + barWidth: 3, + dotData: FlDotData(show: history.length <= 10), + belowBarData: BarAreaData( + show: true, + color: const Color.fromRGBO(33, 150, 243, 0.15), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _emptyCard(String text) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + text, + style: const TextStyle(color: Colors.grey), + ), + ); + } +} + +class _HistoryTableCard extends StatelessWidget { + final List history; + + const _HistoryTableCard({required this.history}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Riwayat Tekanan Hari Ini', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + if (history.isEmpty) + const Text('Belum ada data histori hari ini.') + else ...[ + Table( + columnWidths: const { + 0: FlexColumnWidth(2), + 1: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: BoxDecoration(color: Colors.grey.shade100), + children: const [ + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text('Waktu', + style: TextStyle(fontWeight: FontWeight.bold)), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text('Tekanan', + style: TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ), + ...history.reversed.take(10).map( + (item) => TableRow( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text(formatClockTime(item.timestamp)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: + Text('${item.pressure.toStringAsFixed(1)} hPa'), + ), + ], + ), + ), + ], + ), + if (history.length > 10) ...[ + const SizedBox(height: 12), + Text( + 'Menampilkan 10 data terakhir dari ${history.length} data.', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ], ], ), ); diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index 4e28b7d..a3f1e56 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -113,17 +113,21 @@ class EvaporasiBloc extends Bloc { Emitter emit, ) { // Hindari update dobel: jika timestamp event sama dengan yang terakhir, jangan ubah bucket. - final previous = state.history.isNotEmpty ? state.history.last.timestamp : null; + final previous = + state.history.isNotEmpty ? state.history.last.timestamp : null; + + final updatedHistory = List.from(state.history)..add(event.data); + final isDuplicate = previous != null && event.data.timestamp.toUtc() == previous.toUtc(); // Update bucket berdasarkan timestamp event (bukan jam lokal sekarang). - final updated = isDuplicate ? state.dailyValues : List.from(state.dailyValues); + final updated = + isDuplicate ? state.dailyValues : List.from(state.dailyValues); final updatedTemp = isDuplicate ? state.dailyTemperatures : List.from(state.dailyTemperatures); - final eventTime = event.data.timestamp; final now = DateTime.now(); @@ -143,6 +147,8 @@ class EvaporasiBloc extends Bloc { _emitEvaporasiAlert(status, rain, event.data.evaporasi); emit(state.copyWith( + history: updatedHistory, + listData: updatedHistory, currentValue: event.data.evaporasi, temperature: event.data.suhu, waterLevel: event.data.tinggiAir, diff --git a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart index f7cf78c..060d254 100644 --- a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart +++ b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart @@ -52,6 +52,7 @@ class FirebaseMonitoringRepo implements MonitoringRepository { final Map data = snapshot.value as Map; final list = data.values.map((item) { + print(item); return mapper(item as Map); }).toList(); diff --git a/packages/monitoring_repository/lib/src/models/atmospheric_conditions.dart b/packages/monitoring_repository/lib/src/models/atmospheric_conditions.dart index c0707f7..e88a2f0 100644 --- a/packages/monitoring_repository/lib/src/models/atmospheric_conditions.dart +++ b/packages/monitoring_repository/lib/src/models/atmospheric_conditions.dart @@ -14,12 +14,65 @@ class AtmosphericConditions { }); factory AtmosphericConditions.fromJson(Map json) { + double toDoubleSafe(dynamic value) { + if (value is num) return value.toDouble(); + if (value is String) { + final normalized = value.trim().replaceAll(',', '.'); + final match = RegExp(r'[-+]?[0-9]*\.?[0-9]+').firstMatch(normalized); + if (match != null) { + return double.tryParse(match.group(0)!) ?? 0.0; + } + return double.tryParse(normalized) ?? 0.0; + } + return 0.0; + } + + DateTime parseTimestamp(dynamic rawTimestamp) { + if (rawTimestamp is int) { + return DateTime.fromMillisecondsSinceEpoch(rawTimestamp); + } + if (rawTimestamp is double) { + return DateTime.fromMillisecondsSinceEpoch(rawTimestamp.toInt()); + } + if (rawTimestamp is String) { + final trimmed = rawTimestamp.trim(); + final unixValue = int.tryParse(trimmed); + if (unixValue != null) { + if (unixValue < 1000000000000) { + return DateTime.fromMillisecondsSinceEpoch(unixValue * 1000); + } + return DateTime.fromMillisecondsSinceEpoch(unixValue); + } + final parsed = DateTime.tryParse(trimmed); + if (parsed != null) return parsed; + } + return DateTime.now(); + } + + final temperature = toDoubleSafe( + json['temperature'] ?? json['temp'] ?? json['t'] ?? 0, + ); + final humidity = toDoubleSafe(json['humidity'] ?? json['hum'] ?? 0); + final pressure = toDoubleSafe( + json['pressure'] ?? json['press'] ?? json['tekanan'] ?? 0, + ); + final altitude = toDoubleSafe( + json['altitude'] ?? json['alt'] ?? json['height'] ?? 0, + ); + + DateTime timestamp = DateTime.now(); + final rawTimestamp = + json['timestamp'] ?? json['time'] ?? json['waktu'] ?? json['datetime']; + if (rawTimestamp != null) { + timestamp = parseTimestamp(rawTimestamp); + } + return AtmosphericConditions( - temperature: (json['temperature'] ?? 0).toDouble(), - humidity: (json['humidity'] ?? 0).toDouble(), - pressure: (json['pressure'] ?? 0).toDouble(), - altitude: (json['altitude'] ?? 0).toDouble(), - timestamp: DateTime.now(), // karena kamu pakai latest + temperature: temperature, + humidity: humidity, + pressure: pressure, + altitude: altitude, + timestamp: timestamp, ); } } diff --git a/packages/monitoring_repository/lib/src/models/evaporasi.dart b/packages/monitoring_repository/lib/src/models/evaporasi.dart index f048180..a3500e9 100644 --- a/packages/monitoring_repository/lib/src/models/evaporasi.dart +++ b/packages/monitoring_repository/lib/src/models/evaporasi.dart @@ -47,7 +47,20 @@ class Evaporasi { json['evaporasi_k'], ); - final suhuVal = toDoubleSafe( + /// ========================= + /// SUHU + /// ========================= + final suhuParsed = toDoubleSafe( + json['suhu'] ?? + json['suhu_air'] ?? + json['suhuAir'] ?? + json['temp'] ?? + json['temperature'], + ); + + // Filter nilai rusak + final suhuVal = (suhuParsed < -50 || suhuParsed > 100) ? 0.0 : suhuParsed; + toDoubleSafe( json['suhu'] ?? json['suhu_air'] ?? json['suhuAir'] ?? @@ -58,7 +71,7 @@ class Evaporasi { // Banyak kemungkinan penamaan field tinggi air. // Pakai beberapa alias agar tidak default 0. final tinggiVal = toDoubleSafe( - json['tinggi_air_cm'] ?? + json['tinggi'] ?? json['tinggi_air'] ?? json['tinggiAir'] ?? json['tinggiAir_cm'] ?? @@ -70,17 +83,14 @@ class Evaporasi { json['tinggiAir_m'], ); - // Default timestamp: fallback now (kalau field waktu tidak ada). // Catatan: untuk Firebase seharusnya timestamp dikirim konsisten (ms atau ISO string). DateTime timestamp = DateTime.now(); // dukung beberapa kemungkinan penamaan timestamp - final rawTimestamp = - json['timestamp'] ?? json['time'] ?? json['waktu'] ?? json['datetime']; - - if (rawTimestamp != null) { + final rawTimestamp = json['timestamp'] ?? json['time'] ?? json['datetime']; + if (rawTimestamp != null) { if (rawTimestamp is int) { timestamp = DateTime.fromMillisecondsSinceEpoch(rawTimestamp); } else if (rawTimestamp is double) {