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 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 dayNames = [ 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min', ]; FlowMonitoringData _parseMonitoringData(Map 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 parseMonitoringDataResult( Map 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 _generateRecapRows( List> 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 = []; 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> generateRecapRowsResult( List> 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 getErrorInfoResult(Object error) { return ServiceResult.success( message: 'Error flow berhasil dipetakan', data: FlowDataError.fromException(error), ); } ServiceResult hasActiveCycleResult(Map? cycleData) { return ServiceResult.success( message: 'Status siklus aktif berhasil diperiksa', data: cycleData != null && cycleData.isNotEmpty, ); } ServiceResult extractStartDateResult( Map? cycleData, ) { final startDate = CyclePhaseUtils.extractStartDate(cycleData); return ServiceResult.success( message: 'Tanggal mulai siklus berhasil diambil', data: startDate, ); } }