diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..ba14f6b
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,64 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+// Load key.properties
+def keystoreProperties = new Properties()
+def keystorePropertiesFile = rootProject.file("key.properties")
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+}
+
+android {
+ namespace = "com.example.smartinfuse"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11
+ }
+
+ defaultConfig {
+ applicationId = "com.example.smartinfuse"
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ signingConfigs {
+ debug {
+ // default debug keystore
+ }
+
+ release {
+ if (keystorePropertiesFile.exists()) {
+ storeFile file(keystoreProperties['storeFile'])
+ storePassword keystoreProperties['storePassword']
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ signingConfig = signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
+
+dependencies {}
+
+apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
deleted file mode 100644
index f04f562..0000000
--- a/android/app/build.gradle.kts
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
- id("com.android.application")
- id("kotlin-android")
- // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
- id("dev.flutter.flutter-gradle-plugin")
-}
-
-android {
- namespace = "com.example.smartinfuse"
- compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11.toString()
- }
-
- defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.example.smartinfuse"
- // You can update the following values to match your application needs.
- // For more information, see: https://flutter.dev/to/review-gradle-config.
- minSdk = flutter.minSdkVersion
- targetSdk = flutter.targetSdkVersion
- versionCode = flutter.versionCode
- versionName = flutter.versionName
- }
-
- buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
- }
- }
-}
-
-flutter {
- source = "../.."
-}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 8125d18..ddad42f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -41,5 +41,15 @@
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..6e70052
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,35 @@
+buildscript {
+ ext.kotlin_version = '2.1.0'
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.3.0"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.gms:google-services:4.3.15'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
deleted file mode 100644
index dbee657..0000000
--- a/android/build.gradle.kts
+++ /dev/null
@@ -1,24 +0,0 @@
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-val newBuildDir: Directory =
- rootProject.layout.buildDirectory
- .dir("../../build")
- .get()
-rootProject.layout.buildDirectory.value(newBuildDir)
-
-subprojects {
- val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
- project.layout.buildDirectory.value(newSubprojectBuildDir)
-}
-subprojects {
- project.evaluationDependsOn(":app")
-}
-
-tasks.register("clean") {
- delete(rootProject.layout.buildDirectory)
-}
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
new file mode 100644
index 0000000..c667740
--- /dev/null
+++ b/android/build/reports/problems/problems-report.html
@@ -0,0 +1,663 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gradle Configuration Cache
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index fb605bc..3eb433c 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -20,7 +20,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
- id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+ id ("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
include(":app")
diff --git a/assets/images/logo.png b/assets/images/logo.png
index ce5af91..999fe8c 100644
Binary files a/assets/images/logo.png and b/assets/images/logo.png differ
diff --git a/lib/app/models/chart_data_point.dart b/lib/app/models/chart_data_point.dart
index 5f97303..27d0e52 100644
--- a/lib/app/models/chart_data_point.dart
+++ b/lib/app/models/chart_data_point.dart
@@ -1,4 +1,3 @@
-// lib/app/models/chart_data_point.dart
class ChartDataPoint {
final int hour;
final double dropsPerMinute;
@@ -7,4 +6,9 @@ class ChartDataPoint {
required this.hour,
required this.dropsPerMinute,
});
+
+ @override
+ String toString() {
+ return 'ChartDataPoint(hour: $hour, dropsPerMinute: $dropsPerMinute)';
+ }
}
\ No newline at end of file
diff --git a/lib/app/models/device_model.dart b/lib/app/models/device_model.dart
new file mode 100644
index 0000000..fcb67cd
--- /dev/null
+++ b/lib/app/models/device_model.dart
@@ -0,0 +1,32 @@
+// lib/app/models/device_model.dart
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class DeviceModel {
+ final String idDevices;
+ final String roomId;
+ final String devicesName;
+
+ DeviceModel({
+ required this.idDevices,
+ required this.roomId,
+ required this.devicesName,
+ });
+
+ // From Firestore
+ factory DeviceModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return DeviceModel(
+ idDevices: doc.id,
+ roomId: data['room_id'] ?? '',
+ devicesName: data['devices_name'] ?? '',
+ );
+ }
+
+ // To Firestore
+ Map toFirestore() {
+ return {
+ 'room_id': roomId,
+ 'devices_name': devicesName,
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/history_model.dart b/lib/app/models/history_model.dart
new file mode 100644
index 0000000..9d6dba8
--- /dev/null
+++ b/lib/app/models/history_model.dart
@@ -0,0 +1,63 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class HistoryModel {
+ final String id;
+ final String deviceId;
+ final double dropPerMinute;
+ final DateTime timestamp;
+
+ HistoryModel({
+ required this.id,
+ required this.deviceId,
+ required this.dropPerMinute,
+ required this.timestamp,
+ });
+
+ factory HistoryModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return HistoryModel(
+ id: doc.id,
+ deviceId: data['device_id'] ?? '',
+ dropPerMinute: (data['drop_per_menit'] ?? 0).toDouble(),
+ timestamp: (data['timestamp'] as Timestamp).toDate(),
+ );
+ }
+
+ Map toFirestore() {
+ return {
+ 'device_id': deviceId,
+ 'drop_per_menit': dropPerMinute,
+ 'timestamp': Timestamp.fromDate(timestamp),
+ };
+ }
+
+ factory HistoryModel.fromMap(String id, Map data) {
+ return HistoryModel(
+ id: id,
+ deviceId: data['device_id'] ?? '',
+ dropPerMinute: (data['drop_per_menit'] ?? 0).toDouble(),
+ timestamp: data['timestamp'] is Timestamp
+ ? (data['timestamp'] as Timestamp).toDate()
+ : DateTime.parse(data['timestamp']),
+ );
+ }
+
+ HistoryModel copyWith({
+ String? id,
+ String? deviceId,
+ double? dropPerMinute,
+ DateTime? timestamp,
+ }) {
+ return HistoryModel(
+ id: id ?? this.id,
+ deviceId: deviceId ?? this.deviceId,
+ dropPerMinute: dropPerMinute ?? this.dropPerMinute,
+ timestamp: timestamp ?? this.timestamp,
+ );
+ }
+
+ @override
+ String toString() {
+ return 'HistoryModel(id: $id, deviceId: $deviceId, dropPerMinute: $dropPerMinute, timestamp: $timestamp)';
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/notification_model.dart b/lib/app/models/notification_model.dart
new file mode 100644
index 0000000..45b332b
--- /dev/null
+++ b/lib/app/models/notification_model.dart
@@ -0,0 +1,70 @@
+// lib/app/models/notification_model.dart
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class NotificationModel {
+ final String id;
+ final String patientId;
+ final String deviceId;
+ final String title;
+ final String message;
+ final bool isRead;
+ final DateTime createdAt;
+
+ NotificationModel({
+ required this.id,
+ required this.patientId,
+ required this.deviceId,
+ required this.title,
+ required this.message,
+ this.isRead = false,
+ required this.createdAt,
+ });
+
+ // From Firestore
+ factory NotificationModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return NotificationModel(
+ id: doc.id,
+ patientId: data['patient_id'] ?? '',
+ deviceId: data['device_id'] ?? '',
+ title: data['title'] ?? '',
+ message: data['message'] ?? '',
+ isRead: data['is_read'] ?? false,
+ createdAt: data['created_at'] != null
+ ? (data['created_at'] as Timestamp).toDate()
+ : DateTime.now(),
+ );
+ }
+
+ // To Firestore
+ Map toFirestore() {
+ return {
+ 'patient_id': patientId,
+ 'device_id': deviceId,
+ 'title': title,
+ 'message': message,
+ 'is_read': isRead,
+ 'created_at': Timestamp.fromDate(createdAt),
+ };
+ }
+
+ NotificationModel copyWith({
+ String? id,
+ String? patientId,
+ String? deviceId,
+ String? title,
+ String? message,
+ bool? isRead,
+ DateTime? createdAt,
+ }) {
+ return NotificationModel(
+ id: id ?? this.id,
+ patientId: patientId ?? this.patientId,
+ deviceId: deviceId ?? this.deviceId,
+ title: title ?? this.title,
+ message: message ?? this.message,
+ isRead: isRead ?? this.isRead,
+ createdAt: createdAt ?? this.createdAt,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/patient_model.dart b/lib/app/models/patient_model.dart
new file mode 100644
index 0000000..1517663
--- /dev/null
+++ b/lib/app/models/patient_model.dart
@@ -0,0 +1,44 @@
+// lib/app/models/patient_model.dart
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class PatientModel {
+ final String id;
+ final String namePatient;
+ final String nameGuardian;
+ final String deviceId;
+ final String roomId;
+ final String? userId; // Firebase Auth UID
+
+ PatientModel({
+ required this.id,
+ required this.namePatient,
+ required this.nameGuardian,
+ required this.deviceId,
+ required this.roomId,
+ this.userId,
+ });
+
+ // From Firestore
+ factory PatientModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return PatientModel(
+ id: doc.id,
+ namePatient: data['name_patient'] ?? '',
+ nameGuardian: data['name_guardian'] ?? '',
+ deviceId: data['device_id'] ?? '',
+ roomId: data['room_id'] ?? '',
+ userId: data['user_id'],
+ );
+ }
+
+ // To Firestore
+ Map toFirestore() {
+ return {
+ 'name_patient': namePatient,
+ 'name_guardian': nameGuardian,
+ 'device_id': deviceId,
+ 'room_id': roomId,
+ if (userId != null) 'user_id': userId,
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/realtime_monitoring_model.dart b/lib/app/models/realtime_monitoring_model.dart
new file mode 100644
index 0000000..452f7ca
--- /dev/null
+++ b/lib/app/models/realtime_monitoring_model.dart
@@ -0,0 +1,95 @@
+// lib/app/models/realtime_monitoring_model.dart
+
+class RealtimeMonitoringModel {
+ final String deviceId;
+ final String roomId;
+ final bool servoOpen;
+ final DateTime lastCommand;
+ final String deviceStatus;
+ final double dropRate;
+ final DateTime lastUpdate;
+ final int servoAngleOpen;
+ final int servoAngleClose;
+
+ RealtimeMonitoringModel({
+ required this.deviceId,
+ required this.roomId,
+ required this.servoOpen,
+ required this.lastCommand,
+ required this.deviceStatus,
+ required this.dropRate,
+ required this.lastUpdate,
+ this.servoAngleOpen = 90,
+ this.servoAngleClose = 0,
+ });
+
+ // From Realtime Database
+ factory RealtimeMonitoringModel.fromRealtimeDB(
+ String deviceId,
+ String roomId,
+ Map data,
+ ) {
+ final controlling = data['controlling'] as Map? ?? {};
+ final monitoring = data['monitoring'] as Map? ?? {};
+ final settings = data['settings'] as Map? ?? {};
+
+ return RealtimeMonitoringModel(
+ deviceId: deviceId,
+ roomId: roomId,
+ servoOpen: controlling['servo_open'] ?? false,
+ lastCommand: DateTime.parse(
+ controlling['last_command'] ?? DateTime.now().toIso8601String(),
+ ),
+ deviceStatus: monitoring['device_status'] ?? 'disconnected',
+ dropRate: (monitoring['drop_rate'] ?? 0).toDouble(),
+ lastUpdate: DateTime.parse(
+ monitoring['last_update'] ?? DateTime.now().toIso8601String(),
+ ),
+ servoAngleOpen: settings['servo_angle_open'] ?? 90,
+ servoAngleClose: settings['servo_angle_close'] ?? 0,
+ );
+ }
+
+ // To Realtime Database
+ Map toRealtimeDB() {
+ return {
+ 'controlling': {
+ 'servo_open': servoOpen,
+ 'last_command': lastCommand.toIso8601String(),
+ },
+ 'monitoring': {
+ 'device_status': deviceStatus,
+ 'drop_rate': dropRate,
+ 'last_update': lastUpdate.toIso8601String(),
+ },
+ 'settings': {
+ 'servo_angle_open': servoAngleOpen,
+ 'servo_angle_close': servoAngleClose,
+ },
+ };
+ }
+
+ RealtimeMonitoringModel copyWith({
+ String? deviceId,
+ String? roomId,
+ bool? servoOpen,
+ DateTime? lastCommand,
+ String? deviceStatus,
+ double? dropRate,
+ DateTime? lastUpdate,
+ int? servoAngleOpen,
+ int? servoAngleClose,
+ }) {
+ return RealtimeMonitoringModel(
+ deviceId: deviceId ?? this.deviceId,
+ roomId: roomId ?? this.roomId,
+ servoOpen: servoOpen ?? this.servoOpen,
+ lastCommand: lastCommand ?? this.lastCommand,
+ deviceStatus: deviceStatus ?? this.deviceStatus,
+ dropRate: dropRate ?? this.dropRate,
+ lastUpdate: lastUpdate ?? this.lastUpdate,
+ servoAngleOpen: servoAngleOpen ?? this.servoAngleOpen,
+ servoAngleClose: servoAngleClose ?? this.servoAngleClose,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/room_model.dart b/lib/app/models/room_model.dart
new file mode 100644
index 0000000..3e61348
--- /dev/null
+++ b/lib/app/models/room_model.dart
@@ -0,0 +1,63 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class RoomModel {
+ final String id;
+ final String roomName;
+ final int capacity;
+ final DateTime createdAt;
+
+ RoomModel({
+ required this.id,
+ required this.roomName,
+ required this.capacity,
+ required this.createdAt,
+ });
+
+ factory RoomModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return RoomModel(
+ id: doc.id,
+ roomName: data['room_name'] ?? '',
+ capacity: data['capacity'] ?? 0,
+ createdAt: (data['created_at'] as Timestamp).toDate(),
+ );
+ }
+
+ Map toFirestore() {
+ return {
+ 'room_name': roomName,
+ 'capacity': capacity,
+ 'created_at': Timestamp.fromDate(createdAt),
+ };
+ }
+
+ factory RoomModel.fromMap(String id, Map data) {
+ return RoomModel(
+ id: id,
+ roomName: data['room_name'] ?? '',
+ capacity: data['capacity'] ?? 0,
+ createdAt: data['created_at'] is Timestamp
+ ? (data['created_at'] as Timestamp).toDate()
+ : DateTime.parse(data['created_at']),
+ );
+ }
+
+ RoomModel copyWith({
+ String? id,
+ String? roomName,
+ int? capacity,
+ DateTime? createdAt,
+ }) {
+ return RoomModel(
+ id: id ?? this.id,
+ roomName: roomName ?? this.roomName,
+ capacity: capacity ?? this.capacity,
+ createdAt: createdAt ?? this.createdAt,
+ );
+ }
+
+ @override
+ String toString() {
+ return 'RoomModel(id: $id, roomName: $roomName, capacity: $capacity, createdAt: $createdAt)';
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/schedule_model.dart b/lib/app/models/schedule_model.dart
new file mode 100644
index 0000000..05a8261
--- /dev/null
+++ b/lib/app/models/schedule_model.dart
@@ -0,0 +1,49 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class ScheduleModel {
+ final String id;
+ final String patientId;
+ final String medicineDetail;
+ final String fluidDetail;
+ final DateTime scheduleDate;
+ final String timeOfDay;
+ final DateTime createdAt;
+
+ ScheduleModel({
+ required this.id,
+ required this.patientId,
+ required this.medicineDetail,
+ required this.fluidDetail,
+ required this.scheduleDate,
+ required this.timeOfDay,
+ required this.createdAt,
+ });
+
+ factory ScheduleModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return ScheduleModel(
+ id: doc.id,
+ patientId: data['patient_id'] ?? '',
+ medicineDetail: data['medicine_detail'] ?? '',
+ fluidDetail: data['fluid_detail'] ?? '',
+ scheduleDate: data['schedule_date'] != null
+ ? (data['schedule_date'] as Timestamp).toDate()
+ : DateTime.now(),
+ timeOfDay: data['time_of_day'] ?? 'Pagi',
+ createdAt: data['created_at'] != null
+ ? (data['created_at'] as Timestamp).toDate()
+ : DateTime.now(),
+ );
+ }
+
+ Map toFirestore() {
+ return {
+ 'patient_id': patientId,
+ 'medicine_detail': medicineDetail,
+ 'fluid_detail': fluidDetail,
+ 'schedule_date': Timestamp.fromDate(scheduleDate),
+ 'time_of_day': timeOfDay,
+ 'created_at': Timestamp.fromDate(createdAt),
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/app/models/user_model.dart b/lib/app/models/user_model.dart
new file mode 100644
index 0000000..5de2aee
--- /dev/null
+++ b/lib/app/models/user_model.dart
@@ -0,0 +1,59 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class UserModel {
+ final String id;
+ final String email;
+ final String name;
+ final String role;
+ final String? deviceId;
+ final DateTime? createdAt;
+
+ UserModel({
+ required this.id,
+ required this.email,
+ required this.name,
+ required this.role,
+ this.deviceId,
+ this.createdAt,
+ });
+
+ Map toFirestore() {
+ return {
+ 'email': email,
+ 'name': name,
+ 'role': role,
+ if (deviceId != null) 'device_id': deviceId,
+ 'created_at': createdAt ?? FieldValue.serverTimestamp(),
+ };
+ }
+
+ factory UserModel.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return UserModel(
+ id: doc.id,
+ email: data['email'] ?? '',
+ name: data['name'] ?? '',
+ role: data['role'] ?? '',
+ deviceId: data['device_id'],
+ createdAt: (data['created_at'] as Timestamp?)?.toDate(),
+ );
+ }
+
+ UserModel copyWith({
+ String? id,
+ String? email,
+ String? name,
+ String? role,
+ String? deviceId,
+ DateTime? createdAt,
+ }) {
+ return UserModel(
+ id: id ?? this.id,
+ email: email ?? this.email,
+ name: name ?? this.name,
+ role: role ?? this.role,
+ deviceId: deviceId ?? this.deviceId,
+ createdAt: createdAt ?? this.createdAt,
+ );
+ }
+}
diff --git a/lib/app/modules/device-detail/controllers/device_detail_controller.dart b/lib/app/modules/device-detail/controllers/device_detail_controller.dart
index ce58342..8c27953 100644
--- a/lib/app/modules/device-detail/controllers/device_detail_controller.dart
+++ b/lib/app/modules/device-detail/controllers/device_detail_controller.dart
@@ -1,23 +1,192 @@
import 'package:get/get.dart';
+import 'dart:async';
+import '../../../models/patient_model.dart';
+import '../../../models/device_model.dart';
+import '../../../models/realtime_monitoring_model.dart';
+import '../../../models/history_model.dart';
+import '../../../models/chart_data_point.dart';
+import '../../../services/firestore_service.dart';
+import '../../../services/realtime_database_service.dart';
+import '../../../widgets/app_snackbar.dart';
class DeviceDetailController extends GetxController {
- //TODO: Implement DeviceDetailController
+ final FirestoreService _firestoreService = FirestoreService();
+ final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
+
+ String? deviceId;
+ String? roomId;
+
+ final deviceName = ''.obs;
+ final patientName = ''.obs;
+ final roomName = ''.obs;
+ final dropsPerMinute = '0'.obs;
+ final lastUpdate = DateTime.now().obs;
+ final isServoActive = false.obs;
+ final deviceStatus = 'disconnected'.obs;
+ final isLoading = true.obs;
+
+ final RxList chartData = [].obs;
+
+ StreamSubscription? _realtimeSubscription;
+ StreamSubscription? _historySubscription;
+
+ PatientModel? patient;
+ DeviceModel? device;
+ RealtimeMonitoringModel? realtimeData;
- final count = 0.obs;
@override
void onInit() {
super.onInit();
- }
-
- @override
- void onReady() {
- super.onReady();
+ _getArguments();
+ if (deviceId != null && roomId != null) {
+ _initializeDeviceDetail();
+ } else {
+ Get.back();
+ AppSnackbar.error('Device ID tidak ditemukan');
+ }
}
@override
void onClose() {
+ _realtimeSubscription?.cancel();
+ _historySubscription?.cancel();
super.onClose();
}
- void increment() => count.value++;
+ void _getArguments() {
+ final args = Get.arguments as Map?;
+ if (args != null) {
+ deviceId = args['deviceId'];
+ roomId = args['roomId'];
+ }
+ }
+
+ Future _initializeDeviceDetail() async {
+ try {
+ isLoading.value = true;
+
+ await _loadPatientData();
+ await _loadDeviceData();
+ _listenToRealtimeData();
+ _listenToHistoryData();
+
+ isLoading.value = false;
+ } catch (e) {
+ isLoading.value = false;
+ AppSnackbar.error('Gagal memuat data');
+ }
+ }
+
+ Future _loadPatientData() async {
+ patient = await _firestoreService.getPatientByDeviceId(deviceId!);
+ if (patient != null) {
+ patientName.value = patient!.namePatient;
+ }
+ }
+
+ Future _loadDeviceData() async {
+ device = await _firestoreService.getDeviceById(deviceId!);
+ if (device != null) {
+ deviceName.value = device!.devicesName;
+ roomName.value = await _getRoomName(device!.roomId);
+ }
+ }
+
+ Future _getRoomName(String roomId) async {
+ try {
+ final rooms = await _firestoreService.getRoomsStream().first;
+ final room = rooms.firstWhereOrNull(
+ (r) =>
+ r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
+ );
+ return room?.roomName ?? roomId;
+ } catch (e) {
+ return roomId;
+ }
+ }
+
+ void _listenToRealtimeData() {
+ final dbRoomId = roomId!.toLowerCase().replaceAll(' ', '_');
+
+ _realtimeSubscription = _realtimeService
+ .getDeviceStream(dbRoomId, deviceId!)
+ .listen((data) {
+ if (data != null) {
+ realtimeData = data;
+ dropsPerMinute.value = data.dropRate.toStringAsFixed(0);
+ lastUpdate.value = data.lastUpdate;
+ isServoActive.value = data.servoOpen;
+ deviceStatus.value = data.deviceStatus;
+ }
+ }, onError: (error) => AppSnackbar.error('Koneksi realtime terputus'));
+ }
+
+ void _listenToHistoryData() {
+ _historySubscription = _firestoreService
+ .getDeviceHistoriesStream(deviceId!)
+ .listen(
+ (histories) {
+ if (histories.isNotEmpty) {
+ _processHistoryData(histories);
+ } else {
+ chartData.value = [];
+ }
+ },
+ onError: (error) {
+ chartData.value = [];
+ AppSnackbar.error('Gagal memuat data history');
+ },
+ );
+ }
+
+ void _processHistoryData(List histories) {
+ histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
+
+ final latestHistories = histories.take(10).toList();
+
+ latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
+
+ final List points = latestHistories.map((history) {
+ return ChartDataPoint(
+ hour: history.timestamp.hour,
+ dropsPerMinute: history.dropPerMinute,
+ );
+ }).toList();
+
+ chartData.value = points;
+ }
+
+ Future toggleServo(bool value) async {
+ try {
+ final dbRoomId = roomId!.toLowerCase().replaceAll(' ', '_');
+ await _realtimeService.controlServo(dbRoomId, deviceId!, value);
+ isServoActive.value = value;
+ AppSnackbar.success(value ? 'Infus dibuka' : 'Infus ditutup');
+ } catch (e) {
+ AppSnackbar.error('Gagal mengontrol servo');
+ }
+ }
+
+ String getLastUpdateText() {
+ final difference = DateTime.now().difference(lastUpdate.value);
+ if (difference.inMinutes < 1) return 'just now';
+ if (difference.inMinutes < 60) return '${difference.inMinutes}m ago';
+ if (difference.inHours < 24) return '${difference.inHours}h ago';
+ return '${difference.inDays}d ago';
+ }
+
+ void navigateToHistory() {
+ Get.toNamed(
+ '/history',
+ arguments: {
+ 'deviceId': deviceId,
+ 'deviceName': deviceName.value,
+ 'patientName': patientName.value,
+ },
+ );
+ }
+
+ Future refreshData() async {
+ await _loadDeviceData();
+ }
}
diff --git a/lib/app/modules/device-detail/views/device_detail_view.dart b/lib/app/modules/device-detail/views/device_detail_view.dart
index 0ed9285..9921e9e 100644
--- a/lib/app/modules/device-detail/views/device_detail_view.dart
+++ b/lib/app/modules/device-detail/views/device_detail_view.dart
@@ -1,22 +1,376 @@
import 'package:flutter/material.dart';
-
import 'package:get/get.dart';
-
+import '../../../widgets/history_grafik_card.dart';
import '../controllers/device_detail_controller.dart';
class DeviceDetailView extends GetView {
const DeviceDetailView({super.key});
+
@override
Widget build(BuildContext context) {
return Scaffold(
+ backgroundColor: Colors.grey[50],
appBar: AppBar(
- title: const Text('DeviceDetailView'),
- centerTitle: true,
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Colors.black),
+ onPressed: () => Get.back(),
+ ),
+ title: const Text(
+ 'Detail Alat',
+ style: TextStyle(
+ color: Colors.black,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.refresh, color: Colors.black),
+ onPressed: controller.refreshData,
+ ),
+ ],
),
- body: const Center(
- child: Text(
- 'DeviceDetailView is working',
- style: TextStyle(fontSize: 20),
+ body: Obx(() {
+ if (controller.isLoading.value) {
+ return const Center(
+ child: CircularProgressIndicator(color: Color(0xFF2196F3)),
+ );
+ }
+
+ return RefreshIndicator(
+ onRefresh: controller.refreshData,
+ color: const Color(0xFF2196F3),
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSection(
+ title: 'Monitoring Infus',
+ onSeeAll: () {},
+ child: _buildMonitoringCard(),
+ ),
+ const SizedBox(height: 20),
+ _buildSection(
+ title: 'Kontrol Alat',
+ onSeeAll: () {},
+ child: _buildControlCard(),
+ ),
+ const SizedBox(height: 20),
+ _buildSection(
+ title: 'History Data',
+ onSeeAll: controller.navigateToHistory,
+ child: Obx(() {
+ if (controller.chartData.isEmpty) {
+ return _buildEmptyChart();
+ }
+ return HistoryGrafikCard(
+ dataPoints: controller.chartData.value,
+ );
+ }),
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+ }
+
+ Widget _buildSection({
+ required String title,
+ required Widget child,
+ VoidCallback? onSeeAll,
+ }) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 12),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ title,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: Colors.black87,
+ ),
+ ),
+ if (onSeeAll != null)
+ GestureDetector(
+ onTap: onSeeAll,
+ child: const Text(
+ 'See all',
+ style: TextStyle(
+ fontSize: 13,
+ color: Color(0xFF2196F3),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: child,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildMonitoringCard() {
+ return Obx(
+ () => Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Colors.grey[200]!),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.08),
+ blurRadius: 12,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ Stack(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: const Color(0xFFE3F2FD),
+ borderRadius: BorderRadius.circular(14),
+ ),
+ child: const Icon(
+ Icons.water_drop_outlined,
+ size: 32,
+ color: Color(0xFF2196F3),
+ ),
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ child: Container(
+ width: 12,
+ height: 12,
+ decoration: BoxDecoration(
+ color: controller.deviceStatus.value == 'connected'
+ ? const Color(0xFF4CAF50)
+ : Colors.red,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 2),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ controller.dropsPerMinute.value,
+ style: const TextStyle(
+ fontSize: 22,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF2196F3),
+ height: 1,
+ ),
+ ),
+ const SizedBox(width: 6),
+ const Padding(
+ padding: EdgeInsets.only(bottom: 2),
+ child: Text(
+ 'tetes per menit',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.black54,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Last update: ${controller.getLastUpdateText()}',
+ style: TextStyle(fontSize: 11, color: Colors.grey[600]),
+ ),
+ const SizedBox(height: 12),
+ if (controller.patientName.value.isNotEmpty)
+ RichText(
+ text: TextSpan(
+ style: TextStyle(fontSize: 12, color: Colors.grey[700]),
+ children: [
+ const TextSpan(
+ text: 'Nama Pasien: ',
+ style: TextStyle(fontWeight: FontWeight.w500),
+ ),
+ TextSpan(
+ text: controller.patientName.value,
+ style: const TextStyle(
+ fontWeight: FontWeight.w600,
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 8,
+ runSpacing: 6,
+ children: [
+ if (controller.deviceId != null)
+ _buildBadge(
+ controller.deviceId!,
+ const Color(0xFFE3F2FD),
+ const Color(0xFF2196F3),
+ ),
+ if (controller.roomName.value.isNotEmpty)
+ _buildBadge(
+ controller.roomName.value,
+ Colors.grey[100]!,
+ Colors.grey[700]!,
+ ),
+ _buildBadge(
+ controller.deviceStatus.value == 'connected'
+ ? 'Online'
+ : 'Offline',
+ controller.deviceStatus.value == 'connected'
+ ? const Color(0xFFE8F5E9)
+ : Colors.red[50]!,
+ controller.deviceStatus.value == 'connected'
+ ? const Color(0xFF2E7D32)
+ : Colors.red[700]!,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildControlCard() {
+ return Obx(
+ () => Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Colors.grey[200]!),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.08),
+ blurRadius: 12,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: const Color(0xFFE3F2FD),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Icon(
+ Icons.settings_input_component,
+ size: 28,
+ color: Color(0xFF2196F3),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Kontrol Servo',
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ color: Colors.black87,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ controller.isServoActive.value
+ ? 'Infus sedang mengalir'
+ : 'Infus ditutup',
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
+ ),
+ ],
+ ),
+ ),
+ Switch(
+ value: controller.isServoActive.value,
+ onChanged: controller.deviceStatus.value == 'connected'
+ ? controller.toggleServo
+ : null,
+ activeColor: const Color(0xFF2196F3),
+ activeTrackColor: const Color(0xFFBBDEFB),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildEmptyChart() {
+ return Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey[200]!),
+ ),
+ child: Center(
+ child: Column(
+ children: [
+ Icon(Icons.show_chart, size: 48, color: Colors.grey[400]),
+ const SizedBox(height: 12),
+ Text(
+ 'Belum ada data history',
+ style: TextStyle(fontSize: 14, color: Colors.grey[600]),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBadge(String text, Color bgColor, Color textColor) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: bgColor,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ text,
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ color: textColor,
),
),
);
diff --git a/lib/app/modules/home-patient/controllers/home_patient_controller.dart b/lib/app/modules/home-patient/controllers/home_patient_controller.dart
index 5e25ab0..0ac39a6 100644
--- a/lib/app/modules/home-patient/controllers/home_patient_controller.dart
+++ b/lib/app/modules/home-patient/controllers/home_patient_controller.dart
@@ -1,92 +1,91 @@
-// lib/app/modules/home_patient/controllers/home_patient_controller.dart
import 'package:get/get.dart';
import 'package:flutter/material.dart';
-import '../../../models/chart_data_point.dart'; // Import model yang terpisah
+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 '../../../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 {
- // Patient info
- final patientName = 'Mrs. Shinta'.obs;
-
- // Banner carousel
+ final FirestoreService _firestoreService = FirestoreService();
+ final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
+
+ 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',
];
-
- // Current monitoring data
+
final currentInfusData = InfusData(
- dropsPerMinute: '30 tetes per menit',
- room: 'MAWAR 002',
- deviceId: 'SI001',
- updateTime: '5min ago',
+ dropsPerMinute: '0',
+ room: '-',
+ deviceId: '-',
+ updateTime: '-',
+ deviceStatus: 'disconnected',
).obs;
-
- // Chart data dengan 10 data points
- final chartDataPoints = [
- ChartDataPoint(hour: 8, dropsPerMinute: 25),
- ChartDataPoint(hour: 9, dropsPerMinute: 28),
- ChartDataPoint(hour: 10, dropsPerMinute: 30),
- ChartDataPoint(hour: 11, dropsPerMinute: 32),
- ChartDataPoint(hour: 12, dropsPerMinute: 35),
- ChartDataPoint(hour: 13, dropsPerMinute: 33),
- ChartDataPoint(hour: 14, dropsPerMinute: 30),
- ChartDataPoint(hour: 15, dropsPerMinute: 28),
- ChartDataPoint(hour: 16, dropsPerMinute: 26),
- ChartDataPoint(hour: 17, dropsPerMinute: 30),
- ].obs;
-
- // Notifications list
- final notificationsList = [
- NotificationData(
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Mohon hubungi perawat segera.',
- room: 'MAWAR 002',
- deviceId: 'SI001',
- timeAgo: '5min ago',
- ),
- NotificationData(
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Mohon hubungi perawat segera.',
- room: 'MELATI 001',
- deviceId: 'SI002',
- timeAgo: '10min ago',
- ),
- ].obs;
+
+ final chartDataPoints = [].obs;
+ final notificationsList = [].obs;
+
+ final isLoading = true.obs;
+ final isLoadingHistory = true.obs;
+ final isLoadingNotifications = true.obs;
+
+ StreamSubscription? _realtimeSubscription;
+ StreamSubscription? _notificationsSubscription;
+ StreamSubscription? _historySubscription;
@override
void onInit() {
super.onInit();
+ _loadDataFromArguments();
}
@override
@@ -96,14 +95,255 @@ class HomePatientController extends GetxController {
@override
void onClose() {
+ _realtimeSubscription?.cancel();
+ _notificationsSubscription?.cancel();
+ _historySubscription?.cancel();
super.onClose();
}
+ void _loadDataFromArguments() async {
+ try {
+ final args = Get.arguments as Map?;
+
+ 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 _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(); // Cancel previous subscription if exists
+
+ _realtimeSubscription = _realtimeService
+ .getDeviceStream(dbRoomId, deviceId.value)
+ .listen(
+ (device) {
+ if (device != null) {
+ _updateCurrentInfusData(device);
+ } 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');
+ }
+ }
+
+ 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');
+ }
+ }
+
+ void _loadHistoryData() {
+ try {
+ isLoadingHistory.value = true;
+
+ _historySubscription?.cancel(); // Cancel previous subscription if exists
+
+ _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 histories) {
+ try {
+ histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
+ final latestHistories = histories.take(10).toList();
+ latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
+ final List points = latestHistories.map((history) {
+ return ChartDataPoint(
+ hour: history.timestamp.hour,
+ 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(); // Cancel previous subscription if exists
+
+ _notificationsSubscription = _firestoreService
+ .getNotificationsStream()
+ .listen(
+ (notifications) async {
+ try {
+ final filteredNotifications = notifications
+ .where((n) => n.deviceId == deviceId.value)
+ .toList();
+
+ final List 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');
+ // Continue with other notifications
+ }
+ }
+
+ notifList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
+
+ notificationsList.value = notifList;
+ 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;
}
- // Show call dialog
void showCallDialog(BuildContext context) {
showDialog(
context: context,
@@ -116,36 +356,161 @@ class HomePatientController extends GetxController {
'Hubungi Perawat',
style: TextStyle(fontWeight: FontWeight.bold),
),
- content: const Text('Apakah Anda ingin menghubungi perawat?'),
+ 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: () {
+ onPressed: () async {
Navigator.of(context).pop();
- // Implement call functionality
- Get.snackbar(
- 'Menghubungi',
- 'Sedang menghubungi perawat...',
- snackPosition: SnackPosition.BOTTOM,
- backgroundColor: Colors.blue,
- colorText: Colors.white,
- );
+ await _openWhatsApp();
},
style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xFF2196F3),
+ backgroundColor: const Color(
+ 0xFF25D366,
+ ), // WhatsApp green color
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
- child: const Text('Hubungi'),
+ child: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.phone, size: 18),
+ SizedBox(width: 8),
+ Text('Hubungi'),
+ ],
+ ),
),
],
);
},
);
}
-}
\ No newline at end of file
+
+ Future _openWhatsApp() async {
+ try {
+ const String nursePhoneNumber =
+ '6281234567890';
+
+ 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 refreshData() async {
+ try {
+ await _loadPatientData();
+ // Tidak perlu reload history karena sudah menggunakan stream
+ // History akan auto-update
+ } catch (e) {
+ print('Error refreshing data: $e');
+ AppSnackbar.error('Gagal refresh data: $e');
+ }
+ }
+}
diff --git a/lib/app/modules/home-patient/views/home_patient_view.dart b/lib/app/modules/home-patient/views/home_patient_view.dart
index 8cdb071..a9ebbb4 100644
--- a/lib/app/modules/home-patient/views/home_patient_view.dart
+++ b/lib/app/modules/home-patient/views/home_patient_view.dart
@@ -1,217 +1,304 @@
-// lib/app/modules/home_patient/views/home_patient_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/home_patient_controller.dart';
import '../../../widgets/banner_slider.dart';
-import '../../../widgets/monitoring_patient_card.dart';
+import '../../../widgets/monitoring_card.dart';
import '../../../widgets/history_grafik_card.dart';
import '../../../widgets/notification_patient_card.dart';
+import '../../home/controllers/home_controller.dart';
class HomePatientView extends GetView {
const HomePatientView({super.key});
-
+
@override
Widget build(BuildContext context) {
Get.put(HomePatientController());
-
+
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
- child: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Header
- Padding(
- padding: const EdgeInsets.all(20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () => Text(
- 'Halo ${controller.patientName.value}',
- style: const TextStyle(
- fontSize: 24,
+ child: RefreshIndicator(
+ onRefresh: controller.refreshData,
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Obx(
+ () => Text(
+ 'Halo ${controller.patientName.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ InkWell(
+ onTap: () => controller.showLogoutDialog(context),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey[300]!),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Icon(Icons.logout, color: Colors.red),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Banner Slider
+ BannerSlider(
+ bannerImages: controller.bannerImages,
+ currentCarouselIndex: controller.currentCarouselIndex,
+ onPageChanged: controller.updateCarouselIndex,
+ ),
+
+ const SizedBox(height: 15),
+
+ // Monitoring Infus Section
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Monitoring Infus',
+ style: TextStyle(
+ fontSize: 18,
fontWeight: FontWeight.bold,
),
),
- ),
- Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(10),
- ),
- child: const Icon(
- Icons.logout,
- color: Colors.red,
- ),
- ),
- ],
- ),
- ),
-
- // Banner Slider - Gunakan widget universal
- BannerSlider(
- bannerImages: controller.bannerImages,
- currentCarouselIndex: controller.currentCarouselIndex,
- onPageChanged: controller.updateCarouselIndex,
- ),
-
- const SizedBox(height: 15),
-
- // Monitoring Infus Section
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- 'Monitoring Infus',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- TextButton(
- onPressed: () {},
- child: const Text(
- 'See all',
- style: TextStyle(
- color: Color(0xFF2196F3),
- fontWeight: FontWeight.w600,
+ TextButton(
+ onPressed: () {
+ // TODO: Navigate to monitoring detail
+ },
+ child: const Text(
+ 'See all',
+ style: TextStyle(
+ color: Color(0xFF2196F3),
+ fontWeight: FontWeight.w600,
+ ),
),
),
- ),
- ],
- ),
- ),
-
- // Monitoring Card
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Obx(
- () => MonitoringPatientCard(
- dropsPerMinute: controller.currentInfusData.value.dropsPerMinute,
- room: controller.currentInfusData.value.room,
- deviceId: controller.currentInfusData.value.deviceId,
- updateTime: controller.currentInfusData.value.updateTime,
- onTap: () {
- // Handle bookmark tap
- },
+ ],
),
),
- ),
-
- const SizedBox(height: 15),
-
- // History Data Section
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- 'History Data',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- TextButton(
- onPressed: () {},
- child: const Text(
- 'See all',
- style: TextStyle(
- color: Color(0xFF2196F3),
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ],
- ),
- ),
-
- // Chart Card
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Obx(
- () => HistoryGrafikCard(
- dataPoints: controller.chartDataPoints.toList(),
- onBookmarkTap: () {
- // Handle bookmark tap
- },
- ),
- ),
- ),
-
- const SizedBox(height: 15),
-
- // History Notifikasi Section
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- 'History Notifikasi',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- TextButton(
- onPressed: () {},
- child: const Text(
- 'See all',
- style: TextStyle(
- color: Color(0xFF2196F3),
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ],
- ),
- ),
-
- // Notifications List
- Obx(
- () => ListView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
+
+ // Monitoring Card - Gunakan MonitoringCard dari widgets
+ Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
- itemCount: controller.notificationsList.length,
- itemBuilder: (context, index) {
- final notification = controller.notificationsList[index];
- return NotificationPatientCard(
- title: notification.title,
- message: notification.message,
- room: notification.room,
- deviceId: notification.deviceId,
- timeAgo: notification.timeAgo,
+ child: Obx(() {
+ if (controller.isLoading.value) {
+ return Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Colors.grey[200]!),
+ ),
+ child: const Center(child: CircularProgressIndicator()),
+ );
+ }
+
+ final infusData = controller.currentInfusData.value;
+
+ // Konversi ke InfusMonitoring untuk MonitoringCard
+ final monitoring = InfusMonitoring(
+ id: controller.deviceId.value,
+ patientName: controller.patientName.value,
+ deviceId: infusData.deviceId,
+ room: infusData.room,
+ roomId: '', // Not needed for display
+ dropsPerMinute: infusData.dropsPerMinute,
+ lastUpdate:
+ DateTime.now(), // Will be calculated from updateTime
+ deviceStatus: infusData.deviceStatus,
+ );
+
+ return MonitoringCard(
+ monitoring: monitoring,
onTap: () {
- // Handle notification tap
+ // TODO: Navigate to detail
},
);
- },
+ }),
),
- ),
-
- const SizedBox(height: 20),
- ],
+
+ const SizedBox(height: 15),
+
+ // History Data Section
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'History Data',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ // TODO: Navigate to history detail
+ },
+ child: const Text(
+ 'See all',
+ style: TextStyle(
+ color: Color(0xFF2196F3),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Chart Card - Gunakan HistoryGrafikCard dari widgets
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Obx(() {
+ if (controller.isLoadingHistory.value) {
+ return Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey[200]!),
+ ),
+ child: const Center(child: CircularProgressIndicator()),
+ );
+ }
+
+ return HistoryGrafikCard(
+ dataPoints: controller.chartDataPoints.toList(),
+ onBookmarkTap: () {
+ // TODO: Bookmark functionality
+ },
+ );
+ }),
+ ),
+
+ const SizedBox(height: 15),
+
+ // History Notifikasi Section
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'History Notifikasi',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ // TODO: Navigate to notifications
+ },
+ child: const Text(
+ 'See all',
+ style: TextStyle(
+ color: Color(0xFF2196F3),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Notifications List
+ Obx(() {
+ if (controller.isLoadingNotifications.value) {
+ return const Padding(
+ padding: EdgeInsets.all(32),
+ child: Center(child: CircularProgressIndicator()),
+ );
+ }
+
+ if (controller.notificationsList.isEmpty) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: Colors.grey[50],
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey[200]!),
+ ),
+ child: Center(
+ child: Column(
+ children: [
+ Icon(
+ Icons.notifications_none,
+ size: 48,
+ color: Colors.grey[400],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Belum ada notifikasi',
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ return ListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: controller.notificationsList.length,
+ itemBuilder: (context, index) {
+ final notification = controller.notificationsList[index];
+ return NotificationPatientCard(
+ title: notification.title,
+ message: notification.message,
+ room: notification.room,
+ deviceId: notification.deviceId,
+ timeAgo: notification.timeAgo,
+ onTap: () {
+ // TODO: Handle notification tap
+ },
+ );
+ },
+ );
+ }),
+
+ const SizedBox(height: 20),
+ ],
+ ),
),
),
),
-
+
// Floating Action Button - Call Nurse
floatingActionButton: FloatingActionButton(
onPressed: () => controller.showCallDialog(context),
backgroundColor: const Color(0xFF2196F3),
- child: const Icon(
- Icons.phone,
- color: Colors.white,
- ),
+ child: const Icon(Icons.phone, color: Colors.white),
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart
index be3771c..5eb410f 100644
--- a/lib/app/modules/home/controllers/home_controller.dart
+++ b/lib/app/modules/home/controllers/home_controller.dart
@@ -1,64 +1,146 @@
-// lib/app/modules/home/controllers/home_controller.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 '../../../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 DateTime createdAt; // Tambahkan properti createdAt
+ 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.createdAt, // Pastikan ini diisi saat pembuatan objek
+ required this.deviceStatus,
});
}
class HomeController extends GetxController {
final searchController = TextEditingController();
+ final FirestoreService _firestoreService = FirestoreService();
+ final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
- // Banner carousel
final currentCarouselIndex = 0.obs;
final bannerImages = [
'assets/images/banner-slider1.png',
'assets/images/banner-slider2.png',
];
- final monitoringList = [
- InfusMonitoring(
- id: '1',
- patientName: 'Mrs. Shinta',
- deviceId: 'SI001',
- room: 'MAWAR 002',
- dropsPerMinute: '30 tetes per menit',
- lastUpdate: DateTime.now().subtract(const Duration(minutes: 5)),
- createdAt: DateTime.now(), // Menambahkan createdAt
- ),
- InfusMonitoring(
- id: '2',
- patientName: 'Mr. Suparjo',
- deviceId: 'SI001',
- room: 'MELATI 001',
- dropsPerMinute: '30 tetes per menit',
- lastUpdate: DateTime.now().subtract(const Duration(minutes: 5)),
- createdAt: DateTime.now(), // Menambahkan createdAt
- ),
- ].obs;
+ final monitoringList = [].obs;
+ final isLoading = true.obs;
+
+ StreamSubscription? _patientsSubscription;
+ StreamSubscription? _realtimeSubscription;
+
+ @override
+ void onInit() {
+ super.onInit();
+ _initializeMonitoring();
+ }
@override
void onClose() {
searchController.dispose();
+ _patientsSubscription?.cancel();
+ _realtimeSubscription?.cancel();
super.onClose();
}
+ void _initializeMonitoring() {
+ isLoading.value = true;
+
+ _patientsSubscription = _firestoreService.getPatientsStream().listen(
+ (patients) async {
+ _realtimeSubscription = _realtimeService.getAllDevicesStream().listen((
+ realtimeDevices,
+ ) {
+ _combineMonitoringData(patients, realtimeDevices);
+ });
+ },
+ onError: (error) {
+ isLoading.value = false;
+ AppSnackbar.error('Gagal memuat data');
+ },
+ );
+ }
+
+ void _combineMonitoringData(
+ List patients,
+ List realtimeDevices,
+ ) async {
+ final List combinedData = [];
+
+ for (var patient in patients) {
+ final realtimeDevice = realtimeDevices.firstWhereOrNull(
+ (device) => device.deviceId == patient.deviceId,
+ );
+
+ final roomName = await _getRoomName(patient.roomId);
+ final dropRate = realtimeDevice?.dropRate ?? 0.0;
+ final lastUpdate = realtimeDevice?.lastUpdate ?? DateTime.now();
+ final deviceStatus = realtimeDevice?.deviceStatus ?? 'disconnected';
+
+ 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;
+ }
+
+ Future _getRoomName(String roomId) async {
+ try {
+ final rooms = await _firestoreService.getRoomsStream().first;
+ final room = rooms.firstWhereOrNull(
+ (r) =>
+ r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
+ );
+ return room?.roomName ?? roomId;
+ } catch (e) {
+ return roomId;
+ }
+ }
+
+ 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;
}
@@ -68,33 +150,54 @@ class HomeController extends GetxController {
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),
+ style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
+ ),
+ content: const Text(
+ 'Yakin ingin keluar?',
+ style: TextStyle(fontSize: 16),
),
- content: const Text('Apakah Anda yakin ingin keluar?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
- child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
+ 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: () {
+ onPressed: () async {
Navigator.of(context).pop();
- // Navigasi ke halaman login
+ 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'),
+ child: const Text(
+ 'Logout',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
),
],
);
@@ -105,7 +208,7 @@ class HomeController extends GetxController {
String getLastUpdateText(DateTime lastUpdate) {
final difference = DateTime.now().difference(lastUpdate);
if (difference.inMinutes < 60) {
- return '${difference.inMinutes}min ago';
+ return '${difference.inMinutes}m ago';
} else if (difference.inHours < 24) {
return '${difference.inHours}h ago';
} else {
diff --git a/lib/app/modules/home/views/home_view.dart b/lib/app/modules/home/views/home_view.dart
index 8fa3fbc..5a146a7 100644
--- a/lib/app/modules/home/views/home_view.dart
+++ b/lib/app/modules/home/views/home_view.dart
@@ -1,4 +1,3 @@
-// lib/app/modules/home/views/home_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/home_controller.dart';
@@ -7,99 +6,129 @@ import '../../../widgets/monitoring_card.dart';
class HomeView extends GetView {
const HomeView({super.key});
-
+
@override
Widget build(BuildContext context) {
Get.put(HomeController());
-
+
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Header
- Padding(
- padding: const EdgeInsets.all(20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- 'Halo Perawat',
- style: TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.bold,
- ),
- ),
- InkWell(
- onTap: () => controller.showLogoutDialog(context),
- child: Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(10),
- ),
- child: const Icon(Icons.logout, color: Colors.red),
- ),
- ),
- ],
- ),
- ),
-
- // Banner Slider - Gunakan widget universal
+ _buildHeader(context),
BannerSlider(
bannerImages: controller.bannerImages,
currentCarouselIndex: controller.currentCarouselIndex,
onPageChanged: controller.updateCarouselIndex,
),
-
const SizedBox(height: 15),
-
- // Monitoring Infus Section
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- 'Monitoring Infus',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- TextButton(
- onPressed: () {},
- child: const Text(
- 'See all',
- style: TextStyle(color: Color(0xFF0091EA)),
- ),
- ),
- ],
- ),
- ),
-
- // Monitoring List
- Expanded(
- child: Obx(
- () => ListView.builder(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- itemCount: controller.monitoringList.length,
- itemBuilder: (context, index) {
- final item = controller.monitoringList[index];
- return MonitoringCard(
- item: item,
- onTap: () {
- // Handle tap jika diperlukan
- },
- );
- },
- ),
- ),
- ),
+ _buildSectionHeader(),
+ Expanded(child: _buildMonitoringList()),
],
),
),
);
}
+
+ Widget _buildHeader(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Halo Perawat',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ InkWell(
+ onTap: () => controller.showLogoutDialog(context),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey[300]!),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Icon(Icons.logout, color: Colors.red),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSectionHeader() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Monitoring Infus',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ TextButton(
+ onPressed: () => Get.toNamed('/all-monitoring'),
+ child: const Text(
+ 'Lihat Semua',
+ style: TextStyle(color: Color(0xFF0091EA)),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMonitoringList() {
+ return Obx(() {
+ if (controller.isLoading.value) {
+ return const Center(
+ child: CircularProgressIndicator(color: Color(0xFF0091EA)),
+ );
+ }
+
+ if (controller.monitoringList.isEmpty) {
+ return _buildEmptyState();
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: controller.monitoringList.length,
+ itemBuilder: (context, index) {
+ final item = controller.monitoringList[index];
+ return MonitoringCard(
+ monitoring: item,
+ onTap: () => Get.toNamed(
+ '/device-detail',
+ arguments: {
+ 'deviceId': item.deviceId,
+ 'roomId': item.roomId,
+ },
+ ),
+ );
+ },
+ );
+ });
+ }
+
+ Widget _buildEmptyState() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.medical_services_outlined,
+ size: 80,
+ color: Colors.grey[400],
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Tidak ada data monitoring',
+ style: TextStyle(fontSize: 16, color: Colors.grey[600]),
+ ),
+ ],
+ ),
+ );
+ }
}
\ No newline at end of file
diff --git a/lib/app/modules/login/controllers/login_controller.dart b/lib/app/modules/login/controllers/login_controller.dart
index 481478d..19aa2ac 100644
--- a/lib/app/modules/login/controllers/login_controller.dart
+++ b/lib/app/modules/login/controllers/login_controller.dart
@@ -1,39 +1,172 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:firebase_auth/firebase_auth.dart';
+import 'package:cloud_firestore/cloud_firestore.dart';
class LoginController extends GetxController {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final isPasswordHidden = true.obs;
+ final isLoading = false.obs;
+
+ final FirebaseAuth _auth = FirebaseAuth.instance;
+ final FirebaseFirestore _firestore = FirebaseFirestore.instance;
void togglePasswordVisibility() {
isPasswordHidden.value = !isPasswordHidden.value;
}
- void login() {
+ void login() async {
+ // Validasi input
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
Get.snackbar(
'Error',
- 'Username dan password harus diisi',
+ 'Email dan password harus diisi',
backgroundColor: Colors.red[100],
+ colorText: Colors.red[900],
+ snackPosition: SnackPosition.TOP,
);
return;
}
- Get.offAllNamed('/navbar'); // Adjust to your navigation path
+ // Validasi format email
+ if (!GetUtils.isEmail(usernameController.text)) {
+ Get.snackbar(
+ 'Error',
+ 'Format email tidak valid',
+ backgroundColor: Colors.red[100],
+ colorText: Colors.red[900],
+ snackPosition: SnackPosition.TOP,
+ );
+ return;
+ }
+
+ try {
+ isLoading.value = true;
+
+ // Login dengan Firebase Authentication
+ UserCredential userCredential = await _auth.signInWithEmailAndPassword(
+ email: usernameController.text.trim(),
+ password: passwordController.text.trim(),
+ );
+
+ if (userCredential.user != null) {
+ // Cek apakah user ada di collection users
+ final userDoc = await _firestore
+ .collection('users')
+ .doc(userCredential.user!.uid)
+ .get();
+
+ if (userDoc.exists) {
+ // User ada di collection users (Guardian/Patient)
+ final userData = userDoc.data()!;
+ final deviceId = userData['device_id'] as String?;
+ final userName = userData['name'] as String? ?? 'User';
+
+ if (deviceId != null && deviceId.isNotEmpty) {
+ // User adalah guardian/patient dengan device_id
+ Get.snackbar(
+ 'Berhasil',
+ 'Selamat datang, $userName!',
+ backgroundColor: Colors.green[100],
+ colorText: Colors.green[900],
+ snackPosition: SnackPosition.TOP,
+ );
+
+ // Navigasi ke home_patient dengan device_id
+ Get.offAllNamed(
+ '/home-patient',
+ arguments: {
+ 'deviceId': deviceId,
+ 'userName': userName,
+ 'userEmail': usernameController.text.trim(),
+ },
+ );
+ } else {
+ // User ada tapi tidak punya device_id
+ Get.snackbar(
+ 'Error',
+ 'Akun Anda belum terdaftar dengan device',
+ backgroundColor: Colors.orange[100],
+ colorText: Colors.orange[900],
+ snackPosition: SnackPosition.TOP,
+ );
+ await _auth.signOut();
+ }
+ } else {
+ // User TIDAK ada di collection users = Perawat/Admin
+ Get.snackbar(
+ 'Berhasil',
+ 'Login berhasil! Selamat datang, Perawat',
+ backgroundColor: Colors.green[100],
+ colorText: Colors.green[900],
+ snackPosition: SnackPosition.TOP,
+ );
+
+ // Navigasi ke navbar/home perawat
+ Get.offAllNamed('/navbar');
+ }
+ }
+ } on FirebaseAuthException catch (e) {
+ String errorMessage = '';
+
+ switch (e.code) {
+ case 'user-not-found':
+ errorMessage = 'Email tidak terdaftar';
+ break;
+ case 'wrong-password':
+ errorMessage = 'Password salah';
+ break;
+ case 'invalid-email':
+ errorMessage = 'Format email tidak valid';
+ break;
+ case 'user-disabled':
+ errorMessage = 'Akun telah dinonaktifkan';
+ break;
+ case 'too-many-requests':
+ errorMessage = 'Terlalu banyak percobaan login. Coba lagi nanti';
+ break;
+ case 'network-request-failed':
+ errorMessage = 'Tidak ada koneksi internet';
+ break;
+ default:
+ errorMessage = 'Terjadi kesalahan: ${e.message}';
+ }
+
+ Get.snackbar(
+ 'Login Gagal',
+ errorMessage,
+ backgroundColor: Colors.red[100],
+ colorText: Colors.red[900],
+ snackPosition: SnackPosition.TOP,
+ duration: const Duration(seconds: 3),
+ );
+ } catch (e) {
+ Get.snackbar(
+ 'Error',
+ 'Terjadi kesalahan: $e',
+ backgroundColor: Colors.red[100],
+ colorText: Colors.red[900],
+ snackPosition: SnackPosition.TOP,
+ );
+ } finally {
+ isLoading.value = false;
+ }
}
void contactNurse() async {
- const phoneNumber = '+62 812-3190-1277';
- final url = 'https://wa.me/$phoneNumber'; // WhatsApp link for contacting
- if (await canLaunch(url)) {
- await launch(url);
+ const phoneNumber = '+6281231901277'; // Tanpa spasi
+ final url = Uri.parse('https://wa.me/$phoneNumber');
+
+ if (await canLaunchUrl(url)) {
+ await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
Get.snackbar(
'Error',
'Tidak dapat membuka WhatsApp',
backgroundColor: Colors.red[100],
+ colorText: Colors.red[900],
);
}
}
diff --git a/lib/app/modules/login/views/login_view.dart b/lib/app/modules/login/views/login_view.dart
index 08e5d53..d8e6500 100644
--- a/lib/app/modules/login/views/login_view.dart
+++ b/lib/app/modules/login/views/login_view.dart
@@ -14,118 +14,126 @@ class LoginView extends GetView {
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 20),
- const Text(
- 'Login',
- style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 50),
- Center(
- child: Image.asset(
- 'assets/images/banner-logo.png',
- width: 300,
- height: 130,
- fit: BoxFit.contain,
+ child: Center(
+ // Wrap the entire content inside Center widget to center vertically
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 20),
+ const Text(
+ 'Login',
+ style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
- ),
- const SizedBox(height: 50),
- Container(
- decoration: BoxDecoration(
- color: Colors.grey[100],
- borderRadius: BorderRadius.circular(15),
- ),
- child: TextField(
- controller: controller.usernameController,
- decoration: InputDecoration(
- hintText: 'Masukkan username',
- hintStyle: TextStyle(color: Colors.grey[400]),
- prefixIcon: Icon(
- Icons.mail_outline,
- color: Colors.grey[400],
- ),
- border: InputBorder.none,
- contentPadding: const EdgeInsets.all(20),
- ),
- ),
- ),
- const SizedBox(height: 20),
- Obx(
- () => Container(
+ const SizedBox(height: 50),
+ Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(15),
),
child: TextField(
- controller: controller.passwordController,
- obscureText: controller.isPasswordHidden.value,
+ controller: controller.usernameController,
+ keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
- hintText: 'Masukkan password',
+ hintText: 'Masukkan email',
hintStyle: TextStyle(color: Colors.grey[400]),
prefixIcon: Icon(
- Icons.lock_outline,
+ Icons.mail_outline,
color: Colors.grey[400],
),
- suffixIcon: IconButton(
- icon: Icon(
- controller.isPasswordHidden.value
- ? Icons.visibility_off_outlined
- : Icons.visibility_outlined,
- color: Colors.grey[400],
- ),
- onPressed: controller.togglePasswordVisibility,
- ),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(20),
),
),
),
- ),
- const SizedBox(height: 30),
- SizedBox(
- width: double.infinity,
- height: 55,
- child: ElevatedButton(
- onPressed: controller.login,
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xFF1976D2),
- shape: RoundedRectangleBorder(
+ const SizedBox(height: 20),
+ Obx(
+ () => Container(
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
borderRadius: BorderRadius.circular(15),
),
- ),
- child: const Text(
- 'Login',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- color: Colors.white,
- ),
- ),
- ),
- ),
- const SizedBox(height: 20),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text(
- "Tidak memiliki akun? ",
- style: TextStyle(color: Colors.black87),
- ),
- TextButton(
- onPressed: controller.contactNurse,
- child: const Text(
- 'Hubungi Perawat',
- style: TextStyle(
- color: Color(0xFF1976D2),
- fontWeight: FontWeight.w600,
+ child: TextField(
+ controller: controller.passwordController,
+ obscureText: controller.isPasswordHidden.value,
+ decoration: InputDecoration(
+ hintText: 'Masukkan password',
+ hintStyle: TextStyle(color: Colors.grey[400]),
+ prefixIcon: Icon(
+ Icons.lock_outline,
+ color: Colors.grey[400],
+ ),
+ suffixIcon: IconButton(
+ icon: Icon(
+ controller.isPasswordHidden.value
+ ? Icons.visibility_off_outlined
+ : Icons.visibility_outlined,
+ color: Colors.grey[400],
+ ),
+ onPressed: controller.togglePasswordVisibility,
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.all(20),
),
),
),
- ],
- ),
- ],
+ ),
+ const SizedBox(height: 30),
+ Obx(
+ () => SizedBox(
+ width: double.infinity,
+ height: 55,
+ child: ElevatedButton(
+ onPressed: controller.isLoading.value
+ ? null
+ : controller.login,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF1976D2),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15),
+ ),
+ ),
+ child: controller.isLoading.value
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ ),
+ )
+ : const Text(
+ 'Login',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text(
+ "Tidak memiliki akun? ",
+ style: TextStyle(color: Colors.black87),
+ ),
+ TextButton(
+ onPressed: controller.contactNurse,
+ child: const Text(
+ 'Hubungi Perawat',
+ style: TextStyle(
+ color: Color(0xFF1976D2),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
),
),
),
diff --git a/lib/app/modules/notification/controllers/notification_controller.dart b/lib/app/modules/notification/controllers/notification_controller.dart
index d8bddba..0663046 100644
--- a/lib/app/modules/notification/controllers/notification_controller.dart
+++ b/lib/app/modules/notification/controllers/notification_controller.dart
@@ -1,5 +1,11 @@
-// lib\app\modules\notification\controllers\notification_controller.dart
+// lib/app/modules/notification/controllers/notification_controller.dart
import 'package:get/get.dart';
+import 'package:flutter/material.dart';
+import 'dart:async';
+import '../../../services/firestore_service.dart';
+import '../../../models/notification_model.dart';
+import '../../../models/patient_model.dart';
+import '../../../models/room_model.dart';
class NotificationItem {
final String id;
@@ -46,50 +52,72 @@ class NotificationItem {
}
class NotificationController extends GetxController {
- final selectedTab = 0.obs;
+ final FirestoreService _firestoreService = FirestoreService();
- final _allNotifications = [
- NotificationItem(
- id: '1',
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
- room: 'MAWAR 002',
- deviceId: 'SI001',
- patientName: 'Mrs Shinta',
- time: DateTime.now().subtract(const Duration(minutes: 5)),
- isRead: false,
- ),
- NotificationItem(
- id: '2',
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
- room: 'MAWAR 003',
- deviceId: 'SI002',
- patientName: 'Mr Budi',
- time: DateTime.now().subtract(const Duration(hours: 2)),
- isRead: false,
- ),
- NotificationItem(
- id: '3',
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
- room: 'MAWAR 004',
- deviceId: 'SI003',
- patientName: 'Mrs Ani',
- time: DateTime.now().subtract(const Duration(hours: 5)),
- isRead: true,
- ),
- NotificationItem(
- id: '4',
- title: 'Peringatan!!',
- message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
- room: 'MAWAR 005',
- deviceId: 'SI004',
- patientName: 'Mr Doni',
- time: DateTime.now().subtract(const Duration(days: 1)),
- isRead: true,
- ),
- ].obs;
+ final selectedTab = 0.obs;
+ final _allNotifications = [].obs;
+ final isLoading = true.obs;
+
+ StreamSubscription? _notificationsSubscription;
+
+ @override
+ void onInit() {
+ super.onInit();
+ _initializeNotifications();
+ }
+
+ @override
+ void onClose() {
+ _notificationsSubscription?.cancel();
+ super.onClose();
+ }
+
+ /// Initialize notifications dari Firestore
+ void _initializeNotifications() {
+ isLoading.value = true;
+
+ _notificationsSubscription = _firestoreService
+ .getNotificationsStream()
+ .listen((notificationModels) async {
+ final List items = [];
+
+ for (var notifModel in notificationModels) {
+ // Get patient info
+ final patient = await _firestoreService.getPatientById(
+ notifModel.patientId,
+ );
+
+ // Get room name from patient's roomId
+ String roomName = 'Unknown Room';
+ if (patient != null && patient.roomId.isNotEmpty) {
+ final rooms = await _firestoreService.getRoomsStream().first;
+ final room = rooms.firstWhereOrNull(
+ (r) => r.roomName.toLowerCase() == patient.roomId.toLowerCase(),
+ );
+ roomName = room?.roomName ?? patient.roomId;
+ }
+
+ items.add(
+ NotificationItem(
+ id: notifModel.id,
+ title: notifModel.title,
+ message: notifModel.message,
+ room: roomName,
+ deviceId: notifModel.deviceId,
+ patientName: patient?.namePatient ?? 'Unknown Patient',
+ time: notifModel.createdAt,
+ isRead: notifModel.isRead,
+ ),
+ );
+ }
+
+ _allNotifications.value = items;
+ isLoading.value = false;
+ }, onError: (error) {
+ print('Error listening to notifications: $error');
+ isLoading.value = false;
+ });
+ }
// Computed property untuk notifikasi yang difilter
List get notifications {
@@ -106,15 +134,35 @@ class NotificationController extends GetxController {
selectedTab.value = index;
}
- void deleteNotification(String id) {
- _allNotifications.removeWhere((notif) => notif.id == id);
+ Future deleteNotification(String id) async {
+ try {
+ await _firestoreService.deleteNotification(id);
+ // Local update akan dilakukan otomatis oleh stream
+ } catch (e) {
+ Get.snackbar(
+ 'Error',
+ 'Gagal menghapus notifikasi: $e',
+ snackPosition: SnackPosition.BOTTOM,
+ backgroundColor: Colors.redAccent,
+ colorText: Colors.white,
+ margin: const EdgeInsets.all(16),
+ );
+ }
}
- void markAsRead(String id) {
- final index = _allNotifications.indexWhere((notif) => notif.id == id);
- if (index != -1) {
- _allNotifications[index] = _allNotifications[index].copyWith(isRead: true);
- _allNotifications.refresh();
+ Future markAsRead(String id) async {
+ try {
+ await _firestoreService.markNotificationAsRead(id);
+ // Local update akan dilakukan otomatis oleh stream
+ } catch (e) {
+ Get.snackbar(
+ 'Error',
+ 'Gagal menandai notifikasi: $e',
+ snackPosition: SnackPosition.BOTTOM,
+ backgroundColor: Colors.redAccent,
+ colorText: Colors.white,
+ margin: const EdgeInsets.all(16),
+ );
}
}
@@ -133,4 +181,28 @@ class NotificationController extends GetxController {
int get unreadCount {
return _allNotifications.where((notif) => !notif.isRead).length;
}
+
+ /// Method untuk menambahkan notifikasi baru (bisa dipanggil dari monitoring)
+ Future addNotification({
+ required String patientId,
+ required String deviceId,
+ required String title,
+ required String message,
+ }) async {
+ try {
+ final notification = NotificationModel(
+ id: '', // Will be auto-generated by Firestore
+ patientId: patientId,
+ deviceId: deviceId,
+ title: title,
+ message: message,
+ isRead: false,
+ createdAt: DateTime.now(),
+ );
+
+ await _firestoreService.addNotification(notification);
+ } catch (e) {
+ print('Error adding notification: $e');
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart
index 7438e2d..5df3f21 100644
--- a/lib/app/modules/profile/controllers/profile_controller.dart
+++ b/lib/app/modules/profile/controllers/profile_controller.dart
@@ -1,12 +1,21 @@
-// lib/app/modules/profile/controllers/profile_controller.dart
import 'package:get/get.dart';
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:firebase_database/firebase_database.dart';
+import '../../../services/firestore_service.dart';
+import '../../../services/realtime_database_service.dart';
+import '../../../models/patient_model.dart';
+import '../../../models/device_model.dart';
+import '../../../models/room_model.dart';
+import '../../../widgets/app_snackbar.dart';
class Patient {
final String id;
final String name;
final String room;
final String deviceId;
- final String guardian; // Wali pasien
+ final String guardian;
+ final String email;
+ final String? userId;
final String status;
Patient({
@@ -15,40 +24,372 @@ class Patient {
required this.room,
required this.deviceId,
required this.guardian,
+ required this.email,
+ this.userId,
this.status = 'Aktif',
});
+
+ Map toFirestore() {
+ return {
+ 'name_patient': name,
+ 'name_guardian': guardian,
+ 'device_id': deviceId,
+ 'room_id': room,
+ 'user_id': userId,
+ };
+ }
+
+ factory Patient.fromFirestore(DocumentSnapshot doc) {
+ final data = doc.data() as Map;
+ return Patient(
+ id: doc.id,
+ name: data['name_patient'] ?? '',
+ guardian: data['name_guardian'] ?? '',
+ deviceId: data['device_id'] ?? '',
+ room: data['room_id'] ?? '',
+ email: '',
+ userId: data['user_id'],
+ );
+ }
}
class ProfileController extends GetxController {
- final patients = [
- Patient(
- id: '1',
- name: 'Mrs. Shinta',
- room: 'Ruang Mawar 002',
- deviceId: 'SI001',
- guardian: 'Budi Santoso',
- ),
- Patient(
- id: '2',
- name: 'Mr. Parjo',
- room: 'Ruang Melati 001',
- deviceId: 'SI002',
- guardian: 'Siti Nurhaliza',
- ),
- ].obs;
+ final FirestoreService _firestoreService = FirestoreService();
+ final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
- void addPatient(Patient patient) {
- patients.add(patient);
+ final patients = [].obs;
+ final isLoading = false.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ _listenToPatients();
}
- void updatePatient(String id, Patient updatedPatient) {
- final index = patients.indexWhere((p) => p.id == id);
- if (index != -1) {
- patients[index] = updatedPatient;
+ void refreshPatients() {
+ _listenToPatients();
+ }
+
+ void _listenToPatients() {
+ _firestoreService.getPatientsStream().listen((patientModels) async {
+ final patientsWithEmail = [];
+
+ for (var model in patientModels) {
+ try {
+ final email = await _getGuardianEmail(model.nameGuardian);
+ final roomName = await _getRoomName(model.roomId);
+
+ patientsWithEmail.add(
+ Patient(
+ id: model.id,
+ name: model.namePatient,
+ guardian: model.nameGuardian,
+ deviceId: model.deviceId,
+ room: roomName,
+ email: email,
+ userId: null,
+ ),
+ );
+ } catch (e) {
+ continue;
+ }
+ }
+
+ patients.value = patientsWithEmail;
+ }, onError: (error) => AppSnackbar.error('Gagal memuat data'));
+ }
+
+ Future _getGuardianEmail(String guardianName) async {
+ try {
+ final userDoc = await FirebaseFirestore.instance
+ .collection('users')
+ .where('role', isEqualTo: 'guardian')
+ .where('name', isEqualTo: guardianName)
+ .limit(1)
+ .get();
+
+ return userDoc.docs.isNotEmpty
+ ? userDoc.docs.first.data()['email'] ?? ''
+ : '';
+ } catch (e) {
+ return '';
}
}
- void deletePatient(String id) {
- patients.removeWhere((patient) => patient.id == id);
+ Future _getRoomName(String roomId) async {
+ try {
+ final roomDoc = await FirebaseFirestore.instance
+ .collection('rooms')
+ .doc(roomId)
+ .get();
+ return roomDoc.data()?['room_name'] ?? roomId;
+ } catch (e) {
+ return roomId;
+ }
}
-}
\ No newline at end of file
+
+ Future addPatient({
+ required String patientName,
+ required String guardianName,
+ required String roomName,
+ required String deviceId,
+ required String email,
+ required String password,
+ }) async {
+ try {
+ isLoading.value = true;
+
+ if (!await _validateNewPatient(email, deviceId)) return;
+
+ final roomId = _generateRoomId(roomName);
+
+ // PERBAIKAN: Tambahkan device_id saat create user
+ final userId = await _firestoreService.createUser(
+ email: email,
+ password: password,
+ name: guardianName,
+ role: 'guardian',
+ deviceId: deviceId, // ✅ TAMBAHKAN INI
+ );
+
+ if (userId == null) throw Exception('Gagal membuat user');
+
+ await _ensureRoomExists(roomId, roomName);
+ await _createDevice(deviceId, roomId);
+ await _firestoreService.createPatient(
+ PatientModel(
+ id: '',
+ namePatient: patientName,
+ nameGuardian: guardianName,
+ deviceId: deviceId,
+ roomId: roomId,
+ ),
+ );
+
+ await _realtimeService.updateDeviceStatus(roomId, deviceId, 'connected');
+ await _createDeviceInRealtimeDB(roomId, deviceId);
+
+ Get.back();
+ AppSnackbar.success('Pasien berhasil ditambahkan');
+ } catch (e) {
+ AppSnackbar.error('Gagal menambahkan pasien: $e');
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ Future _validateNewPatient(String email, String deviceId) async {
+ if (await _firestoreService.userExists(email)) {
+ AppSnackbar.warning('Email sudah terdaftar');
+ return false;
+ }
+
+ if (await _firestoreService.deviceExists(deviceId)) {
+ AppSnackbar.warning('Device ID sudah terdaftar');
+ return false;
+ }
+
+ if (await _firestoreService.patientExistsByDevice(deviceId)) {
+ AppSnackbar.warning('Device sudah digunakan');
+ return false;
+ }
+
+ return true;
+ }
+
+ Future updatePatient({
+ required String patientId,
+ required String patientName,
+ required String guardianName,
+ required String roomName,
+ required String deviceId,
+ required String email,
+ String? newPassword,
+ }) async {
+ try {
+ isLoading.value = true;
+
+ final oldPatient = await _firestoreService.getPatientById(patientId);
+ if (oldPatient == null) throw Exception('Pasien tidak ditemukan');
+
+ final roomId = _generateRoomId(roomName);
+
+ if (deviceId != oldPatient.deviceId) {
+ if (await _firestoreService.deviceExists(deviceId)) {
+ AppSnackbar.warning('Device ID sudah terdaftar');
+ return;
+ }
+ }
+
+ await _firestoreService.updatePatient(
+ patientId,
+ PatientModel(
+ id: patientId,
+ namePatient: patientName,
+ nameGuardian: guardianName,
+ deviceId: deviceId,
+ roomId: roomId,
+ ),
+ );
+
+ if (deviceId != oldPatient.deviceId || roomId != oldPatient.roomId) {
+ await _updateDeviceAndRoom(oldPatient, deviceId, roomId);
+ }
+
+ await _ensureRoomExists(roomId, roomName);
+
+ // PERBAIKAN: Update device_id di users collection jika device berubah
+ if (deviceId != oldPatient.deviceId) {
+ final userDoc = await _firestoreService.getUserByEmail(email);
+ if (userDoc != null) {
+ await _firestoreService.updateUser(
+ uid: userDoc.id,
+ name: guardianName,
+ email: email,
+ deviceId: deviceId, // ✅ UPDATE device_id
+ );
+ }
+ }
+
+ Get.back();
+ AppSnackbar.success('Data berhasil diperbarui');
+ } catch (e) {
+ AppSnackbar.error('Gagal memperbarui data: $e');
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ Future _updateDeviceAndRoom(
+ PatientModel oldPatient,
+ String newDeviceId,
+ String newRoomId,
+ ) async {
+ await _firestoreService.deleteDevice(oldPatient.deviceId);
+ await _deleteDeviceFromRealtimeDB(oldPatient.roomId, oldPatient.deviceId);
+ await _createDevice(newDeviceId, newRoomId);
+ await _createDeviceInRealtimeDB(newRoomId, newDeviceId);
+ }
+
+ // PERBAIKAN LENGKAP: Hapus dari semua collection + Authentication
+ Future deletePatient(String patientId, {String? password}) async {
+ try {
+ isLoading.value = true;
+
+ final patient = await _firestoreService.getPatientById(patientId);
+ if (patient == null) throw Exception('Pasien tidak ditemukan');
+
+ // 1. Hapus dari Realtime Database
+ await _deleteDeviceFromRealtimeDB(patient.roomId, patient.deviceId);
+
+ // 2. Hapus histories berdasarkan device_id
+ await _firestoreService.deleteHistoriesByDevice(patient.deviceId);
+
+ // 3. Hapus notifications berdasarkan device_id
+ await _firestoreService.deleteNotificationsByDevice(patient.deviceId);
+
+ // 4. Hapus schedules berdasarkan device_id
+ await _firestoreService.deleteSchedulesByDevice(patient.deviceId);
+
+ // 5. Hapus device dari Firestore
+ await _firestoreService.deleteDevice(patient.deviceId);
+
+ // 6. Cari user berdasarkan guardian name dan device_id
+ final userDoc = await FirebaseFirestore.instance
+ .collection('users')
+ .where('role', isEqualTo: 'guardian')
+ .where('name', isEqualTo: patient.nameGuardian)
+ .where('device_id', isEqualTo: patient.deviceId)
+ .limit(1)
+ .get();
+
+ if (userDoc.docs.isNotEmpty) {
+ final userId = userDoc.docs.first.id;
+ final userEmail = userDoc.docs.first.data()['email'] as String;
+
+ // 7. Hapus dari collection users
+ await _firestoreService.deleteUser(userId);
+
+ // 8. Hapus dari Firebase Authentication
+ // CATATAN: Memerlukan password untuk delete dari Authentication
+ if (password != null && password.isNotEmpty) {
+ await _firestoreService.deleteUserFromAuth(userEmail, password);
+ } else {
+ // Jika tidak ada password, tampilkan warning
+ print(
+ '⚠️ User tidak terhapus dari Authentication karena password tidak tersedia',
+ );
+ AppSnackbar.warning(
+ 'User terhapus dari database, tapi tidak dari Authentication. Hubungi admin untuk menghapus akun sepenuhnya.',
+ );
+ }
+ }
+
+ // 9. Hapus patient dari Firestore
+ await _firestoreService.deletePatient(patientId);
+
+ AppSnackbar.success('Pasien berhasil dihapus');
+ } catch (e) {
+ AppSnackbar.error('Gagal menghapus pasien: $e');
+ print('❌ Delete patient error: $e');
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ Future _ensureRoomExists(String roomId, String roomName) async {
+ if (!await _firestoreService.roomExists(roomId)) {
+ await _firestoreService.createRoom(
+ RoomModel(
+ id: roomId,
+ roomName: roomName,
+ capacity: 10,
+ createdAt: DateTime.now(),
+ ),
+ );
+ }
+ }
+
+ Future _createDevice(String deviceId, String roomId) async {
+ await _firestoreService.createDevice(
+ DeviceModel(
+ idDevices: deviceId,
+ roomId: roomId,
+ devicesName: 'Infusion Device $deviceId',
+ ),
+ );
+ }
+
+ Future _createDeviceInRealtimeDB(String roomId, String deviceId) async {
+ final devicePath = 'realtime/rooms/$roomId/devices/$deviceId';
+ await FirebaseDatabase.instance.ref().child(devicePath).set({
+ 'controlling': {
+ 'servo_open': false,
+ 'last_command': DateTime.now().toIso8601String(),
+ },
+ 'monitoring': {
+ 'device_status': 'connected',
+ 'drop_rate': 0,
+ 'last_update': DateTime.now().toIso8601String(),
+ },
+ 'settings': {'servo_angle_open': 90, 'servo_angle_close': 0},
+ });
+ }
+
+ Future _deleteDeviceFromRealtimeDB(
+ String roomId,
+ String deviceId,
+ ) async {
+ final devicePath = 'realtime/rooms/$roomId/devices/$deviceId';
+ await FirebaseDatabase.instance.ref().child(devicePath).remove();
+ }
+
+ String _generateRoomId(String roomName) {
+ return roomName
+ .trim()
+ .toLowerCase()
+ .replaceAll(RegExp(r'[^a-z0-9]+'), '_')
+ .replaceAll(RegExp(r'_+'), '_')
+ .replaceAll(RegExp(r'^_|_$'), '');
+ }
+}
diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart
index 4ddb129..def83ce 100644
--- a/lib/app/modules/profile/views/profile_view.dart
+++ b/lib/app/modules/profile/views/profile_view.dart
@@ -1,4 +1,3 @@
-// lib/app/modules/profile/views/profile_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
@@ -18,7 +17,6 @@ class ProfileView extends GetView {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Header Section
Container(
color: Colors.white,
padding: const EdgeInsets.all(20),
@@ -36,10 +34,7 @@ class ProfileView extends GetView {
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
- colors: [
- Color(0xFF0091EA),
- Color(0xFF0277BD),
- ],
+ colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
@@ -50,73 +45,121 @@ class ProfileView extends GetView {
),
],
),
- child: ElevatedButton(
- onPressed: () {
- Get.dialog(
- const ModalPatientAddEdit(isEdit: false),
- barrierDismissible: true,
- );
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.transparent,
- shadowColor: Colors.transparent,
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 12,
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- child: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.add, color: Colors.white, size: 20),
- SizedBox(width: 8),
- Text(
- 'Tambah',
- style: TextStyle(
- color: Colors.white,
- fontWeight: FontWeight.w600,
- fontSize: 15,
- ),
+ child: Obx(
+ () => ElevatedButton(
+ onPressed: controller.isLoading.value
+ ? null
+ : () {
+ Get.dialog(
+ const ModalPatientAddEdit(isEdit: false),
+ barrierDismissible: true,
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 12,
),
- ],
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: controller.isLoading.value
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ ),
+ )
+ : const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.add,
+ color: Colors.white,
+ size: 20,
+ ),
+ SizedBox(width: 8),
+ Text(
+ 'Tambah',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ fontSize: 15,
+ ),
+ ),
+ ],
+ ),
),
),
),
],
),
),
-
- // Patient List
Expanded(
- child: Obx(
- () {
- if (controller.patients.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.people_outline,
- size: 80,
- color: Colors.grey[400],
- ),
- const SizedBox(height: 16),
- Text(
- 'Belum ada data pasien',
- style: TextStyle(
- fontSize: 16,
- color: Colors.grey[600],
- ),
- ),
- ],
- ),
- );
- }
+ child: Obx(() {
+ if (controller.isLoading.value && controller.patients.isEmpty) {
+ return const Center(
+ child: CircularProgressIndicator(color: Color(0xFF0091EA)),
+ );
+ }
- return ListView.builder(
+ if (controller.patients.isEmpty) {
+ return RefreshIndicator(
+ onRefresh: () async {
+ controller.refreshPatients();
+ await Future.delayed(const Duration(milliseconds: 500));
+ },
+ color: const Color(0xFF0091EA),
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: SizedBox(
+ height: MediaQuery.of(context).size.height * 0.6,
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.people_outline,
+ size: 80,
+ color: Colors.grey[400],
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Belum ada data pasien',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey[600],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Tarik ke bawah untuk refresh',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.grey[400],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ return RefreshIndicator(
+ onRefresh: () async {
+ controller.refreshPatients();
+ await Future.delayed(const Duration(milliseconds: 500));
+ },
+ color: const Color(0xFF0091EA),
+ child: ListView.builder(
+ physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
@@ -128,51 +171,64 @@ class ProfileView extends GetView {
patient: patient,
onEditTap: () {
Get.dialog(
- ModalPatientAddEdit(
- patient: patient,
- isEdit: true,
- ),
+ ModalPatientAddEdit(patient: patient, isEdit: true),
barrierDismissible: true,
);
},
onDeleteTap: () {
Get.dialog(
AlertDialog(
+ backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
- title: const Text('Hapus Pasien'),
+ title: const Text(
+ 'Hapus Pasien',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ ),
+ ),
content: Text(
'Apakah Anda yakin ingin menghapus ${patient.name}?',
+ style: TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () => Get.back(),
- child: Text(
- 'Batal',
- style: TextStyle(color: Colors.grey[700]),
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.grey[700],
+ padding: const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 20,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
),
+ child: const Text('Batal'),
),
TextButton(
onPressed: () {
controller.deletePatient(patient.id);
Get.back();
- Get.snackbar(
- 'Berhasil',
- 'Pasien berhasil dihapus',
- snackPosition: SnackPosition.BOTTOM,
- backgroundColor: Colors.redAccent,
- colorText: Colors.white,
- margin: const EdgeInsets.all(16),
- icon: const Icon(
- Icons.delete,
- color: Colors.white,
- ),
- );
},
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.white,
+ backgroundColor: Colors.red,
+ padding: const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 20,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
child: const Text(
'Hapus',
- style: TextStyle(color: Colors.red),
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
),
),
],
@@ -181,13 +237,13 @@ class ProfileView extends GetView {
},
);
},
- );
- },
- ),
+ ),
+ );
+ }),
),
],
),
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/app/modules/schedule/controllers/schedule_controller.dart b/lib/app/modules/schedule/controllers/schedule_controller.dart
index bebf9da..3b2e979 100644
--- a/lib/app/modules/schedule/controllers/schedule_controller.dart
+++ b/lib/app/modules/schedule/controllers/schedule_controller.dart
@@ -1,75 +1,219 @@
-// lib/app/modules/schedule/controllers/schedule_controller.dart
import 'package:get/get.dart';
+import 'dart:async';
+import '../../../services/firestore_service.dart';
+import '../../../models/schedule_model.dart';
+import '../../../widgets/app_snackbar.dart';
class PatientSchedule {
final String id;
final String name;
final String room;
final String deviceId;
- final String medicineType;
- final String fluidType;
final String medicineTime;
final String fluidTime;
+ final String timeOfDay;
PatientSchedule({
required this.id,
required this.name,
required this.room,
required this.deviceId,
- required this.medicineType,
- required this.fluidType,
required this.medicineTime,
required this.fluidTime,
+ required this.timeOfDay,
});
}
class ScheduleController extends GetxController {
+ final FirestoreService _firestoreService = FirestoreService();
+
final selectedTab = 0.obs;
final selectedDate = DateTime.now().obs;
+ final allSchedules = [].obs;
+ final filteredSchedules = [].obs;
+ final isLoading = false.obs;
- final schedules = [
- PatientSchedule(
- id: '1',
- name: 'Mrs. Shinta',
- room: 'Ruang Mawar 002',
- deviceId: 'SI001',
- medicineType: 'Obat & Dosis',
- fluidType: 'Cairan & Tetes',
- medicineTime: 'Amoxicillin 3 ml',
- fluidTime: 'Mnt 30 tetes /jam',
- ),
- PatientSchedule(
- id: '2',
- name: 'Mr. Parjo',
- room: 'Ruang Melati 001',
- deviceId: 'SI001',
- medicineType: 'Obat & Dosis',
- fluidType: 'Cairan & Tetes',
- medicineTime: 'Amoxicillin 3 ml',
- fluidTime: 'Mnt 30 tetes /jam',
- ),
- ].obs;
+ StreamSubscription? _schedulesSubscription;
- void changeTab(int index) {
+ final Map _tabToTimeOfDay = {
+ 0: 'Pagi',
+ 1: 'Siang',
+ 2: 'Sore',
+ 3: 'Malam',
+ };
+
+ @override
+ void onInit() {
+ super.onInit();
+ _loadSchedules();
+ }
+
+ @override
+ void onClose() {
+ _schedulesSubscription?.cancel();
+ super.onClose();
+ }
+
+ void _loadSchedules() {
+ isLoading.value = true;
+ _schedulesSubscription?.cancel();
+
+ _schedulesSubscription = _firestoreService
+ .getSchedulesStream(date: selectedDate.value)
+ .listen(
+ (scheduleModels) async {
+ try {
+ final List items = [];
+
+ for (var scheduleModel in scheduleModels) {
+ try {
+ final patient = await _firestoreService.getPatientById(scheduleModel.patientId);
+
+ if (patient != null) {
+ final roomName = await _getRoomName(patient.roomId);
+ items.add(
+ PatientSchedule(
+ id: scheduleModel.id,
+ name: patient.namePatient,
+ room: roomName,
+ deviceId: patient.deviceId,
+ medicineTime: scheduleModel.medicineDetail,
+ fluidTime: scheduleModel.fluidDetail,
+ timeOfDay: scheduleModel.timeOfDay,
+ ),
+ );
+ }
+ } catch (e) {
+ continue;
+ }
+ }
+
+ allSchedules.value = items;
+ _applyTimeFilter();
+ isLoading.value = false;
+ } catch (e) {
+ isLoading.value = false;
+ }
+ },
+ onError: (error) {
+ isLoading.value = false;
+ AppSnackbar.error('Gagal memuat jadwal');
+ },
+ );
+ }
+
+ void _applyTimeFilter() {
+ final selectedTime = _tabToTimeOfDay[selectedTab.value];
+ filteredSchedules.value = allSchedules
+ .where((schedule) => schedule.timeOfDay == selectedTime)
+ .toList();
+ }
+
+ Future _getRoomName(String roomId) async {
+ try {
+ final rooms = await _firestoreService.getRoomsStream().first;
+ final room = rooms.firstWhereOrNull(
+ (r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
+ );
+ return room?.roomName ?? roomId;
+ } catch (e) {
+ return roomId;
+ }
+ }
+
+ void changeTab(int index) {
selectedTab.value = index;
+ _applyTimeFilter();
}
void selectDate(DateTime date) {
selectedDate.value = date;
+ _loadSchedules();
}
- void addSchedule(PatientSchedule schedule) {
- schedules.add(schedule);
+ void refreshSchedules() {
+ _loadSchedules();
}
- void updateSchedule(String id, PatientSchedule updatedSchedule) {
- final index = schedules.indexWhere((s) => s.id == id);
- if (index != -1) {
- schedules[index] = updatedSchedule;
+ Future addSchedule(PatientSchedule schedule, DateTime scheduleDate) async {
+ try {
+ final patients = await _firestoreService.getPatientsStream().first;
+ final patient = patients.firstWhereOrNull((p) => p.id == schedule.id);
+
+ if (patient == null) throw Exception('Pasien tidak ditemukan');
+
+ final scheduleModel = ScheduleModel(
+ id: '',
+ patientId: patient.id,
+ medicineDetail: schedule.medicineTime,
+ fluidDetail: schedule.fluidTime,
+ scheduleDate: scheduleDate,
+ timeOfDay: schedule.timeOfDay,
+ createdAt: DateTime.now(),
+ );
+
+ await _firestoreService.addSchedule(scheduleModel);
+ AppSnackbar.success('Jadwal berhasil ditambahkan');
+ } catch (e) {
+ AppSnackbar.error('Gagal menambahkan jadwal');
}
}
- void deleteSchedule(String id) {
- schedules.removeWhere((s) => s.id == id);
+ Future updateSchedule(String id, PatientSchedule updatedSchedule, DateTime scheduleDate) async {
+ try {
+ final patients = await _firestoreService.getPatientsStream().first;
+ final patient = patients.firstWhereOrNull((p) => p.id == updatedSchedule.id);
+
+ if (patient == null) throw Exception('Pasien tidak ditemukan');
+
+ final scheduleModel = ScheduleModel(
+ id: id,
+ patientId: patient.id,
+ medicineDetail: updatedSchedule.medicineTime,
+ fluidDetail: updatedSchedule.fluidTime,
+ scheduleDate: scheduleDate,
+ timeOfDay: updatedSchedule.timeOfDay,
+ createdAt: DateTime.now(),
+ );
+
+ await _firestoreService.updateSchedule(id, scheduleModel);
+ AppSnackbar.success('Jadwal berhasil diperbarui');
+ } catch (e) {
+ AppSnackbar.error('Gagal memperbarui jadwal');
+ }
+ }
+
+ Future deleteSchedule(String id) async {
+ try {
+ await _firestoreService.deleteSchedule(id);
+ AppSnackbar.success('Jadwal berhasil dihapus');
+ } catch (e) {
+ AppSnackbar.error('Gagal menghapus jadwal');
+ }
+ }
+
+ Future>> getPatientsList() async {
+ try {
+ final patients = await _firestoreService.getPatientsStream().first;
+ final List