HydroNutrify_Ari_e41221567/lib/data/services/flow_service.dart

344 lines
9.5 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Color;
import 'package:mobile_monitoring/data/models/service_result.dart';
import 'package:mobile_monitoring/core/utils/cycle_phase_utils.dart';
/// Model untuk monitoring data di Flow page
class FlowMonitoringData {
final double ph;
final double tds;
final String phStatus;
final String tdsStatus;
final Color phColor;
final Color tdsColor;
final bool pumpPhUp;
final bool pumpPhDown;
final bool pumpNutrient;
final bool pumpWater;
FlowMonitoringData({
required this.ph,
required this.tds,
required this.phStatus,
required this.tdsStatus,
required this.phColor,
required this.tdsColor,
required this.pumpPhUp,
required this.pumpPhDown,
required this.pumpNutrient,
required this.pumpWater,
});
}
/// Model untuk recap row
class RecapRow {
final String day;
final String morning;
final String afternoon;
final String night;
final DateTime date;
RecapRow({
required this.day,
required this.morning,
required this.afternoon,
required this.night,
required this.date,
});
Map<String, dynamic> toMap() {
return {
'day': day,
'morning': morning,
'afternoon': afternoon,
'night': night,
'date': date,
};
}
}
/// Model untuk aggregate data per slot waktu
class _SlotAggregate {
double? _phSum;
double? _tdsSum;
int _count = 0;
void add({required double ph, required double tds}) {
_phSum = (_phSum ?? 0) + ph;
_tdsSum = (_tdsSum ?? 0) + tds;
_count++;
}
bool get hasData => _count > 0;
double? get phAverage => _count > 0 ? _phSum! / _count : null;
double? get tdsAverage => _count > 0 ? _tdsSum! / _count : null;
}
/// Error info untuk Flow page
class FlowDataError {
final String displayMessage;
final String detailMessage;
final bool isAuthError;
FlowDataError({
required this.displayMessage,
required this.detailMessage,
required this.isAuthError,
});
factory FlowDataError.fromException(Object error) {
final errorMsg = error.toString();
debugPrint('[FlowService] Error: $errorMsg');
bool isNotAuthError = errorMsg.contains('terautentikasi');
if (isNotAuthError) {
return FlowDataError(
displayMessage: 'Silakan Login',
detailMessage: 'Sesi Anda telah berakhir, login kembali',
isAuthError: true,
);
} else {
return FlowDataError(
displayMessage: 'Gagal memuat data',
detailMessage: 'Pastikan koneksi internet stabil',
isAuthError: false,
);
}
}
}
/// Service untuk Flow Page - Backend Logic
/// Tidak ada dependency ke UI (no BuildContext, no Color from material)
class FlowService {
static const List<String> dayNames = [
'Sen',
'Sel',
'Rab',
'Kam',
'Jum',
'Sab',
'Min',
];
FlowMonitoringData _parseMonitoringData(Map<String, dynamic> data) {
debugPrint('[FlowService] parseMonitoringData()');
// Extract values
final double ph = (data['pH_value'] as num?)?.toDouble() ?? 0.0;
final double tds = (data['nutrient_level'] as num?)?.toDouble() ?? 0.0;
// Extract pump status
final bool pumpPhUp = data['pump_ph_up_status'] ?? false;
final bool pumpPhDown = data['pump_ph_down_status'] ?? false;
final bool pumpNutrient = data['pump_nutrient_status'] ?? false;
final bool pumpWater = data['pump_water_status'] ?? false;
// Determine abnormality
final bool phPumpOn = pumpPhUp || pumpPhDown;
final bool nutrientPumpOn = pumpNutrient || pumpWater;
// Determine status and colors
final String phStatus = phPumpOn ? 'Abnormal' : 'Normal';
final String tdsStatus = nutrientPumpOn ? 'Abnormal' : 'Normal';
// Use color codes instead of Color objects for backend
// Frontend will convert these to Color objects
final Color phColor = phPumpOn
? const Color(0xFFFF9800)
: const Color(0xFF4CAF50);
final Color tdsColor = nutrientPumpOn
? const Color(0xFFFF9800)
: const Color(0xFF4CAF50);
debugPrint('[FlowService] pH: $ph, status: $phStatus');
debugPrint('[FlowService] TDS: $tds, status: $tdsStatus');
return FlowMonitoringData(
ph: ph,
tds: tds,
phStatus: phStatus,
tdsStatus: tdsStatus,
phColor: phColor,
tdsColor: tdsColor,
pumpPhUp: pumpPhUp,
pumpPhDown: pumpPhDown,
pumpNutrient: pumpNutrient,
pumpWater: pumpWater,
);
}
ServiceResult<FlowMonitoringData> parseMonitoringDataResult(
Map<String, dynamic> data,
) {
try {
final parsed = _parseMonitoringData(data);
return ServiceResult.success(
message: 'Data flow monitoring berhasil diproses',
data: parsed,
);
} catch (e) {
return ServiceResult.failure(
message: 'Gagal memproses data flow monitoring',
errorCode: 'parse_error',
);
}
}
List<RecapRow> _generateRecapRows(
List<Map<String, dynamic>> docs,
DateTime? startDate,
) {
debugPrint('[FlowService] generateRecapRows()');
debugPrint('[FlowService] Total docs: ${docs.length}');
debugPrint('[FlowService] Start date: $startDate');
final now = DateTime.now();
final filterEndDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
// Determine filter start date
DateTime filterStartDate;
if (startDate != null) {
final plantingStart = DateTime(
startDate.year,
startDate.month,
startDate.day,
);
final sevenDaysAgo = now.subtract(const Duration(days: 6));
filterStartDate = plantingStart.isBefore(sevenDaysAgo)
? sevenDaysAgo
: plantingStart;
} else {
filterStartDate = now.subtract(const Duration(days: 6));
}
final dayCount = filterEndDate.difference(filterStartDate).inDays + 1;
debugPrint(
'[FlowService] Filter start: $filterStartDate, end: $filterEndDate',
);
debugPrint('[FlowService] Day count: $dayCount');
// Create buckets for each day and time slot
final buckets = List.generate(
dayCount,
(_) => List.generate(3, (_) => _SlotAggregate()),
);
// Aggregate data into buckets
int processedCount = 0;
for (final data in docs) {
final timestamp = data['timestamp'];
DateTime? dt;
if (timestamp is Timestamp) {
dt = timestamp.toDate();
} else if (timestamp is DateTime) {
dt = timestamp;
}
if (dt == null ||
dt.isBefore(filterStartDate) ||
dt.isAfter(filterEndDate)) {
continue;
}
final ph = (data['pH_value'] as num?)?.toDouble() ?? 0.0;
final tds = (data['nutrient_level'] as num?)?.toDouble() ?? 0.0;
final dayIdx = dt.difference(filterStartDate).inDays;
if (dayIdx >= 0 && dayIdx < dayCount) {
final slotIdx = _getSlotIndexForHour(dt.hour);
buckets[dayIdx][slotIdx].add(ph: ph, tds: tds);
processedCount++;
}
}
debugPrint('[FlowService] Processed $processedCount data points');
// Generate rows
final rows = <RecapRow>[];
for (var i = 0; i < dayCount; i++) {
final date = filterStartDate.add(Duration(days: i));
final dayName =
'${dayNames[(date.weekday - 1) % 7]} ${date.day}/${date.month}';
rows.add(
RecapRow(
day: dayName,
morning: _formatSlotText(buckets[i][0]),
afternoon: _formatSlotText(buckets[i][1]),
night: _formatSlotText(buckets[i][2]),
date: date,
),
);
}
debugPrint('[FlowService] Generated ${rows.length} recap rows');
return rows;
}
ServiceResult<List<RecapRow>> generateRecapRowsResult(
List<Map<String, dynamic>> docs,
DateTime? startDate,
) {
try {
final rows = _generateRecapRows(docs, startDate);
return ServiceResult.success(
message: 'Rekap monitoring berhasil dibuat',
data: rows,
);
} catch (e) {
return ServiceResult.failure(
message: 'Gagal membuat rekap monitoring',
errorCode: 'recap_error',
);
}
}
/// Get slot index based on hour (0=morning, 1=afternoon, 2=night)
int _getSlotIndexForHour(int hour) {
if (hour >= 5 && hour < 12) {
return 0; // Morning (5-12)
} else if (hour >= 12 && hour < 17) {
return 1; // Afternoon (12-17)
} else {
return 2; // Night (17-5)
}
}
/// Format slot aggregate data to display text
String _formatSlotText(_SlotAggregate agg) {
if (!agg.hasData) return '-';
final phAvg = agg.phAverage?.toStringAsFixed(1);
final tdsAvg = agg.tdsAverage?.toStringAsFixed(0);
final phPart = phAvg != null ? 'pH $phAvg' : '';
final tdsPart = tdsAvg != null
? '${phPart.isNotEmpty ? ' / ' : ''}${tdsAvg}ppm'
: '';
return (phPart + tdsPart).isEmpty ? '-' : phPart + tdsPart;
}
ServiceResult<FlowDataError> getErrorInfoResult(Object error) {
return ServiceResult.success(
message: 'Error flow berhasil dipetakan',
data: FlowDataError.fromException(error),
);
}
ServiceResult<bool> hasActiveCycleResult(Map<String, dynamic>? cycleData) {
return ServiceResult.success(
message: 'Status siklus aktif berhasil diperiksa',
data: cycleData != null && cycleData.isNotEmpty,
);
}
ServiceResult<DateTime?> extractStartDateResult(
Map<String, dynamic>? cycleData,
) {
final startDate = CyclePhaseUtils.extractStartDate(cycleData);
return ServiceResult.success(
message: 'Tanggal mulai siklus berhasil diambil',
data: startDate,
);
}
}