592 lines
18 KiB
Dart
592 lines
18 KiB
Dart
import 'package:get/get.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:url_launcher/url_launcher.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/chart_data_point.dart';
|
|
import '../../../models/history_model.dart';
|
|
import '../../../models/notification_model.dart';
|
|
import '../../../models/realtime_monitoring_model.dart';
|
|
import '../../../widgets/app_snackbar.dart';
|
|
|
|
class InfusData {
|
|
final String dropsPerMinute;
|
|
final String room;
|
|
final String deviceId;
|
|
final String updateTime;
|
|
final String deviceStatus;
|
|
|
|
InfusData({
|
|
required this.dropsPerMinute,
|
|
required this.room,
|
|
required this.deviceId,
|
|
required this.updateTime,
|
|
required this.deviceStatus,
|
|
});
|
|
}
|
|
|
|
class NotificationData {
|
|
final String id;
|
|
final String title;
|
|
final String message;
|
|
final String room;
|
|
final String deviceId;
|
|
final String timeAgo;
|
|
final DateTime createdAt;
|
|
|
|
NotificationData({
|
|
required this.id,
|
|
required this.title,
|
|
required this.message,
|
|
required this.room,
|
|
required this.deviceId,
|
|
required this.timeAgo,
|
|
required this.createdAt,
|
|
});
|
|
}
|
|
|
|
class HomePatientController extends GetxController {
|
|
final FirestoreService _firestoreService = FirestoreService();
|
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
|
final NotificationService _notificationService = NotificationService();
|
|
|
|
final deviceId = ''.obs;
|
|
final roomId = ''.obs;
|
|
final patientName = 'Patient'.obs;
|
|
final userEmail = ''.obs;
|
|
|
|
final currentCarouselIndex = 0.obs;
|
|
final bannerImages = [
|
|
'assets/images/banner-slider3.png',
|
|
'assets/images/banner-slider4.png',
|
|
];
|
|
|
|
final currentInfusData = InfusData(
|
|
dropsPerMinute: '0',
|
|
room: '-',
|
|
deviceId: '-',
|
|
updateTime: '-',
|
|
deviceStatus: 'disconnected',
|
|
).obs;
|
|
|
|
final isBuzzerActive = false.obs;
|
|
|
|
final chartDataPoints = <ChartDataPoint>[].obs;
|
|
final notificationsList = <NotificationData>[].obs;
|
|
|
|
final isLoading = true.obs;
|
|
final isLoadingHistory = true.obs;
|
|
final isLoadingNotifications = true.obs;
|
|
|
|
StreamSubscription? _realtimeSubscription;
|
|
StreamSubscription? _notificationsSubscription;
|
|
StreamSubscription? _historySubscription;
|
|
|
|
final Set<String> _alertedDevices = {};
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeNotificationService();
|
|
_loadDataFromArguments();
|
|
}
|
|
|
|
@override
|
|
void onReady() {
|
|
super.onReady();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_realtimeSubscription?.cancel();
|
|
_notificationsSubscription?.cancel();
|
|
_historySubscription?.cancel();
|
|
super.onClose();
|
|
}
|
|
|
|
Future<void> _initializeNotificationService() async {
|
|
try {
|
|
await _notificationService.initialize();
|
|
print('Notification service initialized');
|
|
} catch (e) {
|
|
print('Error initializing notification service: $e');
|
|
}
|
|
}
|
|
|
|
void _loadDataFromArguments() async {
|
|
try {
|
|
final args = Get.arguments as Map<String, dynamic>?;
|
|
|
|
if (args != null) {
|
|
deviceId.value = args['deviceId'] ?? '';
|
|
patientName.value = args['userName'] ?? 'Patient';
|
|
userEmail.value = args['userEmail'] ?? '';
|
|
}
|
|
|
|
if (deviceId.value.isEmpty) {
|
|
AppSnackbar.error('Device ID tidak ditemukan');
|
|
isLoading.value = false;
|
|
return;
|
|
}
|
|
|
|
await _loadPatientData();
|
|
|
|
if (roomId.value.isNotEmpty) {
|
|
_startRealtimeMonitoring();
|
|
_loadHistoryData();
|
|
_loadNotifications();
|
|
} else {
|
|
AppSnackbar.error('Room ID tidak ditemukan');
|
|
isLoading.value = false;
|
|
}
|
|
} catch (e) {
|
|
AppSnackbar.error('Gagal memuat data: $e');
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _loadPatientData() async {
|
|
try {
|
|
isLoading.value = true;
|
|
|
|
final patient = await _firestoreService.getPatientByDeviceId(
|
|
deviceId.value,
|
|
);
|
|
|
|
if (patient != null) {
|
|
patientName.value = patient.namePatient;
|
|
roomId.value = patient.roomId;
|
|
|
|
final room = await _firestoreService.getRoomById(patient.roomId);
|
|
|
|
if (currentInfusData.value.deviceStatus == 'disconnected') {
|
|
currentInfusData.value = InfusData(
|
|
dropsPerMinute: '0',
|
|
room: room?.roomName ?? patient.roomId,
|
|
deviceId: patient.deviceId,
|
|
updateTime: 'Loading...',
|
|
deviceStatus: 'disconnected',
|
|
);
|
|
}
|
|
} else {
|
|
AppSnackbar.error('Data pasien tidak ditemukan');
|
|
}
|
|
} catch (e) {
|
|
AppSnackbar.error('Gagal memuat data pasien: $e');
|
|
print('Error loading patient data: $e');
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
void _startRealtimeMonitoring() {
|
|
try {
|
|
if (roomId.value.isEmpty || deviceId.value.isEmpty) {
|
|
print('Room ID or Device ID is empty');
|
|
return;
|
|
}
|
|
|
|
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
|
|
|
_realtimeSubscription?.cancel();
|
|
|
|
_realtimeSubscription = _realtimeService
|
|
.getDeviceStream(dbRoomId, deviceId.value)
|
|
.listen(
|
|
(device) async {
|
|
if (device != null) {
|
|
await _updateCurrentInfusData(device);
|
|
await _checkDropRate(device);
|
|
isBuzzerActive.value = device.buzzerActive;
|
|
} else {
|
|
print('Received null device data');
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('Realtime stream error: $error');
|
|
AppSnackbar.error('Koneksi realtime terputus');
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
} catch (e) {
|
|
print('Error starting monitoring: $e');
|
|
AppSnackbar.error('Gagal memulai monitoring: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _updateCurrentInfusData(RealtimeMonitoringModel device) async {
|
|
try {
|
|
final room = await _firestoreService.getRoomById(device.roomId);
|
|
|
|
currentInfusData.value = InfusData(
|
|
dropsPerMinute: '${device.dropRate.toStringAsFixed(0)}',
|
|
room: room?.roomName ?? device.roomId,
|
|
deviceId: device.deviceId,
|
|
updateTime: _getLastUpdateText(device.lastUpdate),
|
|
deviceStatus: device.deviceStatus,
|
|
);
|
|
} catch (e) {
|
|
print('Error updating infus data: $e');
|
|
AppSnackbar.error('Gagal update data: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _checkDropRate(RealtimeMonitoringModel device) async {
|
|
try {
|
|
final deviceId = device.deviceId;
|
|
final dropRate = device.dropRate;
|
|
final deviceStatus = device.deviceStatus;
|
|
|
|
if (dropRate == 0.0 && deviceStatus == 'connected') {
|
|
if (!_alertedDevices.contains(deviceId)) {
|
|
final patient = await _firestoreService.getPatientByDeviceId(
|
|
deviceId,
|
|
);
|
|
final room = await _firestoreService.getRoomById(device.roomId);
|
|
|
|
if (patient != null) {
|
|
await _notificationService.showDropRateAlertForPatient(
|
|
patientId: patient.id,
|
|
patientName: patient.namePatient,
|
|
deviceId: deviceId,
|
|
roomName: room?.roomName ?? device.roomId,
|
|
);
|
|
|
|
_alertedDevices.add(deviceId);
|
|
print('Alert sent for device: $deviceId');
|
|
}
|
|
}
|
|
} else if (dropRate > 0.0) {
|
|
if (_alertedDevices.contains(deviceId)) {
|
|
_alertedDevices.remove(deviceId);
|
|
_notificationService.clearCooldown(deviceId);
|
|
await _notificationService.cancelNotificationByDeviceId(deviceId);
|
|
print('Device reset and notification dismissed: $deviceId');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('Error checking drop rate: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> turnOffBuzzer() async {
|
|
try {
|
|
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
|
await _realtimeService.controlBuzzer(dbRoomId, deviceId.value, false);
|
|
isBuzzerActive.value = false;
|
|
AppSnackbar.success('Buzzer dimatikan');
|
|
} catch (e) {
|
|
print('Error turning off buzzer: $e');
|
|
AppSnackbar.error('Gagal mematikan buzzer');
|
|
}
|
|
}
|
|
|
|
void _loadHistoryData() {
|
|
try {
|
|
isLoadingHistory.value = true;
|
|
|
|
_historySubscription?.cancel();
|
|
|
|
_historySubscription = _firestoreService
|
|
.getDeviceHistoriesStream(deviceId.value)
|
|
.listen(
|
|
(histories) {
|
|
try {
|
|
if (histories.isNotEmpty) {
|
|
_processHistoryData(histories);
|
|
} else {
|
|
chartDataPoints.value = [];
|
|
}
|
|
isLoadingHistory.value = false;
|
|
} catch (e) {
|
|
print('Error processing history: $e');
|
|
chartDataPoints.value = [];
|
|
isLoadingHistory.value = false;
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('History stream error: $error');
|
|
chartDataPoints.value = [];
|
|
isLoadingHistory.value = false;
|
|
AppSnackbar.error('Gagal memuat history: $error');
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
} catch (e) {
|
|
print('Error loading history: $e');
|
|
isLoadingHistory.value = false;
|
|
AppSnackbar.error('Gagal memuat history: $e');
|
|
}
|
|
}
|
|
|
|
void _processHistoryData(List<HistoryModel> histories) {
|
|
try {
|
|
// Ambil 10 data TERBARU
|
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
|
final latestHistories = histories.take(10).toList();
|
|
|
|
// Urutkan lagi dari lama -> baru supaya grafik terbaca kiri ke kanan
|
|
latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
|
|
final List<ChartDataPoint> points = latestHistories.map((history) {
|
|
return ChartDataPoint(
|
|
timestamp: history.timestamp,
|
|
dropsPerMinute: history.dropPerMinute,
|
|
);
|
|
}).toList();
|
|
|
|
chartDataPoints.value = points;
|
|
} catch (e) {
|
|
print('Error processing history data: $e');
|
|
chartDataPoints.value = [];
|
|
}
|
|
}
|
|
|
|
void _loadNotifications() {
|
|
try {
|
|
isLoadingNotifications.value = true;
|
|
|
|
_notificationsSubscription?.cancel();
|
|
|
|
_notificationsSubscription = _firestoreService
|
|
.getNotificationsStream()
|
|
.listen(
|
|
(notifications) async {
|
|
try {
|
|
final filteredNotifications = notifications
|
|
.where((n) => n.deviceId == deviceId.value)
|
|
.toList();
|
|
|
|
final List<NotificationData> notifList = [];
|
|
|
|
for (var notif in filteredNotifications) {
|
|
try {
|
|
final patient = await _firestoreService
|
|
.getPatientByDeviceId(notif.deviceId);
|
|
final room = patient != null
|
|
? await _firestoreService.getRoomById(patient.roomId)
|
|
: null;
|
|
|
|
notifList.add(
|
|
NotificationData(
|
|
id: notif.id,
|
|
title: notif.title,
|
|
message: notif.message,
|
|
room: room?.roomName ?? '-',
|
|
deviceId: notif.deviceId,
|
|
timeAgo: _getLastUpdateText(notif.createdAt),
|
|
createdAt: notif.createdAt,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
print('Error processing notification ${notif.id}: $e');
|
|
}
|
|
}
|
|
|
|
notifList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
|
|
notificationsList.value = notifList.take(2).toList();
|
|
isLoadingNotifications.value = false;
|
|
} catch (e) {
|
|
print('Error processing notifications: $e');
|
|
isLoadingNotifications.value = false;
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('Notifications stream error: $error');
|
|
isLoadingNotifications.value = false;
|
|
AppSnackbar.error('Gagal memuat notifikasi: $error');
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
} catch (e) {
|
|
print('Error loading notifications: $e');
|
|
isLoadingNotifications.value = false;
|
|
AppSnackbar.error('Gagal memuat notifikasi: $e');
|
|
}
|
|
}
|
|
|
|
void updateCarouselIndex(int index) {
|
|
currentCarouselIndex.value = index;
|
|
}
|
|
|
|
void showCallDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
title: const Text(
|
|
'Hubungi Perawat',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: const Text(
|
|
'Apakah Anda ingin menghubungi perawat melalui WhatsApp?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
Navigator.of(context).pop();
|
|
await _openWhatsApp();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF25D366),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.phone, size: 18),
|
|
SizedBox(width: 8),
|
|
Text('Hubungi'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _openWhatsApp() async {
|
|
try {
|
|
const String nursePhoneNumber = '6281231901277';
|
|
|
|
final String message = Uri.encodeComponent(
|
|
'Halo, saya ${patientName.value} di ruangan ${currentInfusData.value.room}. '
|
|
'Saya membutuhkan bantuan perawat untuk device ${deviceId.value}.',
|
|
);
|
|
|
|
final String whatsappUrl =
|
|
'https://wa.me/$nursePhoneNumber?text=$message';
|
|
|
|
if (await canLaunchUrl(Uri.parse(whatsappUrl))) {
|
|
await launchUrl(
|
|
Uri.parse(whatsappUrl),
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
} else {
|
|
AppSnackbar.error('Tidak dapat membuka WhatsApp');
|
|
}
|
|
} catch (e) {
|
|
print('Error opening WhatsApp: $e');
|
|
AppSnackbar.error('Gagal menghubungi perawat: $e');
|
|
}
|
|
}
|
|
|
|
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();
|
|
try {
|
|
await FirebaseAuth.instance.signOut();
|
|
Get.offAllNamed('/login');
|
|
} catch (e) {
|
|
print('Error logging out: $e');
|
|
AppSnackbar.error('Gagal logout: $e');
|
|
}
|
|
},
|
|
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) {
|
|
try {
|
|
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';
|
|
}
|
|
} catch (e) {
|
|
print('Error calculating time difference: $e');
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
Future<void> refreshData() async {
|
|
try {
|
|
await _loadPatientData();
|
|
} catch (e) {
|
|
print('Error refreshing data: $e');
|
|
AppSnackbar.error('Gagal refresh data: $e');
|
|
}
|
|
}
|
|
|
|
void clearDeviceAlert(String deviceId) {
|
|
_alertedDevices.remove(deviceId);
|
|
_notificationService.clearCooldown(deviceId);
|
|
}
|
|
|
|
void clearAllAlerts() {
|
|
_alertedDevices.clear();
|
|
_notificationService.clearAllCooldowns();
|
|
}
|
|
} |