fitur pasien home
This commit is contained in:
parent
b87e9b50df
commit
314bcc3b59
|
|
@ -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'
|
||||||
|
|
@ -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 = "../.."
|
|
||||||
}
|
|
||||||
|
|
@ -41,5 +41,15 @@
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Tambahan untuk WhatsApp -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
</intent>
|
||||||
|
<package android:name="com.whatsapp" />
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Delete>("clean") {
|
|
||||||
delete(rootProject.layout.buildDirectory)
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -20,7 +20,7 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.9.1" apply false
|
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")
|
include(":app")
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/app/models/chart_data_point.dart
|
|
||||||
class ChartDataPoint {
|
class ChartDataPoint {
|
||||||
final int hour;
|
final int hour;
|
||||||
final double dropsPerMinute;
|
final double dropsPerMinute;
|
||||||
|
|
@ -7,4 +6,9 @@ class ChartDataPoint {
|
||||||
required this.hour,
|
required this.hour,
|
||||||
required this.dropsPerMinute,
|
required this.dropsPerMinute,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ChartDataPoint(hour: $hour, dropsPerMinute: $dropsPerMinute)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
return DeviceModel(
|
||||||
|
idDevices: doc.id,
|
||||||
|
roomId: data['room_id'] ?? '',
|
||||||
|
devicesName: data['devices_name'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To Firestore
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'room_id': roomId,
|
||||||
|
'devices_name': devicesName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
return HistoryModel(
|
||||||
|
id: doc.id,
|
||||||
|
deviceId: data['device_id'] ?? '',
|
||||||
|
dropPerMinute: (data['drop_per_menit'] ?? 0).toDouble(),
|
||||||
|
timestamp: (data['timestamp'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'device_id': deviceId,
|
||||||
|
'drop_per_menit': dropPerMinute,
|
||||||
|
'timestamp': Timestamp.fromDate(timestamp),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory HistoryModel.fromMap(String id, Map<String, dynamic> 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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
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<String, dynamic> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
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<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'name_patient': namePatient,
|
||||||
|
'name_guardian': nameGuardian,
|
||||||
|
'device_id': deviceId,
|
||||||
|
'room_id': roomId,
|
||||||
|
if (userId != null) 'user_id': userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<dynamic, dynamic> data,
|
||||||
|
) {
|
||||||
|
final controlling = data['controlling'] as Map<dynamic, dynamic>? ?? {};
|
||||||
|
final monitoring = data['monitoring'] as Map<dynamic, dynamic>? ?? {};
|
||||||
|
final settings = data['settings'] as Map<dynamic, dynamic>? ?? {};
|
||||||
|
|
||||||
|
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<String, dynamic> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
return RoomModel(
|
||||||
|
id: doc.id,
|
||||||
|
roomName: data['room_name'] ?? '',
|
||||||
|
capacity: data['capacity'] ?? 0,
|
||||||
|
createdAt: (data['created_at'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'room_name': roomName,
|
||||||
|
'capacity': capacity,
|
||||||
|
'created_at': Timestamp.fromDate(createdAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory RoomModel.fromMap(String id, Map<String, dynamic> 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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic>;
|
||||||
|
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<String, dynamic> 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic> 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<String, dynamic>;
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,192 @@
|
||||||
import 'package:get/get.dart';
|
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 {
|
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<ChartDataPoint> chartData = <ChartDataPoint>[].obs;
|
||||||
|
|
||||||
|
StreamSubscription? _realtimeSubscription;
|
||||||
|
StreamSubscription? _historySubscription;
|
||||||
|
|
||||||
|
PatientModel? patient;
|
||||||
|
DeviceModel? device;
|
||||||
|
RealtimeMonitoringModel? realtimeData;
|
||||||
|
|
||||||
final count = 0.obs;
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
_getArguments();
|
||||||
|
if (deviceId != null && roomId != null) {
|
||||||
@override
|
_initializeDeviceDetail();
|
||||||
void onReady() {
|
} else {
|
||||||
super.onReady();
|
Get.back();
|
||||||
|
AppSnackbar.error('Device ID tidak ditemukan');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
_realtimeSubscription?.cancel();
|
||||||
|
_historySubscription?.cancel();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void increment() => count.value++;
|
void _getArguments() {
|
||||||
|
final args = Get.arguments as Map<String, dynamic>?;
|
||||||
|
if (args != null) {
|
||||||
|
deviceId = args['deviceId'];
|
||||||
|
roomId = args['roomId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _loadPatientData() async {
|
||||||
|
patient = await _firestoreService.getPatientByDeviceId(deviceId!);
|
||||||
|
if (patient != null) {
|
||||||
|
patientName.value = patient!.namePatient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDeviceData() async {
|
||||||
|
device = await _firestoreService.getDeviceById(deviceId!);
|
||||||
|
if (device != null) {
|
||||||
|
deviceName.value = device!.devicesName;
|
||||||
|
roomName.value = await _getRoomName(device!.roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _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<HistoryModel> 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<ChartDataPoint> points = latestHistories.map((history) {
|
||||||
|
return ChartDataPoint(
|
||||||
|
hour: history.timestamp.hour,
|
||||||
|
dropsPerMinute: history.dropPerMinute,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
chartData.value = points;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> refreshData() async {
|
||||||
|
await _loadDeviceData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,376 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import '../../../widgets/history_grafik_card.dart';
|
||||||
import '../controllers/device_detail_controller.dart';
|
import '../controllers/device_detail_controller.dart';
|
||||||
|
|
||||||
class DeviceDetailView extends GetView<DeviceDetailController> {
|
class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||||
const DeviceDetailView({super.key});
|
const DeviceDetailView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey[50],
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('DeviceDetailView'),
|
backgroundColor: Colors.white,
|
||||||
centerTitle: true,
|
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(
|
body: Obx(() {
|
||||||
child: Text(
|
if (controller.isLoading.value) {
|
||||||
'DeviceDetailView is working',
|
return const Center(
|
||||||
style: TextStyle(fontSize: 20),
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,91 @@
|
||||||
// lib/app/modules/home_patient/controllers/home_patient_controller.dart
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/material.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 {
|
class InfusData {
|
||||||
final String dropsPerMinute;
|
final String dropsPerMinute;
|
||||||
final String room;
|
final String room;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String updateTime;
|
final String updateTime;
|
||||||
|
final String deviceStatus;
|
||||||
|
|
||||||
InfusData({
|
InfusData({
|
||||||
required this.dropsPerMinute,
|
required this.dropsPerMinute,
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.updateTime,
|
required this.updateTime,
|
||||||
|
required this.deviceStatus,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationData {
|
class NotificationData {
|
||||||
|
final String id;
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
final String room;
|
final String room;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String timeAgo;
|
final String timeAgo;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
NotificationData({
|
NotificationData({
|
||||||
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.timeAgo,
|
required this.timeAgo,
|
||||||
|
required this.createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomePatientController extends GetxController {
|
class HomePatientController extends GetxController {
|
||||||
// Patient info
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
final patientName = 'Mrs. Shinta'.obs;
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||||
|
|
||||||
// Banner carousel
|
final deviceId = ''.obs;
|
||||||
|
final roomId = ''.obs;
|
||||||
|
final patientName = 'Patient'.obs;
|
||||||
|
final userEmail = ''.obs;
|
||||||
|
|
||||||
final currentCarouselIndex = 0.obs;
|
final currentCarouselIndex = 0.obs;
|
||||||
final bannerImages = [
|
final bannerImages = [
|
||||||
'assets/images/banner-slider3.png',
|
'assets/images/banner-slider3.png',
|
||||||
'assets/images/banner-slider4.png',
|
'assets/images/banner-slider4.png',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Current monitoring data
|
|
||||||
final currentInfusData = InfusData(
|
final currentInfusData = InfusData(
|
||||||
dropsPerMinute: '30 tetes per menit',
|
dropsPerMinute: '0',
|
||||||
room: 'MAWAR 002',
|
room: '-',
|
||||||
deviceId: 'SI001',
|
deviceId: '-',
|
||||||
updateTime: '5min ago',
|
updateTime: '-',
|
||||||
|
deviceStatus: 'disconnected',
|
||||||
).obs;
|
).obs;
|
||||||
|
|
||||||
// Chart data dengan 10 data points
|
final chartDataPoints = <ChartDataPoint>[].obs;
|
||||||
final chartDataPoints = <ChartDataPoint>[
|
final notificationsList = <NotificationData>[].obs;
|
||||||
ChartDataPoint(hour: 8, dropsPerMinute: 25),
|
|
||||||
ChartDataPoint(hour: 9, dropsPerMinute: 28),
|
final isLoading = true.obs;
|
||||||
ChartDataPoint(hour: 10, dropsPerMinute: 30),
|
final isLoadingHistory = true.obs;
|
||||||
ChartDataPoint(hour: 11, dropsPerMinute: 32),
|
final isLoadingNotifications = true.obs;
|
||||||
ChartDataPoint(hour: 12, dropsPerMinute: 35),
|
|
||||||
ChartDataPoint(hour: 13, dropsPerMinute: 33),
|
StreamSubscription? _realtimeSubscription;
|
||||||
ChartDataPoint(hour: 14, dropsPerMinute: 30),
|
StreamSubscription? _notificationsSubscription;
|
||||||
ChartDataPoint(hour: 15, dropsPerMinute: 28),
|
StreamSubscription? _historySubscription;
|
||||||
ChartDataPoint(hour: 16, dropsPerMinute: 26),
|
|
||||||
ChartDataPoint(hour: 17, dropsPerMinute: 30),
|
|
||||||
].obs;
|
|
||||||
|
|
||||||
// Notifications list
|
|
||||||
final notificationsList = <NotificationData>[
|
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
_loadDataFromArguments();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -96,14 +95,255 @@ class HomePatientController extends GetxController {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
_realtimeSubscription?.cancel();
|
||||||
|
_notificationsSubscription?.cancel();
|
||||||
|
_historySubscription?.cancel();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _loadDataFromArguments() async {
|
||||||
|
try {
|
||||||
|
final args = Get.arguments as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
deviceId.value = args['deviceId'] ?? '';
|
||||||
|
patientName.value = args['userName'] ?? 'Patient';
|
||||||
|
userEmail.value = args['userEmail'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceId.value.isEmpty) {
|
||||||
|
AppSnackbar.error('Device ID tidak ditemukan');
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _loadPatientData();
|
||||||
|
|
||||||
|
if (roomId.value.isNotEmpty) {
|
||||||
|
_startRealtimeMonitoring();
|
||||||
|
_loadHistoryData();
|
||||||
|
_loadNotifications();
|
||||||
|
} else {
|
||||||
|
AppSnackbar.error('Room ID tidak ditemukan');
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.error('Gagal memuat data: $e');
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPatientData() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
final patient = await _firestoreService.getPatientByDeviceId(
|
||||||
|
deviceId.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (patient != null) {
|
||||||
|
patientName.value = patient.namePatient;
|
||||||
|
roomId.value = patient.roomId;
|
||||||
|
|
||||||
|
final room = await _firestoreService.getRoomById(patient.roomId);
|
||||||
|
|
||||||
|
if (currentInfusData.value.deviceStatus == 'disconnected') {
|
||||||
|
currentInfusData.value = InfusData(
|
||||||
|
dropsPerMinute: '0',
|
||||||
|
room: room?.roomName ?? patient.roomId,
|
||||||
|
deviceId: patient.deviceId,
|
||||||
|
updateTime: 'Loading...',
|
||||||
|
deviceStatus: 'disconnected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AppSnackbar.error('Data pasien tidak ditemukan');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.error('Gagal memuat data pasien: $e');
|
||||||
|
print('Error loading patient data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startRealtimeMonitoring() {
|
||||||
|
try {
|
||||||
|
if (roomId.value.isEmpty || deviceId.value.isEmpty) {
|
||||||
|
print('Room ID or Device ID is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
||||||
|
|
||||||
|
_realtimeSubscription?.cancel(); // 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<HistoryModel> 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<ChartDataPoint> 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<NotificationData> notifList = [];
|
||||||
|
|
||||||
|
for (var notif in filteredNotifications) {
|
||||||
|
try {
|
||||||
|
final patient = await _firestoreService
|
||||||
|
.getPatientByDeviceId(notif.deviceId);
|
||||||
|
final room = patient != null
|
||||||
|
? await _firestoreService.getRoomById(patient.roomId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
notifList.add(
|
||||||
|
NotificationData(
|
||||||
|
id: notif.id,
|
||||||
|
title: notif.title,
|
||||||
|
message: notif.message,
|
||||||
|
room: room?.roomName ?? '-',
|
||||||
|
deviceId: notif.deviceId,
|
||||||
|
timeAgo: _getLastUpdateText(notif.createdAt),
|
||||||
|
createdAt: notif.createdAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error processing notification ${notif.id}: $e');
|
||||||
|
// 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) {
|
void updateCarouselIndex(int index) {
|
||||||
currentCarouselIndex.value = index;
|
currentCarouselIndex.value = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show call dialog
|
|
||||||
void showCallDialog(BuildContext context) {
|
void showCallDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -116,36 +356,161 @@ class HomePatientController extends GetxController {
|
||||||
'Hubungi Perawat',
|
'Hubungi Perawat',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
content: const Text('Apakah Anda ingin menghubungi perawat?'),
|
content: const Text(
|
||||||
|
'Apakah Anda ingin menghubungi perawat melalui WhatsApp?',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
|
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
// Implement call functionality
|
await _openWhatsApp();
|
||||||
Get.snackbar(
|
|
||||||
'Menghubungi',
|
|
||||||
'Sedang menghubungi perawat...',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF2196F3),
|
backgroundColor: const Color(
|
||||||
|
0xFF25D366,
|
||||||
|
), // WhatsApp green color
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Future<void> _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<void> 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,217 +1,304 @@
|
||||||
// lib/app/modules/home_patient/views/home_patient_view.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/home_patient_controller.dart';
|
import '../controllers/home_patient_controller.dart';
|
||||||
import '../../../widgets/banner_slider.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/history_grafik_card.dart';
|
||||||
import '../../../widgets/notification_patient_card.dart';
|
import '../../../widgets/notification_patient_card.dart';
|
||||||
|
import '../../home/controllers/home_controller.dart';
|
||||||
|
|
||||||
class HomePatientView extends GetView<HomePatientController> {
|
class HomePatientView extends GetView<HomePatientController> {
|
||||||
const HomePatientView({super.key});
|
const HomePatientView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(HomePatientController());
|
Get.put(HomePatientController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: RefreshIndicator(
|
||||||
child: Column(
|
onRefresh: controller.refreshData,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: SingleChildScrollView(
|
||||||
children: [
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
// Header
|
child: Column(
|
||||||
Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.all(20),
|
children: [
|
||||||
child: Row(
|
// Header
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(20),
|
||||||
Obx(
|
child: Row(
|
||||||
() => Text(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
'Halo ${controller.patientName.value}',
|
children: [
|
||||||
style: const TextStyle(
|
Expanded(
|
||||||
fontSize: 24,
|
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,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
TextButton(
|
||||||
Container(
|
onPressed: () {
|
||||||
padding: const EdgeInsets.all(8),
|
// TODO: Navigate to monitoring detail
|
||||||
decoration: BoxDecoration(
|
},
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
child: const Text(
|
||||||
borderRadius: BorderRadius.circular(10),
|
'See all',
|
||||||
),
|
style: TextStyle(
|
||||||
child: const Icon(
|
color: Color(0xFF2196F3),
|
||||||
Icons.logout,
|
fontWeight: FontWeight.w600,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 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
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
// Monitoring Card - Gunakan MonitoringCard dari widgets
|
||||||
const SizedBox(height: 15),
|
Padding(
|
||||||
|
|
||||||
// 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(),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
itemCount: controller.notificationsList.length,
|
child: Obx(() {
|
||||||
itemBuilder: (context, index) {
|
if (controller.isLoading.value) {
|
||||||
final notification = controller.notificationsList[index];
|
return Container(
|
||||||
return NotificationPatientCard(
|
padding: const EdgeInsets.all(32),
|
||||||
title: notification.title,
|
decoration: BoxDecoration(
|
||||||
message: notification.message,
|
color: Colors.white,
|
||||||
room: notification.room,
|
borderRadius: BorderRadius.circular(16),
|
||||||
deviceId: notification.deviceId,
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
timeAgo: notification.timeAgo,
|
),
|
||||||
|
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: () {
|
onTap: () {
|
||||||
// Handle notification tap
|
// TODO: Navigate to detail
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
// 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
|
// Floating Action Button - Call Nurse
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => controller.showCallDialog(context),
|
onPressed: () => controller.showCallDialog(context),
|
||||||
backgroundColor: const Color(0xFF2196F3),
|
backgroundColor: const Color(0xFF2196F3),
|
||||||
child: const Icon(
|
child: const Icon(Icons.phone, color: Colors.white),
|
||||||
Icons.phone,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,146 @@
|
||||||
// lib/app/modules/home/controllers/home_controller.dart
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/material.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 {
|
class InfusMonitoring {
|
||||||
final String id;
|
final String id;
|
||||||
final String patientName;
|
final String patientName;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String room;
|
final String room;
|
||||||
|
final String roomId;
|
||||||
final String dropsPerMinute;
|
final String dropsPerMinute;
|
||||||
final DateTime lastUpdate;
|
final DateTime lastUpdate;
|
||||||
final DateTime createdAt; // Tambahkan properti createdAt
|
final String deviceStatus;
|
||||||
|
|
||||||
InfusMonitoring({
|
InfusMonitoring({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.patientName,
|
required this.patientName,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.room,
|
required this.room,
|
||||||
|
required this.roomId,
|
||||||
required this.dropsPerMinute,
|
required this.dropsPerMinute,
|
||||||
required this.lastUpdate,
|
required this.lastUpdate,
|
||||||
required this.createdAt, // Pastikan ini diisi saat pembuatan objek
|
required this.deviceStatus,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeController extends GetxController {
|
class HomeController extends GetxController {
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||||
|
|
||||||
// Banner carousel
|
|
||||||
final currentCarouselIndex = 0.obs;
|
final currentCarouselIndex = 0.obs;
|
||||||
final bannerImages = [
|
final bannerImages = [
|
||||||
'assets/images/banner-slider1.png',
|
'assets/images/banner-slider1.png',
|
||||||
'assets/images/banner-slider2.png',
|
'assets/images/banner-slider2.png',
|
||||||
];
|
];
|
||||||
|
|
||||||
final monitoringList = <InfusMonitoring>[
|
final monitoringList = <InfusMonitoring>[].obs;
|
||||||
InfusMonitoring(
|
final isLoading = true.obs;
|
||||||
id: '1',
|
|
||||||
patientName: 'Mrs. Shinta',
|
StreamSubscription? _patientsSubscription;
|
||||||
deviceId: 'SI001',
|
StreamSubscription? _realtimeSubscription;
|
||||||
room: 'MAWAR 002',
|
|
||||||
dropsPerMinute: '30 tetes per menit',
|
@override
|
||||||
lastUpdate: DateTime.now().subtract(const Duration(minutes: 5)),
|
void onInit() {
|
||||||
createdAt: DateTime.now(), // Menambahkan createdAt
|
super.onInit();
|
||||||
),
|
_initializeMonitoring();
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
searchController.dispose();
|
searchController.dispose();
|
||||||
|
_patientsSubscription?.cancel();
|
||||||
|
_realtimeSubscription?.cancel();
|
||||||
super.onClose();
|
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<PatientModel> patients,
|
||||||
|
List<RealtimeMonitoringModel> realtimeDevices,
|
||||||
|
) async {
|
||||||
|
final List<InfusMonitoring> 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<String> _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<void> controlInfusServo(
|
||||||
|
String deviceId,
|
||||||
|
String roomId,
|
||||||
|
bool open,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final dbRoomId = roomId.toLowerCase().replaceAll(' ', '_');
|
||||||
|
await _realtimeService.controlServo(dbRoomId, deviceId, open);
|
||||||
|
AppSnackbar.success(open ? 'Infus dibuka' : 'Infus ditutup');
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.error('Gagal mengontrol infus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void updateCarouselIndex(int index) {
|
void updateCarouselIndex(int index) {
|
||||||
currentCarouselIndex.value = index;
|
currentCarouselIndex.value = index;
|
||||||
}
|
}
|
||||||
|
|
@ -68,33 +150,54 @@ class HomeController extends GetxController {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Konfirmasi Logout',
|
'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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
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(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
// Navigasi ke halaman login
|
await FirebaseAuth.instance.signOut();
|
||||||
Get.offAllNamed('/login');
|
Get.offAllNamed('/login');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
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) {
|
String getLastUpdateText(DateTime lastUpdate) {
|
||||||
final difference = DateTime.now().difference(lastUpdate);
|
final difference = DateTime.now().difference(lastUpdate);
|
||||||
if (difference.inMinutes < 60) {
|
if (difference.inMinutes < 60) {
|
||||||
return '${difference.inMinutes}min ago';
|
return '${difference.inMinutes}m ago';
|
||||||
} else if (difference.inHours < 24) {
|
} else if (difference.inHours < 24) {
|
||||||
return '${difference.inHours}h ago';
|
return '${difference.inHours}h ago';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/app/modules/home/views/home_view.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/home_controller.dart';
|
import '../controllers/home_controller.dart';
|
||||||
|
|
@ -7,99 +6,129 @@ import '../../../widgets/monitoring_card.dart';
|
||||||
|
|
||||||
class HomeView extends GetView<HomeController> {
|
class HomeView extends GetView<HomeController> {
|
||||||
const HomeView({super.key});
|
const HomeView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(HomeController());
|
Get.put(HomeController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
_buildHeader(context),
|
||||||
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
|
|
||||||
BannerSlider(
|
BannerSlider(
|
||||||
bannerImages: controller.bannerImages,
|
bannerImages: controller.bannerImages,
|
||||||
currentCarouselIndex: controller.currentCarouselIndex,
|
currentCarouselIndex: controller.currentCarouselIndex,
|
||||||
onPageChanged: controller.updateCarouselIndex,
|
onPageChanged: controller.updateCarouselIndex,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
_buildSectionHeader(),
|
||||||
// Monitoring Infus Section
|
Expanded(child: _buildMonitoringList()),
|
||||||
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
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,39 +1,172 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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 {
|
class LoginController extends GetxController {
|
||||||
final usernameController = TextEditingController();
|
final usernameController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
final isPasswordHidden = true.obs;
|
final isPasswordHidden = true.obs;
|
||||||
|
final isLoading = false.obs;
|
||||||
|
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
void togglePasswordVisibility() {
|
void togglePasswordVisibility() {
|
||||||
isPasswordHidden.value = !isPasswordHidden.value;
|
isPasswordHidden.value = !isPasswordHidden.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void login() {
|
void login() async {
|
||||||
|
// Validasi input
|
||||||
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Username dan password harus diisi',
|
'Email dan password harus diisi',
|
||||||
backgroundColor: Colors.red[100],
|
backgroundColor: Colors.red[100],
|
||||||
|
colorText: Colors.red[900],
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
);
|
);
|
||||||
return;
|
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 {
|
void contactNurse() async {
|
||||||
const phoneNumber = '+62 812-3190-1277';
|
const phoneNumber = '+6281231901277'; // Tanpa spasi
|
||||||
final url = 'https://wa.me/$phoneNumber'; // WhatsApp link for contacting
|
final url = Uri.parse('https://wa.me/$phoneNumber');
|
||||||
if (await canLaunch(url)) {
|
|
||||||
await launch(url);
|
if (await canLaunchUrl(url)) {
|
||||||
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Tidak dapat membuka WhatsApp',
|
'Tidak dapat membuka WhatsApp',
|
||||||
backgroundColor: Colors.red[100],
|
backgroundColor: Colors.red[100],
|
||||||
|
colorText: Colors.red[900],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,118 +14,126 @@ class LoginView extends GetView<LoginController> {
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// Wrap the entire content inside Center widget to center vertically
|
||||||
children: [
|
child: Column(
|
||||||
const SizedBox(height: 20),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const Text(
|
children: [
|
||||||
'Login',
|
const SizedBox(height: 20),
|
||||||
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
const Text(
|
||||||
),
|
'Login',
|
||||||
const SizedBox(height: 50),
|
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
||||||
Center(
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/images/banner-logo.png',
|
|
||||||
width: 300,
|
|
||||||
height: 130,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 50),
|
||||||
const SizedBox(height: 50),
|
Container(
|
||||||
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(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
color: Colors.grey[100],
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller.passwordController,
|
controller: controller.usernameController,
|
||||||
obscureText: controller.isPasswordHidden.value,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Masukkan password',
|
hintText: 'Masukkan email',
|
||||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||||
prefixIcon: Icon(
|
prefixIcon: Icon(
|
||||||
Icons.lock_outline,
|
Icons.mail_outline,
|
||||||
color: Colors.grey[400],
|
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,
|
border: InputBorder.none,
|
||||||
contentPadding: const EdgeInsets.all(20),
|
contentPadding: const EdgeInsets.all(20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 30),
|
Obx(
|
||||||
SizedBox(
|
() => Container(
|
||||||
width: double.infinity,
|
decoration: BoxDecoration(
|
||||||
height: 55,
|
color: Colors.grey[100],
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: controller.login,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFF1976D2),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
),
|
child: TextField(
|
||||||
child: const Text(
|
controller: controller.passwordController,
|
||||||
'Login',
|
obscureText: controller.isPasswordHidden.value,
|
||||||
style: TextStyle(
|
decoration: InputDecoration(
|
||||||
fontSize: 18,
|
hintText: 'Masukkan password',
|
||||||
fontWeight: FontWeight.w600,
|
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||||
color: Colors.white,
|
prefixIcon: Icon(
|
||||||
),
|
Icons.lock_outline,
|
||||||
),
|
color: Colors.grey[400],
|
||||||
),
|
),
|
||||||
),
|
suffixIcon: IconButton(
|
||||||
const SizedBox(height: 20),
|
icon: Icon(
|
||||||
Row(
|
controller.isPasswordHidden.value
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? Icons.visibility_off_outlined
|
||||||
children: [
|
: Icons.visibility_outlined,
|
||||||
const Text(
|
color: Colors.grey[400],
|
||||||
"Tidak memiliki akun? ",
|
),
|
||||||
style: TextStyle(color: Colors.black87),
|
onPressed: controller.togglePasswordVisibility,
|
||||||
),
|
),
|
||||||
TextButton(
|
border: InputBorder.none,
|
||||||
onPressed: controller.contactNurse,
|
contentPadding: const EdgeInsets.all(20),
|
||||||
child: const Text(
|
|
||||||
'Hubungi Perawat',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xFF1976D2),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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: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 {
|
class NotificationItem {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -46,50 +52,72 @@ class NotificationItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationController extends GetxController {
|
class NotificationController extends GetxController {
|
||||||
final selectedTab = 0.obs;
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
|
|
||||||
final _allNotifications = <NotificationItem>[
|
final selectedTab = 0.obs;
|
||||||
NotificationItem(
|
final _allNotifications = <NotificationItem>[].obs;
|
||||||
id: '1',
|
final isLoading = true.obs;
|
||||||
title: 'Peringatan!!',
|
|
||||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
StreamSubscription? _notificationsSubscription;
|
||||||
room: 'MAWAR 002',
|
|
||||||
deviceId: 'SI001',
|
@override
|
||||||
patientName: 'Mrs Shinta',
|
void onInit() {
|
||||||
time: DateTime.now().subtract(const Duration(minutes: 5)),
|
super.onInit();
|
||||||
isRead: false,
|
_initializeNotifications();
|
||||||
),
|
}
|
||||||
NotificationItem(
|
|
||||||
id: '2',
|
@override
|
||||||
title: 'Peringatan!!',
|
void onClose() {
|
||||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
_notificationsSubscription?.cancel();
|
||||||
room: 'MAWAR 003',
|
super.onClose();
|
||||||
deviceId: 'SI002',
|
}
|
||||||
patientName: 'Mr Budi',
|
|
||||||
time: DateTime.now().subtract(const Duration(hours: 2)),
|
/// Initialize notifications dari Firestore
|
||||||
isRead: false,
|
void _initializeNotifications() {
|
||||||
),
|
isLoading.value = true;
|
||||||
NotificationItem(
|
|
||||||
id: '3',
|
_notificationsSubscription = _firestoreService
|
||||||
title: 'Peringatan!!',
|
.getNotificationsStream()
|
||||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
.listen((notificationModels) async {
|
||||||
room: 'MAWAR 004',
|
final List<NotificationItem> items = [];
|
||||||
deviceId: 'SI003',
|
|
||||||
patientName: 'Mrs Ani',
|
for (var notifModel in notificationModels) {
|
||||||
time: DateTime.now().subtract(const Duration(hours: 5)),
|
// Get patient info
|
||||||
isRead: true,
|
final patient = await _firestoreService.getPatientById(
|
||||||
),
|
notifModel.patientId,
|
||||||
NotificationItem(
|
);
|
||||||
id: '4',
|
|
||||||
title: 'Peringatan!!',
|
// Get room name from patient's roomId
|
||||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
String roomName = 'Unknown Room';
|
||||||
room: 'MAWAR 005',
|
if (patient != null && patient.roomId.isNotEmpty) {
|
||||||
deviceId: 'SI004',
|
final rooms = await _firestoreService.getRoomsStream().first;
|
||||||
patientName: 'Mr Doni',
|
final room = rooms.firstWhereOrNull(
|
||||||
time: DateTime.now().subtract(const Duration(days: 1)),
|
(r) => r.roomName.toLowerCase() == patient.roomId.toLowerCase(),
|
||||||
isRead: true,
|
);
|
||||||
),
|
roomName = room?.roomName ?? patient.roomId;
|
||||||
].obs;
|
}
|
||||||
|
|
||||||
|
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
|
// Computed property untuk notifikasi yang difilter
|
||||||
List<NotificationItem> get notifications {
|
List<NotificationItem> get notifications {
|
||||||
|
|
@ -106,15 +134,35 @@ class NotificationController extends GetxController {
|
||||||
selectedTab.value = index;
|
selectedTab.value = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteNotification(String id) {
|
Future<void> deleteNotification(String id) async {
|
||||||
_allNotifications.removeWhere((notif) => notif.id == id);
|
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) {
|
Future<void> markAsRead(String id) async {
|
||||||
final index = _allNotifications.indexWhere((notif) => notif.id == id);
|
try {
|
||||||
if (index != -1) {
|
await _firestoreService.markNotificationAsRead(id);
|
||||||
_allNotifications[index] = _allNotifications[index].copyWith(isRead: true);
|
// Local update akan dilakukan otomatis oleh stream
|
||||||
_allNotifications.refresh();
|
} 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 {
|
int get unreadCount {
|
||||||
return _allNotifications.where((notif) => !notif.isRead).length;
|
return _allNotifications.where((notif) => !notif.isRead).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Method untuk menambahkan notifikasi baru (bisa dipanggil dari monitoring)
|
||||||
|
Future<void> 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
// lib/app/modules/profile/controllers/profile_controller.dart
|
|
||||||
import 'package:get/get.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 {
|
class Patient {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String room;
|
final String room;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String guardian; // Wali pasien
|
final String guardian;
|
||||||
|
final String email;
|
||||||
|
final String? userId;
|
||||||
final String status;
|
final String status;
|
||||||
|
|
||||||
Patient({
|
Patient({
|
||||||
|
|
@ -15,40 +24,372 @@ class Patient {
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.guardian,
|
required this.guardian,
|
||||||
|
required this.email,
|
||||||
|
this.userId,
|
||||||
this.status = 'Aktif',
|
this.status = 'Aktif',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> 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<String, dynamic>;
|
||||||
|
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 {
|
class ProfileController extends GetxController {
|
||||||
final patients = <Patient>[
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
Patient(
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||||
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;
|
|
||||||
|
|
||||||
void addPatient(Patient patient) {
|
final patients = <Patient>[].obs;
|
||||||
patients.add(patient);
|
final isLoading = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
_listenToPatients();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePatient(String id, Patient updatedPatient) {
|
void refreshPatients() {
|
||||||
final index = patients.indexWhere((p) => p.id == id);
|
_listenToPatients();
|
||||||
if (index != -1) {
|
}
|
||||||
patients[index] = updatedPatient;
|
|
||||||
|
void _listenToPatients() {
|
||||||
|
_firestoreService.getPatientsStream().listen((patientModels) async {
|
||||||
|
final patientsWithEmail = <Patient>[];
|
||||||
|
|
||||||
|
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<String> _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) {
|
Future<String> _getRoomName(String roomId) async {
|
||||||
patients.removeWhere((patient) => patient.id == id);
|
try {
|
||||||
|
final roomDoc = await FirebaseFirestore.instance
|
||||||
|
.collection('rooms')
|
||||||
|
.doc(roomId)
|
||||||
|
.get();
|
||||||
|
return roomDoc.data()?['room_name'] ?? roomId;
|
||||||
|
} catch (e) {
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Future<void> 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<bool> _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<void> 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<void> _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<void> 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<void> _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<void> _createDevice(String deviceId, String roomId) async {
|
||||||
|
await _firestoreService.createDevice(
|
||||||
|
DeviceModel(
|
||||||
|
idDevices: deviceId,
|
||||||
|
roomId: roomId,
|
||||||
|
devicesName: 'Infusion Device $deviceId',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _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'^_|_$'), '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/app/modules/profile/views/profile_view.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../controllers/profile_controller.dart';
|
import '../controllers/profile_controller.dart';
|
||||||
|
|
@ -18,7 +17,6 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header Section
|
|
||||||
Container(
|
Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -36,10 +34,7 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
Color(0xFF0091EA),
|
|
||||||
Color(0xFF0277BD),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
|
|
@ -50,73 +45,121 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ElevatedButton(
|
child: Obx(
|
||||||
onPressed: () {
|
() => ElevatedButton(
|
||||||
Get.dialog(
|
onPressed: controller.isLoading.value
|
||||||
const ModalPatientAddEdit(isEdit: false),
|
? null
|
||||||
barrierDismissible: true,
|
: () {
|
||||||
);
|
Get.dialog(
|
||||||
},
|
const ModalPatientAddEdit(isEdit: false),
|
||||||
style: ElevatedButton.styleFrom(
|
barrierDismissible: true,
|
||||||
backgroundColor: Colors.transparent,
|
);
|
||||||
shadowColor: Colors.transparent,
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
style: ElevatedButton.styleFrom(
|
||||||
horizontal: 20,
|
backgroundColor: Colors.transparent,
|
||||||
vertical: 12,
|
shadowColor: Colors.transparent,
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
shape: RoundedRectangleBorder(
|
horizontal: 20,
|
||||||
borderRadius: BorderRadius.circular(12),
|
vertical: 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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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(
|
Expanded(
|
||||||
child: Obx(
|
child: Obx(() {
|
||||||
() {
|
if (controller.isLoading.value && controller.patients.isEmpty) {
|
||||||
if (controller.patients.isEmpty) {
|
return const Center(
|
||||||
return Center(
|
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
||||||
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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20,
|
horizontal: 20,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
|
@ -128,51 +171,64 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
patient: patient,
|
patient: patient,
|
||||||
onEditTap: () {
|
onEditTap: () {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
ModalPatientAddEdit(
|
ModalPatientAddEdit(patient: patient, isEdit: true),
|
||||||
patient: patient,
|
|
||||||
isEdit: true,
|
|
||||||
),
|
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onDeleteTap: () {
|
onDeleteTap: () {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
title: const Text('Hapus Pasien'),
|
title: const Text(
|
||||||
|
'Hapus Pasien',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Apakah Anda yakin ingin menghapus ${patient.name}?',
|
'Apakah Anda yakin ingin menghapus ${patient.name}?',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: Text(
|
style: TextButton.styleFrom(
|
||||||
'Batal',
|
foregroundColor: Colors.grey[700],
|
||||||
style: TextStyle(color: Colors.grey[700]),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: const Text('Batal'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.deletePatient(patient.id);
|
controller.deletePatient(patient.id);
|
||||||
Get.back();
|
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(
|
child: const Text(
|
||||||
'Hapus',
|
'Hapus',
|
||||||
style: TextStyle(color: Colors.red),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -181,13 +237,13 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,219 @@
|
||||||
// lib/app/modules/schedule/controllers/schedule_controller.dart
|
|
||||||
import 'package:get/get.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 {
|
class PatientSchedule {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String room;
|
final String room;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String medicineType;
|
|
||||||
final String fluidType;
|
|
||||||
final String medicineTime;
|
final String medicineTime;
|
||||||
final String fluidTime;
|
final String fluidTime;
|
||||||
|
final String timeOfDay;
|
||||||
|
|
||||||
PatientSchedule({
|
PatientSchedule({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.medicineType,
|
|
||||||
required this.fluidType,
|
|
||||||
required this.medicineTime,
|
required this.medicineTime,
|
||||||
required this.fluidTime,
|
required this.fluidTime,
|
||||||
|
required this.timeOfDay,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduleController extends GetxController {
|
class ScheduleController extends GetxController {
|
||||||
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
|
|
||||||
final selectedTab = 0.obs;
|
final selectedTab = 0.obs;
|
||||||
final selectedDate = DateTime.now().obs;
|
final selectedDate = DateTime.now().obs;
|
||||||
|
final allSchedules = <PatientSchedule>[].obs;
|
||||||
|
final filteredSchedules = <PatientSchedule>[].obs;
|
||||||
|
final isLoading = false.obs;
|
||||||
|
|
||||||
final schedules = <PatientSchedule>[
|
StreamSubscription? _schedulesSubscription;
|
||||||
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;
|
|
||||||
|
|
||||||
void changeTab(int index) {
|
final Map<int, String> _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<PatientSchedule> 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<String> _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;
|
selectedTab.value = index;
|
||||||
|
_applyTimeFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectDate(DateTime date) {
|
void selectDate(DateTime date) {
|
||||||
selectedDate.value = date;
|
selectedDate.value = date;
|
||||||
|
_loadSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addSchedule(PatientSchedule schedule) {
|
void refreshSchedules() {
|
||||||
schedules.add(schedule);
|
_loadSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSchedule(String id, PatientSchedule updatedSchedule) {
|
Future<void> addSchedule(PatientSchedule schedule, DateTime scheduleDate) async {
|
||||||
final index = schedules.indexWhere((s) => s.id == id);
|
try {
|
||||||
if (index != -1) {
|
final patients = await _firestoreService.getPatientsStream().first;
|
||||||
schedules[index] = updatedSchedule;
|
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) {
|
Future<void> updateSchedule(String id, PatientSchedule updatedSchedule, DateTime scheduleDate) async {
|
||||||
schedules.removeWhere((s) => s.id == id);
|
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<void> deleteSchedule(String id) async {
|
||||||
|
try {
|
||||||
|
await _firestoreService.deleteSchedule(id);
|
||||||
|
AppSnackbar.success('Jadwal berhasil dihapus');
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.error('Gagal menghapus jadwal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, String>>> getPatientsList() async {
|
||||||
|
try {
|
||||||
|
final patients = await _firestoreService.getPatientsStream().first;
|
||||||
|
final List<Map<String, String>> result = [];
|
||||||
|
|
||||||
|
for (var patient in patients) {
|
||||||
|
try {
|
||||||
|
final roomName = await _getRoomName(patient.roomId);
|
||||||
|
result.add({
|
||||||
|
'id': patient.id,
|
||||||
|
'name': patient.namePatient,
|
||||||
|
'room': roomName,
|
||||||
|
'device': patient.deviceId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib\app\modules\schedule\views\schedule_view.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
@ -12,235 +11,138 @@ class ScheduleView extends GetView<ScheduleController> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(ScheduleController());
|
Get.put(ScheduleController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header Section
|
_buildHeader(context),
|
||||||
Container(
|
|
||||||
color: Colors.white,
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Title
|
|
||||||
const Text(
|
|
||||||
'Schedule Obat',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF424242),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Date Picker & Add Button
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// Date Picker
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => _selectDate(context),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[100],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.calendar_today,
|
|
||||||
size: 18,
|
|
||||||
color: Color(0xFF0091EA),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Obx(
|
|
||||||
() => Text(
|
|
||||||
DateFormat(
|
|
||||||
'EEEE, dd MMMM yyyy',
|
|
||||||
'id_ID',
|
|
||||||
).format(controller.selectedDate.value),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF424242),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_drop_down,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
|
|
||||||
// Add Button
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.3),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: // Ubah bagian tombol Add di schedule_view.dart:
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Get.dialog(
|
|
||||||
const ModalScheduleAddEdit(isEdit: false),
|
|
||||||
barrierDismissible: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(12),
|
|
||||||
child: Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Time Filter Tabs
|
|
||||||
Obx(
|
|
||||||
() => Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildTabButton('Pagi', 0)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: _buildTabButton('Siang', 1)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: _buildTabButton('Sore', 2)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: _buildTabButton('Malam', 3)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
Expanded(child: _buildScheduleList()),
|
||||||
// Schedule List
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.schedules.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.event_note,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Tidak ada jadwal',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
itemCount: controller.schedules.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final schedule = controller.schedules[index];
|
|
||||||
return ScheduleCard(
|
|
||||||
schedule: schedule,
|
|
||||||
onEditTap: () {
|
|
||||||
Get.dialog(
|
|
||||||
ModalScheduleAddEdit(
|
|
||||||
schedule: schedule,
|
|
||||||
isEdit: true,
|
|
||||||
),
|
|
||||||
barrierDismissible: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDeleteTap: () {
|
|
||||||
Get.dialog(
|
|
||||||
AlertDialog(
|
|
||||||
title: const Text('Hapus Jadwal'),
|
|
||||||
content: const Text(
|
|
||||||
'Apakah Anda yakin ingin menghapus jadwal ini?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text('Batal'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
controller.deleteSchedule(schedule.id);
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Berhasil',
|
|
||||||
'Jadwal berhasil dihapus',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.redAccent,
|
|
||||||
colorText: Colors.white,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Hapus',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Schedule Obat',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDatePicker(context)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildAddButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildTimeFilterTabs(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDatePicker(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _selectDate(context),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xFF0091EA),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
DateFormat(
|
||||||
|
'EEEE, dd MMMM yyyy',
|
||||||
|
'id_ID',
|
||||||
|
).format(controller.selectedDate.value),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(Icons.arrow_drop_down, color: Colors.grey[600]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAddButton() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFF0091EA).withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => Get.dialog(
|
||||||
|
const ModalScheduleAddEdit(isEdit: false),
|
||||||
|
barrierDismissible: true,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: Icon(Icons.add, color: Colors.white, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeFilterTabs() {
|
||||||
|
return Obx(
|
||||||
|
() => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTabButton('Pagi', 0)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: _buildTabButton('Siang', 1)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: _buildTabButton('Sore', 2)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: _buildTabButton('Malam', 3)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTabButton(String title, int index) {
|
Widget _buildTabButton(String title, int index) {
|
||||||
final isSelected = controller.selectedTab.value == index;
|
final isSelected = controller.selectedTab.value == index;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -252,7 +154,6 @@ class ScheduleView extends GetView<ScheduleController> {
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected ? const Color(0xFF0091EA) : Colors.grey.shade300,
|
color: isSelected ? const Color(0xFF0091EA) : Colors.grey.shade300,
|
||||||
width: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -268,6 +169,122 @@ class ScheduleView extends GetView<ScheduleController> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildScheduleList() {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.filteredSchedules.isEmpty) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
controller.refreshSchedules();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
},
|
||||||
|
color: const Color(0xFF0091EA),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: SizedBox(
|
||||||
|
height: MediaQuery.of(Get.context!).size.height * 0.6,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.event_note, size: 80, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Tidak ada jadwal',
|
||||||
|
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.refreshSchedules();
|
||||||
|
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),
|
||||||
|
itemCount: controller.filteredSchedules.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final schedule = controller.filteredSchedules[index];
|
||||||
|
return ScheduleCard(
|
||||||
|
schedule: schedule,
|
||||||
|
onEditTap: () => Get.dialog(
|
||||||
|
ModalScheduleAddEdit(schedule: schedule, isEdit: true),
|
||||||
|
barrierDismissible: true,
|
||||||
|
),
|
||||||
|
onDeleteTap: () => _showDeleteDialog(schedule.id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDeleteDialog(String scheduleId) {
|
||||||
|
Get.dialog(
|
||||||
|
AlertDialog(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
title: const Text(
|
||||||
|
'Hapus Jadwal',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
content: const Text(
|
||||||
|
'Yakin ingin menghapus jadwal ini?',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.blue,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.deleteSchedule(scheduleId);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
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(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _selectDate(BuildContext context) async {
|
Future<void> _selectDate(BuildContext context) async {
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -287,8 +304,6 @@ class ScheduleView extends GetView<ScheduleController> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) controller.selectDate(picked);
|
||||||
controller.selectDate(picked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
// lib\app\modules\splash-screen\controllers\splash_screen_controller.dart
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
|
|
||||||
class SplashScreenController extends GetxController {
|
class SplashScreenController extends GetxController {
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_navigateToLogin();
|
_checkAuthStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToLogin() async {
|
void _checkAuthStatus() async {
|
||||||
await Future.delayed(const Duration(seconds: 4));
|
await Future.delayed(const Duration(seconds: 4));
|
||||||
Get.offNamed(Routes.LOGIN);
|
|
||||||
|
// Cek apakah user sudah login
|
||||||
|
User? user = _auth.currentUser;
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
// User sudah login, langsung ke home/navbar
|
||||||
|
Get.offAllNamed(Routes.NAVBAR);
|
||||||
|
} else {
|
||||||
|
// User belum login, ke halaman login
|
||||||
|
Get.offAllNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,467 @@
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import '../models/patient_model.dart';
|
||||||
|
import '../models/device_model.dart';
|
||||||
|
import '../models/room_model.dart';
|
||||||
|
import '../models/notification_model.dart';
|
||||||
|
import '../models/schedule_model.dart';
|
||||||
|
import '../models/history_model.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
class FirestoreService {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
|
// ==================== USER MANAGEMENT ====================
|
||||||
|
|
||||||
|
Future<bool> userExists(String email) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('users')
|
||||||
|
.where('email', isEqualTo: email)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
return snapshot.docs.isNotEmpty;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> createUser({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String name,
|
||||||
|
required String role,
|
||||||
|
String? deviceId,
|
||||||
|
}) async {
|
||||||
|
final userCredential = await _auth.createUserWithEmailAndPassword(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
final uid = userCredential.user!.uid;
|
||||||
|
|
||||||
|
await _firestore.collection('users').doc(uid).set({
|
||||||
|
'email': email,
|
||||||
|
'name': name,
|
||||||
|
'role': role,
|
||||||
|
if (deviceId != null) 'device_id': deviceId, // TAMBAH device_id
|
||||||
|
'created_at': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await _auth.signOut();
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateUser({
|
||||||
|
required String uid,
|
||||||
|
required String name,
|
||||||
|
String? email,
|
||||||
|
String? deviceId,
|
||||||
|
}) async {
|
||||||
|
final updateData = {'name': name};
|
||||||
|
if (email != null) updateData['email'] = email;
|
||||||
|
if (deviceId != null)
|
||||||
|
updateData['device_id'] = deviceId; // Support update device_id
|
||||||
|
await _firestore.collection('users').doc(uid).update(updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteUser(String uid) async {
|
||||||
|
await _firestore.collection('users').doc(uid).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteUserFromAuth(String email, String password) async {
|
||||||
|
try {
|
||||||
|
// Sign in temporary dengan user yang akan dihapus
|
||||||
|
final userCredential = await _auth.signInWithEmailAndPassword(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hapus user dari Authentication
|
||||||
|
await userCredential.user?.delete();
|
||||||
|
|
||||||
|
// Sign out
|
||||||
|
await _auth.signOut();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting user from auth: $e');
|
||||||
|
// Jika gagal, ignore error (misalnya user sudah tidak ada)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DocumentSnapshot?> getUserByEmail(String email) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('users')
|
||||||
|
.where('email', isEqualTo: email)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
return snapshot.docs.isNotEmpty ? snapshot.docs.first : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserModel?> getUserModelByEmail(String email) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('users')
|
||||||
|
.where('email', isEqualTo: email)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
return snapshot.docs.isNotEmpty
|
||||||
|
? UserModel.fromFirestore(snapshot.docs.first)
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DEVICE MANAGEMENT ====================
|
||||||
|
|
||||||
|
Future<bool> deviceExists(String deviceId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('devices').doc(deviceId).get();
|
||||||
|
return doc.exists;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> patientExistsByDevice(String deviceId) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('patients')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
return snapshot.docs.isNotEmpty;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<DeviceModel>> getDevicesStream() {
|
||||||
|
return _firestore
|
||||||
|
.collection('devices')
|
||||||
|
.snapshots()
|
||||||
|
.map(
|
||||||
|
(snapshot) => snapshot.docs
|
||||||
|
.map((doc) => DeviceModel.fromFirestore(doc))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DeviceModel?> getDeviceById(String deviceId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('devices').doc(deviceId).get();
|
||||||
|
return doc.exists ? DeviceModel.fromFirestore(doc) : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDevice(DeviceModel device) async {
|
||||||
|
await _firestore
|
||||||
|
.collection('devices')
|
||||||
|
.doc(device.idDevices)
|
||||||
|
.set(device.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteDevice(String deviceId) async {
|
||||||
|
await _firestore.collection('devices').doc(deviceId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PATIENT MANAGEMENT ====================
|
||||||
|
|
||||||
|
Stream<List<PatientModel>> getPatientsStream() {
|
||||||
|
return _firestore
|
||||||
|
.collection('patients')
|
||||||
|
.snapshots()
|
||||||
|
.map(
|
||||||
|
(snapshot) => snapshot.docs
|
||||||
|
.map((doc) => PatientModel.fromFirestore(doc))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PatientModel?> getPatientById(String patientId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('patients').doc(patientId).get();
|
||||||
|
return doc.exists ? PatientModel.fromFirestore(doc) : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PatientModel?> getPatientByDeviceId(String deviceId) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('patients')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
return snapshot.docs.isNotEmpty
|
||||||
|
? PatientModel.fromFirestore(snapshot.docs.first)
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createPatient(PatientModel patient) async {
|
||||||
|
await _firestore.collection('patients').add(patient.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePatient(String patientId, PatientModel patient) async {
|
||||||
|
await _firestore
|
||||||
|
.collection('patients')
|
||||||
|
.doc(patientId)
|
||||||
|
.update(patient.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePatient(String patientId) async {
|
||||||
|
await _firestore.collection('patients').doc(patientId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ROOM MANAGEMENT ====================
|
||||||
|
|
||||||
|
Future<bool> roomExists(String roomId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('rooms').doc(roomId).get();
|
||||||
|
return doc.exists;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<RoomModel>> getRoomsStream() {
|
||||||
|
return _firestore
|
||||||
|
.collection('rooms')
|
||||||
|
.snapshots()
|
||||||
|
.map(
|
||||||
|
(snapshot) =>
|
||||||
|
snapshot.docs.map((doc) => RoomModel.fromFirestore(doc)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<RoomModel?> getRoomById(String roomId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('rooms').doc(roomId).get();
|
||||||
|
return doc.exists ? RoomModel.fromFirestore(doc) : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createRoom(RoomModel room) async {
|
||||||
|
await _firestore.collection('rooms').doc(room.id).set(room.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteRoom(String roomId) async {
|
||||||
|
await _firestore.collection('rooms').doc(roomId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== HISTORY MANAGEMENT ====================
|
||||||
|
|
||||||
|
Stream<List<HistoryModel>> getDeviceHistoriesStream(String deviceId) {
|
||||||
|
return _firestore
|
||||||
|
.collection('histories')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
final histories = snapshot.docs
|
||||||
|
.map((doc) => HistoryModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
|
||||||
|
return histories;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<HistoryModel>> getDeviceHistories(
|
||||||
|
String deviceId, {
|
||||||
|
int limit = 100,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('histories')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final histories = snapshot.docs
|
||||||
|
.map((doc) => HistoryModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
|
||||||
|
return histories.take(limit).toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<HistoryModel>> getDeviceHistoriesInRange(
|
||||||
|
String deviceId,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('histories')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.where('timestamp', isGreaterThanOrEqualTo: startDate)
|
||||||
|
.where('timestamp', isLessThanOrEqualTo: endDate)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final histories = snapshot.docs
|
||||||
|
.map((doc) => HistoryModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
|
||||||
|
return histories;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<HistoryModel>> getDeviceHistoriesToday(String deviceId) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startOfDay = DateTime(now.year, now.month, now.day);
|
||||||
|
final endOfDay = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||||
|
return getDeviceHistoriesInRange(deviceId, startOfDay, endOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addHistory(HistoryModel history) async {
|
||||||
|
await _firestore.collection('histories').add(history.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteOldHistories({int daysToKeep = 30}) async {
|
||||||
|
final cutoffDate = DateTime.now().subtract(Duration(days: daysToKeep));
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('histories')
|
||||||
|
.where('timestamp', isLessThan: cutoffDate)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
for (var doc in snapshot.docs) {
|
||||||
|
batch.delete(doc.reference);
|
||||||
|
}
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BARU: Hapus semua histories berdasarkan device_id
|
||||||
|
Future<void> deleteHistoriesByDevice(String deviceId) async {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('histories')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
for (var doc in snapshot.docs) {
|
||||||
|
batch.delete(doc.reference);
|
||||||
|
}
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== NOTIFICATION MANAGEMENT ====================
|
||||||
|
|
||||||
|
Stream<List<NotificationModel>> getNotificationsStream() {
|
||||||
|
return _firestore
|
||||||
|
.collection('notifications')
|
||||||
|
.orderBy('created_at', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map(
|
||||||
|
(snapshot) => snapshot.docs
|
||||||
|
.map((doc) => NotificationModel.fromFirestore(doc))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addNotification(NotificationModel notification) async {
|
||||||
|
await _firestore
|
||||||
|
.collection('notifications')
|
||||||
|
.add(notification.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> markNotificationAsRead(String notificationId) async {
|
||||||
|
await _firestore.collection('notifications').doc(notificationId).update({
|
||||||
|
'is_read': true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteNotification(String notificationId) async {
|
||||||
|
await _firestore.collection('notifications').doc(notificationId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BARU: Hapus semua notifications berdasarkan device_id
|
||||||
|
Future<void> deleteNotificationsByDevice(String deviceId) async {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('notifications')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
for (var doc in snapshot.docs) {
|
||||||
|
batch.delete(doc.reference);
|
||||||
|
}
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SCHEDULE MANAGEMENT ====================
|
||||||
|
|
||||||
|
Stream<List<ScheduleModel>> getSchedulesStream({
|
||||||
|
DateTime? date,
|
||||||
|
String? timeOfDay,
|
||||||
|
}) {
|
||||||
|
Query query = _firestore.collection('schedules');
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
final startOfDay = DateTime(date.year, date.month, date.day);
|
||||||
|
final endOfDay = DateTime(date.year, date.month, date.day, 23, 59, 59);
|
||||||
|
query = query
|
||||||
|
.where('schedule_date', isGreaterThanOrEqualTo: startOfDay)
|
||||||
|
.where('schedule_date', isLessThanOrEqualTo: endOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOfDay != null) {
|
||||||
|
query = query.where('time_of_day', isEqualTo: timeOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.snapshots().map(
|
||||||
|
(snapshot) =>
|
||||||
|
snapshot.docs.map((doc) => ScheduleModel.fromFirestore(doc)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addSchedule(ScheduleModel schedule) async {
|
||||||
|
await _firestore.collection('schedules').add(schedule.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateSchedule(String scheduleId, ScheduleModel schedule) async {
|
||||||
|
await _firestore
|
||||||
|
.collection('schedules')
|
||||||
|
.doc(scheduleId)
|
||||||
|
.update(schedule.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSchedule(String scheduleId) async {
|
||||||
|
await _firestore.collection('schedules').doc(scheduleId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BARU: Hapus semua schedules berdasarkan device_id
|
||||||
|
Future<void> deleteSchedulesByDevice(String deviceId) async {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('schedules')
|
||||||
|
.where('device_id', isEqualTo: deviceId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
for (var doc in snapshot.docs) {
|
||||||
|
batch.delete(doc.reference);
|
||||||
|
}
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
|
import '../models/realtime_monitoring_model.dart';
|
||||||
|
|
||||||
|
class RealtimeDatabaseService {
|
||||||
|
final DatabaseReference _database = FirebaseDatabase.instance.ref();
|
||||||
|
|
||||||
|
// ==================== MONITORING ====================
|
||||||
|
|
||||||
|
Stream<Map<String, RealtimeMonitoringModel>> getRoomDevicesStream(
|
||||||
|
String roomId,
|
||||||
|
) {
|
||||||
|
return _database
|
||||||
|
.child('realtime/rooms/$roomId/devices')
|
||||||
|
.onValue
|
||||||
|
.map((event) {
|
||||||
|
final Map<String, RealtimeMonitoringModel> devices = {};
|
||||||
|
|
||||||
|
if (event.snapshot.value != null) {
|
||||||
|
final data = event.snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
data.forEach((deviceId, deviceData) {
|
||||||
|
if (deviceData is Map) {
|
||||||
|
devices[deviceId] = RealtimeMonitoringModel.fromRealtimeDB(
|
||||||
|
deviceId,
|
||||||
|
roomId,
|
||||||
|
deviceData as Map<dynamic, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<RealtimeMonitoringModel?> getDeviceStream(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
) {
|
||||||
|
return _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId')
|
||||||
|
.onValue
|
||||||
|
.map((event) {
|
||||||
|
if (event.snapshot.value != null) {
|
||||||
|
final data = event.snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
return RealtimeMonitoringModel.fromRealtimeDB(deviceId, roomId, data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<RealtimeMonitoringModel>> getAllDevicesStream() {
|
||||||
|
return _database.child('realtime/rooms').onValue.map((event) {
|
||||||
|
final List<RealtimeMonitoringModel> allDevices = [];
|
||||||
|
|
||||||
|
if (event.snapshot.value != null) {
|
||||||
|
final rooms = event.snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
|
||||||
|
rooms.forEach((roomId, roomData) {
|
||||||
|
if (roomData is Map && roomData['devices'] != null) {
|
||||||
|
final devices = roomData['devices'] as Map<dynamic, dynamic>;
|
||||||
|
|
||||||
|
devices.forEach((deviceId, deviceData) {
|
||||||
|
if (deviceData is Map) {
|
||||||
|
allDevices.add(
|
||||||
|
RealtimeMonitoringModel.fromRealtimeDB(
|
||||||
|
deviceId,
|
||||||
|
roomId,
|
||||||
|
deviceData as Map<dynamic, dynamic>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDevices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<RealtimeMonitoringModel?> getDevice(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final snapshot =
|
||||||
|
await _database.child('realtime/rooms/$roomId/devices/$deviceId').get();
|
||||||
|
|
||||||
|
if (snapshot.value != null) {
|
||||||
|
final data = snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
return RealtimeMonitoringModel.fromRealtimeDB(deviceId, roomId, data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CONTROL ====================
|
||||||
|
|
||||||
|
Future<void> controlServo(String roomId, String deviceId, bool open) async {
|
||||||
|
await _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId/controlling')
|
||||||
|
.update({
|
||||||
|
'servo_open': open,
|
||||||
|
'last_command': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateDropRate(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
double dropRate,
|
||||||
|
) async {
|
||||||
|
await _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
||||||
|
.update({
|
||||||
|
'drop_rate': dropRate,
|
||||||
|
'last_update': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateDeviceStatus(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
String status,
|
||||||
|
) async {
|
||||||
|
await _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
||||||
|
.update({
|
||||||
|
'device_status': status,
|
||||||
|
'last_update': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateServoSettings(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
int angleOpen,
|
||||||
|
int angleClose,
|
||||||
|
) async {
|
||||||
|
await _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId/settings')
|
||||||
|
.update({
|
||||||
|
'servo_angle_open': angleOpen,
|
||||||
|
'servo_angle_close': angleClose,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UTILITY ====================
|
||||||
|
|
||||||
|
Future<bool> isDeviceConnected(String roomId, String deviceId) async {
|
||||||
|
try {
|
||||||
|
final device = await getDevice(roomId, deviceId);
|
||||||
|
return device?.deviceStatus == 'connected';
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double> getDeviceDropRate(String roomId, String deviceId) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _database
|
||||||
|
.child(
|
||||||
|
'realtime/rooms/$roomId/devices/$deviceId/monitoring/drop_rate')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.value != null) {
|
||||||
|
return (snapshot.value as num).toDouble();
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
} catch (e) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class AppSnackbar {
|
||||||
|
static void success(String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void error(String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void warning(String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Peringatan',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.orangeAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void info(String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Info',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
// lib/app/widgets/history_grafik_card.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import '../models/chart_data_point.dart'; // Import model yang terpisah
|
import '../models/chart_data_point.dart';
|
||||||
|
|
||||||
class HistoryGrafikCard extends StatelessWidget {
|
class HistoryGrafikCard extends StatelessWidget {
|
||||||
final List<ChartDataPoint> dataPoints; // Ubah dari dynamic ke List<ChartDataPoint>
|
final List<ChartDataPoint> dataPoints;
|
||||||
final VoidCallback? onBookmarkTap;
|
final VoidCallback? onBookmarkTap;
|
||||||
|
|
||||||
const HistoryGrafikCard({
|
const HistoryGrafikCard({
|
||||||
|
|
@ -15,6 +14,40 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (dataPoints.isEmpty) {
|
||||||
|
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]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hours = dataPoints.map((p) => p.hour.toDouble()).toList();
|
||||||
|
final drops = dataPoints.map((p) => p.dropsPerMinute.toDouble()).toList();
|
||||||
|
|
||||||
|
final minX = hours.reduce((a, b) => a < b ? a : b);
|
||||||
|
final maxX = hours.reduce((a, b) => a > b ? a : b);
|
||||||
|
final minY = (drops.reduce((a, b) => a < b ? a : b) - 5.0).clamp(
|
||||||
|
0.0,
|
||||||
|
double.infinity,
|
||||||
|
);
|
||||||
|
final maxY = drops.reduce((a, b) => a > b ? a : b) + 5.0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -33,34 +66,27 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Container(
|
||||||
children: [
|
padding: const EdgeInsets.all(8),
|
||||||
Container(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.all(8),
|
color: const Color(0xFFE3F2FD),
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: const Color(0xFFE3F2FD),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: const Icon(
|
||||||
),
|
Icons.insert_chart_outlined,
|
||||||
child: const Icon(
|
size: 20,
|
||||||
Icons.insert_chart_outlined,
|
color: Color(0xFF2196F3),
|
||||||
size: 20,
|
),
|
||||||
color: Color(0xFF2196F3),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
),
|
const Text(
|
||||||
const SizedBox(width: 12),
|
'Grafik Infus Pasien',
|
||||||
const Text(
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
'Grafik Infus Pasien',
|
|
||||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// FL Chart
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 180,
|
height: 180,
|
||||||
child: LineChart(
|
child: LineChart(
|
||||||
|
|
@ -68,7 +94,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
gridData: FlGridData(
|
gridData: FlGridData(
|
||||||
show: true,
|
show: true,
|
||||||
drawVerticalLine: false,
|
drawVerticalLine: false,
|
||||||
horizontalInterval: 5,
|
horizontalInterval: (maxY - minY) / 4,
|
||||||
getDrawingHorizontalLine: (value) {
|
getDrawingHorizontalLine: (value) {
|
||||||
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
||||||
},
|
},
|
||||||
|
|
@ -85,7 +111,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
reservedSize: 30,
|
reservedSize: 30,
|
||||||
interval: 1,
|
interval: dataPoints.length > 5 ? 2.0 : 1.0,
|
||||||
getTitlesWidget: (double value, TitleMeta meta) {
|
getTitlesWidget: (double value, TitleMeta meta) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
|
@ -104,7 +130,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
leftTitles: AxisTitles(
|
leftTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
interval: 5,
|
interval: (maxY - minY) / 4,
|
||||||
reservedSize: 35,
|
reservedSize: 35,
|
||||||
getTitlesWidget: (double value, TitleMeta meta) {
|
getTitlesWidget: (double value, TitleMeta meta) {
|
||||||
return Text(
|
return Text(
|
||||||
|
|
@ -126,17 +152,17 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
bottom: BorderSide(color: Colors.grey[300]!),
|
bottom: BorderSide(color: Colors.grey[300]!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
minX: 8,
|
minX: minX,
|
||||||
maxX: 17,
|
maxX: maxX,
|
||||||
minY: 20,
|
minY: minY,
|
||||||
maxY: 40,
|
maxY: maxY,
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
spots: dataPoints
|
spots: dataPoints
|
||||||
.map(
|
.map(
|
||||||
(point) => FlSpot(
|
(point) => FlSpot(
|
||||||
point.hour.toDouble(),
|
point.hour.toDouble(),
|
||||||
point.dropsPerMinute,
|
point.dropsPerMinute.toDouble(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/app/widgets/modal_patient_add_edit.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../modules/profile/controllers/profile_controller.dart';
|
import '../modules/profile/controllers/profile_controller.dart';
|
||||||
|
|
@ -7,11 +6,7 @@ class ModalPatientAddEdit extends StatefulWidget {
|
||||||
final Patient? patient;
|
final Patient? patient;
|
||||||
final bool isEdit;
|
final bool isEdit;
|
||||||
|
|
||||||
const ModalPatientAddEdit({
|
const ModalPatientAddEdit({super.key, this.patient, this.isEdit = false});
|
||||||
super.key,
|
|
||||||
this.patient,
|
|
||||||
this.isEdit = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ModalPatientAddEdit> createState() => _ModalPatientAddEditState();
|
State<ModalPatientAddEdit> createState() => _ModalPatientAddEditState();
|
||||||
|
|
@ -24,22 +19,25 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
late TextEditingController _roomController;
|
late TextEditingController _roomController;
|
||||||
late TextEditingController _deviceIdController;
|
late TextEditingController _deviceIdController;
|
||||||
late TextEditingController _guardianController;
|
late TextEditingController _guardianController;
|
||||||
|
late TextEditingController _emailController;
|
||||||
|
late TextEditingController _passwordController;
|
||||||
|
|
||||||
|
bool _isSaving = false;
|
||||||
|
bool _obscurePassword = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController = TextEditingController(
|
_initControllers();
|
||||||
text: widget.patient?.name ?? '',
|
}
|
||||||
);
|
|
||||||
_roomController = TextEditingController(
|
void _initControllers() {
|
||||||
text: widget.patient?.room ?? '',
|
_nameController = TextEditingController(text: widget.patient?.name ?? '');
|
||||||
);
|
_roomController = TextEditingController(text: widget.patient?.room ?? '');
|
||||||
_deviceIdController = TextEditingController(
|
_deviceIdController = TextEditingController(text: widget.patient?.deviceId ?? '');
|
||||||
text: widget.patient?.deviceId ?? '',
|
_guardianController = TextEditingController(text: widget.patient?.guardian ?? '');
|
||||||
);
|
_emailController = TextEditingController(text: widget.patient?.email ?? '');
|
||||||
_guardianController = TextEditingController(
|
_passwordController = TextEditingController();
|
||||||
text: widget.patient?.guardian ?? '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -48,6 +46,8 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
_roomController.dispose();
|
_roomController.dispose();
|
||||||
_deviceIdController.dispose();
|
_deviceIdController.dispose();
|
||||||
_guardianController.dispose();
|
_guardianController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,221 +58,336 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 500),
|
constraints: const BoxConstraints(maxWidth: 500),
|
||||||
decoration: BoxDecoration(
|
decoration: _dialogDecoration(),
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
_buildHeader(),
|
||||||
Container(
|
_buildForm(),
|
||||||
padding: const EdgeInsets.all(24),
|
_buildFooter(),
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(24),
|
|
||||||
topRight: Radius.circular(24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.person_add,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.isEdit ? 'Edit Pasien' : 'Tambah Pasien Baru',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
splashRadius: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Form Content
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Nama Pasien
|
|
||||||
_buildLabel('Nama Pasien', Icons.person),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTextField(
|
|
||||||
controller: _nameController,
|
|
||||||
hint: 'Contoh: Mrs. Shinta',
|
|
||||||
icon: Icons.person_outline,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Nama pasien harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Ruangan
|
|
||||||
_buildLabel('Ruangan', Icons.meeting_room),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTextField(
|
|
||||||
controller: _roomController,
|
|
||||||
hint: 'Contoh: Ruang Mawar 002',
|
|
||||||
icon: Icons.meeting_room_outlined,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Ruangan harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Device ID
|
|
||||||
_buildLabel('Device ID', Icons.devices),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTextField(
|
|
||||||
controller: _deviceIdController,
|
|
||||||
hint: 'Contoh: SI001',
|
|
||||||
icon: Icons.router_outlined,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Device ID harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Wali Pasien
|
|
||||||
_buildLabel('Wali Pasien', Icons.family_restroom),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTextField(
|
|
||||||
controller: _guardianController,
|
|
||||||
hint: 'Contoh: Budi Santoso',
|
|
||||||
icon: Icons.family_restroom_outlined,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Wali pasien harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom Buttons
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[50],
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(24),
|
|
||||||
bottomRight: Radius.circular(24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Batal',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.4),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _handleSave,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.isEdit ? 'Simpan' : 'Tambah',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoxDecoration _dialogDecoration() {
|
||||||
|
return BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(24),
|
||||||
|
topRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildHeaderIcon(),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.isEdit ? 'Edit Pasien' : 'Tambah Pasien Baru',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _isSaving ? null : () => Get.back(),
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderIcon() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
widget.isEdit ? Icons.edit : Icons.person_add,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildForm() {
|
||||||
|
return Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildNameField(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildGuardianField(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildEmailField(),
|
||||||
|
if (!widget.isEdit) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildPasswordField(),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildRoomField(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildDeviceIdField(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNameField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Nama Pasien', Icons.person),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _nameController,
|
||||||
|
hint: 'Contoh: Mrs. Shinta',
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
validator: (value) => value?.isEmpty ?? true ? 'Wajib diisi' : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGuardianField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Nama Wali', Icons.family_restroom),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _guardianController,
|
||||||
|
hint: 'Contoh: Budi Santoso',
|
||||||
|
icon: Icons.family_restroom_outlined,
|
||||||
|
validator: (value) => value?.isEmpty ?? true ? 'Wajib diisi' : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmailField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Email Wali', Icons.email),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _emailController,
|
||||||
|
hint: 'Contoh: wali@example.com',
|
||||||
|
icon: Icons.email_outlined,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
enabled: !widget.isEdit,
|
||||||
|
validator: (value) {
|
||||||
|
if (widget.isEdit) return null;
|
||||||
|
if (value?.isEmpty ?? true) return 'Wajib diisi';
|
||||||
|
if (!GetUtils.isEmail(value!)) return 'Email tidak valid';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.isEdit)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'Email tidak dapat diubah',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.orange[700],
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Password', Icons.lock),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
hint: 'Minimal 6 karakter',
|
||||||
|
icon: Icons.lock_outline,
|
||||||
|
obscureText: _obscurePassword,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value?.isEmpty ?? true) return 'Wajib diisi';
|
||||||
|
if (value!.length < 6) return 'Minimal 6 karakter';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRoomField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Ruangan', Icons.meeting_room),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _roomController,
|
||||||
|
hint: 'Contoh: Ruang Mawar',
|
||||||
|
icon: Icons.meeting_room_outlined,
|
||||||
|
validator: (value) => value?.isEmpty ?? true ? 'Wajib diisi' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'ID otomatis: "Ruang Mawar" → ruang_mawar',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDeviceIdField() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLabel('Device ID', Icons.devices),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildTextField(
|
||||||
|
controller: _deviceIdController,
|
||||||
|
hint: 'Contoh: INF-ICU-001',
|
||||||
|
icon: Icons.router_outlined,
|
||||||
|
validator: (value) {
|
||||||
|
if (value?.isEmpty ?? true) return 'Wajib diisi';
|
||||||
|
if (!RegExp(r'^[A-Z]+-[A-Z]+-\d{3}$').hasMatch(value!)) {
|
||||||
|
return 'Format: INF-ROOM-XXX';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Format: INF-ROOM-XXX',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooter() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(24),
|
||||||
|
bottomRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCancelButton()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: _buildSaveButton()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCancelButton() {
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: _isSaving ? null : () => Get.back(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Batal',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _isSaving ? Colors.grey[400] : Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFF0091EA).withOpacity(0.4),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isSaving ? null : _handleSave,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: _isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
widget.isEdit ? 'Simpan' : 'Tambah',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildLabel(String text, IconData icon) {
|
Widget _buildLabel(String text, IconData icon) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -295,15 +410,23 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
required String hint,
|
required String hint,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
String? Function(String?)? validator,
|
String? Function(String?)? validator,
|
||||||
|
bool obscureText = false,
|
||||||
|
Widget? suffixIcon,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
bool enabled = true,
|
||||||
}) {
|
}) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
enabled: !_isSaving && enabled,
|
||||||
|
obscureText: obscureText,
|
||||||
|
keyboardType: keyboardType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.grey[50],
|
fillColor: enabled ? Colors.grey[50] : Colors.grey[200],
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||||
|
|
@ -312,6 +435,10 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Color(0xFF0091EA), width: 2),
|
borderSide: const BorderSide(color: Color(0xFF0091EA), width: 2),
|
||||||
|
|
@ -329,58 +456,38 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSave() {
|
Future<void> _handleSave() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) return;
|
||||||
final controller = Get.find<ProfileController>();
|
|
||||||
|
|
||||||
|
setState(() => _isSaving = true);
|
||||||
|
|
||||||
|
final controller = Get.find<ProfileController>();
|
||||||
|
|
||||||
|
try {
|
||||||
if (widget.isEdit && widget.patient != null) {
|
if (widget.isEdit && widget.patient != null) {
|
||||||
// Update existing patient
|
await controller.updatePatient(
|
||||||
controller.updatePatient(
|
patientId: widget.patient!.id,
|
||||||
widget.patient!.id,
|
patientName: _nameController.text.trim(),
|
||||||
Patient(
|
guardianName: _guardianController.text.trim(),
|
||||||
id: widget.patient!.id,
|
roomName: _roomController.text.trim(),
|
||||||
name: _nameController.text.trim(),
|
deviceId: _deviceIdController.text.trim(),
|
||||||
room: _roomController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
deviceId: _deviceIdController.text.trim(),
|
newPassword: _passwordController.text.isNotEmpty
|
||||||
guardian: _guardianController.text.trim(),
|
? _passwordController.text.trim()
|
||||||
),
|
: null,
|
||||||
);
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Berhasil',
|
|
||||||
'Data pasien berhasil diperbarui',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
|
||||||
colorText: Colors.white,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Add new patient
|
await controller.addPatient(
|
||||||
controller.addPatient(
|
patientName: _nameController.text.trim(),
|
||||||
Patient(
|
guardianName: _guardianController.text.trim(),
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
roomName: _roomController.text.trim(),
|
||||||
name: _nameController.text.trim(),
|
deviceId: _deviceIdController.text.trim(),
|
||||||
room: _roomController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
deviceId: _deviceIdController.text.trim(),
|
password: _passwordController.text.trim(),
|
||||||
guardian: _guardianController.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Berhasil',
|
|
||||||
'Pasien baru berhasil ditambahkan',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
|
||||||
colorText: Colors.white,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isSaving = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// lib/app/widgets/modal_schedule_add_edit.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import '../modules/schedule/controllers/schedule_controller.dart';
|
import '../modules/schedule/controllers/schedule_controller.dart';
|
||||||
|
|
||||||
class ModalScheduleAddEdit extends StatefulWidget {
|
class ModalScheduleAddEdit extends StatefulWidget {
|
||||||
|
|
@ -16,55 +16,49 @@ class ModalScheduleAddEdit extends StatefulWidget {
|
||||||
class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
String? _selectedPatient;
|
String? _selectedPatientId;
|
||||||
String? _selectedTime; // Pagi, Siang, Sore, Malam
|
String? _selectedTime;
|
||||||
|
DateTime? _selectedDate;
|
||||||
late TextEditingController _medicineController;
|
late TextEditingController _medicineController;
|
||||||
late TextEditingController _fluidController;
|
late TextEditingController _fluidController;
|
||||||
|
|
||||||
// Dummy data pasien
|
List<Map<String, String>> _patients = [];
|
||||||
final List<Map<String, String>> _patients = [
|
bool _isLoadingPatients = true;
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'name': 'Mrs. Shinta',
|
|
||||||
'room': 'Ruang Mawar 002',
|
|
||||||
'device': 'SI001',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'name': 'Mr. Parjo',
|
|
||||||
'room': 'Ruang Melati 001',
|
|
||||||
'device': 'SI002',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'name': 'Mrs. Siti',
|
|
||||||
'room': 'Ruang Anggrek 003',
|
|
||||||
'device': 'SI003',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '4',
|
|
||||||
'name': 'Mr. Budi',
|
|
||||||
'room': 'Ruang Dahlia 004',
|
|
||||||
'device': 'SI004',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Waktu
|
|
||||||
final List<String> _times = ['Pagi', 'Siang', 'Sore', 'Malam'];
|
final List<String> _times = ['Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_medicineController = TextEditingController(
|
_medicineController = TextEditingController(text: widget.schedule?.medicineTime ?? '');
|
||||||
text: widget.schedule?.medicineTime ?? '',
|
_fluidController = TextEditingController(text: widget.schedule?.fluidTime ?? '');
|
||||||
);
|
_selectedDate = DateTime.now();
|
||||||
_fluidController = TextEditingController(
|
|
||||||
text: widget.schedule?.fluidTime ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.isEdit && widget.schedule != null) {
|
if (widget.isEdit && widget.schedule != null) {
|
||||||
_selectedPatient = widget.schedule!.id;
|
_selectedPatientId = widget.schedule!.id;
|
||||||
_selectedTime = 'Pagi'; // Default atau bisa diambil dari schedule
|
_selectedTime = widget.schedule!.timeOfDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadPatients();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPatients() async {
|
||||||
|
try {
|
||||||
|
final controller = Get.find<ScheduleController>();
|
||||||
|
final patientsList = await controller.getPatientsList();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_patients = patientsList;
|
||||||
|
_isLoadingPatients = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingPatients = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,8 +70,8 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String>? get _selectedPatientData {
|
Map<String, String>? get _selectedPatientData {
|
||||||
if (_selectedPatient == null) return null;
|
if (_selectedPatientId == null) return null;
|
||||||
return _patients.firstWhere((p) => p['id'] == _selectedPatient);
|
return _patients.firstWhereOrNull((p) => p['id'] == _selectedPatientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -101,194 +95,116 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
_buildHeader(),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(24),
|
|
||||||
topRight: Radius.circular(24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.calendar_today,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.isEdit ? 'Edit Jadwal' : 'Tambah Jadwal Baru',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
splashRadius: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Form Content
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: SingleChildScrollView(
|
child: _isLoadingPatients
|
||||||
padding: const EdgeInsets.all(24),
|
? const Center(
|
||||||
child: Form(
|
child: Padding(
|
||||||
key: _formKey,
|
padding: EdgeInsets.all(40),
|
||||||
child: Column(
|
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 1. Pilih Pasien (Search Dropdown)
|
|
||||||
_buildLabel('Pilih Pasien', Icons.person),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildPatientDropdown(),
|
|
||||||
|
|
||||||
// Info Pasien Terpilih
|
|
||||||
if (_selectedPatientData != null) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildSelectedPatientInfo(),
|
|
||||||
],
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// 2. Pilih Waktu (Pagi, Siang, Sore, Malam)
|
|
||||||
_buildLabel('Pilih Waktu', Icons.access_time),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTimeDropdown(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// 3. Informasi Obat
|
|
||||||
_buildLabel('Informasi Obat', Icons.medication),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildTextField(
|
|
||||||
controller: _medicineController,
|
|
||||||
hint: 'Contoh: Amoxicillin 3 ml',
|
|
||||||
icon: Icons.medical_services_outlined,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Informasi obat harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
// 4. Detail Cairan & Tetes
|
padding: const EdgeInsets.all(24),
|
||||||
_buildLabel('Detail Cairan & Tetes', Icons.water_drop),
|
child: Form(
|
||||||
const SizedBox(height: 8),
|
key: _formKey,
|
||||||
_buildTextField(
|
child: Column(
|
||||||
controller: _fluidController,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
hint: 'Contoh: Mnt 30 tetes/jam',
|
children: [
|
||||||
icon: Icons.opacity,
|
_buildLabel('Pilih Tanggal', Icons.calendar_today),
|
||||||
validator: (value) {
|
const SizedBox(height: 8),
|
||||||
if (value == null || value.isEmpty) {
|
_buildDatePicker(context),
|
||||||
return 'Detail cairan harus diisi';
|
const SizedBox(height: 20),
|
||||||
}
|
_buildLabel('Pilih Pasien', Icons.person),
|
||||||
return null;
|
const SizedBox(height: 8),
|
||||||
},
|
_buildPatientDropdown(),
|
||||||
),
|
if (_selectedPatientData != null) ...[
|
||||||
],
|
const SizedBox(height: 12),
|
||||||
),
|
_buildSelectedPatientInfo(),
|
||||||
),
|
],
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
),
|
_buildLabel('Pilih Waktu', Icons.access_time),
|
||||||
|
const SizedBox(height: 8),
|
||||||
// Bottom Buttons
|
_buildTimeDropdown(),
|
||||||
Container(
|
const SizedBox(height: 20),
|
||||||
padding: const EdgeInsets.all(24),
|
_buildLabel('Informasi Obat', Icons.medication),
|
||||||
decoration: BoxDecoration(
|
const SizedBox(height: 8),
|
||||||
color: Colors.grey[50],
|
_buildTextField(
|
||||||
borderRadius: const BorderRadius.only(
|
controller: _medicineController,
|
||||||
bottomLeft: Radius.circular(24),
|
hint: 'Contoh: Amoxicillin 3 ml',
|
||||||
bottomRight: Radius.circular(24),
|
validator: (value) {
|
||||||
),
|
if (value == null || value.isEmpty) {
|
||||||
),
|
return 'Informasi obat harus diisi';
|
||||||
child: Row(
|
}
|
||||||
children: [
|
return null;
|
||||||
Expanded(
|
},
|
||||||
child: OutlinedButton(
|
),
|
||||||
onPressed: () => Get.back(),
|
const SizedBox(height: 20),
|
||||||
style: OutlinedButton.styleFrom(
|
_buildLabel('Detail Cairan & Tetes', Icons.water_drop),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
const SizedBox(height: 8),
|
||||||
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
_buildTextField(
|
||||||
shape: RoundedRectangleBorder(
|
controller: _fluidController,
|
||||||
borderRadius: BorderRadius.circular(12),
|
hint: 'Contoh: Mnt 30 tetes/jam',
|
||||||
),
|
validator: (value) {
|
||||||
),
|
if (value == null || value.isEmpty) {
|
||||||
child: Text(
|
return 'Detail cairan harus diisi';
|
||||||
'Batal',
|
}
|
||||||
style: TextStyle(
|
return null;
|
||||||
fontSize: 16,
|
},
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
color: Colors.grey[700],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.4),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _handleSave,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.isEdit ? 'Simpan' : 'Tambah',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
_buildBottomButtons(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: [Color(0xFF0091EA), Color(0xFF0277BD)]),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(24),
|
||||||
|
topRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.calendar_today, color: Colors.white, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.isEdit ? 'Edit Jadwal' : 'Tambah Jadwal Baru',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildLabel(String text, IconData icon) {
|
Widget _buildLabel(String text, IconData icon) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -306,9 +222,80 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDatePicker(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _selectedDate ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
builder: (context, child) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: Color(0xFF0091EA),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSurface: Color(0xFF424242),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDate = picked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.calendar_today, size: 18, color: Color(0xFF0091EA)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
_selectedDate != null
|
||||||
|
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(_selectedDate!)
|
||||||
|
: 'Pilih tanggal...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _selectedDate != null ? const Color(0xFF424242) : Colors.grey[400],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(Icons.arrow_drop_down, color: Colors.grey[600]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildPatientDropdown() {
|
Widget _buildPatientDropdown() {
|
||||||
|
if (_patients.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Tidak ada pasien tersedia',
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
value: _selectedPatient,
|
value: _selectedPatientId,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cari dan pilih pasien...',
|
hintText: 'Cari dan pilih pasien...',
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
|
|
@ -330,10 +317,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent),
|
borderSide: const BorderSide(color: Colors.redAccent),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
horizontal: 16,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
|
@ -352,7 +336,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedPatient = value;
|
_selectedPatientId = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
|
|
@ -364,117 +348,99 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedPatientInfo() {
|
Widget _buildSelectedPatientInfo() {
|
||||||
final patient = _selectedPatientData!;
|
final patient = _selectedPatientData!;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.05),
|
color: const Color(0xFF0091EA).withOpacity(0.05),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.2),
|
color: const Color(0xFF0091EA).withOpacity(0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF0091EA).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.person,
|
|
||||||
color: Color(0xFF0091EA),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Column(
|
Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
// ===== NAMA =====
|
color: const Color(0xFF0091EA).withOpacity(0.1),
|
||||||
Text(
|
borderRadius: BorderRadius.circular(8),
|
||||||
patient['name']!,
|
),
|
||||||
style: const TextStyle(
|
child: const Icon(Icons.person, color: Color(0xFF0091EA), size: 20),
|
||||||
fontSize: 15,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
const SizedBox(width: 12),
|
||||||
color: Color(0xFF424242),
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
patient['name']!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
Row(
|
||||||
|
children: [
|
||||||
// ===== ROOM + DEVICE ID =====
|
Container(
|
||||||
Row(
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
// ROOM
|
color: const Color(0xFFE3F2FD),
|
||||||
Container(
|
borderRadius: BorderRadius.circular(6),
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 10,
|
child: Text(
|
||||||
vertical: 4,
|
patient['room']!,
|
||||||
),
|
style: const TextStyle(
|
||||||
decoration: BoxDecoration(
|
fontSize: 11,
|
||||||
color: const Color(0xFFE3F2FD),
|
color: Color(0xFF2196F3),
|
||||||
borderRadius: BorderRadius.circular(6),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
child: Text(
|
|
||||||
patient['room']!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
const SizedBox(width: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
// DEVICE ID
|
color: Colors.grey[100],
|
||||||
Container(
|
borderRadius: BorderRadius.circular(6),
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 10,
|
child: Row(
|
||||||
vertical: 4,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: Colors.grey[100],
|
width: 6,
|
||||||
borderRadius: BorderRadius.circular(6),
|
height: 6,
|
||||||
),
|
decoration: const BoxDecoration(
|
||||||
child: Row(
|
color: Color(0xFF4CAF50),
|
||||||
mainAxisSize: MainAxisSize.min,
|
shape: BoxShape.circle,
|
||||||
children: [
|
),
|
||||||
Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF4CAF50),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
const SizedBox(width: 6),
|
Text(
|
||||||
Text(
|
patient['device']!,
|
||||||
patient['device']!,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 11,
|
||||||
fontSize: 11,
|
color: Colors.grey[700],
|
||||||
color: Colors.grey[700],
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTimeDropdown() {
|
Widget _buildTimeDropdown() {
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
|
|
@ -500,10 +466,7 @@ Widget _buildSelectedPatientInfo() {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent),
|
borderSide: const BorderSide(color: Colors.redAccent),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
horizontal: 16,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
|
@ -568,7 +531,6 @@ Widget _buildSelectedPatientInfo() {
|
||||||
Widget _buildTextField({
|
Widget _buildTextField({
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required String hint,
|
required String hint,
|
||||||
required IconData icon,
|
|
||||||
String? Function(String?)? validator,
|
String? Function(String?)? validator,
|
||||||
}) {
|
}) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
|
@ -599,73 +561,112 @@ Widget _buildSelectedPatientInfo() {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
|
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
horizontal: 16,
|
),
|
||||||
vertical: 14,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomButtons() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(24),
|
||||||
|
bottomRight: Radius.circular(24),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Batal',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFF0091EA).withOpacity(0.4),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _handleSave,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.isEdit ? 'Simpan' : 'Tambah',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSave() {
|
void _handleSave() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
if (_selectedDate == null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Pilih tanggal terlebih dahulu',
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final controller = Get.find<ScheduleController>();
|
final controller = Get.find<ScheduleController>();
|
||||||
final selectedPatientData = _patients.firstWhere(
|
|
||||||
(p) => p['id'] == _selectedPatient,
|
final schedule = PatientSchedule(
|
||||||
|
id: _selectedPatientId!,
|
||||||
|
name: _selectedPatientData!['name']!,
|
||||||
|
room: _selectedPatientData!['room']!,
|
||||||
|
deviceId: _selectedPatientData!['device']!,
|
||||||
|
medicineTime: _medicineController.text.trim(),
|
||||||
|
fluidTime: _fluidController.text.trim(),
|
||||||
|
timeOfDay: _selectedTime!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isEdit && widget.schedule != null) {
|
if (widget.isEdit && widget.schedule != null) {
|
||||||
controller.updateSchedule(
|
controller.updateSchedule(widget.schedule!.id, schedule, _selectedDate!);
|
||||||
widget.schedule!.id,
|
|
||||||
PatientSchedule(
|
|
||||||
id: widget.schedule!.id,
|
|
||||||
name: selectedPatientData['name']!,
|
|
||||||
room: selectedPatientData['room']!,
|
|
||||||
deviceId: selectedPatientData['device']!,
|
|
||||||
medicineType: 'Obat & Dosis',
|
|
||||||
fluidType: 'Cairan & Tetes',
|
|
||||||
medicineTime: _medicineController.text.trim(),
|
|
||||||
fluidTime: _fluidController.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Berhasil',
|
|
||||||
'Jadwal $_selectedTime berhasil diperbarui',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
|
||||||
colorText: Colors.white,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
controller.addSchedule(
|
controller.addSchedule(schedule, _selectedDate!);
|
||||||
PatientSchedule(
|
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
|
||||||
name: selectedPatientData['name']!,
|
|
||||||
room: selectedPatientData['room']!,
|
|
||||||
deviceId: selectedPatientData['device']!,
|
|
||||||
medicineType: 'Obat & Dosis',
|
|
||||||
fluidType: 'Cairan & Tetes',
|
|
||||||
medicineTime: _medicineController.text.trim(),
|
|
||||||
fluidTime: _fluidController.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Berhasil',
|
|
||||||
'Jadwal $_selectedTime berhasil ditambahkan',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
|
||||||
colorText: Colors.white,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Get.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
// lib/app/widgets/monitoring_card.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import '../modules/home/controllers/home_controller.dart';
|
import '../modules/home/controllers/home_controller.dart';
|
||||||
|
|
||||||
class MonitoringCard extends StatelessWidget {
|
class MonitoringCard extends StatelessWidget {
|
||||||
final InfusMonitoring item;
|
final InfusMonitoring monitoring;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const MonitoringCard({super.key, required this.item, this.onTap});
|
const MonitoringCard({
|
||||||
|
super.key,
|
||||||
|
required this.monitoring,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isConnected = monitoring.deviceStatus == 'connected';
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
|
@ -20,7 +24,7 @@ class MonitoringCard extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: Colors.grey.shade200, width: 1.5),
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.08),
|
color: Colors.grey.withOpacity(0.08),
|
||||||
|
|
@ -30,155 +34,166 @@ class MonitoringCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
// LEFT ICON
|
_buildIcon(isConnected),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE3F2FD),
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.water_drop_outlined,
|
|
||||||
size: 36,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
Expanded(child: _buildContent(isConnected)),
|
||||||
// RIGHT CONTENT
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// DROPS
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
item.dropsPerMinute.split(' ')[0],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 3),
|
|
||||||
child: Text(
|
|
||||||
'tetes/menit',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
// LAST UPDATE
|
|
||||||
Text(
|
|
||||||
'Last update • ${Get.find<HomeController>().getLastUpdateText(item.lastUpdate)}',
|
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// ROOM & DEVICE
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
// ROOM BADGE
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE3F2FD),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
item.room,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// DEVICE BADGE
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF4CAF50),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
item.deviceId,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// PATIENT NAME BADGE (AUTO WIDTH - GREEN THEME)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE8F5E9), // hijau soft
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
item.patientName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF2E7D32),
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Widget _buildIcon(bool isConnected) {
|
||||||
|
return 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: isConnected ? const Color(0xFF4CAF50) : Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(bool isConnected) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDropRate(),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildLastUpdate(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildPatientInfo(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildBadges(isConnected),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDropRate() {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
monitoring.dropsPerMinute.replaceAll(' tpm', ''),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLastUpdate() {
|
||||||
|
return Text(
|
||||||
|
'Last update: ${_getLastUpdateText(monitoring.lastUpdate)}',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPatientInfo() {
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||||
|
children: [
|
||||||
|
const TextSpan(
|
||||||
|
text: 'Nama Pasien: ',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: monitoring.patientName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBadges(bool isConnected) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
_buildBadge(
|
||||||
|
monitoring.deviceId,
|
||||||
|
const Color(0xFFE3F2FD),
|
||||||
|
const Color(0xFF2196F3),
|
||||||
|
),
|
||||||
|
_buildBadge(
|
||||||
|
monitoring.room,
|
||||||
|
Colors.grey[100]!,
|
||||||
|
Colors.grey[700]!,
|
||||||
|
),
|
||||||
|
_buildBadge(
|
||||||
|
isConnected ? 'Online' : 'Offline',
|
||||||
|
isConnected ? const Color(0xFFE8F5E9) : Colors.red[50]!,
|
||||||
|
isConnected ? const Color(0xFF2E7D32) : Colors.red[700]!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getLastUpdateText(DateTime lastUpdate) {
|
||||||
|
final diff = DateTime.now().difference(lastUpdate);
|
||||||
|
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
|
||||||
|
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||||
|
return '${diff.inDays}d ago';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/app/widgets/schedule_card.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../modules/schedule/controllers/schedule_controller.dart';
|
import '../modules/schedule/controllers/schedule_controller.dart';
|
||||||
|
|
||||||
|
|
@ -34,238 +33,203 @@ class ScheduleCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// ================= HEADER =================
|
_buildHeader(),
|
||||||
Row(
|
const SizedBox(height: 12),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Container(height: 1, color: Colors.grey.shade200),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildMedicineAndFluid(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Text(
|
||||||
child: Column(
|
schedule.name,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: const TextStyle(
|
||||||
children: [
|
fontWeight: FontWeight.bold,
|
||||||
Text(
|
fontSize: 16,
|
||||||
schedule.name,
|
color: Color(0xFF424242),
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: Color(0xFF424242),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// ===== ROOM =====
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE3F2FD),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
schedule.room,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
|
|
||||||
// ===== DEVICE ID =====
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[100],
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF4CAF50),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
schedule.deviceId, // ✅ pastikan ada di model
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
/// ACTION BUTTONS
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// ===== EDIT =====
|
Container(
|
||||||
InkWell(
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
onTap: onEditTap,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
color: const Color(0xFFE3F2FD),
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(6),
|
||||||
padding: const EdgeInsets.all(6),
|
),
|
||||||
decoration: BoxDecoration(
|
child: Text(
|
||||||
color: Colors.amber.withOpacity(0.15),
|
schedule.room,
|
||||||
borderRadius: BorderRadius.circular(8),
|
style: const TextStyle(
|
||||||
),
|
fontSize: 11,
|
||||||
child: const Icon(
|
color: Color(0xFF2196F3),
|
||||||
Icons.edit,
|
fontWeight: FontWeight.w600,
|
||||||
size: 18,
|
|
||||||
color: Colors.amber,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
// ===== DELETE =====
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
InkWell(
|
decoration: BoxDecoration(
|
||||||
onTap: onDeleteTap,
|
color: Colors.grey[100],
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(6),
|
||||||
child: Container(
|
),
|
||||||
padding: const EdgeInsets.all(6),
|
child: Row(
|
||||||
decoration: BoxDecoration(
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: Colors.redAccent.withOpacity(0.15),
|
children: [
|
||||||
borderRadius: BorderRadius.circular(8),
|
Container(
|
||||||
),
|
width: 6,
|
||||||
child: const Icon(
|
height: 6,
|
||||||
Icons.delete,
|
decoration: const BoxDecoration(
|
||||||
size: 18,
|
color: Color(0xFF4CAF50),
|
||||||
color: Colors.redAccent,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
schedule.deviceId,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
Row(
|
||||||
|
children: [
|
||||||
// ================= DIVIDER =================
|
InkWell(
|
||||||
Container(height: 1, color: Colors.grey.shade200),
|
onTap: onEditTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
const SizedBox(height: 16),
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
// ================= MEDICINE & FLUID =================
|
decoration: BoxDecoration(
|
||||||
Row(
|
color: Colors.amber.withOpacity(0.15),
|
||||||
children: [
|
borderRadius: BorderRadius.circular(8),
|
||||||
// -------- Medicine --------
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF2196F3).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.medication,
|
|
||||||
size: 16,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
schedule.medicineType,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: Text(
|
|
||||||
schedule.medicineTime,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFF424242),
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
child: const Icon(Icons.edit, size: 18, color: Colors.amber),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
InkWell(
|
||||||
|
onTap: onDeleteTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.redAccent.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.delete, size: 18, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SizedBox(width: 16),
|
Widget _buildMedicineAndFluid() {
|
||||||
|
return Row(
|
||||||
// -------- Fluid --------
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF00BCD4).withOpacity(0.1),
|
color: const Color(0xFF2196F3).withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.water_drop,
|
|
||||||
size: 16,
|
|
||||||
color: Color(0xFF00BCD4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
schedule.fluidType,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
child: const Icon(Icons.medication, size: 16, color: Color(0xFF2196F3)),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: 4),
|
const SizedBox(width: 8),
|
||||||
child: Text(
|
Text(
|
||||||
schedule.fluidTime,
|
'Obat & Dosis',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF424242),
|
color: Colors.grey[600],
|
||||||
fontSize: 13,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: Text(
|
||||||
|
schedule.medicineTime,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF00BCD4).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.water_drop, size: 16, color: Color(0xFF00BCD4)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Cairan & Tetes',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: Text(
|
||||||
|
schedule.fluidTime,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,34 +1,23 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'app/routes/app_pages.dart';
|
import 'app/routes/app_pages.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Initialize Firebase
|
await Firebase.initializeApp();
|
||||||
await Firebase.initializeApp();
|
await GetStorage.init();
|
||||||
|
await initializeDateFormatting('id_ID', null);
|
||||||
await GetStorage.init();
|
|
||||||
await GetStorage.init('app_data');
|
|
||||||
|
|
||||||
await initializeDateFormatting('id_ID', null);
|
runApp(
|
||||||
|
GetMaterialApp(
|
||||||
runApp(const MyApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GetMaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: "SMART INFUSION",
|
title: "SMART INFUSE",
|
||||||
initialRoute: Routes.SPLASH_SCREEN,
|
initialRoute: Routes.SPLASH_SCREEN,
|
||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ dependencies:
|
||||||
firebase_auth: ^6.1.4
|
firebase_auth: ^6.1.4
|
||||||
firebase_database: ^12.1.2
|
firebase_database: ^12.1.2
|
||||||
cloud_firestore: ^6.1.2
|
cloud_firestore: ^6.1.2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue