353 lines
10 KiB
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();
|
|
}
|
|
}
|