pepe
This commit is contained in:
parent
948184da6e
commit
096a0fd928
|
|
@ -9,7 +9,7 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
final DatabaseReference _ref;
|
||||
|
||||
static const _path = 'Monitoring/settings/evaporasi';
|
||||
static const _rtdbResetPath = 'Monitoring/reset_dmax';
|
||||
static const _rtdbResetPath = 'Monitoring/reset_evaporasi';
|
||||
static const _rtdbRealtimePath = 'Monitoring/realtime/dmax_saat_ini';
|
||||
|
||||
EvaporasiSettingsBloc({DatabaseReference? ref})
|
||||
|
|
@ -18,7 +18,7 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
on<EvaporasiSettingsStarted>(_onStarted);
|
||||
on<EvaporasiThresholdRendahChanged>(_onThresholdRendahChanged);
|
||||
on<EvaporasiThresholdTinggiChanged>(_onThresholdTinggiChanged);
|
||||
on<EvaporasiRumusKalibrasiChanged>(_onRumusChanged);
|
||||
// rumusKalibrasi removed: calculation is handled on ESP32 hardware
|
||||
on<EvaporasiKoreksiOffsetChanged>(_onOffsetChanged);
|
||||
on<EvaporasiPumpStartChanged>(_onPumpStartChanged);
|
||||
on<EvaporasiPumpEndChanged>(_onPumpEndChanged);
|
||||
|
|
@ -45,7 +45,6 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
emit(state.copyWith(
|
||||
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
||||
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
||||
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
||||
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0),
|
||||
pumpStartTime: (data['pump_start_time'] as String?) ??
|
||||
(data['jam_pompa_mulai'] != null ? '${_toInt(data['jam_pompa_mulai'], 6).toString().padLeft(2, '0')}:00' : '06:00'),
|
||||
|
|
@ -134,10 +133,7 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
Emitter<EvaporasiSettingsState> emit,
|
||||
) => emit(state.copyWith(thresholdTinggi: event.value));
|
||||
|
||||
void _onRumusChanged(
|
||||
EvaporasiRumusKalibrasiChanged event,
|
||||
Emitter<EvaporasiSettingsState> emit,
|
||||
) => emit(state.copyWith(rumusKalibrasi: event.rumus));
|
||||
// rumusKalibrasi handling removed — device firmware defines the formula
|
||||
|
||||
void _onOffsetChanged(
|
||||
EvaporasiKoreksiOffsetChanged event,
|
||||
|
|
@ -221,17 +217,20 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
try {
|
||||
final pumpStartHour = int.tryParse(state.pumpStartTime.split(':').first) ?? 0;
|
||||
final pumpEndHour = int.tryParse(state.pumpEndTime.split(':').first) ?? 0;
|
||||
await _ref.set({
|
||||
await FirebaseDatabase.instance
|
||||
.ref('Monitoring/settings/kalibrasi')
|
||||
.update({
|
||||
'd0': state.d0,
|
||||
'dmax': state.dmaxManual,
|
||||
});
|
||||
await _ref.update({
|
||||
'threshold_rendah': state.thresholdRendah,
|
||||
'threshold_tinggi': state.thresholdTinggi,
|
||||
'rumus_kalibrasi': state.rumusKalibrasi,
|
||||
'koreksi_offset': state.koreksiOffset,
|
||||
'pump_start_time': state.pumpStartTime,
|
||||
'pump_end_time': state.pumpEndTime,
|
||||
'jam_pompa_mulai': pumpStartHour,
|
||||
'jam_pompa_selesai': pumpEndHour,
|
||||
'd0': state.d0,
|
||||
'dmax_manual': state.dmaxManual,
|
||||
'standar_tinggi_cm': state.standarTinggiCm,
|
||||
'batas_kritis_cm': state.batasKritisCm,
|
||||
'interval_realtime_ms': state.intervalRealtime_ms,
|
||||
|
|
|
|||
|
|
@ -20,11 +20,6 @@ class EvaporasiThresholdTinggiChanged extends EvaporasiSettingsEvent {
|
|||
@override List<Object?> get props => [value];
|
||||
}
|
||||
|
||||
class EvaporasiRumusKalibrasiChanged extends EvaporasiSettingsEvent {
|
||||
final String rumus;
|
||||
const EvaporasiRumusKalibrasiChanged(this.rumus);
|
||||
@override List<Object?> get props => [rumus];
|
||||
}
|
||||
|
||||
class EvaporasiKoreksiOffsetChanged extends EvaporasiSettingsEvent {
|
||||
final double value;
|
||||
|
|
|
|||
|
|
@ -444,147 +444,6 @@ class _StatusChip extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _RumusSelector extends StatelessWidget {
|
||||
final String selected;
|
||||
final ValueChanged<String> onChanged;
|
||||
const _RumusSelector({required this.selected, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const options = [
|
||||
(
|
||||
value: 'selisih_max',
|
||||
label: 'Selisih Maksimum',
|
||||
subtitle: 'E = max(H kemarin) − max(H hari ini)',
|
||||
icon: Icons.trending_down_rounded,
|
||||
),
|
||||
(
|
||||
value: 'rata_harian',
|
||||
label: 'Rata-rata Harian',
|
||||
subtitle: 'E = rata(H kemarin) − rata(H hari ini)',
|
||||
icon: Icons.bar_chart_rounded,
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Metode Kalkulasi',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54)),
|
||||
const SizedBox(height: 10),
|
||||
...options.map((opt) {
|
||||
final isSelected = opt.value == selected;
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(opt.value),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.blue.shade50 : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? Colors.blue.shade400 : Colors.grey.shade200,
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(opt.icon,
|
||||
color: isSelected
|
||||
? Colors.blue.shade700
|
||||
: Colors.grey.shade500,
|
||||
size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(opt.label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? Colors.blue.shade800
|
||||
: Colors.black87)),
|
||||
const SizedBox(height: 2),
|
||||
Text(opt.subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: isSelected
|
||||
? Colors.blue.shade600
|
||||
: Colors.grey.shade500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle_rounded,
|
||||
color: Colors.blue.shade600, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FormulaPreview extends StatelessWidget {
|
||||
final EvaporasiSettingsState state;
|
||||
const _FormulaPreview({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rumus = state.rumusKalibrasi == 'selisih_max'
|
||||
? 'E = max(H₁) − max(H₂) + offset'
|
||||
: 'E = avg(H₁) − avg(H₂) + offset';
|
||||
final offsetStr = state.koreksiOffset >= 0
|
||||
? '+${state.koreksiOffset.toStringAsFixed(2)}'
|
||||
: state.koreksiOffset.toStringAsFixed(2);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade900,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Icon(Icons.functions_rounded,
|
||||
size: 15, color: Colors.amber.shade300),
|
||||
const SizedBox(width: 8),
|
||||
Text('Preview Rumus',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.amber.shade300,
|
||||
fontWeight: FontWeight.bold)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Text(rumus,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade300,
|
||||
fontFamily: 'monospace')),
|
||||
const SizedBox(height: 6),
|
||||
Text('offset = $offsetStr mm',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.green.shade300,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
final String text;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ enum EvaporasiSettingsStatus { loading, loaded, saving, saved, error }
|
|||
class EvaporasiSettingsState extends Equatable {
|
||||
final double thresholdRendah;
|
||||
final double thresholdTinggi;
|
||||
final String rumusKalibrasi;
|
||||
final double koreksiOffset;
|
||||
final String pumpStartTime;
|
||||
final String pumpEndTime;
|
||||
|
|
@ -45,7 +44,6 @@ class EvaporasiSettingsState extends Equatable {
|
|||
const EvaporasiSettingsState({
|
||||
this.thresholdRendah = 2.0,
|
||||
this.thresholdTinggi = 10.0,
|
||||
this.rumusKalibrasi = 'selisih_max',
|
||||
this.koreksiOffset = 0.0,
|
||||
this.pumpStartTime = '06:00',
|
||||
this.pumpEndTime = '18:00',
|
||||
|
|
@ -82,7 +80,6 @@ class EvaporasiSettingsState extends Equatable {
|
|||
EvaporasiSettingsState copyWith({
|
||||
double? thresholdRendah,
|
||||
double? thresholdTinggi,
|
||||
String? rumusKalibrasi,
|
||||
double? koreksiOffset,
|
||||
String? pumpStartTime,
|
||||
String? pumpEndTime,
|
||||
|
|
@ -118,7 +115,6 @@ class EvaporasiSettingsState extends Equatable {
|
|||
return EvaporasiSettingsState(
|
||||
thresholdRendah: thresholdRendah ?? this.thresholdRendah,
|
||||
thresholdTinggi: thresholdTinggi ?? this.thresholdTinggi,
|
||||
rumusKalibrasi: rumusKalibrasi ?? this.rumusKalibrasi,
|
||||
koreksiOffset: koreksiOffset ?? this.koreksiOffset,
|
||||
pumpStartTime: pumpStartTime ?? this.pumpStartTime,
|
||||
pumpEndTime: pumpEndTime ?? this.pumpEndTime,
|
||||
|
|
@ -155,7 +151,7 @@ class EvaporasiSettingsState extends Equatable {
|
|||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
||||
thresholdRendah, thresholdTinggi, koreksiOffset,
|
||||
pumpStartTime, pumpEndTime, d0, dmaxManual,
|
||||
intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms,
|
||||
standarTinggiCm, batasKritisCm, tempCompActive, tempCompCoef, tempRefC,
|
||||
|
|
|
|||
|
|
@ -151,58 +151,6 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
_EvaporasiRealtimeUpdated event,
|
||||
Emitter<EvaporasiState> emit,
|
||||
) async {
|
||||
final updatedHistory = List<Evaporasi>.from(state.history);
|
||||
final dupIdx = updatedHistory.indexWhere(
|
||||
(e) => e.timestamp.toUtc() == event.data.timestamp.toUtc(),
|
||||
);
|
||||
if (dupIdx >= 0) {
|
||||
updatedHistory[dupIdx] = event.data;
|
||||
} else {
|
||||
updatedHistory.add(event.data);
|
||||
}
|
||||
updatedHistory.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
// Regenerasikan data chart secara dinamis agar selalu sinkron dengan data real-time
|
||||
List<double> updatedChart;
|
||||
List<double> updatedTemp;
|
||||
List<String> updatedLabels = state.chartLabels;
|
||||
|
||||
final isSingle = _isSameDay(state.startDate, state.endDate);
|
||||
|
||||
if (isSingle) {
|
||||
updatedChart = TimeSeriesMapper.toSpecificDate(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.evaporasi,
|
||||
targetDate: state.startDate,
|
||||
);
|
||||
updatedTemp = TimeSeriesMapper.toSpecificDate(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.suhu,
|
||||
targetDate: state.startDate,
|
||||
);
|
||||
updatedLabels = List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00');
|
||||
} else {
|
||||
final evapResult = TimeSeriesMapper.toDateRange(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.evaporasi,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
final tempResult = TimeSeriesMapper.toDateRange(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.suhu,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
updatedChart = evapResult.values;
|
||||
updatedTemp = tempResult.values;
|
||||
updatedLabels = evapResult.labels;
|
||||
}
|
||||
|
||||
await _loadThresholdSettings();
|
||||
|
||||
final (status, willRain) = computeStatus(
|
||||
|
|
@ -213,19 +161,9 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
_emitAlert(status, willRain, event.data.evaporasi);
|
||||
|
||||
emit(state.copyWith(
|
||||
history: updatedHistory,
|
||||
filteredHistory: state.selectedDateFilter != null
|
||||
? updatedHistory.where((e) =>
|
||||
e.timestamp.year == state.selectedDateFilter!.year &&
|
||||
e.timestamp.month == state.selectedDateFilter!.month &&
|
||||
e.timestamp.day == state.selectedDateFilter!.day).toList()
|
||||
: updatedHistory,
|
||||
currentValue: event.data.evaporasi,
|
||||
temperature: event.data.suhu,
|
||||
waterLevel: event.data.tinggiAir,
|
||||
chartValues: updatedChart,
|
||||
chartTemperatures: updatedTemp,
|
||||
chartLabels: updatedLabels,
|
||||
weatherStatus: status,
|
||||
willRain: willRain,
|
||||
currentData: event.data,
|
||||
|
|
|
|||
|
|
@ -363,10 +363,6 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
_statusCard(state),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Kontrol Database Evaporasi ───────────────
|
||||
const EvaporasiControlPanel(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Range Selector ──────────────────────────
|
||||
const Text(
|
||||
'Tren Evaporasi & Suhu',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class EvaporasiHistoryList extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final grouped = _groupByDate(history);
|
||||
final sortedKeys = grouped.keys.toList()
|
||||
..sort((a, b) => b.compareTo(a)); // terbaru di atas
|
||||
..sort((a, b) => b.compareTo(a));
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -68,15 +68,11 @@ class EvaporasiHistoryList extends StatelessWidget {
|
|||
String _formatDateLabel(String key) {
|
||||
final dt = DateTime.parse(key);
|
||||
final today = DateTime.now();
|
||||
if (dt.year == today.year &&
|
||||
dt.month == today.month &&
|
||||
dt.day == today.day) {
|
||||
if (dt.year == today.year && dt.month == today.month && dt.day == today.day) {
|
||||
return 'Hari Ini — ${DateFormat('dd MMMM yyyy', 'id_ID').format(dt)}';
|
||||
}
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
if (dt.year == yesterday.year &&
|
||||
dt.month == yesterday.month &&
|
||||
dt.day == yesterday.day) {
|
||||
if (dt.year == yesterday.year && dt.month == yesterday.month && dt.day == yesterday.day) {
|
||||
return 'Kemarin — ${DateFormat('dd MMMM yyyy', 'id_ID').format(dt)}';
|
||||
}
|
||||
return DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(dt);
|
||||
|
|
@ -105,9 +101,12 @@ class _HeaderBar extends StatelessWidget {
|
|||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'History',
|
||||
'Riwayat Data',
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (filtered)
|
||||
|
|
@ -171,37 +170,35 @@ class _ChipButton extends StatelessWidget {
|
|||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Group per tanggal
|
||||
// ════════════════════════════════════════════════════════════
|
||||
class _DateGroup extends StatelessWidget {
|
||||
// ════════════════════════════════════════════════════
|
||||
class _DateGroup extends StatefulWidget {
|
||||
final String label;
|
||||
final List<Evaporasi> items;
|
||||
|
||||
const _DateGroup({required this.label, required this.items});
|
||||
|
||||
double get _avgEvap {
|
||||
if (items.isEmpty) return 0;
|
||||
return items.map((e) => e.evaporasi).reduce((a, b) => a + b) / items.length;
|
||||
@override
|
||||
State<_DateGroup> createState() => _DateGroupState();
|
||||
}
|
||||
|
||||
double get _maxEvap {
|
||||
if (items.isEmpty) return 0;
|
||||
return items.map((e) => e.evaporasi).reduce((a, b) => a > b ? a : b);
|
||||
class _DateGroupState extends State<_DateGroup> {
|
||||
bool _expanded = true;
|
||||
|
||||
double get _avgEvap {
|
||||
if (widget.items.isEmpty) return 0;
|
||||
return widget.items.map((e) => e.evaporasi).reduce((a, b) => a + b) /
|
||||
widget.items.length;
|
||||
}
|
||||
|
||||
double get _avgTemp {
|
||||
if (items.isEmpty) return 0;
|
||||
if (widget.items.isEmpty) return 0;
|
||||
final validTemps =
|
||||
items.where((e) => e.suhu >= -50 && e.suhu <= 100).toList();
|
||||
widget.items.where((e) => e.suhu >= -50 && e.suhu <= 100).toList();
|
||||
if (validTemps.isEmpty) return 0;
|
||||
return validTemps.map((e) => e.suhu).reduce((a, b) => a + b) /
|
||||
validTemps.length;
|
||||
}
|
||||
|
||||
double get _maxTemp {
|
||||
if (items.isEmpty) return 0;
|
||||
return items.map((e) => e.suhu).reduce((a, b) => a > b ? a : b);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
|
@ -217,6 +214,11 @@ class _DateGroup extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
|
|
@ -234,17 +236,75 @@ class _DateGroup extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
Text(widget.label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'rata-rata ${_avgEvap.toStringAsFixed(2)} mm',
|
||||
'${widget.items.length} data • rata-rata ${_avgEvap.toStringAsFixed(2)} mm • suhu rata-rata ${_avgTemp.toStringAsFixed(1)} °C',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_expanded
|
||||
? Icons.keyboard_arrow_up_rounded
|
||||
: Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_expanded)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.items.length,
|
||||
separatorBuilder: (_, __) => Divider(height: 1, color: Colors.grey.shade100),
|
||||
itemBuilder: (context, i) => _HistoryItemTile(item: widget.items[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryItemTile extends StatelessWidget {
|
||||
final Evaporasi item;
|
||||
|
||||
const _HistoryItemTile({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 52,
|
||||
child: Text(
|
||||
DateFormat('HH:mm:ss').format(item.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'rata-rata suhu ${_avgTemp.toStringAsFixed(1)} °C',
|
||||
'${item.evaporasi.toStringAsFixed(2)} mm • ${item.suhu.toStringAsFixed(1)} °C • ${item.tinggiAir.toStringAsFixed(1)} cm',
|
||||
style: const TextStyle(fontSize: 13, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.status,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
|
|
@ -252,7 +312,6 @@ class _DateGroup extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue