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"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</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>
|
||||
</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 {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
id ("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
|
|||
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 {
|
||||
final int hour;
|
||||
final double dropsPerMinute;
|
||||
|
|
@ -7,4 +6,9 @@ class ChartDataPoint {
|
|||
required this.hour,
|
||||
required this.dropsPerMinute,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChartDataPoint(hour: $hour, dropsPerMinute: $dropsPerMinute)';
|
||||
}
|
||||
}
|
||||
|
|
@ -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 'dart:async';
|
||||
import '../../../models/patient_model.dart';
|
||||
import '../../../models/device_model.dart';
|
||||
import '../../../models/realtime_monitoring_model.dart';
|
||||
import '../../../models/history_model.dart';
|
||||
import '../../../models/chart_data_point.dart';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../services/realtime_database_service.dart';
|
||||
import '../../../widgets/app_snackbar.dart';
|
||||
|
||||
class DeviceDetailController extends GetxController {
|
||||
//TODO: Implement DeviceDetailController
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||
|
||||
String? deviceId;
|
||||
String? roomId;
|
||||
|
||||
final deviceName = ''.obs;
|
||||
final patientName = ''.obs;
|
||||
final roomName = ''.obs;
|
||||
final dropsPerMinute = '0'.obs;
|
||||
final lastUpdate = DateTime.now().obs;
|
||||
final isServoActive = false.obs;
|
||||
final deviceStatus = 'disconnected'.obs;
|
||||
final isLoading = true.obs;
|
||||
|
||||
final RxList<ChartDataPoint> chartData = <ChartDataPoint>[].obs;
|
||||
|
||||
StreamSubscription? _realtimeSubscription;
|
||||
StreamSubscription? _historySubscription;
|
||||
|
||||
PatientModel? patient;
|
||||
DeviceModel? device;
|
||||
RealtimeMonitoringModel? realtimeData;
|
||||
|
||||
final count = 0.obs;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_getArguments();
|
||||
if (deviceId != null && roomId != null) {
|
||||
_initializeDeviceDetail();
|
||||
} else {
|
||||
Get.back();
|
||||
AppSnackbar.error('Device ID tidak ditemukan');
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_realtimeSubscription?.cancel();
|
||||
_historySubscription?.cancel();
|
||||
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:get/get.dart';
|
||||
|
||||
import '../../../widgets/history_grafik_card.dart';
|
||||
import '../controllers/device_detail_controller.dart';
|
||||
|
||||
class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||
const DeviceDetailView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: const Text('DeviceDetailView'),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
body: const Center(
|
||||
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: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF2196F3)),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.refreshData,
|
||||
color: const Color(0xFF2196F3),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSection(
|
||||
title: 'Monitoring Infus',
|
||||
onSeeAll: () {},
|
||||
child: _buildMonitoringCard(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildSection(
|
||||
title: 'Kontrol Alat',
|
||||
onSeeAll: () {},
|
||||
child: _buildControlCard(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildSection(
|
||||
title: 'History Data',
|
||||
onSeeAll: controller.navigateToHistory,
|
||||
child: Obx(() {
|
||||
if (controller.chartData.isEmpty) {
|
||||
return _buildEmptyChart();
|
||||
}
|
||||
return HistoryGrafikCard(
|
||||
dataPoints: controller.chartData.value,
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection({
|
||||
required String title,
|
||||
required Widget child,
|
||||
VoidCallback? onSeeAll,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
GestureDetector(
|
||||
onTap: onSeeAll,
|
||||
child: const Text(
|
||||
'See all',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF2196F3),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonitoringCard() {
|
||||
return Obx(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.water_drop_outlined,
|
||||
size: 32,
|
||||
color: Color(0xFF2196F3),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.deviceStatus.value == 'connected'
|
||||
? const Color(0xFF4CAF50)
|
||||
: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
controller.dropsPerMinute.value,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2196F3),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
'DeviceDetailView is working',
|
||||
style: TextStyle(fontSize: 20),
|
||||
'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:flutter/material.dart';
|
||||
import '../../../models/chart_data_point.dart'; // Import model yang terpisah
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:async';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../services/realtime_database_service.dart';
|
||||
import '../../../models/patient_model.dart';
|
||||
import '../../../models/chart_data_point.dart';
|
||||
import '../../../models/history_model.dart';
|
||||
import '../../../models/notification_model.dart';
|
||||
import '../../../models/realtime_monitoring_model.dart';
|
||||
import '../../../widgets/app_snackbar.dart';
|
||||
|
||||
class InfusData {
|
||||
final String dropsPerMinute;
|
||||
final String room;
|
||||
final String deviceId;
|
||||
final String updateTime;
|
||||
final String deviceStatus;
|
||||
|
||||
InfusData({
|
||||
required this.dropsPerMinute,
|
||||
required this.room,
|
||||
required this.deviceId,
|
||||
required this.updateTime,
|
||||
required this.deviceStatus,
|
||||
});
|
||||
}
|
||||
|
||||
class NotificationData {
|
||||
final String id;
|
||||
final String title;
|
||||
final String message;
|
||||
final String room;
|
||||
final String deviceId;
|
||||
final String timeAgo;
|
||||
final DateTime createdAt;
|
||||
|
||||
NotificationData({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.room,
|
||||
required this.deviceId,
|
||||
required this.timeAgo,
|
||||
required this.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
class HomePatientController extends GetxController {
|
||||
// Patient info
|
||||
final patientName = 'Mrs. Shinta'.obs;
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||
|
||||
final deviceId = ''.obs;
|
||||
final roomId = ''.obs;
|
||||
final patientName = 'Patient'.obs;
|
||||
final userEmail = ''.obs;
|
||||
|
||||
// Banner carousel
|
||||
final currentCarouselIndex = 0.obs;
|
||||
final bannerImages = [
|
||||
'assets/images/banner-slider3.png',
|
||||
'assets/images/banner-slider4.png',
|
||||
];
|
||||
|
||||
// Current monitoring data
|
||||
final currentInfusData = InfusData(
|
||||
dropsPerMinute: '30 tetes per menit',
|
||||
room: 'MAWAR 002',
|
||||
deviceId: 'SI001',
|
||||
updateTime: '5min ago',
|
||||
dropsPerMinute: '0',
|
||||
room: '-',
|
||||
deviceId: '-',
|
||||
updateTime: '-',
|
||||
deviceStatus: 'disconnected',
|
||||
).obs;
|
||||
|
||||
// Chart data dengan 10 data points
|
||||
final chartDataPoints = <ChartDataPoint>[
|
||||
ChartDataPoint(hour: 8, dropsPerMinute: 25),
|
||||
ChartDataPoint(hour: 9, dropsPerMinute: 28),
|
||||
ChartDataPoint(hour: 10, dropsPerMinute: 30),
|
||||
ChartDataPoint(hour: 11, dropsPerMinute: 32),
|
||||
ChartDataPoint(hour: 12, dropsPerMinute: 35),
|
||||
ChartDataPoint(hour: 13, dropsPerMinute: 33),
|
||||
ChartDataPoint(hour: 14, dropsPerMinute: 30),
|
||||
ChartDataPoint(hour: 15, dropsPerMinute: 28),
|
||||
ChartDataPoint(hour: 16, dropsPerMinute: 26),
|
||||
ChartDataPoint(hour: 17, dropsPerMinute: 30),
|
||||
].obs;
|
||||
final chartDataPoints = <ChartDataPoint>[].obs;
|
||||
final notificationsList = <NotificationData>[].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;
|
||||
final isLoading = true.obs;
|
||||
final isLoadingHistory = true.obs;
|
||||
final isLoadingNotifications = true.obs;
|
||||
|
||||
StreamSubscription? _realtimeSubscription;
|
||||
StreamSubscription? _notificationsSubscription;
|
||||
StreamSubscription? _historySubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadDataFromArguments();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -96,14 +95,255 @@ class HomePatientController extends GetxController {
|
|||
|
||||
@override
|
||||
void onClose() {
|
||||
_realtimeSubscription?.cancel();
|
||||
_notificationsSubscription?.cancel();
|
||||
_historySubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _loadDataFromArguments() async {
|
||||
try {
|
||||
final args = Get.arguments as Map<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) {
|
||||
currentCarouselIndex.value = index;
|
||||
}
|
||||
|
||||
// Show call dialog
|
||||
void showCallDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -116,36 +356,161 @@ class HomePatientController extends GetxController {
|
|||
'Hubungi Perawat',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: const Text('Apakah Anda ingin menghubungi perawat?'),
|
||||
content: const Text(
|
||||
'Apakah Anda ingin menghubungi perawat melalui WhatsApp?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
// Implement call functionality
|
||||
Get.snackbar(
|
||||
'Menghubungi',
|
||||
'Sedang menghubungi perawat...',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.blue,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
await _openWhatsApp();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
backgroundColor: const Color(
|
||||
0xFF25D366,
|
||||
), // WhatsApp green color
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Hubungi'),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.phone, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Hubungi'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,11 +1,11 @@
|
|||
// lib/app/modules/home_patient/views/home_patient_view.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/home_patient_controller.dart';
|
||||
import '../../../widgets/banner_slider.dart';
|
||||
import '../../../widgets/monitoring_patient_card.dart';
|
||||
import '../../../widgets/monitoring_card.dart';
|
||||
import '../../../widgets/history_grafik_card.dart';
|
||||
import '../../../widgets/notification_patient_card.dart';
|
||||
import '../../home/controllers/home_controller.dart';
|
||||
|
||||
class HomePatientView extends GetView<HomePatientController> {
|
||||
const HomePatientView({super.key});
|
||||
|
|
@ -17,7 +17,10 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: controller.refreshData,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -27,31 +30,35 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Obx(
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'Halo ${controller.patientName.value}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
),
|
||||
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,
|
||||
child: const Icon(Icons.logout, color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Banner Slider - Gunakan widget universal
|
||||
// Banner Slider
|
||||
BannerSlider(
|
||||
bannerImages: controller.bannerImages,
|
||||
currentCarouselIndex: controller.currentCarouselIndex,
|
||||
|
|
@ -74,7 +81,9 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
// TODO: Navigate to monitoring detail
|
||||
},
|
||||
child: const Text(
|
||||
'See all',
|
||||
style: TextStyle(
|
||||
|
|
@ -87,20 +96,44 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
),
|
||||
|
||||
// Monitoring Card
|
||||
// Monitoring Card - Gunakan MonitoringCard dari widgets
|
||||
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,
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final infusData = controller.currentInfusData.value;
|
||||
|
||||
// Konversi ke InfusMonitoring untuk MonitoringCard
|
||||
final monitoring = InfusMonitoring(
|
||||
id: controller.deviceId.value,
|
||||
patientName: controller.patientName.value,
|
||||
deviceId: infusData.deviceId,
|
||||
room: infusData.room,
|
||||
roomId: '', // Not needed for display
|
||||
dropsPerMinute: infusData.dropsPerMinute,
|
||||
lastUpdate:
|
||||
DateTime.now(), // Will be calculated from updateTime
|
||||
deviceStatus: infusData.deviceStatus,
|
||||
);
|
||||
|
||||
return MonitoringCard(
|
||||
monitoring: monitoring,
|
||||
onTap: () {
|
||||
// Handle bookmark tap
|
||||
// TODO: Navigate to detail
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
|
@ -119,7 +152,9 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
// TODO: Navigate to history detail
|
||||
},
|
||||
child: const Text(
|
||||
'See all',
|
||||
style: TextStyle(
|
||||
|
|
@ -132,17 +167,29 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
),
|
||||
|
||||
// Chart Card
|
||||
// Chart Card - Gunakan HistoryGrafikCard dari widgets
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Obx(
|
||||
() => HistoryGrafikCard(
|
||||
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: () {
|
||||
// Handle bookmark tap
|
||||
// TODO: Bookmark functionality
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
|
@ -161,7 +208,9 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
// TODO: Navigate to notifications
|
||||
},
|
||||
child: const Text(
|
||||
'See all',
|
||||
style: TextStyle(
|
||||
|
|
@ -175,8 +224,48 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
),
|
||||
|
||||
// Notifications List
|
||||
Obx(
|
||||
() => ListView.builder(
|
||||
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),
|
||||
|
|
@ -190,27 +279,25 @@ class HomePatientView extends GetView<HomePatientController> {
|
|||
deviceId: notification.deviceId,
|
||||
timeAgo: notification.timeAgo,
|
||||
onTap: () {
|
||||
// Handle notification tap
|
||||
// TODO: Handle notification tap
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Floating Action Button - Call Nurse
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => controller.showCallDialog(context),
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
child: const Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: const Icon(Icons.phone, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,146 @@
|
|||
// lib/app/modules/home/controllers/home_controller.dart
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'dart:async';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../services/realtime_database_service.dart';
|
||||
import '../../../models/patient_model.dart';
|
||||
import '../../../models/room_model.dart';
|
||||
import '../../../models/realtime_monitoring_model.dart';
|
||||
import '../../../widgets/app_snackbar.dart';
|
||||
|
||||
class InfusMonitoring {
|
||||
final String id;
|
||||
final String patientName;
|
||||
final String deviceId;
|
||||
final String room;
|
||||
final String roomId;
|
||||
final String dropsPerMinute;
|
||||
final DateTime lastUpdate;
|
||||
final DateTime createdAt; // Tambahkan properti createdAt
|
||||
final String deviceStatus;
|
||||
|
||||
InfusMonitoring({
|
||||
required this.id,
|
||||
required this.patientName,
|
||||
required this.deviceId,
|
||||
required this.room,
|
||||
required this.roomId,
|
||||
required this.dropsPerMinute,
|
||||
required this.lastUpdate,
|
||||
required this.createdAt, // Pastikan ini diisi saat pembuatan objek
|
||||
required this.deviceStatus,
|
||||
});
|
||||
}
|
||||
|
||||
class HomeController extends GetxController {
|
||||
final searchController = TextEditingController();
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||
|
||||
// Banner carousel
|
||||
final currentCarouselIndex = 0.obs;
|
||||
final bannerImages = [
|
||||
'assets/images/banner-slider1.png',
|
||||
'assets/images/banner-slider2.png',
|
||||
];
|
||||
|
||||
final monitoringList = <InfusMonitoring>[
|
||||
InfusMonitoring(
|
||||
id: '1',
|
||||
patientName: 'Mrs. Shinta',
|
||||
deviceId: 'SI001',
|
||||
room: 'MAWAR 002',
|
||||
dropsPerMinute: '30 tetes per menit',
|
||||
lastUpdate: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
createdAt: DateTime.now(), // Menambahkan createdAt
|
||||
),
|
||||
InfusMonitoring(
|
||||
id: '2',
|
||||
patientName: 'Mr. Suparjo',
|
||||
deviceId: 'SI001',
|
||||
room: 'MELATI 001',
|
||||
dropsPerMinute: '30 tetes per menit',
|
||||
lastUpdate: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
createdAt: DateTime.now(), // Menambahkan createdAt
|
||||
),
|
||||
].obs;
|
||||
final monitoringList = <InfusMonitoring>[].obs;
|
||||
final isLoading = true.obs;
|
||||
|
||||
StreamSubscription? _patientsSubscription;
|
||||
StreamSubscription? _realtimeSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeMonitoring();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
_patientsSubscription?.cancel();
|
||||
_realtimeSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _initializeMonitoring() {
|
||||
isLoading.value = true;
|
||||
|
||||
_patientsSubscription = _firestoreService.getPatientsStream().listen(
|
||||
(patients) async {
|
||||
_realtimeSubscription = _realtimeService.getAllDevicesStream().listen((
|
||||
realtimeDevices,
|
||||
) {
|
||||
_combineMonitoringData(patients, realtimeDevices);
|
||||
});
|
||||
},
|
||||
onError: (error) {
|
||||
isLoading.value = false;
|
||||
AppSnackbar.error('Gagal memuat data');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _combineMonitoringData(
|
||||
List<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) {
|
||||
currentCarouselIndex.value = index;
|
||||
}
|
||||
|
|
@ -68,33 +150,54 @@ class HomeController extends GetxController {
|
|||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
title: const Text(
|
||||
'Konfirmasi Logout',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
content: const Text(
|
||||
'Yakin ingin keluar?',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
content: const Text('Apakah Anda yakin ingin keluar?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey[600],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
// Navigasi ke halaman login
|
||||
await FirebaseAuth.instance.signOut();
|
||||
Get.offAllNamed('/login');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Logout'),
|
||||
child: const Text(
|
||||
'Logout',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -105,7 +208,7 @@ class HomeController extends GetxController {
|
|||
String getLastUpdateText(DateTime lastUpdate) {
|
||||
final difference = DateTime.now().difference(lastUpdate);
|
||||
if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}min ago';
|
||||
return '${difference.inMinutes}m ago';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}h ago';
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// lib/app/modules/home/views/home_view.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/home_controller.dart';
|
||||
|
|
@ -18,18 +17,30 @@ class HomeView extends GetView<HomeController> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
_buildHeader(context),
|
||||
BannerSlider(
|
||||
bannerImages: controller.bannerImages,
|
||||
currentCarouselIndex: controller.currentCarouselIndex,
|
||||
onPageChanged: controller.updateCarouselIndex,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_buildSectionHeader(),
|
||||
Expanded(child: _buildMonitoringList()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Halo Perawat',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => controller.showLogoutDialog(context),
|
||||
|
|
@ -44,62 +55,80 @@ class HomeView extends GetView<HomeController> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Banner Slider - Gunakan widget universal
|
||||
BannerSlider(
|
||||
bannerImages: controller.bannerImages,
|
||||
currentCarouselIndex: controller.currentCarouselIndex,
|
||||
onPageChanged: controller.updateCarouselIndex,
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Monitoring Infus Section
|
||||
Padding(
|
||||
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,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () => Get.toNamed('/all-monitoring'),
|
||||
child: const Text(
|
||||
'See all',
|
||||
'Lihat Semua',
|
||||
style: TextStyle(color: Color(0xFF0091EA)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Monitoring List
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ListView.builder(
|
||||
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(
|
||||
item: item,
|
||||
onTap: () {
|
||||
// Handle tap jika diperlukan
|
||||
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:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final isPasswordHidden = true.obs;
|
||||
final isLoading = false.obs;
|
||||
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
void togglePasswordVisibility() {
|
||||
isPasswordHidden.value = !isPasswordHidden.value;
|
||||
}
|
||||
|
||||
void login() {
|
||||
void login() async {
|
||||
// Validasi input
|
||||
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Username dan password harus diisi',
|
||||
'Email dan password harus diisi',
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.offAllNamed('/navbar'); // Adjust to your navigation path
|
||||
// Validasi format email
|
||||
if (!GetUtils.isEmail(usernameController.text)) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Format email tidak valid',
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Login dengan Firebase Authentication
|
||||
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
|
||||
email: usernameController.text.trim(),
|
||||
password: passwordController.text.trim(),
|
||||
);
|
||||
|
||||
if (userCredential.user != null) {
|
||||
// Cek apakah user ada di collection users
|
||||
final userDoc = await _firestore
|
||||
.collection('users')
|
||||
.doc(userCredential.user!.uid)
|
||||
.get();
|
||||
|
||||
if (userDoc.exists) {
|
||||
// User ada di collection users (Guardian/Patient)
|
||||
final userData = userDoc.data()!;
|
||||
final deviceId = userData['device_id'] as String?;
|
||||
final userName = userData['name'] as String? ?? 'User';
|
||||
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
// User adalah guardian/patient dengan device_id
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Selamat datang, $userName!',
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
|
||||
// Navigasi ke home_patient dengan device_id
|
||||
Get.offAllNamed(
|
||||
'/home-patient',
|
||||
arguments: {
|
||||
'deviceId': deviceId,
|
||||
'userName': userName,
|
||||
'userEmail': usernameController.text.trim(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// User ada tapi tidak punya device_id
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Akun Anda belum terdaftar dengan device',
|
||||
backgroundColor: Colors.orange[100],
|
||||
colorText: Colors.orange[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
await _auth.signOut();
|
||||
}
|
||||
} else {
|
||||
// User TIDAK ada di collection users = Perawat/Admin
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Login berhasil! Selamat datang, Perawat',
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
|
||||
// Navigasi ke navbar/home perawat
|
||||
Get.offAllNamed('/navbar');
|
||||
}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
String errorMessage = '';
|
||||
|
||||
switch (e.code) {
|
||||
case 'user-not-found':
|
||||
errorMessage = 'Email tidak terdaftar';
|
||||
break;
|
||||
case 'wrong-password':
|
||||
errorMessage = 'Password salah';
|
||||
break;
|
||||
case 'invalid-email':
|
||||
errorMessage = 'Format email tidak valid';
|
||||
break;
|
||||
case 'user-disabled':
|
||||
errorMessage = 'Akun telah dinonaktifkan';
|
||||
break;
|
||||
case 'too-many-requests':
|
||||
errorMessage = 'Terlalu banyak percobaan login. Coba lagi nanti';
|
||||
break;
|
||||
case 'network-request-failed':
|
||||
errorMessage = 'Tidak ada koneksi internet';
|
||||
break;
|
||||
default:
|
||||
errorMessage = 'Terjadi kesalahan: ${e.message}';
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
'Login Gagal',
|
||||
errorMessage,
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan: $e',
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void contactNurse() async {
|
||||
const phoneNumber = '+62 812-3190-1277';
|
||||
final url = 'https://wa.me/$phoneNumber'; // WhatsApp link for contacting
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
const phoneNumber = '+6281231901277'; // Tanpa spasi
|
||||
final url = Uri.parse('https://wa.me/$phoneNumber');
|
||||
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Tidak dapat membuka WhatsApp',
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ class LoginView extends GetView<LoginController> {
|
|||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
// Wrap the entire content inside Center widget to center vertically
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -23,15 +25,6 @@ class LoginView extends GetView<LoginController> {
|
|||
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/images/banner-logo.png',
|
||||
width: 300,
|
||||
height: 130,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
|
|
@ -39,8 +32,9 @@ class LoginView extends GetView<LoginController> {
|
|||
),
|
||||
child: TextField(
|
||||
controller: controller.usernameController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan username',
|
||||
hintText: 'Masukkan email',
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
prefixIcon: Icon(
|
||||
Icons.mail_outline,
|
||||
|
|
@ -84,18 +78,30 @@ class LoginView extends GetView<LoginController> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.login,
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: controller.login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1976D2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: controller.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
|
|
@ -105,6 +111,7 @@ class LoginView extends GetView<LoginController> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -129,6 +136,7 @@ class LoginView extends GetView<LoginController> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
// lib\app\modules\notification\controllers\notification_controller.dart
|
||||
// lib/app/modules/notification/controllers/notification_controller.dart
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../models/notification_model.dart';
|
||||
import '../../../models/patient_model.dart';
|
||||
import '../../../models/room_model.dart';
|
||||
|
||||
class NotificationItem {
|
||||
final String id;
|
||||
|
|
@ -46,50 +52,72 @@ class NotificationItem {
|
|||
}
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
final selectedTab = 0.obs;
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
|
||||
final _allNotifications = <NotificationItem>[
|
||||
final selectedTab = 0.obs;
|
||||
final _allNotifications = <NotificationItem>[].obs;
|
||||
final isLoading = true.obs;
|
||||
|
||||
StreamSubscription? _notificationsSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeNotifications();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_notificationsSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// Initialize notifications dari Firestore
|
||||
void _initializeNotifications() {
|
||||
isLoading.value = true;
|
||||
|
||||
_notificationsSubscription = _firestoreService
|
||||
.getNotificationsStream()
|
||||
.listen((notificationModels) async {
|
||||
final List<NotificationItem> items = [];
|
||||
|
||||
for (var notifModel in notificationModels) {
|
||||
// Get patient info
|
||||
final patient = await _firestoreService.getPatientById(
|
||||
notifModel.patientId,
|
||||
);
|
||||
|
||||
// Get room name from patient's roomId
|
||||
String roomName = 'Unknown Room';
|
||||
if (patient != null && patient.roomId.isNotEmpty) {
|
||||
final rooms = await _firestoreService.getRoomsStream().first;
|
||||
final room = rooms.firstWhereOrNull(
|
||||
(r) => r.roomName.toLowerCase() == patient.roomId.toLowerCase(),
|
||||
);
|
||||
roomName = room?.roomName ?? patient.roomId;
|
||||
}
|
||||
|
||||
items.add(
|
||||
NotificationItem(
|
||||
id: '1',
|
||||
title: 'Peringatan!!',
|
||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
||||
room: 'MAWAR 002',
|
||||
deviceId: 'SI001',
|
||||
patientName: 'Mrs Shinta',
|
||||
time: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
isRead: false,
|
||||
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,
|
||||
),
|
||||
NotificationItem(
|
||||
id: '2',
|
||||
title: 'Peringatan!!',
|
||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
||||
room: 'MAWAR 003',
|
||||
deviceId: 'SI002',
|
||||
patientName: 'Mr Budi',
|
||||
time: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
isRead: false,
|
||||
),
|
||||
NotificationItem(
|
||||
id: '3',
|
||||
title: 'Peringatan!!',
|
||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
||||
room: 'MAWAR 004',
|
||||
deviceId: 'SI003',
|
||||
patientName: 'Mrs Ani',
|
||||
time: DateTime.now().subtract(const Duration(hours: 5)),
|
||||
isRead: true,
|
||||
),
|
||||
NotificationItem(
|
||||
id: '4',
|
||||
title: 'Peringatan!!',
|
||||
message: 'Aliran infus telah terhenti. Segera periksa pasien dan ganti selang infus.',
|
||||
room: 'MAWAR 005',
|
||||
deviceId: 'SI004',
|
||||
patientName: 'Mr Doni',
|
||||
time: DateTime.now().subtract(const Duration(days: 1)),
|
||||
isRead: true,
|
||||
),
|
||||
].obs;
|
||||
);
|
||||
}
|
||||
|
||||
_allNotifications.value = items;
|
||||
isLoading.value = false;
|
||||
}, onError: (error) {
|
||||
print('Error listening to notifications: $error');
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Computed property untuk notifikasi yang difilter
|
||||
List<NotificationItem> get notifications {
|
||||
|
|
@ -106,15 +134,35 @@ class NotificationController extends GetxController {
|
|||
selectedTab.value = index;
|
||||
}
|
||||
|
||||
void deleteNotification(String id) {
|
||||
_allNotifications.removeWhere((notif) => notif.id == id);
|
||||
Future<void> deleteNotification(String id) async {
|
||||
try {
|
||||
await _firestoreService.deleteNotification(id);
|
||||
// Local update akan dilakukan otomatis oleh stream
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus notifikasi: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
margin: const EdgeInsets.all(16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void markAsRead(String id) {
|
||||
final index = _allNotifications.indexWhere((notif) => notif.id == id);
|
||||
if (index != -1) {
|
||||
_allNotifications[index] = _allNotifications[index].copyWith(isRead: true);
|
||||
_allNotifications.refresh();
|
||||
Future<void> markAsRead(String id) async {
|
||||
try {
|
||||
await _firestoreService.markNotificationAsRead(id);
|
||||
// Local update akan dilakukan otomatis oleh stream
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menandai notifikasi: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
margin: const EdgeInsets.all(16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,4 +181,28 @@ class NotificationController extends GetxController {
|
|||
int get unreadCount {
|
||||
return _allNotifications.where((notif) => !notif.isRead).length;
|
||||
}
|
||||
|
||||
/// Method untuk menambahkan notifikasi baru (bisa dipanggil dari monitoring)
|
||||
Future<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:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../services/realtime_database_service.dart';
|
||||
import '../../../models/patient_model.dart';
|
||||
import '../../../models/device_model.dart';
|
||||
import '../../../models/room_model.dart';
|
||||
import '../../../widgets/app_snackbar.dart';
|
||||
|
||||
class Patient {
|
||||
final String id;
|
||||
final String name;
|
||||
final String room;
|
||||
final String deviceId;
|
||||
final String guardian; // Wali pasien
|
||||
final String guardian;
|
||||
final String email;
|
||||
final String? userId;
|
||||
final String status;
|
||||
|
||||
Patient({
|
||||
|
|
@ -15,40 +24,372 @@ class Patient {
|
|||
required this.room,
|
||||
required this.deviceId,
|
||||
required this.guardian,
|
||||
required this.email,
|
||||
this.userId,
|
||||
this.status = 'Aktif',
|
||||
});
|
||||
|
||||
Map<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 {
|
||||
final patients = <Patient>[
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||
|
||||
final patients = <Patient>[].obs;
|
||||
final isLoading = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_listenToPatients();
|
||||
}
|
||||
|
||||
void refreshPatients() {
|
||||
_listenToPatients();
|
||||
}
|
||||
|
||||
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: '1',
|
||||
name: 'Mrs. Shinta',
|
||||
room: 'Ruang Mawar 002',
|
||||
deviceId: 'SI001',
|
||||
guardian: 'Budi Santoso',
|
||||
id: model.id,
|
||||
name: model.namePatient,
|
||||
guardian: model.nameGuardian,
|
||||
deviceId: model.deviceId,
|
||||
room: roomName,
|
||||
email: email,
|
||||
userId: null,
|
||||
),
|
||||
Patient(
|
||||
id: '2',
|
||||
name: 'Mr. Parjo',
|
||||
room: 'Ruang Melati 001',
|
||||
deviceId: 'SI002',
|
||||
guardian: 'Siti Nurhaliza',
|
||||
);
|
||||
} 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 '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getRoomName(String roomId) async {
|
||||
try {
|
||||
final roomDoc = await FirebaseFirestore.instance
|
||||
.collection('rooms')
|
||||
.doc(roomId)
|
||||
.get();
|
||||
return roomDoc.data()?['room_name'] ?? roomId;
|
||||
} catch (e) {
|
||||
return roomId;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
].obs;
|
||||
);
|
||||
|
||||
void addPatient(Patient patient) {
|
||||
patients.add(patient);
|
||||
}
|
||||
await _realtimeService.updateDeviceStatus(roomId, deviceId, 'connected');
|
||||
await _createDeviceInRealtimeDB(roomId, deviceId);
|
||||
|
||||
void updatePatient(String id, Patient updatedPatient) {
|
||||
final index = patients.indexWhere((p) => p.id == id);
|
||||
if (index != -1) {
|
||||
patients[index] = updatedPatient;
|
||||
Get.back();
|
||||
AppSnackbar.success('Pasien berhasil ditambahkan');
|
||||
} catch (e) {
|
||||
AppSnackbar.error('Gagal menambahkan pasien: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void deletePatient(String id) {
|
||||
patients.removeWhere((patient) => patient.id == id);
|
||||
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:get/get.dart';
|
||||
import '../controllers/profile_controller.dart';
|
||||
|
|
@ -18,7 +17,6 @@ class ProfileView extends GetView<ProfileController> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Section
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
@ -36,10 +34,7 @@ class ProfileView extends GetView<ProfileController> {
|
|||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0091EA),
|
||||
Color(0xFF0277BD),
|
||||
],
|
||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
|
|
@ -50,8 +45,11 @@ class ProfileView extends GetView<ProfileController> {
|
|||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
child: Obx(
|
||||
() => ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: () {
|
||||
Get.dialog(
|
||||
const ModalPatientAddEdit(isEdit: false),
|
||||
barrierDismissible: true,
|
||||
|
|
@ -68,10 +66,23 @@ class ProfileView extends GetView<ProfileController> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
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),
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Tambah',
|
||||
|
|
@ -85,16 +96,30 @@ class ProfileView extends GetView<ProfileController> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Patient List
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() {
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value && controller.patients.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.patients.isEmpty) {
|
||||
return Center(
|
||||
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: [
|
||||
|
|
@ -111,12 +136,30 @@ class ProfileView extends GetView<ProfileController> {
|
|||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tarik ke bawah untuk refresh',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
controller.refreshPatients();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
},
|
||||
color: const Color(0xFF0091EA),
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 8,
|
||||
|
|
@ -128,51 +171,64 @@ class ProfileView extends GetView<ProfileController> {
|
|||
patient: patient,
|
||||
onEditTap: () {
|
||||
Get.dialog(
|
||||
ModalPatientAddEdit(
|
||||
patient: patient,
|
||||
isEdit: true,
|
||||
),
|
||||
ModalPatientAddEdit(patient: patient, isEdit: true),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
},
|
||||
onDeleteTap: () {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text('Hapus Pasien'),
|
||||
title: const Text(
|
||||
'Hapus Pasien',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus ${patient.name}?',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey[700]),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey[700],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.deletePatient(patient.id);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Pasien berhasil dihapus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
margin: const EdgeInsets.all(16),
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Hapus',
|
||||
style: TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -181,9 +237,9 @@ class ProfileView extends GetView<ProfileController> {
|
|||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,75 +1,219 @@
|
|||
// lib/app/modules/schedule/controllers/schedule_controller.dart
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:async';
|
||||
import '../../../services/firestore_service.dart';
|
||||
import '../../../models/schedule_model.dart';
|
||||
import '../../../widgets/app_snackbar.dart';
|
||||
|
||||
class PatientSchedule {
|
||||
final String id;
|
||||
final String name;
|
||||
final String room;
|
||||
final String deviceId;
|
||||
final String medicineType;
|
||||
final String fluidType;
|
||||
final String medicineTime;
|
||||
final String fluidTime;
|
||||
final String timeOfDay;
|
||||
|
||||
PatientSchedule({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.room,
|
||||
required this.deviceId,
|
||||
required this.medicineType,
|
||||
required this.fluidType,
|
||||
required this.medicineTime,
|
||||
required this.fluidTime,
|
||||
required this.timeOfDay,
|
||||
});
|
||||
}
|
||||
|
||||
class ScheduleController extends GetxController {
|
||||
final FirestoreService _firestoreService = FirestoreService();
|
||||
|
||||
final selectedTab = 0.obs;
|
||||
final selectedDate = DateTime.now().obs;
|
||||
final allSchedules = <PatientSchedule>[].obs;
|
||||
final filteredSchedules = <PatientSchedule>[].obs;
|
||||
final isLoading = false.obs;
|
||||
|
||||
final schedules = <PatientSchedule>[
|
||||
StreamSubscription? _schedulesSubscription;
|
||||
|
||||
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: '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',
|
||||
id: scheduleModel.id,
|
||||
name: patient.namePatient,
|
||||
room: roomName,
|
||||
deviceId: patient.deviceId,
|
||||
medicineTime: scheduleModel.medicineDetail,
|
||||
fluidTime: scheduleModel.fluidDetail,
|
||||
timeOfDay: scheduleModel.timeOfDay,
|
||||
),
|
||||
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;
|
||||
);
|
||||
}
|
||||
} 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;
|
||||
_applyTimeFilter();
|
||||
}
|
||||
|
||||
void selectDate(DateTime date) {
|
||||
selectedDate.value = date;
|
||||
_loadSchedules();
|
||||
}
|
||||
|
||||
void addSchedule(PatientSchedule schedule) {
|
||||
schedules.add(schedule);
|
||||
void refreshSchedules() {
|
||||
_loadSchedules();
|
||||
}
|
||||
|
||||
void updateSchedule(String id, PatientSchedule updatedSchedule) {
|
||||
final index = schedules.indexWhere((s) => s.id == id);
|
||||
if (index != -1) {
|
||||
schedules[index] = updatedSchedule;
|
||||
Future<void> addSchedule(PatientSchedule schedule, DateTime scheduleDate) async {
|
||||
try {
|
||||
final patients = await _firestoreService.getPatientsStream().first;
|
||||
final patient = patients.firstWhereOrNull((p) => p.id == schedule.id);
|
||||
|
||||
if (patient == null) throw Exception('Pasien tidak ditemukan');
|
||||
|
||||
final scheduleModel = ScheduleModel(
|
||||
id: '',
|
||||
patientId: patient.id,
|
||||
medicineDetail: schedule.medicineTime,
|
||||
fluidDetail: schedule.fluidTime,
|
||||
scheduleDate: scheduleDate,
|
||||
timeOfDay: schedule.timeOfDay,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _firestoreService.addSchedule(scheduleModel);
|
||||
AppSnackbar.success('Jadwal berhasil ditambahkan');
|
||||
} catch (e) {
|
||||
AppSnackbar.error('Gagal menambahkan jadwal');
|
||||
}
|
||||
}
|
||||
|
||||
void deleteSchedule(String id) {
|
||||
schedules.removeWhere((s) => s.id == id);
|
||||
Future<void> updateSchedule(String id, PatientSchedule updatedSchedule, DateTime scheduleDate) async {
|
||||
try {
|
||||
final patients = await _firestoreService.getPatientsStream().first;
|
||||
final patient = patients.firstWhereOrNull((p) => p.id == updatedSchedule.id);
|
||||
|
||||
if (patient == null) throw Exception('Pasien tidak ditemukan');
|
||||
|
||||
final scheduleModel = ScheduleModel(
|
||||
id: id,
|
||||
patientId: patient.id,
|
||||
medicineDetail: updatedSchedule.medicineTime,
|
||||
fluidDetail: updatedSchedule.fluidTime,
|
||||
scheduleDate: scheduleDate,
|
||||
timeOfDay: updatedSchedule.timeOfDay,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _firestoreService.updateSchedule(id, scheduleModel);
|
||||
AppSnackbar.success('Jadwal berhasil diperbarui');
|
||||
} catch (e) {
|
||||
AppSnackbar.error('Gagal memperbarui jadwal');
|
||||
}
|
||||
}
|
||||
|
||||
Future<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:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -12,21 +11,28 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(ScheduleController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Section
|
||||
Container(
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(child: _buildScheduleList()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
const Text(
|
||||
'Schedule Obat',
|
||||
style: TextStyle(
|
||||
|
|
@ -36,26 +42,29 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date Picker & Add Button
|
||||
Row(
|
||||
children: [
|
||||
// Date Picker
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -79,19 +88,15 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
Icon(Icons.arrow_drop_down, color: Colors.grey[600]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
);
|
||||
}
|
||||
|
||||
// Add Button
|
||||
Container(
|
||||
Widget _buildAddButton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||
|
|
@ -107,32 +112,23 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: // Ubah bagian tombol Add di schedule_view.dart:
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Get.dialog(
|
||||
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,
|
||||
child: Icon(Icons.add, color: Colors.white, size: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
);
|
||||
}
|
||||
|
||||
// Time Filter Tabs
|
||||
Obx(
|
||||
Widget _buildTimeFilterTabs() {
|
||||
return Obx(
|
||||
() => Row(
|
||||
children: [
|
||||
Expanded(child: _buildTabButton('Pagi', 0)),
|
||||
|
|
@ -144,100 +140,6 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
Expanded(child: _buildTabButton('Malam', 3)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +154,6 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF0091EA) : Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
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 {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
|
|
@ -287,8 +304,6 @@ class ScheduleView extends GetView<ScheduleController> {
|
|||
);
|
||||
},
|
||||
);
|
||||
if (picked != null) {
|
||||
controller.selectDate(picked);
|
||||
}
|
||||
if (picked != null) controller.selectDate(picked);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
// lib\app\modules\splash-screen\controllers\splash_screen_controller.dart
|
||||
import 'package:get/get.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
|
||||
class SplashScreenController extends GetxController {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_navigateToLogin();
|
||||
_checkAuthStatus();
|
||||
}
|
||||
|
||||
void _navigateToLogin() async {
|
||||
void _checkAuthStatus() async {
|
||||
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: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 {
|
||||
final List<ChartDataPoint> dataPoints; // Ubah dari dynamic ke List<ChartDataPoint>
|
||||
final List<ChartDataPoint> dataPoints;
|
||||
final VoidCallback? onBookmarkTap;
|
||||
|
||||
const HistoryGrafikCard({
|
||||
|
|
@ -15,6 +14,40 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -31,9 +64,6 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -56,11 +86,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// FL Chart
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: LineChart(
|
||||
|
|
@ -68,7 +94,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 5,
|
||||
horizontalInterval: (maxY - minY) / 4,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
||||
},
|
||||
|
|
@ -85,7 +111,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
interval: 1,
|
||||
interval: dataPoints.length > 5 ? 2.0 : 1.0,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
|
|
@ -104,7 +130,7 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 5,
|
||||
interval: (maxY - minY) / 4,
|
||||
reservedSize: 35,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Text(
|
||||
|
|
@ -126,17 +152,17 @@ class HistoryGrafikCard extends StatelessWidget {
|
|||
bottom: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
),
|
||||
minX: 8,
|
||||
maxX: 17,
|
||||
minY: 20,
|
||||
maxY: 40,
|
||||
minX: minX,
|
||||
maxX: maxX,
|
||||
minY: minY,
|
||||
maxY: maxY,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: dataPoints
|
||||
.map(
|
||||
(point) => FlSpot(
|
||||
point.hour.toDouble(),
|
||||
point.dropsPerMinute,
|
||||
point.dropsPerMinute.toDouble(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// lib/app/widgets/modal_patient_add_edit.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../modules/profile/controllers/profile_controller.dart';
|
||||
|
|
@ -7,11 +6,7 @@ class ModalPatientAddEdit extends StatefulWidget {
|
|||
final Patient? patient;
|
||||
final bool isEdit;
|
||||
|
||||
const ModalPatientAddEdit({
|
||||
super.key,
|
||||
this.patient,
|
||||
this.isEdit = false,
|
||||
});
|
||||
const ModalPatientAddEdit({super.key, this.patient, this.isEdit = false});
|
||||
|
||||
@override
|
||||
State<ModalPatientAddEdit> createState() => _ModalPatientAddEditState();
|
||||
|
|
@ -24,22 +19,25 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
late TextEditingController _roomController;
|
||||
late TextEditingController _deviceIdController;
|
||||
late TextEditingController _guardianController;
|
||||
late TextEditingController _emailController;
|
||||
late TextEditingController _passwordController;
|
||||
|
||||
bool _isSaving = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(
|
||||
text: widget.patient?.name ?? '',
|
||||
);
|
||||
_roomController = TextEditingController(
|
||||
text: widget.patient?.room ?? '',
|
||||
);
|
||||
_deviceIdController = TextEditingController(
|
||||
text: widget.patient?.deviceId ?? '',
|
||||
);
|
||||
_guardianController = TextEditingController(
|
||||
text: widget.patient?.guardian ?? '',
|
||||
);
|
||||
_initControllers();
|
||||
}
|
||||
|
||||
void _initControllers() {
|
||||
_nameController = TextEditingController(text: widget.patient?.name ?? '');
|
||||
_roomController = TextEditingController(text: widget.patient?.room ?? '');
|
||||
_deviceIdController = TextEditingController(text: widget.patient?.deviceId ?? '');
|
||||
_guardianController = TextEditingController(text: widget.patient?.guardian ?? '');
|
||||
_emailController = TextEditingController(text: widget.patient?.email ?? '');
|
||||
_passwordController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -48,6 +46,8 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
_roomController.dispose();
|
||||
_deviceIdController.dispose();
|
||||
_guardianController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,21 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
decoration: BoxDecoration(
|
||||
decoration: _dialogDecoration(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildForm(),
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _dialogDecoration() {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
|
|
@ -68,36 +82,24 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
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.person_add,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
_buildHeaderIcon(),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
|
@ -110,16 +112,32 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
onPressed: _isSaving ? null : () => Get.back(),
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Form Content
|
||||
Flexible(
|
||||
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(
|
||||
|
|
@ -127,76 +145,171 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nama Pasien
|
||||
_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) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama pasien harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Wajib diisi' : 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),
|
||||
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) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Wali pasien harus diisi';
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom Buttons
|
||||
Container(
|
||||
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],
|
||||
|
|
@ -207,29 +320,35 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
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),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
color: _isSaving ? Colors.grey[400] : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||
|
|
@ -244,16 +363,20 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleSave,
|
||||
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),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
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,
|
||||
|
|
@ -262,14 +385,6 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -295,15 +410,23 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
required String hint,
|
||||
required IconData icon,
|
||||
String? Function(String?)? validator,
|
||||
bool obscureText = false,
|
||||
Widget? suffixIcon,
|
||||
TextInputType? keyboardType,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
enabled: !_isSaving && enabled,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
fillColor: enabled ? Colors.grey[50] : Colors.grey[200],
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||
|
|
@ -312,6 +435,10 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF0091EA), width: 2),
|
||||
|
|
@ -329,58 +456,38 @@ class _ModalPatientAddEditState extends State<ModalPatientAddEdit> {
|
|||
);
|
||||
}
|
||||
|
||||
void _handleSave() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Future<void> _handleSave() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
final controller = Get.find<ProfileController>();
|
||||
|
||||
try {
|
||||
if (widget.isEdit && widget.patient != null) {
|
||||
// Update existing patient
|
||||
controller.updatePatient(
|
||||
widget.patient!.id,
|
||||
Patient(
|
||||
id: widget.patient!.id,
|
||||
name: _nameController.text.trim(),
|
||||
room: _roomController.text.trim(),
|
||||
await controller.updatePatient(
|
||||
patientId: widget.patient!.id,
|
||||
patientName: _nameController.text.trim(),
|
||||
guardianName: _guardianController.text.trim(),
|
||||
roomName: _roomController.text.trim(),
|
||||
deviceId: _deviceIdController.text.trim(),
|
||||
guardian: _guardianController.text.trim(),
|
||||
),
|
||||
);
|
||||
|
||||
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),
|
||||
email: _emailController.text.trim(),
|
||||
newPassword: _passwordController.text.isNotEmpty
|
||||
? _passwordController.text.trim()
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
// Add new patient
|
||||
controller.addPatient(
|
||||
Patient(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
name: _nameController.text.trim(),
|
||||
room: _roomController.text.trim(),
|
||||
await controller.addPatient(
|
||||
patientName: _nameController.text.trim(),
|
||||
guardianName: _guardianController.text.trim(),
|
||||
roomName: _roomController.text.trim(),
|
||||
deviceId: _deviceIdController.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),
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// lib/app/widgets/modal_schedule_add_edit.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../modules/schedule/controllers/schedule_controller.dart';
|
||||
|
||||
class ModalScheduleAddEdit extends StatefulWidget {
|
||||
|
|
@ -16,55 +16,49 @@ class ModalScheduleAddEdit extends StatefulWidget {
|
|||
class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
String? _selectedPatient;
|
||||
String? _selectedTime; // Pagi, Siang, Sore, Malam
|
||||
String? _selectedPatientId;
|
||||
String? _selectedTime;
|
||||
DateTime? _selectedDate;
|
||||
late TextEditingController _medicineController;
|
||||
late TextEditingController _fluidController;
|
||||
|
||||
// Dummy data pasien
|
||||
final List<Map<String, String>> _patients = [
|
||||
{
|
||||
'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',
|
||||
},
|
||||
];
|
||||
List<Map<String, String>> _patients = [];
|
||||
bool _isLoadingPatients = true;
|
||||
|
||||
// Waktu
|
||||
final List<String> _times = ['Pagi', 'Siang', 'Sore', 'Malam'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_medicineController = TextEditingController(
|
||||
text: widget.schedule?.medicineTime ?? '',
|
||||
);
|
||||
_fluidController = TextEditingController(
|
||||
text: widget.schedule?.fluidTime ?? '',
|
||||
);
|
||||
_medicineController = TextEditingController(text: widget.schedule?.medicineTime ?? '');
|
||||
_fluidController = TextEditingController(text: widget.schedule?.fluidTime ?? '');
|
||||
_selectedDate = DateTime.now();
|
||||
|
||||
if (widget.isEdit && widget.schedule != null) {
|
||||
_selectedPatient = widget.schedule!.id;
|
||||
_selectedTime = 'Pagi'; // Default atau bisa diambil dari schedule
|
||||
_selectedPatientId = widget.schedule!.id;
|
||||
_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 {
|
||||
if (_selectedPatient == null) return null;
|
||||
return _patients.firstWhere((p) => p['id'] == _selectedPatient);
|
||||
if (_selectedPatientId == null) return null;
|
||||
return _patients.firstWhereOrNull((p) => p['id'] == _selectedPatientId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -101,14 +95,81 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||
_buildHeader(),
|
||||
Flexible(
|
||||
child: _isLoadingPatients
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLabel('Pilih Tanggal', Icons.calendar_today),
|
||||
const SizedBox(height: 8),
|
||||
_buildDatePicker(context),
|
||||
const SizedBox(height: 20),
|
||||
_buildLabel('Pilih Pasien', Icons.person),
|
||||
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),
|
||||
_buildTimeDropdown(),
|
||||
const SizedBox(height: 20),
|
||||
_buildLabel('Informasi Obat', Icons.medication),
|
||||
const SizedBox(height: 8),
|
||||
_buildTextField(
|
||||
controller: _medicineController,
|
||||
hint: 'Contoh: Amoxicillin 3 ml',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Informasi obat harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildLabel('Detail Cairan & Tetes', Icons.water_drop),
|
||||
const SizedBox(height: 8),
|
||||
_buildTextField(
|
||||
controller: _fluidController,
|
||||
hint: 'Contoh: Mnt 30 tetes/jam',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Detail cairan harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_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),
|
||||
),
|
||||
|
|
@ -121,11 +182,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
|||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.calendar_today,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
child: const Icon(Icons.calendar_today, color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
|
|
@ -145,147 +202,6 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Form Content
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
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),
|
||||
|
||||
// 4. Detail Cairan & Tetes
|
||||
_buildLabel('Detail Cairan & Tetes', Icons.water_drop),
|
||||
const SizedBox(height: 8),
|
||||
_buildTextField(
|
||||
controller: _fluidController,
|
||||
hint: 'Contoh: Mnt 30 tetes/jam',
|
||||
icon: Icons.opacity,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Detail cairan 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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() {
|
||||
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>(
|
||||
value: _selectedPatient,
|
||||
value: _selectedPatientId,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari dan pilih pasien...',
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||
|
|
@ -330,10 +317,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||
isExpanded: true,
|
||||
|
|
@ -352,7 +336,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
|||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedPatient = value;
|
||||
_selectedPatientId = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
|
|
@ -385,19 +369,13 @@ Widget _buildSelectedPatientInfo() {
|
|||
color: const Color(0xFF0091EA).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Color(0xFF0091EA),
|
||||
size: 20,
|
||||
),
|
||||
child: const Icon(Icons.person, color: Color(0xFF0091EA), size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ===== NAMA =====
|
||||
Text(
|
||||
patient['name']!,
|
||||
style: const TextStyle(
|
||||
|
|
@ -407,16 +385,10 @@ Widget _buildSelectedPatientInfo() {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// ===== ROOM + DEVICE ID =====
|
||||
Row(
|
||||
children: [
|
||||
// ROOM
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
|
@ -430,15 +402,9 @@ Widget _buildSelectedPatientInfo() {
|
|||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// DEVICE ID
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
|
@ -500,10 +466,7 @@ Widget _buildSelectedPatientInfo() {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||
isExpanded: true,
|
||||
|
|
@ -568,7 +531,6 @@ Widget _buildSelectedPatientInfo() {
|
|||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return TextFormField(
|
||||
|
|
@ -599,73 +561,112 @@ Widget _buildSelectedPatientInfo() {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
contentPadding: const EdgeInsets.symmetric(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() {
|
||||
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 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) {
|
||||
controller.updateSchedule(
|
||||
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),
|
||||
);
|
||||
controller.updateSchedule(widget.schedule!.id, schedule, _selectedDate!);
|
||||
} else {
|
||||
controller.addSchedule(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
controller.addSchedule(schedule, _selectedDate!);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
// lib/app/widgets/monitoring_card.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../modules/home/controllers/home_controller.dart';
|
||||
|
||||
class MonitoringCard extends StatelessWidget {
|
||||
final InfusMonitoring item;
|
||||
final InfusMonitoring monitoring;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const MonitoringCard({super.key, required this.item, this.onTap});
|
||||
const MonitoringCard({
|
||||
super.key,
|
||||
required this.monitoring,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = monitoring.deviceStatus == 'connected';
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
|
|
@ -20,7 +24,7 @@ class MonitoringCard extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1.5),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
|
|
@ -30,155 +34,166 @@ class MonitoringCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// LEFT ICON
|
||||
_buildIcon(isConnected),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildContent(isConnected)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(bool isConnected) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.water_drop_outlined,
|
||||
size: 36,
|
||||
size: 32,
|
||||
color: Color(0xFF2196F3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// RIGHT CONTENT
|
||||
Expanded(
|
||||
child: Column(
|
||||
Widget _buildContent(bool isConnected) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// DROPS
|
||||
Row(
|
||||
_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(
|
||||
item.dropsPerMinute.split(' ')[0],
|
||||
monitoring.dropsPerMinute.replaceAll(' tpm', ''),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2196F3),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
'tetes/menit',
|
||||
'tetes per menit',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w600,
|
||||
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),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// LAST UPDATE
|
||||
Text(
|
||||
'Last update • ${Get.find<HomeController>().getLastUpdateText(item.lastUpdate)}',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
TextSpan(
|
||||
text: monitoring.patientName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ROOM & DEVICE
|
||||
Wrap(
|
||||
Widget _buildBadges(bool isConnected) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
// ROOM BADGE
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
_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: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
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,
|
||||
text,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
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 '../modules/schedule/controllers/schedule_controller.dart';
|
||||
|
||||
|
|
@ -34,8 +33,18 @@ class ScheduleCard extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ================= HEADER =================
|
||||
Row(
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
Container(height: 1, color: Colors.grey.shade200),
|
||||
const SizedBox(height: 16),
|
||||
_buildMedicineAndFluid(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -51,15 +60,10 @@ class ScheduleCard extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
// ===== ROOM =====
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
|
@ -73,15 +77,9 @@ class ScheduleCard extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// ===== DEVICE ID =====
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
|
@ -99,7 +97,7 @@ class ScheduleCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
schedule.deviceId, // ✅ pastikan ada di model
|
||||
schedule.deviceId,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
|
|
@ -114,11 +112,8 @@ class ScheduleCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// ACTION BUTTONS
|
||||
Row(
|
||||
children: [
|
||||
// ===== EDIT =====
|
||||
InkWell(
|
||||
onTap: onEditTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
|
@ -128,16 +123,10 @@ class ScheduleCard extends StatelessWidget {
|
|||
color: Colors.amber.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.edit,
|
||||
size: 18,
|
||||
color: Colors.amber,
|
||||
),
|
||||
child: const Icon(Icons.edit, size: 18, color: Colors.amber),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// ===== DELETE =====
|
||||
InkWell(
|
||||
onTap: onDeleteTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
|
@ -147,29 +136,18 @@ class ScheduleCard extends StatelessWidget {
|
|||
color: Colors.redAccent.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete,
|
||||
size: 18,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
child: const Icon(Icons.delete, size: 18, color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ================= DIVIDER =================
|
||||
Container(height: 1, color: Colors.grey.shade200),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ================= MEDICINE & FLUID =================
|
||||
Row(
|
||||
Widget _buildMedicineAndFluid() {
|
||||
return Row(
|
||||
children: [
|
||||
// -------- Medicine --------
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -182,15 +160,11 @@ class ScheduleCard extends StatelessWidget {
|
|||
color: const Color(0xFF2196F3).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.medication,
|
||||
size: 16,
|
||||
color: Color(0xFF2196F3),
|
||||
),
|
||||
child: const Icon(Icons.medication, size: 16, color: Color(0xFF2196F3)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
schedule.medicineType,
|
||||
'Obat & Dosis',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 11,
|
||||
|
|
@ -214,10 +188,7 @@ class ScheduleCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// -------- Fluid --------
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -230,15 +201,11 @@ class ScheduleCard extends StatelessWidget {
|
|||
color: const Color(0xFF00BCD4).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.water_drop,
|
||||
size: 16,
|
||||
color: Color(0xFF00BCD4),
|
||||
),
|
||||
child: const Icon(Icons.water_drop, size: 16, color: Color(0xFF00BCD4)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
schedule.fluidType,
|
||||
'Cairan & Tetes',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 11,
|
||||
|
|
@ -263,9 +230,6 @@ class ScheduleCard extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,23 @@
|
|||
import 'package:flutter/material.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:intl/date_symbol_data_local.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'app/routes/app_pages.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize Firebase
|
||||
await Firebase.initializeApp();
|
||||
|
||||
await GetStorage.init();
|
||||
await GetStorage.init('app_data');
|
||||
|
||||
await initializeDateFormatting('id_ID', null);
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp(
|
||||
runApp(
|
||||
GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: "SMART INFUSION",
|
||||
title: "SMART INFUSE",
|
||||
initialRoute: Routes.SPLASH_SCREEN,
|
||||
getPages: AppPages.routes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ dependencies:
|
|||
|
||||
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
|
|||
Loading…
Reference in New Issue