import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'dart:async'; import '../../../services/firestore_service.dart'; import '../../../services/realtime_database_service.dart'; import '../../../services/notification_service.dart'; import '../../../models/patient_model.dart'; import '../../../models/room_model.dart'; import '../../../models/realtime_monitoring_model.dart'; import '../../../widgets/app_snackbar.dart'; class InfusMonitoring { final String id; final String patientName; final String deviceId; final String room; final String roomId; final String dropsPerMinute; final DateTime lastUpdate; final String deviceStatus; InfusMonitoring({ required this.id, required this.patientName, required this.deviceId, required this.room, required this.roomId, required this.dropsPerMinute, required this.lastUpdate, required this.deviceStatus, }); } class HomeController extends GetxController { final searchController = TextEditingController(); final FirestoreService _firestoreService = FirestoreService(); final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService(); final NotificationService _notificationService = NotificationService(); final currentCarouselIndex = 0.obs; final bannerImages = [ 'assets/images/banner-slider1.png', 'assets/images/banner-slider2.png', ]; final monitoringList = [].obs; final isLoading = true.obs; List _cachedRooms = []; StreamSubscription? _patientsSubscription; StreamSubscription? _roomsSubscription; // Map deviceId -> StreamSubscription untuk per-device realtime stream final Map _deviceSubscriptions = {}; // Map deviceId -> data realtime terbaru final Map _realtimeDataMap = {}; List _latestPatients = []; bool _patientsReady = false; final Set _alertedDevices = {}; @override void onInit() { super.onInit(); _initializeNotificationService(); _initializeMonitoring(); } @override void onClose() { searchController.dispose(); _patientsSubscription?.cancel(); _roomsSubscription?.cancel(); // Cancel semua per-device subscriptions for (final sub in _deviceSubscriptions.values) { sub.cancel(); } _deviceSubscriptions.clear(); super.onClose(); } Future _initializeNotificationService() async { try { await _notificationService.initialize(); } catch (e) { print('Error initializing notification service: $e'); } } void _initializeMonitoring() { isLoading.value = true; _patientsReady = false; // 1. Cache rooms _roomsSubscription = _firestoreService.getRoomsStream().listen((rooms) { _cachedRooms = rooms; }); // 2. Patients stream _patientsSubscription = _firestoreService.getPatientsStream().listen( (patients) { _latestPatients = patients; _patientsReady = true; _syncDeviceSubscriptions(patients); }, onError: (error) { print('Error patients stream: $error'); isLoading.value = false; AppSnackbar.error('Gagal memuat data pasien'); }, ); // Timeout fallback Future.delayed(const Duration(seconds: 8), () { if (isLoading.value) { print('Timeout: force stop loading'); isLoading.value = false; } }); } /// Sinkronisasi per-device stream — ikuti pola HomePatientController /// pakai getDeviceStream(dbRoomId, deviceId) bukan getAllDevicesStream() void _syncDeviceSubscriptions(List patients) { final currentDeviceIds = patients.map((p) => p.deviceId).toSet(); final existingDeviceIds = _deviceSubscriptions.keys.toSet(); // Cancel stream untuk device yang sudah tidak ada di patients final removedIds = existingDeviceIds.difference(currentDeviceIds); for (final deviceId in removedIds) { _deviceSubscriptions[deviceId]?.cancel(); _deviceSubscriptions.remove(deviceId); _realtimeDataMap.remove(deviceId); } // Tambah stream untuk device baru final newIds = currentDeviceIds.difference(existingDeviceIds); for (final patient in patients.where((p) => newIds.contains(p.deviceId))) { _subscribeToDevice(patient); } } void _subscribeToDevice(PatientModel patient) { // Format room ID sama persis seperti HomePatientController final dbRoomId = patient.roomId.toLowerCase().replaceAll(' ', '_'); final deviceId = patient.deviceId; print('Subscribing to device: $deviceId in room: $dbRoomId'); final sub = _realtimeService .getDeviceStream(dbRoomId, deviceId) .listen( (device) { if (device != null) { _realtimeDataMap[deviceId] = device; } else { // Device null = disconnected, hapus dari map _realtimeDataMap.remove(deviceId); } // Rebuild setiap ada update dari device manapun if (_patientsReady) { _rebuildMonitoringList(); } }, onError: (error) { print('Error stream device $deviceId: $error'); // Tetap rebuild walau error, device akan tampil sebagai disconnected if (_patientsReady) { _rebuildMonitoringList(); } }, cancelOnError: false, ); _deviceSubscriptions[deviceId] = sub; } void _rebuildMonitoringList() { final List combinedData = []; for (var patient in _latestPatients) { final realtimeDevice = _realtimeDataMap[patient.deviceId]; final roomName = _getRoomNameFromCache(patient.roomId); final dropRate = realtimeDevice?.dropRate ?? 0.0; final lastUpdate = realtimeDevice?.lastUpdate ?? DateTime.now(); final deviceStatus = realtimeDevice?.deviceStatus ?? 'disconnected'; _checkDropRate( patient: patient, dropRate: dropRate, roomName: roomName, deviceStatus: deviceStatus, ); combinedData.add( InfusMonitoring( id: patient.id, patientName: patient.namePatient, deviceId: patient.deviceId, room: roomName, roomId: patient.roomId, dropsPerMinute: '${dropRate.toStringAsFixed(0)} tpm', lastUpdate: lastUpdate, deviceStatus: deviceStatus, ), ); } monitoringList.value = combinedData; isLoading.value = false; } String _getRoomNameFromCache(String roomId) { final room = _cachedRooms.firstWhereOrNull( (r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(), ); return room?.roomName ?? roomId; } Future _checkDropRate({ required PatientModel patient, required double dropRate, required String roomName, required String deviceStatus, }) async { final deviceId = patient.deviceId; if (dropRate == 0.0 && deviceStatus == 'connected') { if (!_alertedDevices.contains(deviceId)) { await _notificationService.showDropRateAlert( patientId: patient.id, patientName: patient.namePatient, deviceId: deviceId, roomName: roomName, ); _alertedDevices.add(deviceId); } } else if (dropRate > 0.0) { if (_alertedDevices.contains(deviceId)) { _alertedDevices.remove(deviceId); _notificationService.clearCooldown(deviceId); await _notificationService.cancelNotificationByDeviceId(deviceId); } } } Future controlInfusServo( String deviceId, String roomId, bool open, ) async { try { final dbRoomId = roomId.toLowerCase().replaceAll(' ', '_'); await _realtimeService.controlServo(dbRoomId, deviceId, open); AppSnackbar.success(open ? 'Infus dibuka' : 'Infus ditutup'); } catch (e) { AppSnackbar.error('Gagal mengontrol infus'); } } void updateCarouselIndex(int index) { currentCarouselIndex.value = index; } void showLogoutDialog(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: const Text( 'Konfirmasi Logout', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), content: const Text( 'Yakin ingin keluar?', style: TextStyle(fontSize: 16), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), style: TextButton.styleFrom( foregroundColor: Colors.grey[600], padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 20, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('Batal'), ), ElevatedButton( onPressed: () async { Navigator.of(context).pop(); await FirebaseAuth.instance.signOut(); Get.offAllNamed('/login'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 20, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text( 'Logout', style: TextStyle(fontWeight: FontWeight.bold), ), ), ], ); }, ); } String getLastUpdateText(DateTime lastUpdate) { final difference = DateTime.now().difference(lastUpdate); if (difference.inMinutes < 1) { return 'Just now'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes}m ago'; } else if (difference.inHours < 24) { return '${difference.inHours}h ago'; } else { return '${difference.inDays}d ago'; } } void clearDeviceAlert(String deviceId) { _alertedDevices.remove(deviceId); _notificationService.clearCooldown(deviceId); } void clearAllAlerts() { _alertedDevices.clear(); _notificationService.clearAllCooldowns(); } }