TKK_E32230859/lib/app/modules/home/controllers/home_controller.dart

353 lines
10 KiB
Dart

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 = <InfusMonitoring>[].obs;
final isLoading = true.obs;
List<RoomModel> _cachedRooms = [];
StreamSubscription? _patientsSubscription;
StreamSubscription? _roomsSubscription;
// Map deviceId -> StreamSubscription untuk per-device realtime stream
final Map<String, StreamSubscription> _deviceSubscriptions = {};
// Map deviceId -> data realtime terbaru
final Map<String, RealtimeMonitoringModel> _realtimeDataMap = {};
List<PatientModel> _latestPatients = [];
bool _patientsReady = false;
final Set<String> _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<void> _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<PatientModel> 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<InfusMonitoring> 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<void> _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<void> 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();
}
}